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
+