23
23
*/
24
24
final class Filters
25
25
{
26
+ public ?string $ locale = null ;
27
+
28
+
26
29
/**
27
30
* Converts HTML to plain text.
28
31
*/
@@ -166,16 +169,13 @@ public static function repeat(FilterInfo $info, $s, int $count): string
166
169
/**
167
170
* Date/time formatting.
168
171
*/
169
- public static function date (string |int |\DateTimeInterface |\DateInterval |null $ time , ?string $ format = null ): ?string
172
+ public function date (string |int |\DateTimeInterface |\DateInterval |null $ time , ?string $ format = null ): ?string
170
173
{
174
+ $ format ??= Latte \Runtime \Filters::$ dateFormat ;
171
175
if ($ time == null ) { // intentionally ==
172
176
return null ;
173
- }
174
-
175
- $ format ??= Latte \Runtime \Filters::$ dateFormat ;
176
- if ($ time instanceof \DateInterval) {
177
+ } elseif ($ time instanceof \DateInterval) {
177
178
return $ time ->format ($ format );
178
-
179
179
} elseif (is_numeric ($ time )) {
180
180
$ time = (new \DateTime )->setTimestamp ((int ) $ time );
181
181
} elseif (!$ time instanceof \DateTimeInterface) {
@@ -186,8 +186,23 @@ public static function date(string|int|\DateTimeInterface|\DateInterval|null $ti
186
186
if (PHP_VERSION_ID >= 80100 ) {
187
187
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 );
188
188
}
189
-
190
189
return @strftime ($ format , $ time ->format ('U ' ) + 0 );
190
+
191
+ } elseif (preg_match ('#^(\+(short|medium|long|full))?(\+time(\+sec)?)?$# ' , '+ ' . $ format , $ m )) {
192
+ $ formatter = new \IntlDateFormatter (
193
+ $ this ->getLocale ('date ' ),
194
+ match ($ m [2 ]) {
195
+ 'short ' => \IntlDateFormatter::SHORT ,
196
+ 'medium ' => \IntlDateFormatter::MEDIUM ,
197
+ 'long ' => \IntlDateFormatter::LONG ,
198
+ 'full ' => \IntlDateFormatter::FULL ,
199
+ '' => \IntlDateFormatter::NONE ,
200
+ },
201
+ isset ($ m [3 ]) ? (isset ($ m [4 ]) ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT ) : \IntlDateFormatter::NONE ,
202
+ );
203
+ $ res = $ formatter ->format ($ time );
204
+ $ res = preg_replace ('~(\d\.) ~ ' , "\$1 \u{a0}" , $ res );
205
+ return $ res ;
191
206
}
192
207
193
208
return $ time ->format ($ format );
@@ -197,7 +212,7 @@ public static function date(string|int|\DateTimeInterface|\DateInterval|null $ti
197
212
/**
198
213
* Converts to human-readable file size.
199
214
*/
200
- public static function bytes (float $ bytes , int $ precision = 2 ): string
215
+ public function bytes (float $ bytes , int $ precision = 2 ): string
201
216
{
202
217
$ bytes = round ($ bytes );
203
218
$ units = ['B ' , 'kB ' , 'MB ' , 'GB ' , 'TB ' , 'PB ' ];
@@ -209,7 +224,15 @@ public static function bytes(float $bytes, int $precision = 2): string
209
224
$ bytes /= 1024 ;
210
225
}
211
226
212
- return round ($ bytes , $ precision ) . ' ' . $ unit ;
227
+ if ($ this ->locale === null ) {
228
+ $ bytes = (string ) round ($ bytes , $ precision );
229
+ } else {
230
+ $ formatter = new \NumberFormatter ($ this ->locale , \NumberFormatter::DECIMAL );
231
+ $ formatter ->setAttribute (\NumberFormatter::MAX_FRACTION_DIGITS , $ precision );
232
+ $ bytes = $ formatter ->format ($ bytes );
233
+ }
234
+
235
+ return $ bytes . ' ' . $ unit ;
213
236
}
214
237
215
238
@@ -455,7 +478,7 @@ public static function batch(iterable $list, int $length, $rest = null): \Genera
455
478
* @param iterable<K, V> $data
456
479
* @return iterable<K, V>
457
480
*/
458
- public static function sort (
481
+ public function sort (
459
482
iterable $ data ,
460
483
?\Closure $ comparison = null ,
461
484
string |int |\Closure |null $ by = null ,
@@ -469,7 +492,16 @@ public static function sort(
469
492
$ by = $ byKey === true ? null : $ byKey ;
470
493
}
471
494
472
- $ comparison ??= fn ($ a , $ b ) => $ a <=> $ b ;
495
+ if ($ comparison ) {
496
+ } elseif ($ this ->locale === null ) {
497
+ $ comparison = fn ($ a , $ b ) => $ a <=> $ b ;
498
+ } else {
499
+ $ collator = new \Collator ($ this ->locale );
500
+ $ comparison = fn ($ a , $ b ) => is_string ($ a ) && is_string ($ b )
501
+ ? $ collator ->compare ($ a , $ b )
502
+ : $ a <=> $ b ;
503
+ }
504
+
473
505
$ comparison = match (true ) {
474
506
$ by === null => $ comparison ,
475
507
$ by instanceof \Closure => fn ($ a , $ b ) => $ comparison ($ by ($ a ), $ by ($ b )),
@@ -650,4 +682,33 @@ public static function random(string|array $values): mixed
650
682
? $ values [array_rand ($ values , 1 )]
651
683
: null ;
652
684
}
685
+
686
+
687
+ /**
688
+ * Formats a number with grouped thousands and optionally decimal digits according to locale.
689
+ */
690
+ public function number (
691
+ float $ number ,
692
+ int $ decimals = 0 ,
693
+ string $ decimalSeparator = '. ' ,
694
+ string $ thousandsSeparator = ', ' ,
695
+ ): string
696
+ {
697
+ if ($ this ->locale === null || func_num_args () > 2 ) {
698
+ return number_format ($ number , $ decimals , $ decimalSeparator , $ thousandsSeparator );
699
+ }
700
+
701
+ $ formatter = new \NumberFormatter ($ this ->locale , \NumberFormatter::DECIMAL );
702
+ $ formatter ->setAttribute (\NumberFormatter::FRACTION_DIGITS , $ decimals );
703
+ return $ formatter ->format ($ number );
704
+ }
705
+
706
+
707
+ private function getLocale (string $ name ): string
708
+ {
709
+ if ($ this ->locale === null ) {
710
+ throw new Latte \RuntimeException ("Filter | $ name requires the locale to be set using Engine::setLocale() " );
711
+ }
712
+ return $ this ->locale ;
713
+ }
653
714
}
0 commit comments