Skip to content

Commit

Permalink
added support for locale, affects |date, |number, |bytes and |sort fi…
Browse files Browse the repository at this point in the history
…lters
  • Loading branch information
dg committed May 27, 2024
1 parent 930973e commit 8d92896
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 62 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"ext-iconv": "to use filters |reverse, |substring",
"ext-mbstring": "to use filters like lower, upper, capitalize, ...",
"ext-fileinfo": "to use filter |datastream",
"ext-intl": "to use Latte\\Engine::setLocale()",
"nette/utils": "to use filter |webalize",
"nette/php-generator": "to use tag {templatePrint}"
},
Expand Down
20 changes: 20 additions & 0 deletions src/Latte/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Engine
private bool $sandboxed = false;
private ?string $phpBinary = null;
private ?string $cacheKey;
private ?string $locale = null;


public function __construct()
Expand Down Expand Up @@ -565,6 +566,25 @@ public function isStrictParsing(): bool
}


/**
* Sets locale for date and number formatting. See PHP intl extension.
*/
public function setLocale(?string $locale): static
{
if ($locale && !extension_loaded('intl')) {
throw new RuntimeException("Locate requires the 'intl' extension to be installed.");
}
$this->locale = $locale;
return $this;
}


public function getLocale(): ?string
{
return $this->locale;
}


public function setLoader(Loader $loader): static
{
$this->loader = $loader;
Expand Down
8 changes: 7 additions & 1 deletion src/Latte/Essential/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public function beforeCompile(Latte\Engine $engine): void
}


public function beforeRender(Runtime\Template $template): void
{
$this->filters->locale = $template->getEngine()->getLocale();
}


public function getTags(): array
{
return [
Expand Down Expand Up @@ -142,7 +148,7 @@ public function getFilters(): array
'lower' => extension_loaded('mbstring')
? [$this->filters, 'lower']
: fn() => throw new RuntimeException('Filter |lower requires mbstring extension.'),
'number' => 'number_format',
'number' => [$this->filters, 'number'],
'padLeft' => [$this->filters, 'padLeft'],
'padRight' => [$this->filters, 'padRight'],
'query' => [$this->filters, 'query'],
Expand Down
83 changes: 72 additions & 11 deletions src/Latte/Essential/Filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
*/
final class Filters
{
public ?string $locale = null;


/**
* Converts HTML to plain text.
*/
Expand Down Expand Up @@ -166,16 +169,13 @@ public static function repeat(FilterInfo $info, $s, int $count): string
/**
* Date/time formatting.
*/
public static function date(string|int|\DateTimeInterface|\DateInterval|null $time, ?string $format = null): ?string
public function date(string|int|\DateTimeInterface|\DateInterval|null $time, ?string $format = null): ?string
{
$format ??= Latte\Runtime\Filters::$dateFormat;
if ($time == null) { // intentionally ==
return null;
}

$format ??= Latte\Runtime\Filters::$dateFormat;
if ($time instanceof \DateInterval) {
} elseif ($time instanceof \DateInterval) {
return $time->format($format);

} elseif (is_numeric($time)) {
$time = (new \DateTime)->setTimestamp((int) $time);
} elseif (!$time instanceof \DateTimeInterface) {
Expand All @@ -186,8 +186,23 @@ public static function date(string|int|\DateTimeInterface|\DateInterval|null $ti
if (PHP_VERSION_ID >= 80100) {
trigger_error("Function strftime() used by filter |date is deprecated since PHP 8.1, use format without % characters like 'Y-m-d'.", E_USER_DEPRECATED);
}

return @strftime($format, $time->format('U') + 0);

} elseif (preg_match('#^(\+(short|medium|long|full))?(\+time(\+sec)?)?$#', '+' . $format, $m)) {
$formatter = new \IntlDateFormatter(
$this->getLocale('date'),
match ($m[2]) {
'short' => \IntlDateFormatter::SHORT,
'medium' => \IntlDateFormatter::MEDIUM,
'long' => \IntlDateFormatter::LONG,
'full' => \IntlDateFormatter::FULL,
'' => \IntlDateFormatter::NONE,
},
isset($m[3]) ? (isset($m[4]) ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT) : \IntlDateFormatter::NONE,
);
$res = $formatter->format($time);
$res = preg_replace('~(\d\.) ~', "\$1\u{a0}", $res);
return $res;
}

return $time->format($format);
Expand All @@ -197,7 +212,7 @@ public static function date(string|int|\DateTimeInterface|\DateInterval|null $ti
/**
* Converts to human-readable file size.
*/
public static function bytes(float $bytes, int $precision = 2): string
public function bytes(float $bytes, int $precision = 2): string
{
$bytes = round($bytes);
$units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
Expand All @@ -209,7 +224,15 @@ public static function bytes(float $bytes, int $precision = 2): string
$bytes /= 1024;
}

return round($bytes, $precision) . ' ' . $unit;
if ($this->locale === null) {
$bytes = (string) round($bytes, $precision);
} else {
$formatter = new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL);
$formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $precision);
$bytes = $formatter->format($bytes);
}

return $bytes . ' ' . $unit;
}


Expand Down Expand Up @@ -455,7 +478,7 @@ public static function batch(iterable $list, int $length, $rest = null): \Genera
* @param iterable<K, V> $data
* @return iterable<K, V>
*/
public static function sort(
public function sort(
iterable $data,
?\Closure $comparison = null,
string|int|\Closure|null $by = null,
Expand All @@ -469,7 +492,16 @@ public static function sort(
$by = $byKey === true ? null : $byKey;
}

$comparison ??= fn($a, $b) => $a <=> $b;
if ($comparison) {
} elseif ($this->locale === null) {
$comparison = fn($a, $b) => $a <=> $b;
} else {
$collator = new \Collator($this->locale);
$comparison = fn($a, $b) => is_string($a) && is_string($b)
? $collator->compare($a, $b)
: $a <=> $b;
}

$comparison = match (true) {
$by === null => $comparison,
$by instanceof \Closure => fn($a, $b) => $comparison($by($a), $by($b)),
Expand Down Expand Up @@ -650,4 +682,33 @@ public static function random(string|array $values): mixed
? $values[array_rand($values, 1)]
: null;
}


/**
* Formats a number with grouped thousands and optionally decimal digits according to locale.
*/
public function number(
float $number,
int $decimals = 0,
string $decimalSeparator = '.',
string $thousandsSeparator = ',',
): string
{
if ($this->locale === null || func_num_args() > 2) {
return number_format($number, $decimals, $decimalSeparator, $thousandsSeparator);
}

$formatter = new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL);
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $decimals);
return $formatter->format($number);
}


private function getLocale(string $name): string
{
if ($this->locale === null) {
throw new Latte\RuntimeException("Filter |$name requires the locale to be set using Engine::setLocale()");
}
return $this->locale;
}
}
16 changes: 13 additions & 3 deletions tests/filters/bytes.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@ use Tester\Assert;
require __DIR__ . '/../bootstrap.php';


Assert::same('0 B', Filters::bytes(0.1));
test('no locale', function () {
$filters = new Filters;

Assert::same('0 B', $filters->bytes(0.1));
Assert::same('-1.03 GB', $filters->bytes(-1024 * 1024 * 1050));
Assert::same('8881.78 PB', $filters->bytes(1e19));
});

Assert::same('-1.03 GB', Filters::bytes(-1024 * 1024 * 1050));

test('with locale', function () {
$filters = new Filters;
$filters->locale = 'cs_CZ';

Assert::same('8881.78 PB', Filters::bytes(1e19));
Assert::same('0 B', $filters->bytes(0.1));
Assert::same('-1,03 GB', $filters->bytes(-1024 * 1024 * 1050));
Assert::same('8 881,78 PB', $filters->bytes(1e19));
});
73 changes: 44 additions & 29 deletions tests/filters/date.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,47 @@ use Tester\Assert;
require __DIR__ . '/../bootstrap.php';


setlocale(LC_TIME, 'C');


Assert::null(Filters::date(null));


Assert::same("23.\u{a0}1.\u{a0}1978", Filters::date(254_400_000));


Assert::same("5.\u{a0}5.\u{a0}1978", Filters::date('1978-05-05'));


Assert::same("5.\u{a0}5.\u{a0}1978", Filters::date(new DateTime('1978-05-05')));


Assert::same('1978-01-23', Filters::date(254_400_000, 'Y-m-d'));


Assert::same('1212-09-26', Filters::date('1212-09-26', 'Y-m-d'));


Assert::same('1212-09-26', Filters::date(new DateTimeImmutable('1212-09-26'), 'Y-m-d'));


Assert::same('30:10:10', Filters::date(new DateInterval('PT30H10M10S'), '%H:%I:%S'));


date_default_timezone_set('America/Los_Angeles');
Assert::same('07:09', Filters::date(1_408_284_571, 'H:i'));
test('no locale', function () {
$filters = new Filters;

Assert::null($filters->date(null));
Assert::same("5.\u{a0}5.\u{a0}1978", $filters->date('1978-05-05'));
Assert::same("5.\u{a0}5.\u{a0}1978", $filters->date(new DateTime('1978-05-05')));
Assert::same('1978-01-23', $filters->date(254_400_000, 'Y-m-d'));
Assert::same('1212-09-26', $filters->date('1212-09-26', 'Y-m-d'));
Assert::same('1212-09-26', $filters->date(new DateTimeImmutable('1212-09-26'), 'Y-m-d'));

// timestamp
date_default_timezone_set('America/Los_Angeles');
Assert::same("23.\u{a0}1.\u{a0}1978", $filters->date(254_400_000));
Assert::same('07:09', $filters->date(1_408_284_571, 'H:i'));
});


test('date interval', function () {
$filters = new Filters;

Assert::same('30:10:10', $filters->date(new DateInterval('PT30H10M10S'), '%H:%I:%S'));
});


test('local date/time', function () {
$filters = new Filters;
$filters->locale = 'cs_CZ';

// date format
Assert::null($filters->date(null, 'medium'));
Assert::same("5.\u{a0}5.\u{a0}1978", $filters->date('1978-05-05', 'medium'));
Assert::same('05.05.78', $filters->date(new DateTime('1978-05-05'), 'short'));
Assert::same("5.\u{a0}5.\u{a0}1978", $filters->date(new DateTime('1978-05-05'), 'medium'));
Assert::same("5.\u{a0}května 1978", $filters->date(new DateTime('1978-05-05'), 'long'));
Assert::same("pátek 5.\u{a0}května 1978", $filters->date(new DateTime('1978-05-05'), 'full'));

// time format
Assert::same('12:13', $filters->date(new DateTime('12:13:14'), 'time'));
Assert::same('12:13:14', $filters->date(new DateTime('12:13:14'), 'time+sec'));

// combined
Assert::same('05.05.78 12:13', $filters->date(new DateTime('1978-05-05 12:13:14'), 'short+time'));
Assert::same('05.05.78 12:13:14', $filters->date(new DateTime('1978-05-05 12:13:14'), 'short+time+sec'));
});
59 changes: 59 additions & 0 deletions tests/filters/number.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/**
* Test: Latte\Essential\Filters::number()
*/

declare(strict_types=1);

use Latte\Essential\Filters;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


test('no locale', function () {
$filters = new Filters;

Assert::same('0', $filters->number(0));
Assert::same('0.00', $filters->number(0, 2));
Assert::same('1,234', $filters->number(1234));
Assert::same('123.46', $filters->number(123.456, 2));
Assert::same('123.457', $filters->number(123.4567, 3));
Assert::same('1 234.56', $filters->number(1234.56, 2, '.', ' '));
Assert::same('1.234,56', $filters->number(1234.56, 2, ',', '.'));
Assert::same('-1,234', $filters->number(-1234));
Assert::same('-1,234.57', $filters->number(-1234.5678, 2));
Assert::same('nan', $filters->number(NAN, 2));

// negative decimals means rounding
Assert::same('100', $filters->number(123.456, -2));
});


test('with locale', function () {
$filters = new Filters;
$filters->locale = 'cs_CZ';

Assert::same('0', $filters->number(0));
Assert::same('0,00', $filters->number(0, 2));
Assert::same('1 234', $filters->number(1234));
Assert::same('123,46', $filters->number(123.456, 2));
Assert::same('123,457', $filters->number(123.4567, 3));
Assert::same('-1 234', $filters->number(-1234));
Assert::same('-1 234,57', $filters->number(-1234.5678, 2));
Assert::same('NaN', $filters->number(NAN, 2));

// negative decimals is invalid, prints all digits
Assert::same('0', $filters->number(0.0, -2));
Assert::same('123,456', $filters->number(123.456, -2));
});


test('disabled locale', function () {
$filters = new Filters;
$filters->locale = 'cs_CZ';

Assert::same('1 234.56', $filters->number(1234.56, 2, '.', ' '));
Assert::same('1.234,56', $filters->number(1234.56, 2, ',', '.'));
});
Loading

0 comments on commit 8d92896

Please sign in to comment.