Skip to content

Commit

Permalink
added support for locale, affects |number, |bytes and |sort filters
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed May 26, 2024
1 parent 70a2088 commit 900359b
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 26 deletions.
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
57 changes: 53 additions & 4 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 @@ -197,7 +200,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 +212,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 +466,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 +480,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 +670,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));
});
50 changes: 50 additions & 0 deletions tests/filters/number.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?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.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));
});


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

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));
});


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, ',', '.'));
});
46 changes: 28 additions & 18 deletions tests/filters/sort.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ function exportIterator(Traversable $iterator): array


test('array', function () {
Assert::same([1 => 11, 0 => 22, 33], Filters::sort([22, 11, 33]));
Assert::same([], Filters::sort([]));
$filters = new Filters;
Assert::same([1 => 11, 0 => 22, 33], $filters->sort([22, 11, 33]));
Assert::same([], $filters->sort([]));
});


test('iterator', function () {
$sorted = Filters::sort(iterator());
$sorted = (new Filters)->sort(iterator());

Assert::same(3, count($sorted));
Assert::equal(
Expand All @@ -52,7 +53,7 @@ test('iterator', function () {


test('re-iteration', function () {
$sorted = Filters::sort(iterator());
$sorted = (new Filters)->sort(iterator());
$res = [
[['a' => 55], ['k' => 22]],
[['a' => 77], ['k' => 33]],
Expand All @@ -72,7 +73,7 @@ test('re-iteration', function () {
test('user comparison + array', function () {
Assert::same(
[2 => 33, 0 => 22, 1 => 11],
Filters::sort([22, 11, 33], fn($a, $b) => $b <=> $a)
(new Filters)->sort([22, 11, 33], fn($a, $b) => $b <=> $a)
);
});

Expand All @@ -84,17 +85,18 @@ test('user comparison + iterator', function () {
[['a' => 77], ['k' => 33]],
[['a' => 55], ['k' => 22]],
],
exportIterator(Filters::sort(iterator(), fn($a, $b) => $b <=> $a)),
exportIterator((new Filters)->sort(iterator(), fn($a, $b) => $b <=> $a)),
);
});


test('array + by', function () {
$filters = new Filters;
Assert::equal(
[1 => (object) ['k' => 11], 0 => ['k' => 22], ['k' => 33]],
Filters::sort([['k' => 22], (object) ['k' => 11], ['k' => 33]], by: 'k'),
$filters->sort([['k' => 22], (object) ['k' => 11], ['k' => 33]], by: 'k'),
);
Assert::same([], Filters::sort([], by: 'k'));
Assert::same([], $filters->sort([], by: 'k'));
});


Expand All @@ -105,15 +107,15 @@ test('iterator + by', function () {
[['a' => 55], ['k' => 22]],
[['a' => 77], ['k' => 33]],
],
exportIterator(Filters::sort(iterator(), by: 'k')),
exportIterator((new Filters)->sort(iterator(), by: 'k')),
);
});


test('callback + array + by', function () {
Assert::same(
[1 => 11, 0 => 22, 33],
Filters::sort([22, 11, 33], by: fn($a) => $a * 11)
(new Filters)->sort([22, 11, 33], by: fn($a) => $a * 11)
);
});

Expand All @@ -125,14 +127,15 @@ test('callback + iterator + by', function () {
[['a' => 55], ['k' => 22]],
[['a' => 66], (object) ['k' => 11]],
],
exportIterator(Filters::sort(iterator(), by: fn($a) => -((array) $a)['k'])),
exportIterator((new Filters)->sort(iterator(), by: fn($a) => -((array) $a)['k'])),
);
});


test('array + byKey', function () {
Assert::same([1 => 11, 0 => 22, 33], Filters::sort([22, 11, 33]));
Assert::same([], Filters::sort([], byKey: true));
$filters = new Filters;
Assert::same([1 => 11, 0 => 22, 33], $filters->sort([22, 11, 33]));
Assert::same([], $filters->sort([], byKey: true));
});


Expand All @@ -143,15 +146,15 @@ test('iterator + byKey', function () {
[['a' => 66], (object) ['k' => 11]],
[['a' => 77], ['k' => 33]],
],
exportIterator(Filters::sort(iterator(), byKey: true)),
exportIterator((new Filters)->sort(iterator(), byKey: true)),
);
});


test('user comparison + array + byKey', function () {
Assert::same(
[2 => 33, 1 => 11, 0 => 22],
Filters::sort([22, 11, 33], fn($a, $b) => $b <=> $a, byKey: true),
(new Filters)->sort([22, 11, 33], fn($a, $b) => $b <=> $a, byKey: true),
);
});

Expand All @@ -163,7 +166,7 @@ test('user comparison + iterator + byKey', function () {
[['a' => 66], (object) ['k' => 11]],
[['a' => 55], ['k' => 22]],
],
exportIterator(Filters::sort(iterator(), fn($a, $b) => $b <=> $a, byKey: true)),
exportIterator((new Filters)->sort(iterator(), fn($a, $b) => $b <=> $a, byKey: true)),
);
});

Expand All @@ -175,7 +178,7 @@ test('iterator + by + byKey', function () {
[['a' => 66], (object) ['k' => 11]],
[['a' => 77], ['k' => 33]],
],
exportIterator(Filters::sort(iterator(), byKey: 'a')),
exportIterator((new Filters)->sort(iterator(), byKey: 'a')),
);
});

Expand All @@ -187,6 +190,13 @@ test('callback + iterator + by + byKey', function () {
[['a' => 66], (object) ['k' => 11]],
[['a' => 55], ['k' => 22]],
],
exportIterator(Filters::sort(iterator(), byKey: fn($a) => -((array) $a)['a'])),
exportIterator((new Filters)->sort(iterator(), byKey: fn($a) => -((array) $a)['a'])),
);
});


test('locale', function () {
$filters = new Filters;
$filters->locale = 'cs_CZ';
Assert::same([22, 2 => 'a', 1 => 'c', 4 => 'd', 3 => 'ch'], $filters->sort([22, 'c', 'a', 'ch', 'd']));
});

0 comments on commit 900359b

Please sign in to comment.