diff --git a/docs/CreditCardFormatter.md b/docs/CreditCardFormatter.md new file mode 100644 index 0000000..eb49349 --- /dev/null +++ b/docs/CreditCardFormatter.md @@ -0,0 +1,130 @@ + + +# CreditCardFormatter + +The `CreditCardFormatter` formats credit card numbers with automatic card type detection. It supports major card types including Visa, MasterCard, American Express, Discover, and JCB. + +## Usage + +### Basic Usage with Auto-Detection + +```php +use Respect\StringFormatter\CreditCardFormatter; + +$formatter = new CreditCardFormatter(); + +echo $formatter->format('4123456789012345'); +// Outputs: "4123 4567 8901 2345" (Visa detected) + +echo $formatter->format('371234567890123'); +// Outputs: "3712 345678 90123" (Amex, different pattern) + +echo $formatter->format('5112345678901234'); +// Outputs: "5112 3456 7890 1234" (MasterCard detected) +``` + +### Input Cleaning + +The formatter automatically removes non-digit characters from the input: + +```php +use Respect\StringFormatter\CreditCardFormatter; + +$formatter = new CreditCardFormatter(); + +echo $formatter->format('4123-4567-8901-2345'); +// Outputs: "4123 4567 8901 2345" + +echo $formatter->format('4123 4567 8901 2345'); +// Outputs: "4123 4567 8901 2345" + +echo $formatter->format('4123.4567.8901.2345'); +// Outputs: "4123 4567 8901 2345" +``` + +### Custom Pattern + +You can specify a custom pattern to override auto-detection: + +```php +use Respect\StringFormatter\CreditCardFormatter; + +$formatter = new CreditCardFormatter('####-####-####-####'); + +echo $formatter->format('4123456789012345'); +// Outputs: "4123-4567-8901-2345" + +$formatterCompact = new CreditCardFormatter('################'); + +echo $formatterCompact->format('4123456789012345'); +// Outputs: "4123456789012345" +``` + +## API + +### `CreditCardFormatter::__construct` + +- `__construct(?string $pattern = null)` + +Creates a new credit card formatter instance. + +**Parameters:** + +- `$pattern`: Custom format pattern or null for auto-detection (default: null) + +**If null**: The formatter automatically detects the card type and applies the appropriate pattern + +**If provided**: Uses the specified pattern for all cards + +### `format` + +- `format(string $input): string` + +Formats the input credit card number. + +**Parameters:** + +- `$input`: The credit card number (can include spaces, dashes, dots, etc.) + +**Returns:** The formatted credit card number + +## Auto-Detection + +The formatter automatically detects card type based on prefix and length: + +| Card Type | Prefix Ranges | Length | Format Pattern | +| -------------------- | ----------------- | ---------- | --------------------------------------- | +| **Visa** | 4 | 13, 16, 19 | `#### #### #### ####` | +| **MasterCard** | 51-55 | 16 | `#### #### #### ####` | +| **American Express** | 34, 37 | 15 | `#### ########## ######` - 4-6-5 format | +| **Discover** | 6011, 644-649, 65 | 16 | `#### #### #### ####` | +| **JCB** | 3528-3589 | 16 | `#### #### #### ####` | +| **Unknown** | (any) | any | `#### #### #### ####` - default pattern | + +## Examples + +| Input | Output | Card Type | +| --------------------- | --------------------- | -------------- | +| `4123456789012345` | `4123 4567 8901 2345` | Visa | +| `5112345678901234` | `5112 3456 7890 1234` | MasterCard | +| `341234567890123` | `3412 345678 90123` | Amex | +| `371234567890123` | `3712 345678 90123` | Amex | +| `6011000990139424` | `6011 0009 9013 9424` | Discover | +| `3528000012345678` | `3528 0000 1234 5678` | JCB | +| `1234567890123456` | `1234 5678 9012 3456` | Unknown | +| `4123-4567-8901-2345` | `4123 4567 8901 2345` | Visa (cleaned) | +| `4123 4567 8901 2345` | `4123 4567 8901 2345` | Visa (cleaned) | + +## Notes + +- Non-digit characters are automatically removed from the input +- Card type detection is based on card prefix and length (not Luhn validation) +- If card type cannot be determined, uses the default pattern `#### #### #### ####` +- Uses `PatternFormatter` internally for formatting +- Empty strings return empty strings +- Numbers longer than the pattern aretruncated to fit the pattern +- Custom patterns follow `PatternFormatter` syntax (use `#` for digits) diff --git a/docs/LowercaseFormatter.md b/docs/LowercaseFormatter.md new file mode 100644 index 0000000..1becf63 --- /dev/null +++ b/docs/LowercaseFormatter.md @@ -0,0 +1,88 @@ + + +# LowercaseFormatter + +The `LowercaseFormatter` converts strings to lowercase with proper UTF-8 character support for international text. + +## Usage + +### Basic Usage + +```php +use Respect\StringFormatter\LowercaseFormatter; + +$formatter = new LowercaseFormatter(); + +echo $formatter->format('HELLO WORLD'); +// Outputs: "hello world" +``` + +### Unicode Characters + +```php +use Respect\StringFormatter\LowercaseFormatter; + +$formatter = new LowercaseFormatter(); + +echo $formatter->format('CAFÉ FRANÇAIS'); +// Outputs: "café français" + +echo $formatter->format('コンニチハ'); +// Outputs: "コンニチハ" +``` + +### Mixed Content + +```php +use Respect\StringFormatter\LowercaseFormatter; + +$formatter = new LowercaseFormatter(); + +echo $formatter->format('HELLO WORLD 😊'); +// Outputs: "hello world 😊" +``` + +## API + +### `LowercaseFormatter::__construct` + +- `__construct()` + +Creates a new lowercase formatter instance. + +### `format` + +- `format(string $input): string` + +Converts the input string to lowercase using UTF-8 aware conversion. + +**Parameters:** + +- `$input`: The string to convert to lowercase + +**Returns:** The lowercase string + +## Examples + +| Input | Output | Description | +| ------------ | ------------ | ----------------------------- | +| `HELLO` | `hello` | Simple ASCII text | +| `CAFÉ` | `café` | Latin characters with accents | +| `ПРИВЕТ` | `привет` | Cyrillic text | +| `コンニチハ` | `コンニチハ` | Japanese text | +| `HELLO 😊` | `hello 😊` | Text with emoji | +| `ÉÎÔÛ` | `éîôû` | Multiple accented characters | + +## Notes + +- Uses `mb_strtolower()` for proper Unicode handling +- Preserves accent marks and diacritical marks +- Works with all Unicode scripts (Latin, Cyrillic, Greek, CJK, etc.) +- Emoji and special symbols are preserved unchanged +- Combining diacritics are properly handled +- Numbers and special characters remain unchanged +- Empty strings return empty strings diff --git a/docs/SecretCreditCardFormatter.md b/docs/SecretCreditCardFormatter.md new file mode 100644 index 0000000..864d8f4 --- /dev/null +++ b/docs/SecretCreditCardFormatter.md @@ -0,0 +1,117 @@ + + +# SecretCreditCardFormatter + +The `SecretCreditCardFormatter` formats and masks credit card numbers for secure display. It automatically detects card types, formats them appropriately, and masks sensitive portions. + +## Usage + +### Basic Usage + +```php +use Respect\StringFormatter\SecretCreditCardFormatter; + +$formatter = new SecretCreditCardFormatter(); + +echo $formatter->format('4123456789012345'); +// Outputs: "4123 **** **** 2345" (Visa) + +echo $formatter->format('341234567890123'); +// Outputs: "3412 *******012 3" (Amex) + +echo $formatter->format('5112345678901234'); +// Outputs: "5112 **** **** 1234" (MasterCard) +``` + +### Input Cleaning + +The formatter automatically removes non-digit characters from the input: + +```php +use Respect\StringFormatter\SecretCreditCardFormatter; + +$formatter = new SecretCreditCardFormatter(); + +echo $formatter->format('4123-4567-8901-2345'); +// Outputs: "4123 **** **** 2345" +``` + +### Custom Masking + +You can specify custom mask ranges, patterns, or mask characters: + +```php +use Respect\StringFormatter\SecretCreditCardFormatter; + +$formatter = new SecretCreditCardFormatter(maskRange: '6-12', maskChar: 'X'); + +echo $formatter->format('4123456789012345'); +// Outputs: "4123 XXXXXX 2345" +``` + +## API + +### `SecretCreditCardFormatter::__construct` + +- `__construct(?string $pattern = null, ?string $maskRange = null, string $maskChar = '*')` + +Creates a new secret credit card formatter instance. + +**Parameters:** + +- `$pattern`: Custom format pattern or null for auto-detection (default: null) +- `$maskRange`: Mask range specification or null for auto-detection (default: null) +- `$maskChar`: Character to use for masking (default: '\*') + +### `format` + +- `format(string $input): string` + +Formats and masks the input credit card number. + +**Parameters:** + +- `$input`: The credit card number (can include spaces, dashes, dots, etc.) + +**Returns:** The formatted and masked credit card number + +## Masking + +The formatter applies masking after formatting to ensure predictable positions: + +| Card Type | Example Input | Mask Range | Output | +| -------------------- | ------------------ | ----------- | --------------------- | +| **Visa** | `4123456789012345` | `6-9,11-14` | `4123 **** **** 2345` | +| **MasterCard** | `5112345678901234` | `6-9,11-14` | `5112 **** **** 1234` | +| **American Express** | `341234567890123` | `6-12` | `3412 *******012 3` | +| **Discover** | `6011000990139424` | `6-9,11-14` | `6011 **** **** 9424` | +| **JCB** | `3528000012345678` | `6-9,11-14` | `3528 **** **** 5678` | + +## Examples + +| Input | Output | Card Type | +| --------------------- | --------------------- | -------------- | +| `4123456789012345` | `4123 **** **** 2345` | Visa | +| `5112345678901234` | `5112 **** **** 1234` | MasterCard | +| `341234567890123` | `3412 *******012 3` | Amex | +| `371234567890123` | `3712 *******012 3` | Amex | +| `6011000990139424` | `6011 **** **** 9424` | Discover | +| `3528000012345678` | `3528 **** **** 5678` | JCB | +| `4123-4567-8901-2345` | `4123 **** **** 2345` | Visa (cleaned) | +| `4123 4567 8901 2345` | `4123 **** **** 2345` | Visa (cleaned) | + +## Notes + +- Composes `CreditCardFormatter` for formatting and `MaskFormatter` for masking +- Formats the card number first, then applies masking to the formatted string +- Mask ranges are applied to 1-based positions in the formatted string +- Commas in mask ranges specify multiple separate ranges to mask +- Non-digit characters are automatically removed from input +- Empty strings return formatted empty string with default pattern spacing +- Custom patterns follow `PatternFormatter` syntax (use `#` for digits) +- For custom masking, use `MaskFormatter` range syntax (1-based positions) +- Uses `CreditCardFormatter` for card type detection and formatting diff --git a/docs/TrimFormatter.md b/docs/TrimFormatter.md new file mode 100644 index 0000000..da567b8 --- /dev/null +++ b/docs/TrimFormatter.md @@ -0,0 +1,130 @@ + + +# TrimFormatter + +The `TrimFormatter` removes characters from the edges of strings with configurable masking and side selection, fully supporting UTF-8 Unicode characters. + +## Usage + +### Basic Usage + +```php +use Respect\StringFormatter\TrimFormatter; + +$formatter = new TrimFormatter(); + +echo $formatter->format(' hello world '); +// Outputs: "hello world" +``` + +### Trim Specific Side + +```php +use Respect\StringFormatter\TrimFormatter; + +$formatter = new TrimFormatter(' ', 'left'); + +echo $formatter->format(' hello '); +// Outputs: "hello " + +$formatterRight = new TrimFormatter(' ', 'right'); + +echo $formatterRight->format(' hello '); +// Outputs: " hello" +``` + +### Custom Mask + +```php +use Respect\StringFormatter\TrimFormatter; + +$formatter = new TrimFormatter('-._'); + +echo $formatter->format('---hello---'); +// Outputs: "hello" + +echo $formatter->format('._hello_._'); +// Outputs: "hello" +``` + +### Unicode Characters + +```php +use Respect\StringFormatter\TrimFormatter; + +// Trim CJK full-width spaces +$formatter = new TrimFormatter(' '); + +echo $formatter->format(' hello世界 '); +// Outputs: "hello世界" + +// Trim emoji +$formatterEmoji = new TrimFormatter('😊'); + +echo $formatterEmoji->format('😊hello😊'); +// Outputs: "hello" +``` + +## API + +### `TrimFormatter::__construct` + +- `__construct(string $mask = " \t\n\r\0\x0B", string $side = "both")` + +Creates a new trim formatter instance. + +**Parameters:** + +- `$mask`: The characters to trim from the string edges (default: whitespace characters) +- `$side`: Which side(s) to trim: "left", "right", or "both" (default: "both") + +**Throws:** `InvalidFormatterException` when `$side` is not "left", "right", or "both" + +### `format` + +- `format(string $input): string` + +Removes characters from the specified side(s) of the input string. + +**Parameters:** + +- `$input`: The string to trim + +**Returns:** The trimmed string + +## Examples + +| Configuration | Input | Output | Description | +| ------------------ | --------------- | ------------ | ------------------------------- | +| default | `" hello "` | `"hello"` | Trim spaces from both sides | +| `"left"` | `" hello "` | `"hello "` | Trim spaces from left only | +| `"right"` | `" hello "` | `" hello"` | Trim spaces from right only | +| `"-"` | `"---hello---"` | `"hello"` | Trim hyphens from both sides | +| `"-._"` | `"-._hello_.-"` | `"hello"` | Trim multiple custom characters | +| `":"` (`"left"`) | `":::hello:::"` | `"hello:::"` | Trim colons from left only | +| `" "` (CJK space) | `" hello"` | `"hello"` | Trim CJK full-width space | +| `"😊"` | `"😊hello😊"` | `"hello"` | Trim emoji | + +## Notes + +- Fully UTF-8 aware - handles all Unicode scripts including CJK, emoji, and complex characters +- Special regex characters in the mask (e.g., `.`, `*`, `?`, `+`) are automatically escaped +- Empty strings return empty strings +- If the mask is empty or contains no characters present in the input, the string is returned unchanged +- Trimming operations are character-oriented, not byte-oriented +- Combining characters are handled correctly (trimming considers the full character sequence) + +### Default Mask + +The default mask includes standard whitespace characters: + +- Space (` `) +- Tab (`\t`) +- Newline (`\n`) +- Carriage return (`\r`) +- Null byte (`\0`) +- Vertical tab (`\x0B`) diff --git a/docs/UppercaseFormatter.md b/docs/UppercaseFormatter.md new file mode 100644 index 0000000..43859bd --- /dev/null +++ b/docs/UppercaseFormatter.md @@ -0,0 +1,88 @@ + + +# UppercaseFormatter + +The `UppercaseFormatter` converts strings to uppercase with proper UTF-8 character support for international text. + +## Usage + +### Basic Usage + +```php +use Respect\StringFormatter\UppercaseFormatter; + +$formatter = new UppercaseFormatter(); + +echo $formatter->format('hello world'); +// Outputs: "HELLO WORLD" +``` + +### Unicode Characters + +```php +use Respect\StringFormatter\UppercaseFormatter; + +$formatter = new UppercaseFormatter(); + +echo $formatter->format('café français'); +// Outputs: "CAFÉ FRANÇAIS" + +echo $formatter->format('こんにちは'); +// Outputs: "コンニチハ" +``` + +### Mixed Content + +```php +use Respect\StringFormatter\UppercaseFormatter; + +$formatter = new UppercaseFormatter(); + +echo $formatter->format('Hello World 😊'); +// Outputs: "HELLO WORLD 😊" +``` + +## API + +### `UppercaseFormatter::__construct` + +- `__construct()` + +Creates a new uppercase formatter instance. + +### `format` + +- `format(string $input): string` + +Converts the input string to uppercase using UTF-8 aware conversion. + +**Parameters:** + +- `$input`: The string to convert to uppercase + +**Returns:** The uppercase string + +## Examples + +| Input | Output | Description | +| ------------ | ------------ | --------------------------------------- | +| `hello` | `HELLO` | Simple ASCII text | +| `café` | `CAFÉ` | Latin characters with accents | +| `привет` | `ПРИВЕТ` | Cyrillic text | +| `こんにちは` | `コンニチハ` | Japanese hiragana converted to katakana | +| `Hello 😊` | `HELLO 😊` | Text with emoji | +| `éîôû` | `ÉÎÔÛ` | Multiple accented characters | + +## Notes + +- Uses `mb_strtoupper()` for proper Unicode handling +- Preserves accent marks and diacritical marks +- Works with all Unicode scripts (Latin, Cyrillic, Greek, CJK, etc.) +- Emoji and special symbols are preserved unchanged +- Combining diacritics are properly handled +- Numbers and special characters remain unchanged +- Empty strings return empty strings diff --git a/src/CreditCardFormatter.php b/src/CreditCardFormatter.php new file mode 100644 index 0000000..db73f70 --- /dev/null +++ b/src/CreditCardFormatter.php @@ -0,0 +1,80 @@ + '#### ########## ######', + 'visa' => '#### #### #### ####', + 'mastercard' => '#### #### #### ####', + 'discover' => '#### #### #### ####', + 'jcb' => '#### #### #### ####', + 'default' => '#### #### #### ####', + ]; + + public function __construct( + private string|null $pattern = null, + ) { + } + + public function format(string $input): string + { + $cleaned = $this->cleanInput($input); + $pattern = $this->pattern ?? $this->detectPattern($cleaned); + + $formatter = new PatternFormatter($pattern); + + return $formatter->format($cleaned); + } + + public function cleanInput(string $input): string + { + return preg_replace('/[^0-9]/', '', $input) ?? ''; + } + + public function detectPattern(string $input): string + { + if ($input === '') { + return self::CARD_PATTERNS['default']; + } + + return $this->getPatternForCardType($this->detectCardType($input)); + } + + public function getPatternForCardType(string|null $cardType): string + { + return self::CARD_PATTERNS[$cardType] ?? self::CARD_PATTERNS['default']; + } + + private function detectCardType(string|null $input): string|null + { + if ($input === '' || $input === null) { + return null; + } + + $firstTwo = mb_substr($input, 0, 2); + + if ($firstTwo === '34' || $firstTwo === '37') { + return 'amex'; + } + + if ($firstTwo === '35') { + return 'jcb'; + } + + $first = mb_substr($input, 0, 1); + + return match ($first) { + '4' => 'visa', + '5' => 'mastercard', + '6' => 'discover', + default => null, + }; + } +} diff --git a/src/LowercaseFormatter.php b/src/LowercaseFormatter.php new file mode 100644 index 0000000..1557ad8 --- /dev/null +++ b/src/LowercaseFormatter.php @@ -0,0 +1,15 @@ +pattern); + $cleaned = $creditCardFormatter->cleanInput($input); + + $formatted = $creditCardFormatter->format($cleaned); + $maskRange = $this->maskRange ?? $this->detectMaskRange($cleaned); + + return (new MaskFormatter($maskRange, $this->maskChar))->format($formatted); + } + + private function detectMaskRange(string $cleaned): string + { + $length = mb_strlen($cleaned); + + if ($length === 15) { + return '6-12'; + } + + return '6-9,11-14'; + } +} diff --git a/src/TrimFormatter.php b/src/TrimFormatter.php new file mode 100644 index 0000000..d08cbe5 --- /dev/null +++ b/src/TrimFormatter.php @@ -0,0 +1,85 @@ +validateSide(); + } + + public function format(string $input): string + { + if ($this->side === self::LEFT || $this->side === self::BOTH) { + $input = $this->trimLeft($input); + } + + if ($this->side === self::RIGHT || $this->side === self::BOTH) { + $input = $this->trimRight($input); + } + + return $input; + } + + private function validateSide(): void + { + if (!in_array($this->side, [self::LEFT, self::RIGHT, self::BOTH], true)) { + throw new InvalidFormatterException( + sprintf( + 'Invalid side "%s". Must be "left", "right", or "both".', + $this->side, + ), + ); + } + } + + private function trimLeft(string $input): string + { + $regex = sprintf('/^[%s]++/u', $this->escapeRegex($this->mask)); + + return preg_replace($regex, '', $input) ?? $input; + } + + private function trimRight(string $input): string + { + $regex = sprintf('/[%s]++$/u', $this->escapeRegex($this->mask)); + + return preg_replace($regex, '', $input) ?? $input; + } + + private function escapeRegex(string $mask): string + { + $specialChars = '/\\^$.|?*+()[{'; + $chars = preg_split('//u', $mask, -1, PREG_SPLIT_NO_EMPTY) ?: []; + $escaped = []; + + foreach ($chars as $char) { + if (in_array($char, preg_split('//u', $specialChars, -1, PREG_SPLIT_NO_EMPTY) ?: [], true)) { + $char = '\\' . $char; + } + + $escaped[] = $char; + } + + return implode('', $escaped); + } +} diff --git a/src/UppercaseFormatter.php b/src/UppercaseFormatter.php new file mode 100644 index 0000000..31946f2 --- /dev/null +++ b/src/UppercaseFormatter.php @@ -0,0 +1,15 @@ +format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForMasterCard')] + public function testShouldFormatMasterCard(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForAmexCards')] + public function testShouldFormatAmexCards(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForDiscoverCards')] + public function testShouldFormatDiscoverCards(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForJcbCards')] + public function testShouldFormatJcbCards(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForUnrecognizedCards')] + public function testShouldFormatUnrecognizedCardsWithDefaultPattern(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForCustomPattern')] + public function testShouldUseCustomPattern(string $input, string $pattern, string $expected): void + { + $formatter = new CreditCardFormatter($pattern); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForInputCleaning')] + public function testShouldCleanNonDigitCharacters(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForEdgeCases')] + public function testShouldHandleEdgeCases(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function testShouldHandleEmptyString(): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format(''); + + self::assertSame(' ', $actual); + } + + #[Test] + #[DataProvider('providerForVisaDifferentLengths')] + public function testShouldHandleVisaDifferentLengths(string $input, string $expected): void + { + $formatter = new CreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForVisaCards(): array + { + return [ + 'visa 16 digits' => ['4123456789012345', '4123 4567 8901 2345'], + 'visa 16 digits with dashes' => ['4123-4567-8901-2345', '4123 4567 8901 2345'], + 'visa 16 digits with spaces' => ['4123 4567 8901 2345', '4123 4567 8901 2345'], + 'visa another' => ['4532015112830366', '4532 0151 1283 0366'], + 'visa starts with 4' => ['4916409457367128', '4916 4094 5736 7128'], + ]; + } + + /** @return array */ + public static function providerForMasterCard(): array + { + return [ + 'mastercard 51' => ['5112345678901234', '5112 3456 7890 1234'], + 'mastercard 55' => ['5512345678901234', '5512 3456 7890 1234'], + 'mastercard 52' => ['5212345678901234', '5212 3456 7890 1234'], + 'mastercard 53' => ['5312345678901234', '5312 3456 7890 1234'], + 'mastercard 54' => ['5412345678901234', '5412 3456 7890 1234'], + ]; + } + + /** @return array */ + public static function providerForAmexCards(): array + { + return [ + 'amex 34' => ['341234567890123', '3412 3456789012 3'], + 'amex 37' => ['371234567890123', '3712 3456789012 3'], + 'amex another 34' => ['347856241795641', '3478 5624179564 1'], + 'amex another 37' => ['378282246310005', '3782 8224631000 5'], + ]; + } + + /** @return array */ + public static function providerForDiscoverCards(): array + { + return [ + 'discover 6011' => ['6011000990139424', '6011 0009 9013 9424'], + 'discover 65' => ['6512345678901234', '6512 3456 7890 1234'], + 'discover 644' => ['6441234567890123', '6441 2345 6789 0123'], + 'discover 645' => ['6451234567890123', '6451 2345 6789 0123'], + 'discover 646' => ['6461234567890123', '6461 2345 6789 0123'], + 'discover 647' => ['6471234567890123', '6471 2345 6789 0123'], + 'discover 648' => ['6481234567890123', '6481 2345 6789 0123'], + 'discover 649' => ['6491234567890123', '6491 2345 6789 0123'], + ]; + } + + /** @return array */ + public static function providerForJcbCards(): array + { + return [ + 'jcb 3528' => ['3528000012345678', '3528 0000 1234 5678'], + 'jcb 3536' => ['3536000012345678', '3536 0000 1234 5678'], + 'jcb 3558' => ['3558000012345678', '3558 0000 1234 5678'], + 'jcb 3589' => ['3589000012345678', '3589 0000 1234 5678'], + ]; + } + + /** @return array */ + public static function providerForUnrecognizedCards(): array + { + return [ + 'unknown 16 digit' => ['1234567890123456', '1234 5678 9012 3456'], + 'unknown starts with 1' => ['1111222233334444', '1111 2222 3333 4444'], + 'unknown starts with 2' => ['2111222233334444', '2111 2222 3333 4444'], + 'unknown starts with 3' => ['3111222233334444', '3111 2222 3333 4444'], + ]; + } + + /** @return array */ + public static function providerForCustomPattern(): array + { + return [ + 'custom pattern without spaces' => ['4123456789012345', '################', '4123456789012345'], + 'custom pattern with dashes' => ['4123456789012345', '####-####-####-####', '4123-4567-8901-2345'], + 'custom pattern groups of 3' => ['4123456789012345', '###-###-###-###-###', '412-345-678-901-234'], + ]; + } + + /** @return array */ + public static function providerForInputCleaning(): array + { + return [ + 'with spaces' => ['4123 4567 8901 2345', '4123 4567 8901 2345'], + 'with dashes' => ['4123-4567-8901-2345', '4123 4567 8901 2345'], + 'with dots' => ['4123.4567.8901.2345', '4123 4567 8901 2345'], + 'mixed separators' => ['4123-4567.8901 2345', '4123 4567 8901 2345'], + 'with letters' => ['4123A4567B8901C2345', '4123 4567 8901 2345'], + 'with special chars' => ['4123!4567@8901#2345', '4123 4567 8901 2345'], + ]; + } + + /** @return array */ + public static function providerForEdgeCases(): array + { + return [ + 'empty string' => ['', ' '], + 'only spaces' => [' ', ' '], + 'only dashes' => ['----', ' '], + 'only dots' => ['....', ' '], + 'only letters' => ['abcd', ' '], + 'short number' => ['123', '123 '], + 'mixed content' => ['abcd4123456789012345abcd', '4123 4567 8901 2345'], + 'numbers longer than pattern' => ['41234567890123456789', '4123 4567 8901 2345'], + ]; + } + + /** @return array */ + public static function providerForVisaDifferentLengths(): array + { + return [ + 'visa 13 digits' => ['4123456789012', '4123 4567 8901 2'], + 'visa 16 digits' => ['4123456789012345', '4123 4567 8901 2345'], + 'visa 19 digits' => ['4123456789012345678', '4123 4567 8901 2345'], + ]; + } +} diff --git a/tests/Unit/LowercaseFormatterTest.php b/tests/Unit/LowercaseFormatterTest.php new file mode 100644 index 0000000..1fbce5f --- /dev/null +++ b/tests/Unit/LowercaseFormatterTest.php @@ -0,0 +1,280 @@ +format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function testShouldHandleEmptyString(): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format(''); + + self::assertSame('', $actual); + } + + #[Test] + #[DataProvider('providerForUnicodeString')] + public function testShouldHandleUnicodeCharacters(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForLatinAccents')] + public function testShouldHandleLatinCharactersWithAccents(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForNonLatinScripts')] + public function testShouldHandleNonLatinScripts(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForEmojiAndSpecialChars')] + public function testShouldHandleEmojiAndSpecialCharacters(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForTurkish')] + public function testShouldHandleTurkishCharacters(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForCombiningDiacritics')] + public function testShouldHandleCombiningDiacritics(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForRightToLeft')] + public function testShouldHandleRightToLeftText(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForMultiByte')] + public function testShouldHandleMultiByteCharacters(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForNumbersAndSpecial')] + public function testShouldHandleNumbersAndSpecialChars(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForMixed')] + public function testShouldHandleMixedContent(string $input, string $expected): void + { + $formatter = new LowercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForValidFormattedString(): array + { + return [ + 'empty string' => ['', ''], + 'single uppercase letter' => ['A', 'a'], + 'all uppercase' => ['HELLO', 'hello'], + 'already lowercase' => ['hello', 'hello'], + 'mixed case' => ['Hello World', 'hello world'], + 'with punctuation' => ['Hello, World!', 'hello, world!'], + 'with numbers' => ['Hello123', 'hello123'], + 'single word' => ['TEST', 'test'], + 'multiple words' => ['Test String Case', 'test string case'], + ]; + } + + /** @return array */ + public static function providerForUnicodeString(): array + { + return [ + 'german umlauts' => ['ÜBER', 'über'], + 'french accents' => ['CAFÉ', 'café'], + 'spanish tilde' => ['NIÑO', 'niño'], + 'portuguese' => ['CORAÇÃO', 'coração'], + 'icelandic' => ['ÞINGVELLIR', 'þingvellir'], + 'scandinavian' => ['ØRSTED', 'ørsted'], + 'polish' => ['ŁĘSKI', 'łęski'], + ]; + } + + /** @return array */ + public static function providerForLatinAccents(): array + { + return [ + 'c-cedilla' => ['CAFÉ FRANÇAIS', 'café français'], + 'umlauts' => ['ÄÖÜ', 'äöü'], + 'tilde' => ['ÃÑÕ', 'ãñõ'], + 'circumflex' => ['ÊÎÔÛ', 'êîôû'], + 'acute' => ['ÁÉÍÓÚ', 'áéíóú'], + 'grave' => ['ÀÈÌÒÙ', 'àèìòù'], + 'mixed accents' => ['RÉSUMÉ DÉJÀ VU', 'résumé déjà vu'], + ]; + } + + /** @return array */ + public static function providerForNonLatinScripts(): array + { + return [ + 'greek uppercase' => ['ΓΕΙΑ ΣΑΣ', 'γεια σας'], + 'cyrillic uppercase' => ['ПРИВЕТ МИР', 'привет мир'], + 'arabic' => ['مرحبا', 'مرحبا'], + 'hebrew' => ['שלום', 'שלום'], + ]; + } + + /** @return array */ + public static function providerForEmojiAndSpecialChars(): array + { + return [ + 'smiley face' => ['HELLO 😊', 'hello 😊'], + 'multiple emoji' => ['HI 👋 BYE 👋', 'hi 👋 bye 👋'], + 'hearts' => ['❤️ LOVE ❤️', '❤️ love ❤️'], + 'special symbols' => ['© ™ ®', '© ™ ®'], + 'math symbols' => ['∑ π ∫', '∑ π ∫'], + ]; + } + + /** @return array */ + public static function providerForTurkish(): array + { + return [ + 'turkish i' => ['İ', 'i̇'], + 'turkish I' => ['I', 'i'], + 'turkish mixed' => ['İSTANBUL', 'i̇stanbul'], + 'capital i with dot' => ['İi', 'i̇i'], + ]; + } + + /** @return array */ + public static function providerForCombiningDiacritics(): array + { + return [ + 'E with combining acute' => ["E\u{0301}", "e\u{0301}"], + 'A with combining grave' => ["A\u{0300}", "a\u{0300}"], + 'combined character' => ['É', 'é'], + 'word with combining marks' => ["CAFE\u{0301}", "cafe\u{0301}"], + ]; + } + + /** @return array */ + public static function providerForRightToLeft(): array + { + return [ + 'arabic word' => ['مرحبا', 'مرحبا'], + 'hebrew word' => ['שלום', 'שלום'], + 'mixed direction' => ['HELLO مرحبا', 'hello مرحبا'], + ]; + } + + /** @return array */ + public static function providerForMultiByte(): array + { + return [ + 'chinese' => ['你好世界', '你好世界'], + 'japanese katakana' => ['コンニチハ', 'コンニチハ'], + 'korean hangul' => ['안녕하세요', '안녕하세요'], + 'cjk characters' => ['简体字繁體字漢字', '简体字繁體字漢字'], + ]; + } + + /** @return array */ + public static function providerForNumbersAndSpecial(): array + { + return [ + 'digits only' => ['1234567890', '1234567890'], + 'mixed alphanumeric' => ['ABC123DEF', 'abc123def'], + 'special chars only' => ['!@#$%^&*()', '!@#$%^&*()'], + 'whitespace' => [' ', ' '], + 'tabs and newlines' => ["HELLO\tWORLD\n", "hello\tworld\n"], + ]; + } + + /** @return array */ + public static function providerForMixed(): array + { + return [ + 'unicode with numbers' => ['CAFÉ123', 'café123'], + 'emoji with text' => ['HELLO WORLD 😊', 'hello world 😊'], + 'cjk with latin' => ['HELLO你好', 'hello你好'], + 'mixed scripts' => ['HELLO 世界 МИР', 'hello 世界 мир'], + 'complex string' => ['CAFÉ 123 😊 你好', 'café 123 😊 你好'], + ]; + } +} diff --git a/tests/Unit/SecretCreditCardFormatterTest.php b/tests/Unit/SecretCreditCardFormatterTest.php new file mode 100644 index 0000000..20b5277 --- /dev/null +++ b/tests/Unit/SecretCreditCardFormatterTest.php @@ -0,0 +1,304 @@ +format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForMasterCard')] + public function testShouldFormatAndMaskMasterCard(string $input, string $expected): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForAmexCards')] + public function testShouldFormatAndMaskAmexCards(string $input, string $expected): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForDiscoverCards')] + public function testShouldFormatAndMaskDiscoverCards(string $input, string $expected): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForJcbCards')] + public function testShouldFormatAndMaskJcbCards(string $input, string $expected): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForUnrecognizedCards')] + public function testShouldFormatAndMaskUnrecognizedCardsWithDefault(string $input, string $expected): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForCustomPattern')] + public function testShouldUseCustomPattern(string $input, string $pattern, string $expected): void + { + $formatter = new SecretCreditCardFormatter($pattern); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForCustomMaskRange')] + public function testShouldUseCustomMaskRange(string $input, string $maskRange, string $expected): void + { + $formatter = new SecretCreditCardFormatter(null, $maskRange); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForCustomMaskChar')] + public function testShouldUseCustomMaskChar(string $input, string $maskChar, string $expected): void + { + $formatter = new SecretCreditCardFormatter(null, null, $maskChar); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForInputCleaning')] + public function testShouldCleanNonDigitCharacters(string $input, string $expected): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForEdgeCases')] + public function testShouldHandleEdgeCases(string $input, string $expected): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function testShouldHandleEmptyString(): void + { + $formatter = new SecretCreditCardFormatter(); + + $actual = $formatter->format(''); + + self::assertSame('', $actual); + } + + #[Test] + #[DataProvider('providerForAllOptions')] + public function testShouldCombineAllCustomOptions(string $input, string $pattern, string $maskRange, string $maskChar, string $expected): void + { + $formatter = new SecretCreditCardFormatter($pattern, $maskRange, $maskChar); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + public static function providerForVisaCards(): array + { + return [ + 'visa 16 digits' => ['4123456789012345', '4123 **** **** 2345'], + 'visa with dashes' => ['4123-4567-8901-2345', '4123 **** **** 2345'], + 'visa with spaces' => ['4123 4567 8901 2345', '4123 **** **** 2345'], + 'visa another' => ['4532015112830366', '4532 **** **** 0366'], + 'visa starts with 4' => ['4916409457367128', '4916 **** **** 7128'], + ]; + } + + /** @return array */ + public static function providerForMasterCard(): array + { + return [ + 'mastercard 51' => ['5112345678901234', '5112 **** **** 1234'], + 'mastercard 55' => ['5512345678901234', '5512 **** **** 1234'], + 'mastercard 52' => ['5212345678901234', '5212 **** **** 1234'], + 'mastercard 53' => ['5312345678901234', '5312 **** **** 1234'], + 'mastercard 54' => ['5412345678901234', '5412 **** **** 1234'], + ]; + } + + /** @return array */ + public static function providerForAmexCards(): array + { + return [ + 'amex 34' => ['341234567890123', '3412 *******012 3'], + 'amex 37' => ['371234567890123', '3712 *******012 3'], + 'amex another 34' => ['347856241795641', '3478 *******956 1'], + 'amex another 37' => ['378282246310005', '3782 *******000 5'], + ]; + } + + /** @return array */ + public static function providerForDiscoverCards(): array + { + return [ + 'discover 6011' => ['6011000990139424', '6011 ******** 9424'], + 'discover 65' => ['6512345678901234', '6512 ******** 1234'], + 'discover 644' => ['6441234567890123', '6441 ******** 0123'], + 'discover 645' => ['6451234567890123', '6451 ******** 0123'], + 'discover 646' => ['6461234567890123', '6461 ******** 0123'], + ]; + } + + /** @return array */ + public static function providerForJcbCards(): array + { + return [ + 'jcb 3528' => ['3528000012345678', '3528 ******** 5678'], + 'jcb 3536' => ['3536000012345678', '3536 ******** 5678'], + 'jcb 3558' => ['3558000012345678', '3558 ******** 5678'], + 'jcb 3589' => ['3589000012345678', '3589 ******** 5678'], + ]; + } + + /** @return array */ + public static function providerForUnrecognizedCards(): array + { + return [ + 'unknown 16 digit' => ['1234567890123456', '1234 ************'], + 'unknown starts with 1' => ['1111222233334444', '1111 ************'], + 'unknown starts with 2' => ['2111222233334444', '2111 ************'], + ]; + } + + /** @return array */ + public static function providerForCustomPattern(): array + { + return [ + 'custom pattern without spaces' => ['4123456789012345', '################', '****************'], + 'custom pattern with dashes' => ['4123456789012345', '####-####-####-####', '****-****-****-2345'], + 'custom pattern groups of 3' => ['4123456789012345', '###-###-###-###-###', '412-***-***-***-234'], + ]; + } + + /** @return array */ + public static function providerForCustomMaskRange(): array + { + return [ + 'mask all except first 4' => ['4123456789012345', '5-', '4123 *************'], + 'mask last 4 only' => ['4123456789012345', '13-16', '4123 4567 8901 ****'], + 'mask middle 4' => ['4123456789012345', '6-9', '4123 **** 8901 2345'], + ]; + } + + /** @return array */ + public static function providerForCustomMaskChar(): array + { + return [ + 'mask with X' => ['4123456789012345', 'X', '4123 XXXXXXXX 2345'], + 'mask with #' => ['4123456789012345', '#', '4123 ######## 2345'], + 'mask with -' => ['4123456789012345', '-', '4123 -------- 2345'], + 'mask with •' => ['4123456789012345', '•', '4123 ••••••••••• 2345'], + ]; + } + + /** @return array */ + public static function providerForInputCleaning(): array + { + return [ + 'with spaces' => ['4123 4567 8901 2345', '4123 ******** 2345'], + 'with dashes' => ['4123-4567-8901-2345', '4123 ******** 2345'], + 'with dots' => ['4123.4567.8901.2345', '4123 ******** 2345'], + 'mixed separators' => ['4123-4567.8901 2345', '4123 ******** 2345'], + 'with letters' => ['4123A4567B8901C2345', '4123 ******** 2345'], + 'with special chars' => ['4123!4567@8901#2345', '4123 ******** 2345'], + ]; + } + + /** @return array */ + public static function providerForEdgeCases(): array + { + return [ + 'empty string' => ['', ''], + 'only spaces' => [' ', ''], + 'only dashes' => ['----', ''], + 'only dots' => ['....', ''], + 'only letters' => ['abcd', ''], + 'short number' => ['123', '123'], + 'mixed content' => ['abcd4123456789012345abcd', '4123 ******** 2345'], + 'numbers longer than pattern' => ['41234567890123456789', '4123 ******** 678'], + ]; + } + + /** @return array */ + public static function providerForAllOptions(): array + { + return [ + 'all custom options' => [ + '4123456789012345', + '####-####-####-####', + '6-9', + 'X', + '4123-XXXX-8901-2345', + ], + 'different pattern and mask' => [ + '341234567890123', + '#### ########## ######', + '4-9', + '#', + '3412 ###### ## ##23', + ], + ]; + } +} diff --git a/tests/Unit/TrimFormatterTest.php b/tests/Unit/TrimFormatterTest.php new file mode 100644 index 0000000..24e762e --- /dev/null +++ b/tests/Unit/TrimFormatterTest.php @@ -0,0 +1,275 @@ +format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForLeftTrim')] + public function testShouldTrimLeft(string $input, string $expected, string $mask = " \t\n\r\0\x0B"): void + { + $formatter = new TrimFormatter($mask, 'left'); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForRightTrim')] + public function testShouldTrimRight(string $input, string $expected, string $mask = " \t\n\r\0\x0B"): void + { + $formatter = new TrimFormatter($mask, 'right'); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForBothTrim')] + public function testShouldTrimBoth(string $input, string $expected, string $mask = " \t\n\r\0\x0B"): void + { + $formatter = new TrimFormatter($mask, 'both'); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function testShouldHandleEmptyString(): void + { + $formatter = new TrimFormatter(); + + $actual = $formatter->format(''); + + self::assertSame('', $actual); + } + + #[Test] + public function testShouldThrowExceptionForInvalidSide(): void + { + $this->expectException(InvalidFormatterException::class); + $this->expectExceptionMessage('Invalid side "middle"'); + + new TrimFormatter(' ', 'middle'); + } + + #[Test] + #[DataProvider('providerForUnicode')] + public function testShouldHandleUnicodeCharacters(string $input, string $expected, string $mask): void + { + $formatter = new TrimFormatter($mask); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForEmoji')] + public function testShouldHandleEmoji(string $input, string $expected, string $mask): void + { + $formatter = new TrimFormatter($mask); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForCustomMask')] + public function testShouldHandleCustomMask(string $input, string $expected, string $mask): void + { + $formatter = new TrimFormatter($mask); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForSpecialChars')] + public function testShouldHandleSpecialCharactersInMask(string $input, string $expected, string $mask): void + { + $formatter = new TrimFormatter($mask); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForMultiByte')] + public function testShouldHandleMultiByteCharacters(string $input, string $expected, string $mask): void + { + $formatter = new TrimFormatter($mask); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForEdgeCases')] + public function testShouldHandleEdgeCases(string $input, string $expected, string $side, string $mask): void + { + $formatter = new TrimFormatter($mask, $side); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForValidFormattedString(): array + { + return [ + 'whitespace both sides' => [' hello ', 'hello'], + 'tab both sides' => ["\thello\t", 'hello'], + 'newline both sides' => ["\nhello\n", 'hello'], + 'mixed whitespace' => [" \t\n hello \t\n", 'hello'], + 'already trimmed' => ['hello', 'hello'], + 'only spaces' => [' ', ''], + 'no characters in mask' => ['hello', 'hello', 'both', 'xyz'], + 'all characters to trim' => [' !!! ', '!!!', 'both', ' '], + ]; + } + + /** @return array */ + public static function providerForLeftTrim(): array + { + return [ + 'spaces left' => [' hello', 'hello'], + 'spaces right not trimmed' => ['hello ', 'hello '], + 'spaces left and right' => [' hello ', 'hello '], + 'tabs left' => ["\thello\t", "hello\t"], + 'mixed whitespace left' => ["\t\n hello world", 'hello world'], + ]; + } + + /** @return array */ + public static function providerForRightTrim(): array + { + return [ + 'spaces right' => ['hello ', 'hello'], + 'spaces left not trimmed' => [' hello', ' hello'], + 'spaces left and right' => [' hello ', ' hello'], + 'tabs right' => ["\thello\t", "\thello"], + 'mixed whitespace right' => ['hello world \t', 'hello world \t'], + ]; + } + + /** @return array */ + public static function providerForBothTrim(): array + { + return [ + 'spaces both' => [' hello ', 'hello'], + 'tabs both' => ["\thello\t", 'hello'], + 'newlines both' => ["\nhello\n", 'hello'], + 'mixed whitespace' => [" \t\n hello \t\n ", 'hello'], + 'single space' => [' hello ', 'hello'], + ]; + } + + /** @return array */ + public static function providerForUnicode(): array + { + return [ + 'cjk spaces' => [' hello ', 'hello', ' '], + 'unicode spaces' => ["\u{2003}hello\u{2003}", 'hello', "\u{2003}"], + 'latin accented chars' => ['éééhelloééé', 'hello', 'é'], + 'greek letters' => ['αααhelloααα', 'hello', 'α'], + 'cyrillic letters' => ['бббhelloббб', 'hello', 'б'], + 'arabic letters' => ['مرحبا', 'ا', 'مرحب'], + ]; + } + + /** @return array */ + public static function providerForEmoji(): array + { + return [ + 'smiley faces' => ['😊😊hello😊😊', 'hello', '😊'], + 'mixed emoji' => ['👋👋hi👋👋', 'hi', '👋'], + 'hearts' => ['❤️❤️love❤️❤️', 'love', '❤️'], + ]; + } + + /** @return array */ + public static function providerForCustomMask(): array + { + return [ + 'custom characters' => ['---hello---', 'hello', '-'], + 'multiple custom chars' => ['-._hello-._', 'hello', '_.-'], + 'dots' => ['...hello...', 'hello', '.'], + 'underscores' => ['___hello___', 'hello', '_'], + 'mixed custom' => ['*-+hello+-*', '*-+hello+-*', '+-*'], + ]; + } + + /** @return array */ + public static function providerForSpecialChars(): array + { + return [ + 'dash' => ['--hello--', 'hello', '-'], + 'asterisk' => ['**hello**', 'hello', '*'], + 'dot' => ['..hello..', 'hello', '.'], + 'dollar sign' => ['$$hello$$', 'hello', '$'], + 'caret' => ['^^hello^^', 'hello', '^'], + 'pipe' => ['||hello||', 'hello', '|'], + 'question mark' => ['??hello??', 'hello', '?'], + 'multiple special' => ['@#$hello$#@', 'hello', '@#$'], + ]; + } + + /** @return array */ + public static function providerForMultiByte(): array + { + return [ + 'chinese spaces' => [' 你好 ', '你好', ' '], + 'japanese whitespace' => [' こんにちは ', 'こんにちは', ' '], + 'korean whitespace' => [' 안녕하세요 ', '안녕하세요', ' '], + 'cjk fullwidth' => ['abc', 'abc', ' '], + 'mixed cjk and ascii' => [' hello 你好 ', 'hello 你好', ' '], + ]; + } + + /** @return array */ + public static function providerForEdgeCases(): array + { + return [ + 'empty string' => ['', '', 'both', ' '], + 'string shorter than mask' => ['a', '', 'both', 'abcdef'], + 'all characters trimmed' => ['--', '', 'both', '-'], + 'only one side trimmed left' => ['--a', 'a', 'left', '-'], + 'only one side trimmed right' => ['a--', 'a', 'right', '-'], + 'no characters to trim' => ['hello', 'hello', 'both', 'xyz'], + 'mask longer than string' => ['hello', 'hello', 'both', 'abcdefgzij'], + 'empty mask' => ['hello', 'hello', 'both', ''], + 'repeated characters' => ['aaaaahelloaaaaa', 'hello', 'both', 'a'], + 'interleaved characters' => ['ababhelloabab', 'hello', 'both', 'ab'], + ]; + } +} diff --git a/tests/Unit/UppercaseFormatterTest.php b/tests/Unit/UppercaseFormatterTest.php new file mode 100644 index 0000000..6009c05 --- /dev/null +++ b/tests/Unit/UppercaseFormatterTest.php @@ -0,0 +1,260 @@ +format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function testShouldHandleEmptyString(): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format(''); + + self::assertSame('', $actual); + } + + #[Test] + #[DataProvider('providerForUnicodeString')] + public function testShouldHandleUnicodeCharacters(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForLatinAccents')] + public function testShouldHandleLatinCharactersWithAccents(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForNonLatinScripts')] + public function testShouldHandleNonLatinScripts(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForEmojiAndSpecialChars')] + public function testShouldHandleEmojiAndSpecialCharacters(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForCombiningDiacritics')] + public function testShouldHandleCombiningDiacritics(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForRightToLeft')] + public function testShouldHandleRightToLeftText(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForMultiByte')] + public function testShouldHandleMultiByteCharacters(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForNumbersAndSpecial')] + public function testShouldHandleNumbersAndSpecialChars(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + #[DataProvider('providerForMixed')] + public function testShouldHandleMixedContent(string $input, string $expected): void + { + $formatter = new UppercaseFormatter(); + + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForValidFormattedString(): array + { + return [ + 'empty string' => ['', ''], + 'single lowercase letter' => ['a', 'A'], + 'all lowercase' => ['hello', 'HELLO'], + 'already uppercase' => ['HELLO', 'HELLO'], + 'mixed case' => ['Hello World', 'HELLO WORLD'], + 'with punctuation' => ['hello, world!', 'HELLO, WORLD!'], + 'with numbers' => ['hello123', 'HELLO123'], + 'single word' => ['test', 'TEST'], + 'multiple words' => ['test string case', 'TEST STRING CASE'], + ]; + } + + /** @return array */ + public static function providerForUnicodeString(): array + { + return [ + 'german umlauts' => ['über', 'ÜBER'], + 'french accents' => ['café', 'CAFÉ'], + 'spanish tilde' => ['niño', 'NIÑO'], + 'portuguese' => ['coração', 'CORAÇÃO'], + 'icelandic' => ['þingvellir', 'ÞINGVELLIR'], + 'scandinavian' => ['ørsted', 'ØRSTED'], + 'polish' => ['łęski', 'ŁĘSKI'], + ]; + } + + /** @return array */ + public static function providerForLatinAccents(): array + { + return [ + 'c-cedilla' => ['café français', 'CAFÉ FRANÇAIS'], + 'umlauts' => ['äöü', 'ÄÖÜ'], + 'tilde' => ['ãñõ', 'ÃÑÕ'], + 'circumflex' => ['êîôû', 'ÊÎÔÛ'], + 'acute' => ['áéíóú', 'ÁÉÍÓÚ'], + 'grave' => ['àèìòù', 'ÀÈÌÒÙ'], + 'mixed accents' => ['résumé déjà vu', 'RÉSUMÉ DÉJÀ VU'], + ]; + } + + /** @return array */ + public static function providerForNonLatinScripts(): array + { + return [ + 'greek lowercase' => ['γεια σας', 'ΓΕΙΑ ΣΑΣ'], + 'cyrillic lowercase' => ['привет мир', 'ПРИВЕТ МИР'], + 'arabic' => ['مرحبا', 'مرحبا'], + 'hebrew' => ['שלום', 'שלום'], + 'thai' => ['สวัสดี', 'สวัสดี'], + ]; + } + + /** @return array */ + public static function providerForEmojiAndSpecialChars(): array + { + return [ + 'smiley face' => ['hello 😊', 'HELLO 😊'], + 'multiple emoji' => ['hi 👋 bye 👋', 'HI 👋 BYE 👋'], + 'hearts' => ['❤️ love ❤️', '❤️ LOVE ❤️'], + 'special symbols' => ['© ™ ®', '© ™ ®'], + 'math symbols' => ['∑ π ∫', '∑ Π ∫'], + ]; + } + + /** @return array */ + public static function providerForCombiningDiacritics(): array + { + return [ + 'e with combining acute' => ["e\u{0301}", "E\u{0301}"], + 'a with combining grave' => ["a\u{0300}", "A\u{0300}"], + 'multiple diacritics' => ["e\u{0301}\u{0301}", "E\u{0301}\u{0301}"], + 'word with combining marks' => ["cafe\u{0301}", "CAFE\u{0301}"], + ]; + } + + /** @return array */ + public static function providerForRightToLeft(): array + { + return [ + 'arabic word' => ['مرحبا', 'مرحبا'], + 'hebrew word' => ['שלום', 'שלום'], + 'mixed direction' => ['hello مرحبا', 'HELLO مرحبا'], + ]; + } + + /** @return array */ + public static function providerForMultiByte(): array + { + return [ + 'chinese' => ['你好世界', '你好世界'], + 'japanese hiragana' => ['こんにちは', 'こんにちは'], + 'japanese katakana' => ['ハローワールド', 'ハローワールド'], + 'korean hangul' => ['안녕하세요', '안녕하세요'], + 'cjk characters' => ['简体字繁體字漢字', '简体字繁體字漢字'], + ]; + } + + /** @return array */ + public static function providerForNumbersAndSpecial(): array + { + return [ + 'digits only' => ['1234567890', '1234567890'], + 'mixed alphanumeric' => ['abc123def', 'ABC123DEF'], + 'special chars only' => ['!@#$%^&*()', '!@#$%^&*()'], + 'whitespace' => [' ', ' '], + 'tabs and newlines' => ["hello\tworld\n", "HELLO\tWORLD\n"], + ]; + } + + /** @return array */ + public static function providerForMixed(): array + { + return [ + 'unicode with numbers' => ['café123', 'CAFÉ123'], + 'emoji with text' => ['Hello World 😊', 'HELLO WORLD 😊'], + 'cjk with latin' => ['Hello你好', 'HELLO你好'], + 'mixed scripts' => ['Hello 世界 Мир', 'HELLO 世界 МИР'], + 'complex string' => ['CAFé 123 😊 你好', 'CAFÉ 123 😊 你好'], + ]; + } +}