diff --git a/app/Libraries/Token_lib.php b/app/Libraries/Token_lib.php index 057af6607..4cc218c8d 100644 --- a/app/Libraries/Token_lib.php +++ b/app/Libraries/Token_lib.php @@ -1,6 +1,6 @@ 'EEE', + '%A' => 'EEEE', + '%b' => 'MMM', + '%B' => 'MMMM', + '%d' => 'dd', + '%e' => 'd', + '%j' => 'D', + '%m' => 'MM', + '%U' => 'w', + '%V' => 'ww', + '%W' => 'ww', + '%y' => 'yy', + '%Y' => 'yyyy', + '%H' => 'HH', + '%I' => 'hh', + '%l' => 'h', + '%M' => 'mm', + '%p' => 'a', + '%P' => 'a', + '%r' => 'hh:mm:ss a', + '%R' => 'HH:mm', + '%S' => 'ss', + '%T' => 'HH:mm:ss', + '%X' => 'HH:mm:ss', + '%z' => 'ZZZZZ', + '%Z' => 'z', + '%C' => 'yyyy', + '%g' => 'yy', + '%G' => 'yyyy', + '%u' => 'e', + '%w' => 'c', + ]; + + private array $validStrftimeFormats = [ + 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'F', 'g', 'G', + 'h', 'H', 'I', 'j', 'm', 'M', 'n', 'p', 'P', 'r', 'R', + 'S', 't', 'T', 'u', 'U', 'V', 'w', 'W', 'x', 'X', 'y', 'Y', 'z', 'Z' + ]; + /** * Expands all the tokens found in a given text string and returns the results. */ public function render(string $tokened_text, array $tokens = [], $save = true): string { - // Apply the transformation for the "%" tokens if any are used - if (strpos($tokened_text, '%') !== false) { - $tokened_text = strftime($tokened_text); // TODO: these need to be converted to IntlDateFormatter::format() + if (str_contains($tokened_text, '%')) { + $tokened_text = $this->applyDateFormats($tokened_text); } - // Call scan to build an array of all of the tokens used in the text to be transformed $token_tree = $this->scan($tokened_text); if (empty($token_tree)) { - if (strpos($tokened_text, '%') !== false) { - return strftime($tokened_text); - } else { - return $tokened_text; - } + return $tokened_text; } $token_values = []; $tokens_to_replace = []; - $this->generate($token_tree, $tokens_to_replace, $token_values, $tokens, $save); + $this->generate($token_tree, $tokens_to_replace, $token_values, $save); return str_replace($tokens_to_replace, $token_values, $tokened_text); } - /** - * Parses out the all the tokens enclosed in braces {} and subparses on the colon : character where supplied - */ + private function applyDateFormats(string $text): string + { + $formatter = new IntlDateFormatter( + null, + IntlDateFormatter::FULL, + IntlDateFormatter::FULL, + null, + null, + '' + ); + + $dateTime = new DateTime(); + + return preg_replace_callback( + '/%([a-zA-Z%]|%%)?/', + function ($match) use ($formatter, $dateTime) { + if ($match[0] === '%%') { + return '%'; + } + + $formatChar = $match[1] ?? ''; + + if ($formatChar === '%') { + return '%'; + } + + if ($formatChar === '' || !in_array($formatChar, $this->validStrftimeFormats, true)) { + return $match[0]; + } + + $intlPattern = $this->strftimeToIntlPatternMap[$match[0]] ?? null; + + if ($intlPattern === null) { + return $match[0]; + } + + $formatter->setPattern($intlPattern); + $result = $formatter->format($dateTime); + + return $result !== false ? $result : $match[0]; + }, + $text + ); + } + public function scan(string $text): array { - // Matches tokens with the following pattern: [$token:$length] preg_match_all('/ \{ # [ - pattern start ([^\s\{\}:]+) # match $token not containing whitespace : { or } @@ -69,12 +144,6 @@ class Token_lib return $token_tree; } - /** - * @param string|null $quantity - * @param string|null $price - * @param string|null $item_id_or_number_or_item_kit_or_receipt - * @return void - */ public function parse_barcode(?string &$quantity, ?string &$price, ?string &$item_id_or_number_or_item_kit_or_receipt): void { $config = config(OSPOS::class)->settings; @@ -90,17 +159,11 @@ class Token_lib $price = (isset($parsed_results['P'])) ? (double) $parsed_results['P'] : null; } } else { - $quantity = 1; // TODO: Quantity is handled using bcmath functions so that it is precision safe. This should be '1' + $quantity = 1; } } - /** - * @param string $string - * @param string $pattern - * @param array $tokens - * @return array - */ - public function parse(string $string, string $pattern, array $tokens = []): array // TODO: $string is a poor name for this parameter. + public function parse(string $string, string $pattern, array $tokens = []): array { $token_tree = $this->scan($pattern); @@ -129,18 +192,9 @@ class Token_lib return $results; } - /** - * @param array $used_tokens - * @param array $tokens_to_replace - * @param array $token_values - * @param array $tokens - * @param bool $save - * @return array - */ - public function generate(array $used_tokens, array &$tokens_to_replace, array &$token_values, array $tokens, bool $save = true): array // TODO: $tokens + private function generate(array $used_tokens, array &$tokens_to_replace, array &$token_values, bool $save = true): void { foreach ($used_tokens as $token_code => $token_info) { - // Generate value here based on the key value $token_value = $this->resolve_token($token_code, [], $save); foreach ($token_info as $length => $token_spec) { @@ -152,16 +206,8 @@ class Token_lib } } } - - return $token_values; } - /** - * @param $token_code - * @param array $tokens - * @param bool $save - * @return string - */ private function resolve_token($token_code, array $tokens = [], bool $save = true): string { foreach (array_merge($tokens, Token::get_tokens()) as $token) { @@ -172,4 +218,4 @@ class Token_lib return ''; } -} +} \ No newline at end of file diff --git a/tests/Libraries/Token_libTest.php b/tests/Libraries/Token_libTest.php new file mode 100644 index 000000000..cdd20782b --- /dev/null +++ b/tests/Libraries/Token_libTest.php @@ -0,0 +1,231 @@ +tokenLib = new Token_lib(); + } + + public function testRenderReturnsInputStringWhenNoTokens(): void + { + $input = 'Hello World'; + $result = $this->tokenLib->render($input, [], false); + $this->assertEquals('Hello World', $result); + } + + public function testRenderHandlesStringWithPercentNotInDateFormat(): void + { + $input = 'Discount: 50%'; + $result = $this->tokenLib->render($input, [], false); + $this->assertStringContainsString('50%', $result); + $this->assertNotEmpty($result); + } + + public function testRenderHandlesInvalidDateFormatPercentDashPercent(): void + { + $input = '%-%-%'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + $this->assertNotEquals('', $result); + } + + public function testRenderHandlesInvalidDateFormatPercentYPercentQPercentBad(): void + { + $input = '%Y-%q-%bad'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + } + + public function testRenderHandlesStringWithPercentAPercent(): void + { + $input = '%a%'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + } + + public function testRenderHandlesExtremelyLongString(): void + { + $input = str_repeat('a', 10000); + $result = $this->tokenLib->render($input, [], false); + $this->assertEquals(str_repeat('a', 10000), $result); + } + + public function testRenderHandlesStringWithMultiplePercentSymbols(): void + { + $input = 'Sale: 25% off, then another 10%'; + $result = $this->tokenLib->render($input, [], false); + $this->assertStringContainsString('25%', $result); + $this->assertStringContainsString('10%', $result); + } + + public function testRenderHandlesStringWithOnlyPercentSymbol(): void + { + $input = '%'; + $result = $this->tokenLib->render($input, [], false); + $this->assertEquals('%', $result); + } + + public function testRenderPreservesTextWithValidDateTokensAndNoOtherTokens(): void + { + $input = 'Date: %Y-%m-%d'; + $result = $this->tokenLib->render($input, [], false); + $this->assertStringContainsString('Date:', $result); + } + + public function testRenderHandlesEmptyString(): void + { + $input = ''; + $result = $this->tokenLib->render($input, [], false); + $this->assertEquals('', $result); + } + + public function testScanExtractsTokens(): void + { + $result = $this->tokenLib->scan('Hello {customer} and {invoice}'); + $this->assertArrayHasKey('customer', $result); + $this->assertArrayHasKey('invoice', $result); + } + + public function testScanExtractsTokensWithLength(): void + { + $result = $this->tokenLib->scan('Invoice: {invoice:10}'); + $this->assertArrayHasKey('invoice', $result); + $this->assertArrayHasKey('10', $result['invoice']); + } + + public function testScanReturnsEmptyArrayForNoTokens(): void + { + $result = $this->tokenLib->scan('Hello World'); + $this->assertEmpty($result); + } + + public function testRenderHandlesConsecutivePercentSigns(): void + { + $input = 'Progress: 100%% complete'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + $this->assertStringContainsString('complete', $result); + } + + public function testRenderHandlesEscapedPercentSigns(): void + { + $input = 'Value: %%'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + } + + public function testRenderHandlesUnclosedBraces(): void + { + $input = "Invoice {CO Date: %Y-%m-%d"; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + } + + public function testRenderHandlesUnopenedBraces(): void + { + $input = "Invoice CO} Date: %Y-%m-%d"; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + } + + public function testRenderHandlesVeryLongStringWithDate(): void + { + $input = str_repeat('buffer ', 500) . '%Y-%m-%d Invoice' . str_repeat('buffer ', 500); + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + $this->assertStringContainsString('buffer', $result); + } + + public function testRenderHandlesMultipleDates(): void + { + $input = '%Y-%m-%d Invoice - %Y-%m-%d'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + } + + public function testRenderHandlesValidYearFormat(): void + { + $input = 'Year: %Y'; + $result = $this->tokenLib->render($input, [], false); + $this->assertMatchesRegularExpression('/Year: \d{4}/', $result); + } + + public function testRenderHandlesValidMonthFormat(): void + { + $input = 'Month: %m'; + $result = $this->tokenLib->render($input, [], false); + $this->assertMatchesRegularExpression('/Month: \d{2}/', $result); + } + + public function testRenderHandlesValidDayFormat(): void + { + $input = 'Day: %d'; + $result = $this->tokenLib->render($input, [], false); + $this->assertMatchesRegularExpression('/Day: \d{2}/', $result); + } + + public function testRenderHandlesFullDateFormat(): void + { + $input = 'Date: %Y-%m-%d'; + $result = $this->tokenLib->render($input, [], false); + $this->assertMatchesRegularExpression('/Date: \d{4}-\d{2}-\d{2}/', $result); + } + + public function testRenderHandlesPercentB(): void + { + $input = 'Month: %B'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + $this->assertStringContainsString('Month:', $result); + $this->assertNotEquals('Month: %B', $result); + } + + public function testRenderHandlesPercentA(): void + { + $input = 'Day: %A'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + $this->assertStringContainsString('Day:', $result); + $this->assertNotEquals('Day: %A', $result); + } + + public function testRenderHandlesComplexPercentFormat(): void + { + $input = 'Report: %Y-%m-%d at %H:%M:%S'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + $this->assertStringContainsString('Report:', $result); + } + + public function testRenderDoesNotReplaceInvalidFormatSpecifiers(): void + { + $input = 'Test: %q invalid %j valid'; + $result = $this->tokenLib->render($input, [], false); + $this->assertStringContainsString('%q', $result); + $this->assertStringContainsString('invalid', $result); + } + + public function testRenderReplacesTimezoneFormat(): void + { + $input = 'Timezone: %z'; + $result = $this->tokenLib->render($input, [], false); + $this->assertNotEmpty($result); + $this->assertStringContainsString('Timezone:', $result); + } + + public function testScanWorksWithMixedContent(): void + { + $result = $this->tokenLib->scan('Text {token1} more %Y-%m-%d text {token2:5} end'); + $this->assertArrayHasKey('token1', $result); + $this->assertArrayHasKey('token2', $result); + } +} \ No newline at end of file diff --git a/tests/Libraries/phpunit.xml b/tests/Libraries/phpunit.xml new file mode 100644 index 000000000..9f202d9fb --- /dev/null +++ b/tests/Libraries/phpunit.xml @@ -0,0 +1,12 @@ + + + + + . + + + \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 7c20c1a2d..fefec4218 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -10,6 +10,9 @@ helpers + + Libraries +