diff --git a/app/Liquid/Filters/Date.php b/app/Liquid/Filters/Date.php index 2f730ac..20c412c 100644 --- a/app/Liquid/Filters/Date.php +++ b/app/Liquid/Filters/Date.php @@ -2,6 +2,7 @@ namespace App\Liquid\Filters; +use App\Liquid\Utils\ExpressionUtils; use Carbon\Carbon; use Keepsuit\Liquid\Filters\FiltersProvider; @@ -22,4 +23,33 @@ class Date extends FiltersProvider return Carbon::now()->subDays($days)->toDateString(); } + + /** + * Format a date string with ordinal day (1st, 2nd, 3rd, etc.) + * + * @param string $dateStr The date string to parse + * @param string $strftimeExp The strftime format string with <> placeholder + * @return string The formatted date with ordinal day + */ + public function ordinalize(string $dateStr, string $strftimeExp): string + { + $date = Carbon::parse($dateStr); + $ordinalDay = $date->ordinal('day'); + + // Convert strftime format to PHP date format + $phpFormat = ExpressionUtils::strftimeToPhpFormat($strftimeExp); + + // Split the format string by the ordinal day placeholder + $parts = explode('<>', $phpFormat); + + if (count($parts) === 2) { + $before = $date->format($parts[0]); + $after = $date->format($parts[1]); + return $before . $ordinalDay . $after; + } + + // Fallback: if no placeholder found, just format normally + return $date->format($phpFormat); + } + } diff --git a/app/Liquid/Utils/ExpressionUtils.php b/app/Liquid/Utils/ExpressionUtils.php index 9ed70d2..9715de2 100644 --- a/app/Liquid/Utils/ExpressionUtils.php +++ b/app/Liquid/Utils/ExpressionUtils.php @@ -158,4 +158,39 @@ class ExpressionUtils return $expression; } + + /** + * Convert strftime format string to PHP date format string + * + * @param string $strftimeFormat The strftime format string + * @return string The PHP date format string + */ + public static function strftimeToPhpFormat(string $strftimeFormat): string + { + $conversions = [ + '%A' => 'l', // Full weekday name + '%a' => 'D', // Abbreviated weekday name + '%B' => 'F', // Full month name + '%b' => 'M', // Abbreviated month name + '%Y' => 'Y', // Full year (4 digits) + '%y' => 'y', // Year without century (2 digits) + '%m' => 'm', // Month as decimal number (01-12) + '%d' => 'd', // Day of month as decimal number (01-31) + '%H' => 'H', // Hour in 24-hour format (00-23) + '%I' => 'h', // Hour in 12-hour format (01-12) + '%M' => 'i', // Minute as decimal number (00-59) + '%S' => 's', // Second as decimal number (00-59) + '%p' => 'A', // AM/PM + '%P' => 'a', // am/pm + '%j' => 'z', // Day of year as decimal number (001-366) + '%w' => 'w', // Weekday as decimal number (0-6, Sunday is 0) + '%U' => 'W', // Week number of year (00-53, Sunday is first day) + '%W' => 'W', // Week number of year (00-53, Monday is first day) + '%c' => 'D M j H:i:s Y', // Date and time representation + '%x' => 'm/d/Y', // Date representation + '%X' => 'H:i:s', // Time representation + ]; + + return str_replace(array_keys($conversions), array_values($conversions), $strftimeFormat); + } } diff --git a/tests/Unit/Liquid/Filters/DateTest.php b/tests/Unit/Liquid/Filters/DateTest.php index d967951..cf31e13 100644 --- a/tests/Unit/Liquid/Filters/DateTest.php +++ b/tests/Unit/Liquid/Filters/DateTest.php @@ -30,3 +30,66 @@ test('days_ago filter with large number works correctly', function (): void { expect($filter->days_ago(100))->toBe($hundredDaysAgo); }); + +test('ordinalize filter formats date with ordinal day', function (): void { + $filter = new Date(); + + expect($filter->ordinalize('2025-10-02', '%A, %B <>, %Y')) + ->toBe('Thursday, October 2nd, 2025'); +}); + +test('ordinalize filter handles datetime string with timezone', function (): void { + $filter = new Date(); + + expect($filter->ordinalize('2025-12-31 16:50:38 -0400', '%A, %b <>')) + ->toBe('Wednesday, Dec 31st'); +}); + +test('ordinalize filter handles different ordinal suffixes', function (): void { + $filter = new Date(); + + // 1st + expect($filter->ordinalize('2025-01-01', '<>')) + ->toBe('1st'); + + // 2nd + expect($filter->ordinalize('2025-01-02', '<>')) + ->toBe('2nd'); + + // 3rd + expect($filter->ordinalize('2025-01-03', '<>')) + ->toBe('3rd'); + + // 4th + expect($filter->ordinalize('2025-01-04', '<>')) + ->toBe('4th'); + + // 11th (special case) + expect($filter->ordinalize('2025-01-11', '<>')) + ->toBe('11th'); + + // 12th (special case) + expect($filter->ordinalize('2025-01-12', '<>')) + ->toBe('12th'); + + // 13th (special case) + expect($filter->ordinalize('2025-01-13', '<>')) + ->toBe('13th'); + + // 21st + expect($filter->ordinalize('2025-01-21', '<>')) + ->toBe('21st'); + + // 22nd + expect($filter->ordinalize('2025-01-22', '<>')) + ->toBe('22nd'); + + // 23rd + expect($filter->ordinalize('2025-01-23', '<>')) + ->toBe('23rd'); + + // 24th + expect($filter->ordinalize('2025-01-24', '<>')) + ->toBe('24th'); +}); +