From 0d41bd3100ca7a86c0467592974e08d9164d464a Mon Sep 17 00:00:00 2001
From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com>
Date: Wed, 27 Dec 2023 11:59:46 +0400
Subject: [PATCH 1/6] Added `DurationFormatter` helper.
---
.../formatter/src/Utils/DurationFormatter.php | 27 +++++++++++
.../src/Utils/DurationFormatterTest.php | 45 +++++++++++++++++++
2 files changed, 72 insertions(+)
create mode 100644 packages/formatter/src/Utils/DurationFormatter.php
create mode 100644 packages/formatter/src/Utils/DurationFormatterTest.php
diff --git a/packages/formatter/src/Utils/DurationFormatter.php b/packages/formatter/src/Utils/DurationFormatter.php
new file mode 100644
index 000000000..99c87c1c0
--- /dev/null
+++ b/packages/formatter/src/Utils/DurationFormatter.php
@@ -0,0 +1,27 @@
+invert ? -1 : 1) * (0
+ + $interval->y * self::SecondsInYear
+ + $interval->m * self::SecondsInMonth
+ + $interval->d * self::SecondsInDay
+ + $interval->h * self::SecondsInHour
+ + $interval->i * self::SecondsInMinute
+ + $interval->s
+ + $interval->f);
+ }
+}
diff --git a/packages/formatter/src/Utils/DurationFormatterTest.php b/packages/formatter/src/Utils/DurationFormatterTest.php
new file mode 100644
index 000000000..0e61ce9bc
--- /dev/null
+++ b/packages/formatter/src/Utils/DurationFormatterTest.php
@@ -0,0 +1,45 @@
+
+ // =========================================================================
+ /**
+ * @dataProvider dataProviderGetTimestamp
+ */
+ public function testGetTimestamp(float $expected, DateInterval $interval): void {
+ self::assertEquals($expected, DurationFormatter::getTimestamp($interval));
+ }
+ //
+
+ //
+ // =========================================================================
+ /**
+ * @return array
+ */
+ public static function dataProviderGetTimestamp(): array {
+ return [
+ 'a' => [
+ 22 * 365 * 24 * 60 * 60 + 22 * 30 * 24 * 60 * 60 + 22 * 24 * 60 * 60 + 22 * 60 * 60 + 22 * 60 + 22,
+ new DateInterval('P22Y22M22DT22H22M22S'),
+ ],
+ 'b' => [
+ -1 * (16 * 24 * 60 * 60 - 0.000484),
+ (new DateTime('2023-12-27T11:22:45.000121+04:00'))->diff(
+ new DateTime('2023-12-11T11:22:45.000605+04:00'),
+ ),
+ ],
+ ];
+ }
+ //
+}
From f58e9e9c067ba60add57c64cb08d50c74b616736 Mon Sep 17 00:00:00 2001
From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com>
Date: Thu, 28 Dec 2023 11:16:50 +0400
Subject: [PATCH 2/6] Added `UnicodeDateTimeFormatParser` to parse ICU
date/time format syntax.
---
.../src/Utils/UnicodeDateTimeFormatParser.php | 108 ++++++++++++++++++
.../Utils/UnicodeDateTimeFormatParserTest.php | 86 ++++++++++++++
.../src/Utils/UnicodeDateTimeFormatToken.php | 15 +++
3 files changed, 209 insertions(+)
create mode 100644 packages/formatter/src/Utils/UnicodeDateTimeFormatParser.php
create mode 100644 packages/formatter/src/Utils/UnicodeDateTimeFormatParserTest.php
create mode 100644 packages/formatter/src/Utils/UnicodeDateTimeFormatToken.php
diff --git a/packages/formatter/src/Utils/UnicodeDateTimeFormatParser.php b/packages/formatter/src/Utils/UnicodeDateTimeFormatParser.php
new file mode 100644
index 000000000..03d0e4411
--- /dev/null
+++ b/packages/formatter/src/Utils/UnicodeDateTimeFormatParser.php
@@ -0,0 +1,108 @@
+
+ */
+class UnicodeDateTimeFormatParser implements IteratorAggregate {
+ public function __construct(
+ protected readonly string $pattern,
+ ) {
+ // empty
+ }
+
+ #[Override]
+ public function getIterator(): Traversable {
+ $text = null;
+ $escape = "'";
+ $replace = [
+ $escape.$escape => $escape,
+ ];
+ $inEscape = false;
+
+ foreach ($this->tokenize($this->pattern) as $token => $value) {
+ $isEscape = $token === $escape;
+ $isPattern = !$isEscape && !$inEscape && preg_match('/[a-z]+/i', $token);
+
+ if ($inEscape) {
+ if ($isEscape) {
+ $value = mb_substr($value, 1);
+ $inEscape = mb_strlen($value) % 2 !== 0;
+ }
+
+ $text .= $value;
+ } elseif ($isEscape) {
+ if (mb_strlen($value) % 2 !== 0) {
+ $text .= mb_substr($value, 1);
+ $inEscape = true;
+ } else {
+ $text .= $value;
+ }
+ } elseif ($isPattern) {
+ if ($text) {
+ yield new UnicodeDateTimeFormatToken($escape, strtr($text, $replace));
+
+ $text = null;
+ }
+
+ yield new UnicodeDateTimeFormatToken($token, $value);
+ } else {
+ $text .= $value;
+ }
+ }
+
+ if ($text) {
+ yield new UnicodeDateTimeFormatToken($escape, strtr($text, $replace));
+ }
+
+ yield from [];
+ }
+
+ /**
+ * @return Iterator
+ */
+ private function tokenize(string $pattern): Iterator {
+ // Split into char & string of the same chars
+ $strings = preg_split('/((.)\g{-1}*)/um', $pattern, flags: PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+
+ if (!$strings) {
+ yield from [];
+
+ return;
+ }
+
+ // Group into char & string
+ $value = null;
+
+ foreach ($strings as $string) {
+ if ($value === null) {
+ $value = $string;
+ } else {
+ yield $string => $value;
+
+ $value = null;
+ }
+ }
+ }
+}
diff --git a/packages/formatter/src/Utils/UnicodeDateTimeFormatParserTest.php b/packages/formatter/src/Utils/UnicodeDateTimeFormatParserTest.php
new file mode 100644
index 000000000..19eeb71dd
--- /dev/null
+++ b/packages/formatter/src/Utils/UnicodeDateTimeFormatParserTest.php
@@ -0,0 +1,86 @@
+
+ // =========================================================================
+ /**
+ * @dataProvider dataProviderGetIterator
+ *
+ * @param array $expected
+ */
+ public function testGetIterator(array $expected, string $format): void {
+ $actual = iterator_to_array(new UnicodeDateTimeFormatParser($format));
+ $actual = array_map(static fn (UnicodeDateTimeFormatToken $token) => [$token->pattern, $token->value], $actual);
+
+ self::assertEquals($expected, $actual);
+ }
+ //
+
+ //
+ // =========================================================================
+ /**
+ * @return array, string}>
+ */
+ public static function dataProviderGetIterator(): array {
+ return [
+ 'a' => [[], ''],
+ 'b' => [
+ [
+ ["'", 'text'],
+ ],
+ "'text'",
+ ],
+ 'c' => [
+ [
+ ['H', 'HH'],
+ ["'", ':'],
+ ['m', 'mm'],
+ ["'", ':'],
+ ['s', 'ss'],
+ ["'", '.'],
+ ['S', 'SSS'],
+ ],
+ 'HH:mm:ss.SSS',
+ ],
+ 'd' => [
+ [
+ ['H', 'HH'],
+ ["'", ":'"],
+ ['m', 'mm'],
+ ["'", ":ss'"],
+ ],
+ "HH:''mm:'ss'''",
+ ],
+ 'e' => [
+ [
+ ["'", "''mm"],
+ ['s', 'sss'],
+ ["'", "'"],
+ ],
+ "'''''mm'sss''",
+ ],
+ 'f' => [
+ [
+ ["'", 'абвгдеё;%:'],
+ ['Y', 'Y'],
+ ["'", '😀'],
+ ['y', 'yyyy'],
+ ],
+ 'абвгдеё;%:Y😀yyyy',
+ ],
+ ];
+ }
+ //
+}
diff --git a/packages/formatter/src/Utils/UnicodeDateTimeFormatToken.php b/packages/formatter/src/Utils/UnicodeDateTimeFormatToken.php
new file mode 100644
index 000000000..fecf23eed
--- /dev/null
+++ b/packages/formatter/src/Utils/UnicodeDateTimeFormatToken.php
@@ -0,0 +1,15 @@
+
Date: Sat, 30 Dec 2023 09:35:46 +0400
Subject: [PATCH 3/6] `DurationFormatter` implementation.
---
.../formatter/src/Utils/DurationFormatter.php | 119 ++++++++++++++++++
.../src/Utils/DurationFormatterTest.php | 37 ++++++
2 files changed, 156 insertions(+)
diff --git a/packages/formatter/src/Utils/DurationFormatter.php b/packages/formatter/src/Utils/DurationFormatter.php
index 99c87c1c0..d16f0136b 100644
--- a/packages/formatter/src/Utils/DurationFormatter.php
+++ b/packages/formatter/src/Utils/DurationFormatter.php
@@ -4,7 +4,40 @@
use DateInterval;
+use function abs;
+use function array_filter;
+use function array_key_exists;
+use function array_reduce;
+use function floor;
+use function is_float;
+use function iterator_to_array;
+use function mb_strlen;
+use function pow;
+use function round;
+use function str_pad;
+
+use const STR_PAD_LEFT;
+
+// @phpcs:disable Generic.Files.LineLength.TooLong
+
/**
+ * Format the duration according to the pattern.
+ *
+ * The syntax is the same as [ICU Date/Time format syntax](https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax).
+ *
+ * | Symbol | Meaning |
+ * |--------|-------------------------------|
+ * | `y` | years |
+ * | `M` | months |
+ * | `d` | days |
+ * | `H` | hours |
+ * | `m` | minutes |
+ * | `s` | seconds |
+ * | `S` | fractional seconds |
+ * | `z` | negative sign (default `-`) |
+ * | `'` | escape for text |
+ * | `''` | two single quotes produce one |
+ *
* @internal
*/
class DurationFormatter {
@@ -14,6 +47,12 @@ class DurationFormatter {
final protected const SecondsInMonth = 30 * self::SecondsInDay;
final protected const SecondsInYear = 365 * self::SecondsInDay;
+ public function __construct(
+ protected readonly string $pattern,
+ ) {
+ // empty
+ }
+
public static function getTimestamp(DateInterval $interval): float {
return ($interval->invert ? -1 : 1) * (0
+ $interval->y * self::SecondsInYear
@@ -24,4 +63,84 @@ public static function getTimestamp(DateInterval $interval): float {
+ $interval->s
+ $interval->f);
}
+
+ public function format(float|int $value): string {
+ $formatted = '';
+ $tokens = iterator_to_array(new UnicodeDateTimeFormatParser($this->pattern));
+ $units = $this->prepare($tokens, abs($value), [
+ 'y' => self::SecondsInYear,
+ 'M' => self::SecondsInMonth,
+ 'd' => self::SecondsInDay,
+ 'H' => self::SecondsInHour,
+ 'm' => self::SecondsInMinute,
+ 's' => 1,
+ 'S' => null,
+ ]);
+
+ foreach ($tokens as $token) {
+ $formatted .= match ($token->pattern) {
+ 'z' => $value < 0 ? '-' : '',
+ "'" => $token->value,
+ default => isset($units[$token->pattern])
+ ? $this->value($units[$token->pattern], mb_strlen($token->value))
+ : '',
+ };
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * @param array $tokens
+ * @param array $units
+ *
+ * @return array
+ */
+ private function prepare(array $tokens, float|int $value, array $units): array {
+ // Calculate values
+ $values = [];
+ $patterns = array_filter(
+ array_reduce(
+ $tokens,
+ static function (array $used, UnicodeDateTimeFormatToken $token) use ($units): array {
+ if (array_key_exists($token->pattern, $units)) {
+ $used[$token->pattern] = true;
+ }
+
+ return $used;
+ },
+ [],
+ ),
+ );
+
+ foreach ($units as $pattern => $multiplier) {
+ if (!isset($patterns[$pattern])) {
+ continue;
+ }
+
+ if ($multiplier !== null) {
+ $values[$pattern] = (int) floor($value / $multiplier);
+ $value = $value - $values[$pattern] * $multiplier;
+ } else {
+ $values[$pattern] = $value;
+ $value = 0;
+ }
+ }
+
+ // Return
+ return $values;
+ }
+
+ private function value(float|int $value, int $length): string {
+ // Float?
+ if (is_float($value)) {
+ $value = (int) round(($value - (int) $value) * pow(10, $length));
+ }
+
+ // Width?
+ $value = str_pad((string) $value, $length, '0', STR_PAD_LEFT);
+
+ // Return
+ return $value;
+ }
}
diff --git a/packages/formatter/src/Utils/DurationFormatterTest.php b/packages/formatter/src/Utils/DurationFormatterTest.php
index 0e61ce9bc..d857acb86 100644
--- a/packages/formatter/src/Utils/DurationFormatterTest.php
+++ b/packages/formatter/src/Utils/DurationFormatterTest.php
@@ -20,6 +20,16 @@ class DurationFormatterTest extends TestCase {
public function testGetTimestamp(float $expected, DateInterval $interval): void {
self::assertEquals($expected, DurationFormatter::getTimestamp($interval));
}
+
+ /**
+ * @dataProvider dataProviderFormat
+ */
+ public function testFormat(string $expected, string $format, float|int $duration): void {
+ $formatter = new DurationFormatter($format);
+ $actual = $formatter->format($duration);
+
+ self::assertEquals($expected, $actual);
+ }
//
//
@@ -41,5 +51,32 @@ public static function dataProviderGetTimestamp(): array {
],
];
}
+
+ /**
+ * @return array
+ */
+ public static function dataProviderFormat(): array {
+ $duration = static function (string $interval): float {
+ return DurationFormatter::getTimestamp(new DateInterval($interval));
+ };
+
+ return [
+ 'S' => ['3', 'S', 12.345678],
+ 'SS' => ['35', 'SS', 12.345678],
+ 'SSS' => ['346', 'SSS', 12.345678],
+ 's.SSS' => ['1.230', 's.SSS', 1.23],
+ 'ss.SS' => ['123.45', 'ss.SS', 123.45],
+ 'ss.SSS' => ['01.230', 'ss.SSS', 1.23],
+ 'm:ss' => ['3:00', 'm:ss', 180],
+ 'mm:ss' => ['03:00', 'mm:ss', -180],
+ 'zmm:ss' => ['-03:00', 'zmm:ss', -180],
+ 'H:m:s' => ['5:3:0', 'H:m:s', 5 * 60 * 60 + 180],
+ 'HH:mm:ss' => ['05:03:00', 'HH:mm:ss', 5 * 60 * 60 + 180],
+ 'y:M:d:H:m:s' => ['1:2:3:1:2:5', 'y:M:d:H:m:s', $duration('P1Y2M3DT1H2M5S')],
+ 'yyy:MM:dd:HH:mm:ss' => ['001:02:03:01:02:05', 'yyy:MM:dd:HH:mm:ss', $duration('P1Y2M3DT1H2M5S')],
+ "y:M:d:'H':m:s" => ['1:2:3:H:62:5', "y:M:d:'H':m:s", $duration('P1Y2M3DT1H2M5S')],
+ "y:'M':d:'H':m:s.SSS" => ['2:M:298:H:62:5.000', "y:'M':d:'H':m:s.SSS", $duration('P1Y22M3DT1H2M5S')],
+ ];
+ }
//
}
From a8450204393e1982ec7fda08f83bace37741d659 Mon Sep 17 00:00:00 2001
From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com>
Date: Sat, 30 Dec 2023 10:34:10 +0400
Subject: [PATCH 4/6] `Formatter::duration()` will also accept `DateInterval`
and `float` + fraction seconds support.
---
packages/formatter/defaults/config.php | 21 +++++++++++++++++
packages/formatter/src/Formatter.php | 29 ++++++++++++++++++++----
packages/formatter/src/FormatterTest.php | 20 ++++++++++++++++
3 files changed, 66 insertions(+), 4 deletions(-)
diff --git a/packages/formatter/defaults/config.php b/packages/formatter/defaults/config.php
index b4adb6479..36216dbdc 100644
--- a/packages/formatter/defaults/config.php
+++ b/packages/formatter/defaults/config.php
@@ -2,6 +2,7 @@
use LastDragon_ru\LaraASP\Core\Utils\ConfigMerger;
use LastDragon_ru\LaraASP\Formatter\Formatter;
+use LastDragon_ru\LaraASP\Formatter\Utils\DurationFormatter;
/**
* -----------------------------------------------------------------------------
@@ -36,6 +37,12 @@
*/
// Formatter::Time => 'custom',
+ /**
+ * Default custom duration format name, you can also use
+ * {@link NumberFormatter::DURATION} for built-in Intl format.
+ */
+ // Formatter::Duration => 'custom',
+
/**
* Global Attributes for {@link NumberFormatter::setAttribute}
*/
@@ -79,6 +86,15 @@
// 'custom' => 'HH:mm:ss.SSS',
// ],
+ /**
+ * Custom duration format for all locales
+ *
+ * @see DurationFormatter
+ */
+ // Formatter::Duration => [
+ // 'custom' => 'HH:mm:ss.SSS',
+ // ],
+
/**
* Intl properties for all locales (will be merged with `options`)
*/
@@ -107,6 +123,11 @@
// 'custom' => 'HH:mm:ss',
// ],
//
+ // // Custom duration format for specific Locale
+ // Formatter::Duration => [
+ // 'custom' => 'HH:mm:ss',
+ // ],
+ //
// // Intl properties for specific Locale (will be merged with all`)
// Formatter::Decimal => [
// Formatter::IntlSymbols => [],
diff --git a/packages/formatter/src/Formatter.php b/packages/formatter/src/Formatter.php
index bd9bcfd4e..bfd85674d 100644
--- a/packages/formatter/src/Formatter.php
+++ b/packages/formatter/src/Formatter.php
@@ -3,6 +3,7 @@
namespace LastDragon_ru\LaraASP\Formatter;
use Closure;
+use DateInterval;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Container\Container;
@@ -15,6 +16,7 @@
use LastDragon_ru\LaraASP\Formatter\Exceptions\FailedToCreateDateFormatter;
use LastDragon_ru\LaraASP\Formatter\Exceptions\FailedToCreateNumberFormatter;
use LastDragon_ru\LaraASP\Formatter\Exceptions\FailedToFormatValue;
+use LastDragon_ru\LaraASP\Formatter\Utils\DurationFormatter;
use Locale;
use NumberFormatter;
use OutOfBoundsException;
@@ -100,13 +102,18 @@ class Formatter {
public const Ordinal = 'ordinal';
/**
- * Options:
- * - none
+ * Options (one of):
+ * - `int`: {@link NumberFormatter::DURATION}
+ * - `string`: the name of custom format
*
* Locales options:
* - {@link Formatter::IntlTextAttributes}
* - {@link Formatter::IntlAttributes}
* - {@link Formatter::IntlSymbols}
+ * - `string`: available only for custom formats: locale specific pattern
+ * (key: `locales..duration.` and/or default pattern
+ * (key: `all.duration.`)
+ * {@link DurationFormatter}
*/
public const Duration = 'duration';
@@ -313,8 +320,22 @@ public function ordinal(?int $value): string {
return $this->formatValue(static::Ordinal, $value);
}
- public function duration(?int $value): string {
- return $this->formatValue(static::Duration, $value);
+ public function duration(DateInterval|float|int|null $value, string $format = null): string {
+ $type = static::Duration;
+ $format = $format ?: $this->getOptions($type);
+ $format = is_string($format)
+ ? Cast::toString($this->getLocaleOptions($type, $format))
+ : $format;
+ $format ??= 'HH:mm:ss.SSS';
+ $value ??= 0;
+ $value = $value instanceof DateInterval
+ ? DurationFormatter::getTimestamp($value)
+ : $value;
+ $value = is_string($format)
+ ? (new DurationFormatter($format))->format($value)
+ : $this->formatValue($type, $value);
+
+ return $value;
}
public function time(
diff --git a/packages/formatter/src/FormatterTest.php b/packages/formatter/src/FormatterTest.php
index 0bef22630..85599f2a1 100644
--- a/packages/formatter/src/FormatterTest.php
+++ b/packages/formatter/src/FormatterTest.php
@@ -126,10 +126,30 @@ public function testPercentConfig(): void {
}
public function testDuration(): void {
+ self::assertEquals('03:25:45.120', $this->formatter->duration(12_345.12));
+ self::assertEquals('03:25:45.001', $this->formatter->forLocale('ru_RU')->duration(12_345.0005));
+ }
+
+ public function testDurationConfig(): void {
+ config([
+ Package::Name.'.options.'.Formatter::Duration => NumberFormatter::DURATION,
+ ]);
+
self::assertEquals('3:25:45', $this->formatter->duration(12_345));
self::assertEquals("12\u{00A0}345", $this->formatter->forLocale('ru_RU')->duration(12_345));
}
+ public function testDurationCustomFormat(): void {
+ config([
+ Package::Name.'.options.'.Formatter::Duration => 'custom',
+ Package::Name.'.all.'.Formatter::Duration.'.custom' => 'mm:ss',
+ Package::Name.'.all.'.Formatter::Duration.'.custom2' => 'H:mm:ss.SSS',
+ ]);
+
+ self::assertEquals('02:03', $this->formatter->duration(123.456));
+ self::assertEquals('0:02:03.456', $this->formatter->duration(123.456, 'custom2'));
+ }
+
public function testTime(): void {
$time = DateTime::createFromFormat('H:i:s', '23:24:59') ?: null;
From 25a9b4ce8dbfb8eab7e25af2464dc03e7185cd53 Mon Sep 17 00:00:00 2001
From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com>
Date: Sat, 30 Dec 2023 20:30:04 +0400
Subject: [PATCH 5/6] Duration documentation.
---
packages/formatter/README.md | 64 +++++++++++++++++++
packages/formatter/docs/Examples/Duration.php | 12 ++++
packages/formatter/docs/Examples/Duration.run | 3 +
3 files changed, 79 insertions(+)
create mode 100644 packages/formatter/docs/Examples/Duration.php
create mode 100755 packages/formatter/docs/Examples/Duration.run
diff --git a/packages/formatter/README.md b/packages/formatter/README.md
index d4af6179b..8f25924dc 100644
--- a/packages/formatter/README.md
+++ b/packages/formatter/README.md
@@ -82,6 +82,70 @@ The `$locale->decimal(123.454321)` is:
Please check [source code](./src/Formatter.php) to see available methods and [config example](defaults/config.php) to available settings 🤗
+# Duration
+
+To format duration you can use built-in Intl formatter, but it doesn't support fraction seconds and have different format between locales (for example, `12345` seconds is `3:25:45` in `en_US` locale, and `12 345` in `ru_RU`). These reasons make difficult to use it in real applications. To make `duration()` more useful, the alternative syntax was added.
+
+[include:docblock]: ./src/Utils/DurationFormatter.php
+[//]: # (start: 8e359fc1ea71d4c4b58c4acdcd3289f180a89cbd39ebdbd10422908bd66b0268)
+[//]: # (warning: Generated automatically. Do not edit.)
+
+The syntax is the same as [ICU Date/Time format syntax](https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax).
+
+| Symbol | Meaning |
+|--------|-------------------------------|
+| `y` | years |
+| `M` | months |
+| `d` | days |
+| `H` | hours |
+| `m` | minutes |
+| `s` | seconds |
+| `S` | fractional seconds |
+| `z` | negative sign (default `-`) |
+| `'` | escape for text |
+| `''` | two single quotes produce one |
+
+[//]: # (end: 8e359fc1ea71d4c4b58c4acdcd3289f180a89cbd39ebdbd10422908bd66b0268)
+
+[include:example]: ./docs/Examples/Duration.php
+[//]: # (start: bb574f6b1315aa7b33a56d897b23ecc4d18dece9ea201b85b54154e144931d3b)
+[//]: # (warning: Generated automatically. Do not edit.)
+
+```php
+make(Formatter::class); // For default app locale
+$locale = $default->forLocale('ru_RU'); // For ru_RU locale
+
+Example::dump($default->duration(123.454321));
+Example::dump($locale->duration(123.4543));
+Example::dump($locale->duration(1_234_543));
+```
+
+The `$default->duration(123.454321)` is:
+
+```plain
+"00:02:03.454"
+```
+
+The `$locale->duration(123.4543)` is:
+
+```plain
+"00:02:03.454"
+```
+
+The `$locale->duration(1234543)` is:
+
+```plain
+"342:55:43.000"
+```
+
+[//]: # (end: bb574f6b1315aa7b33a56d897b23ecc4d18dece9ea201b85b54154e144931d3b)
+
[include:file]: ../../docs/Shared/Contributing.md
[//]: # (start: 057ec3a599c54447e95d6dd2e9f0f6a6621d9eb75446a5e5e471ba9b2f414b89)
[//]: # (warning: Generated automatically. Do not edit.)
diff --git a/packages/formatter/docs/Examples/Duration.php b/packages/formatter/docs/Examples/Duration.php
new file mode 100644
index 000000000..83d186dd3
--- /dev/null
+++ b/packages/formatter/docs/Examples/Duration.php
@@ -0,0 +1,12 @@
+make(Formatter::class); // For default app locale
+$locale = $default->forLocale('ru_RU'); // For ru_RU locale
+
+Example::dump($default->duration(123.454321));
+Example::dump($locale->duration(123.4543));
+Example::dump($locale->duration(1_234_543));
diff --git a/packages/formatter/docs/Examples/Duration.run b/packages/formatter/docs/Examples/Duration.run
new file mode 100755
index 000000000..aefd0de01
--- /dev/null
+++ b/packages/formatter/docs/Examples/Duration.run
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+"${BASH_SOURCE%/*}/../../../../dev/artisan" dev:example "${BASH_SOURCE%.*}.php"
From 2a9b500e6c57f0bcefd1ff8d30eb49b3f034983a Mon Sep 17 00:00:00 2001
From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com>
Date: Sat, 30 Dec 2023 21:33:02 +0400
Subject: [PATCH 6/6] Config documentation.
---
packages/formatter/README.md | 76 +++++++++++++++++++++
packages/formatter/docs/Examples/Config.php | 35 ++++++++++
packages/formatter/docs/Examples/Config.run | 3 +
3 files changed, 114 insertions(+)
create mode 100644 packages/formatter/docs/Examples/Config.php
create mode 100644 packages/formatter/docs/Examples/Config.run
diff --git a/packages/formatter/README.md b/packages/formatter/README.md
index 8f25924dc..0a1b01c2e 100644
--- a/packages/formatter/README.md
+++ b/packages/formatter/README.md
@@ -82,6 +82,82 @@ The `$locale->decimal(123.454321)` is:
Please check [source code](./src/Formatter.php) to see available methods and [config example](defaults/config.php) to available settings 🤗
+# Formats
+
+Some methods like as `date()`/`datetime()`/etc have `$format` argument. The argument specifies not the format but the format name. So you can use the names and do not worry about real formats. It is very important when application big/grow. To specify available names and formats the package config should be published and customized.
+
+```shell
+php artisan vendor:publish --provider=LastDragon_ru\\LaraASP\\Formatter\\Provider --tag=config
+```
+
+[include:example]: ./docs/Examples/Config.php
+[//]: # (start: a0315c77f2fd2868ad7a67f118ff4816a93add9ae6e7d35899828ddc32cfac37)
+[//]: # (warning: Generated automatically. Do not edit.)
+
+```php
+ [
+ Formatter::Date => 'default',
+ ],
+ 'all' => [
+ Formatter::Date => [
+ 'default' => 'd MMM yyyy',
+ 'custom' => 'yyyy/MM/dd',
+ ],
+ ],
+ 'locales' => [
+ 'ru_RU' => [
+ Formatter::Date => [
+ 'custom' => 'dd.MM.yyyy',
+ ],
+ ],
+ ],
+]);
+
+$datetime = Date::make('2023-12-30T20:41:40.000018+04:00');
+$default = Container::getInstance()->make(Formatter::class);
+$locale = $default->forLocale('ru_RU');
+
+Example::dump($default->date($datetime));
+Example::dump($default->date($datetime, 'custom'));
+Example::dump($locale->date($datetime));
+Example::dump($locale->date($datetime, 'custom'));
+```
+
+The `$default->date($datetime)` is:
+
+```plain
+"30 Dec 2023"
+```
+
+The `$default->date($datetime, 'custom')` is:
+
+```plain
+"2023/12/30"
+```
+
+The `$locale->date($datetime)` is:
+
+```plain
+"30 дек. 2023"
+```
+
+The `$locale->date($datetime, 'custom')` is:
+
+```plain
+"30.12.2023"
+```
+
+[//]: # (end: a0315c77f2fd2868ad7a67f118ff4816a93add9ae6e7d35899828ddc32cfac37)
+
# Duration
To format duration you can use built-in Intl formatter, but it doesn't support fraction seconds and have different format between locales (for example, `12345` seconds is `3:25:45` in `en_US` locale, and `12 345` in `ru_RU`). These reasons make difficult to use it in real applications. To make `duration()` more useful, the alternative syntax was added.
diff --git a/packages/formatter/docs/Examples/Config.php b/packages/formatter/docs/Examples/Config.php
new file mode 100644
index 000000000..de27ed829
--- /dev/null
+++ b/packages/formatter/docs/Examples/Config.php
@@ -0,0 +1,35 @@
+ [
+ Formatter::Date => 'default',
+ ],
+ 'all' => [
+ Formatter::Date => [
+ 'default' => 'd MMM yyyy',
+ 'custom' => 'yyyy/MM/dd',
+ ],
+ ],
+ 'locales' => [
+ 'ru_RU' => [
+ Formatter::Date => [
+ 'custom' => 'dd.MM.yyyy',
+ ],
+ ],
+ ],
+]);
+
+$datetime = Date::make('2023-12-30T20:41:40.000018+04:00');
+$default = Container::getInstance()->make(Formatter::class);
+$locale = $default->forLocale('ru_RU');
+
+Example::dump($default->date($datetime));
+Example::dump($default->date($datetime, 'custom'));
+Example::dump($locale->date($datetime));
+Example::dump($locale->date($datetime, 'custom'));
diff --git a/packages/formatter/docs/Examples/Config.run b/packages/formatter/docs/Examples/Config.run
new file mode 100644
index 000000000..aefd0de01
--- /dev/null
+++ b/packages/formatter/docs/Examples/Config.run
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+"${BASH_SOURCE%/*}/../../../../dev/artisan" dev:example "${BASH_SOURCE%.*}.php"