From 27bed8688fa8fe139fac6b4d6e32ca79838e0c41 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Tue, 3 Feb 2026 16:25:25 -0300 Subject: [PATCH] Implement formatters for common units Includes formatters that perform unit promotion. We DO NOT do unit conversion (for example, inches to centimeters), we only choose within the appropriate system (metric or imperial) the best representation for a given value. For example, saying `10cm` is much better than saying `0.1m`. Both are still within the metric system. Considerations made during this change: - We only use unit symbols, not the full names. This simplifies aspects such as translations, since most units are universal but their localized full names are not. There is some space for later accepting full names (new MetricFormatter('meter')) and deciding the output representation automatically. This is also the reason why PHP enums were not used as the source of truth for supported units. - Time unit symbols are *not universal*, but they were considered for inclusion for their value in technical systems (simplifying a log entry or measure). - The SI (colloquially known as "metric system") is the widest adopted standard, so only "Imperial" was prefixed. We used the "Metric" name to denote SI Length units. --- README.md | 20 +++-- docs/ImperialAreaFormatter.md | 34 +++++++++ docs/ImperialLengthFormatter.md | 34 +++++++++ docs/ImperialMassFormatter.md | 38 ++++++++++ docs/MassFormatter.md | 43 +++++++++++ docs/MetricFormatter.md | 60 +++++++++++++++ docs/TimeFormatter.md | 46 +++++++++++ src/ImperialAreaFormatter.php | 33 ++++++++ src/ImperialLengthFormatter.php | 32 ++++++++ src/ImperialMassFormatter.php | 32 ++++++++ src/Internal/UnitPromoter.php | 88 ++++++++++++++++++++++ src/MassFormatter.php | 32 ++++++++ src/MetricFormatter.php | 32 ++++++++ src/Mixin/Builder.php | 27 ++++++- src/Mixin/Chain.php | 21 ++++++ src/TimeFormatter.php | 41 ++++++++++ tests/Unit/ImperialAreaFormatterTest.php | 73 ++++++++++++++++++ tests/Unit/ImperialLengthFormatterTest.php | 74 ++++++++++++++++++ tests/Unit/ImperialMassFormatterTest.php | 74 ++++++++++++++++++ tests/Unit/MassFormatterTest.php | 77 +++++++++++++++++++ tests/Unit/MetricFormatterTest.php | 83 ++++++++++++++++++++ tests/Unit/TimeFormatterTest.php | 78 +++++++++++++++++++ 22 files changed, 1062 insertions(+), 10 deletions(-) create mode 100644 docs/ImperialAreaFormatter.md create mode 100644 docs/ImperialLengthFormatter.md create mode 100644 docs/ImperialMassFormatter.md create mode 100644 docs/MassFormatter.md create mode 100644 docs/MetricFormatter.md create mode 100644 docs/TimeFormatter.md create mode 100644 src/ImperialAreaFormatter.php create mode 100644 src/ImperialLengthFormatter.php create mode 100644 src/ImperialMassFormatter.php create mode 100644 src/Internal/UnitPromoter.php create mode 100644 src/MassFormatter.php create mode 100644 src/MetricFormatter.php create mode 100644 src/TimeFormatter.php create mode 100644 tests/Unit/ImperialAreaFormatterTest.php create mode 100644 tests/Unit/ImperialLengthFormatterTest.php create mode 100644 tests/Unit/ImperialMassFormatterTest.php create mode 100644 tests/Unit/MassFormatterTest.php create mode 100644 tests/Unit/MetricFormatterTest.php create mode 100644 tests/Unit/TimeFormatterTest.php diff --git a/README.md b/README.md index 8a6828b..6d86519 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,19 @@ echo f::create() ## Formatters -| Formatter | Description | -| ---------------------------------------------------- | --------------------------------------------------- | -| [DateFormatter](docs/DateFormatter.md) | Date and time formatting with flexible parsing | -| [MaskFormatter](docs/MaskFormatter.md) | Range-based string masking with Unicode support | -| [NumberFormatter](docs/NumberFormatter.md) | Number formatting with thousands and decimal separators | -| [PatternFormatter](docs/PatternFormatter.md) | Pattern-based string filtering with placeholders | -| [PlaceholderFormatter](docs/PlaceholderFormatter.md) | Template interpolation with placeholder replacement | +| Formatter | Description | +| ---------------------------------------------------------- | ---------------------------------------------------------------- | +| [DateFormatter](docs/DateFormatter.md) | Date and time formatting with flexible parsing | +| [ImperialAreaFormatter](docs/ImperialAreaFormatter.md) | Imperial area promotion (in², ft², yd², ac, mi²) | +| [ImperialLengthFormatter](docs/ImperialLengthFormatter.md) | Imperial length promotion (in, ft, yd, mi) | +| [ImperialMassFormatter](docs/ImperialMassFormatter.md) | Imperial mass promotion (oz, lb, st, ton) | +| [MaskFormatter](docs/MaskFormatter.md) | Range-based string masking with Unicode support | +| [MassFormatter](docs/MassFormatter.md) | Metric mass promotion (mg, g, kg, t) | +| [MetricFormatter](docs/MetricFormatter.md) | Metric length promotion (mm, cm, m, km) | +| [NumberFormatter](docs/NumberFormatter.md) | Number formatting with thousands and decimal separators | +| [PatternFormatter](docs/PatternFormatter.md) | Pattern-based string filtering with placeholders | +| [PlaceholderFormatter](docs/PlaceholderFormatter.md) | Template interpolation with placeholder replacement | +| [TimeFormatter](docs/TimeFormatter.md) | Time promotion (mil, c, dec, y, mo, w, d, h, min, s, ms, us, ns) | ## Contributing diff --git a/docs/ImperialAreaFormatter.md b/docs/ImperialAreaFormatter.md new file mode 100644 index 0000000..0bbd7f8 --- /dev/null +++ b/docs/ImperialAreaFormatter.md @@ -0,0 +1,34 @@ + + +# ImperialAreaFormatter + +The `ImperialAreaFormatter` promotes imperial area values between `in²`, `ft²`, `yd²`, `ac`, and `mi²`. + +- Non-numeric input is returned unchanged. +- Promotion is based on magnitude. +- Output uses symbols only (no spaces), e.g. `1ft²`, `2ac`. + +## Usage + +```php +use Respect\StringFormatter\ImperialAreaFormatter; + +$formatter = new ImperialAreaFormatter('ft²'); + +echo $formatter->format('43560'); +// Outputs: 1ac +``` + +## API + +### `ImperialAreaFormatter::__construct` + +- `__construct(string $unit)` + +The `$unit` is the input unit (the unit you are providing values in). + +Accepted units: `in²`, `ft²`, `yd²`, `ac`, `mi²`. diff --git a/docs/ImperialLengthFormatter.md b/docs/ImperialLengthFormatter.md new file mode 100644 index 0000000..371aea5 --- /dev/null +++ b/docs/ImperialLengthFormatter.md @@ -0,0 +1,34 @@ + + +# ImperialLengthFormatter + +The `ImperialLengthFormatter` promotes imperial length values between `in`, `ft`, `yd`, and `mi`. + +- Non-numeric input is returned unchanged. +- Promotion is based on magnitude. +- Output uses symbols only (no spaces), e.g. `1ft`, `2yd`. + +## Usage + +```php +use Respect\StringFormatter\ImperialLengthFormatter; + +$formatter = new ImperialLengthFormatter('in'); + +echo $formatter->format('12'); +// Outputs: 1ft +``` + +## API + +### `ImperialLengthFormatter::__construct` + +- `__construct(string $unit)` + +The `$unit` is the input unit (the unit you are providing values in). + +Accepted units: `in`, `ft`, `yd`, `mi`. diff --git a/docs/ImperialMassFormatter.md b/docs/ImperialMassFormatter.md new file mode 100644 index 0000000..094c5bc --- /dev/null +++ b/docs/ImperialMassFormatter.md @@ -0,0 +1,38 @@ + + +# ImperialMassFormatter + +The `ImperialMassFormatter` promotes imperial mass values between `oz`, `lb`, `st`, and `ton`. + +- Non-numeric input is returned unchanged. +- Promotion is based on magnitude. +- Output uses symbols only (no spaces), e.g. `1lb`, `8oz`. + +## Usage + +```php +use Respect\StringFormatter\ImperialMassFormatter; + +$formatter = new ImperialMassFormatter('oz'); + +echo $formatter->format('16'); +// Outputs: 1lb +``` + +## API + +### `ImperialMassFormatter::__construct` + +- `__construct(string $unit)` + +The `$unit` is the input unit (the unit you are providing values in). + +Accepted units: `oz`, `lb`, `st`, `ton`. + +## Notes + +- `ton` represents the imperial long ton (`2240lb`). diff --git a/docs/MassFormatter.md b/docs/MassFormatter.md new file mode 100644 index 0000000..6d3c4ec --- /dev/null +++ b/docs/MassFormatter.md @@ -0,0 +1,43 @@ + + +# MassFormatter + +The `MassFormatter` promotes metric *mass* values between `mg`, `g`, `kg`, and `t`. + +- Non-numeric input is returned unchanged. +- Promotion is based on magnitude. +- Output uses symbols only (no spaces), e.g. `1kg`, `500mg`. + +## Usage + +```php +use Respect\StringFormatter\MassFormatter; + +$formatter = new MassFormatter('g'); + +echo $formatter->format('1000'); +// Outputs: 1kg + +echo $formatter->format('0.001'); +// Outputs: 1mg +``` + +## API + +### `MassFormatter::__construct` + +- `__construct(string $unit)` + +The `$unit` is the input unit (the unit you are providing values in). + +Accepted units: `mg`, `g`, `kg`, `t`. + +### `format` + +- `format(string $input): string` + +If the input is numeric, it is promoted to the closest appropriate metric scale and returned with the corresponding symbol. diff --git a/docs/MetricFormatter.md b/docs/MetricFormatter.md new file mode 100644 index 0000000..1e62d67 --- /dev/null +++ b/docs/MetricFormatter.md @@ -0,0 +1,60 @@ + + +# MetricFormatter + +The `MetricFormatter` promotes metric *length* values between `mm`, `cm`, `m`, and `km`. + +- Non-numeric input is returned unchanged. +- Promotion is based on magnitude. +- Output uses symbols only (no spaces), e.g. `1km`, `10cm`. + +## Usage + +```php +use Respect\StringFormatter\MetricFormatter; + +$formatter = new MetricFormatter('m'); + +echo $formatter->format('1000'); +// Outputs: 1km + +echo $formatter->format('0.1'); +// Outputs: 10cm +``` + +## API + +### `MetricFormatter::__construct` + +- `__construct(string $unit)` + +The `$unit` is the input unit (the unit you are providing values in). + +Accepted units: `mm`, `cm`, `m`, `km`. + +### `format` + +- `format(string $input): string` + +If the input is numeric, it is promoted to the closest appropriate metric scale and returned with the corresponding symbol. + +## Behavior + +### Promotion rule + +The formatter chooses a unit where the promoted value is in the range $[1, 1000)$ when possible. If not possible, it uses the smallest (`mm`) or largest (`km`) unit as needed. + +### No rounding + +Values are not rounded. Trailing fractional zeros are trimmed: + +```php +$formatter = new MetricFormatter('m'); + +echo $formatter->format('1.23000'); +// Outputs: 1.23m +``` \ No newline at end of file diff --git a/docs/TimeFormatter.md b/docs/TimeFormatter.md new file mode 100644 index 0000000..fa0344a --- /dev/null +++ b/docs/TimeFormatter.md @@ -0,0 +1,46 @@ + + +# TimeFormatter + +The `TimeFormatter` promotes time values between multiple units. + +- Non-numeric input is returned unchanged. +- Promotion is based on magnitude. +- Output uses symbols only (no spaces), e.g. `1h`, `500ms`. + +## Usage + +```php +use Respect\StringFormatter\TimeFormatter; + +$formatter = new TimeFormatter('s'); + +echo $formatter->format('60'); +// Outputs: 1min + +echo $formatter->format('0.001'); +// Outputs: 1ms +``` + +## API + +### `TimeFormatter::__construct` + +- `__construct(string $unit)` + +The `$unit` is the input unit (the unit you are providing values in). + +Accepted symbols: + +- `ns`, `us`, `ms`, `s`, `min`, `h`, `d`, `w`, `mo`, `y`, `dec`, `c`, `mil` + +## Notes + +- `y` uses a fixed year of 365 days. +- `mo` uses 1/12 of a fixed year (approx. 30.41 days). +- `w` uses 7 days. +- `dec`, `c`, and `mil` are based on that fixed year. diff --git a/src/ImperialAreaFormatter.php b/src/ImperialAreaFormatter.php new file mode 100644 index 0000000..6fbc61a --- /dev/null +++ b/src/ImperialAreaFormatter.php @@ -0,0 +1,33 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class ImperialAreaFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_RATIOS = [ + 'mi²' => [4_014_489_600, 1], + 'ac' => [6_272_640, 1], + 'yd²' => [1_296, 1], + 'ft²' => [144, 1], + 'in²' => [1, 1], + ]; + + public function __construct(private string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported imperial area unit'); + } + } +} diff --git a/src/ImperialLengthFormatter.php b/src/ImperialLengthFormatter.php new file mode 100644 index 0000000..d14a4c1 --- /dev/null +++ b/src/ImperialLengthFormatter.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class ImperialLengthFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_RATIOS = [ + 'mi' => [63_360, 1], + 'yd' => [36, 1], + 'ft' => [12, 1], + 'in' => [1, 1], + ]; + + public function __construct(private string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported imperial length unit'); + } + } +} diff --git a/src/ImperialMassFormatter.php b/src/ImperialMassFormatter.php new file mode 100644 index 0000000..29f3543 --- /dev/null +++ b/src/ImperialMassFormatter.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class ImperialMassFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_RATIOS = [ + 'ton' => [35_840, 1], + 'st' => [224, 1], + 'lb' => [16, 1], + 'oz' => [1, 1], + ]; + + public function __construct(private string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported imperial mass unit'); + } + } +} diff --git a/src/Internal/UnitPromoter.php b/src/Internal/UnitPromoter.php new file mode 100644 index 0000000..4106fc9 --- /dev/null +++ b/src/Internal/UnitPromoter.php @@ -0,0 +1,88 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Internal; + +use function abs; +use function array_key_first; +use function array_key_last; +use function array_keys; +use function is_numeric; + +trait UnitPromoter +{ + public function format(string $input): string + { + return self::promote( + input: $input, + inputUnit: $this->unit, + ratiosToBase: self::UNIT_RATIOS, + orderedUnits: array_keys(self::UNIT_RATIOS), + smallestUnit: array_key_last(self::UNIT_RATIOS), + largestUnit: array_key_first(self::UNIT_RATIOS), + ); + } + + /** + * @param array $ratiosToBase + * @param list $orderedUnits + */ + private static function promote( + string $input, + string $inputUnit, + array $ratiosToBase, + array $orderedUnits, + string $smallestUnit, + string $largestUnit, + ): string { + if (!is_numeric($input)) { + return $input; + } + + $amount = (float) $input; + if ($amount == 0) { + return '0' . $inputUnit; + } + + [$baseNumerator, $baseDenominator] = $ratiosToBase[$inputUnit]; + $baseValue = $amount * $baseNumerator / $baseDenominator; + + $bestUnit = null; + $bestValue = null; + + foreach ($orderedUnits as $unit) { + [$unitNumerator, $unitDenominator] = $ratiosToBase[$unit]; + $candidateValue = $baseValue * $unitDenominator / $unitNumerator; + $absCandidateValue = abs($candidateValue); + + if ($absCandidateValue >= 1 && $absCandidateValue < 1000) { + $bestUnit = $unit; + $bestValue = $candidateValue; + break; + } + } + + if ($bestUnit === null) { + [$largestNumerator, $largestDenominator] = $ratiosToBase[$largestUnit]; + $largestValue = $baseValue * $largestDenominator / $largestNumerator; + if (abs($largestValue) >= 1) { + $bestUnit = $largestUnit; + $bestValue = $largestValue; + } else { + [$smallestNumerator, $smallestDenominator] = $ratiosToBase[$smallestUnit]; + $smallestValue = $baseValue * $smallestDenominator / $smallestNumerator; + $bestUnit = $smallestUnit; + $bestValue = $smallestValue; + } + } + + return (string) $bestValue . $bestUnit; + } +} diff --git a/src/MassFormatter.php b/src/MassFormatter.php new file mode 100644 index 0000000..027eb5b --- /dev/null +++ b/src/MassFormatter.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class MassFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_RATIOS = [ + 't' => [1_000_000, 1], + 'kg' => [1_000, 1], + 'g' => [1, 1], + 'mg' => [1, 1_000], + ]; + + public function __construct(private string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported metric mass unit'); + } + } +} diff --git a/src/MetricFormatter.php b/src/MetricFormatter.php new file mode 100644 index 0000000..d1dd23b --- /dev/null +++ b/src/MetricFormatter.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class MetricFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_RATIOS = [ + 'km' => [1_000, 1], + 'm' => [1, 1], + 'cm' => [1, 100], + 'mm' => [1, 1_000], + ]; + + public function __construct(private string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported metric length unit'); + } + } +} diff --git a/src/Mixin/Builder.php b/src/Mixin/Builder.php index e2ff60f..595054d 100644 --- a/src/Mixin/Builder.php +++ b/src/Mixin/Builder.php @@ -4,6 +4,7 @@ * SPDX-FileCopyrightText: (c) Respect Project Contributors * SPDX-License-Identifier: ISC * SPDX-FileContributor: Henrique Moody + * SPDX-FileContributor: Alexandre Gomes Gaigalas */ declare(strict_types=1); @@ -15,10 +16,30 @@ /** @mixin FormatterBuilder */ interface Builder { - public static function mask(string $range, string $replacement = '*'): Chain; + public function imperialArea(string $unit): FormatterBuilder; - public static function pattern(string $pattern): Chain; + public function imperialLength(string $unit): FormatterBuilder; + + public function imperialMass(string $unit): FormatterBuilder; + + public function date(string $format = 'Y-m-d H:i:s'): FormatterBuilder; + + public function mask(string $range, string $replacement = '*'): FormatterBuilder; + + public function metric(string $unit): FormatterBuilder; + + public function number( + int $decimals = 0, + string $decimalSeparator = '.', + string $thousandsSeparator = ',', + ): FormatterBuilder; + + public function metricMass(string $unit): FormatterBuilder; + + public function pattern(string $pattern): FormatterBuilder; /** @param array $parameters */ - public static function placeholder(array $parameters): Chain; + public function placeholder(array $parameters): FormatterBuilder; + + public function time(string $unit): FormatterBuilder; } diff --git a/src/Mixin/Chain.php b/src/Mixin/Chain.php index 3223a04..9930e1f 100644 --- a/src/Mixin/Chain.php +++ b/src/Mixin/Chain.php @@ -4,6 +4,7 @@ * SPDX-FileCopyrightText: (c) Respect Project Contributors * SPDX-License-Identifier: ISC * SPDX-FileContributor: Henrique Moody + * SPDX-FileContributor: Alexandre Gomes Gaigalas */ declare(strict_types=1); @@ -15,10 +16,30 @@ interface Chain extends Formatter { + public function imperialArea(string $unit): FormatterBuilder; + + public function imperialLength(string $unit): FormatterBuilder; + + public function imperialMass(string $unit): FormatterBuilder; + + public function date(string $format = 'Y-m-d H:i:s'): FormatterBuilder; + public function mask(string $range, string $replacement = '*'): FormatterBuilder; + public function metric(string $unit): FormatterBuilder; + + public function number( + int $decimals = 0, + string $decimalSeparator = '.', + string $thousandsSeparator = ',', + ): FormatterBuilder; + + public function metricMass(string $unit): FormatterBuilder; + public function pattern(string $pattern): FormatterBuilder; /** @param array $parameters */ public function placeholder(array $parameters): FormatterBuilder; + + public function time(string $unit): FormatterBuilder; } diff --git a/src/TimeFormatter.php b/src/TimeFormatter.php new file mode 100644 index 0000000..8b5bcc7 --- /dev/null +++ b/src/TimeFormatter.php @@ -0,0 +1,41 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class TimeFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_RATIOS = [ + 'mil' => [31_536_000_000, 1], + 'c' => [3_153_600_000, 1], + 'dec' => [315_360_000, 1], + 'y' => [31_536_000, 1], + 'mo' => [2_628_000, 1], + 'w' => [604_800, 1], + 'd' => [86_400, 1], + 'h' => [3_600, 1], + 'min' => [60, 1], + 's' => [1, 1], + 'ms' => [1, 1_000], + 'us' => [1, 1_000_000], + 'ns' => [1, 1_000_000_000], + ]; + + public function __construct(private string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported time unit'); + } + } +} diff --git a/tests/Unit/ImperialAreaFormatterTest.php b/tests/Unit/ImperialAreaFormatterTest.php new file mode 100644 index 0000000..aaaaf5d --- /dev/null +++ b/tests/Unit/ImperialAreaFormatterTest.php @@ -0,0 +1,73 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Test\Unit; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Respect\StringFormatter\ImperialAreaFormatter; +use Respect\StringFormatter\InvalidFormatterException; + +#[CoversClass(ImperialAreaFormatter::class)] +final class ImperialAreaFormatterTest extends TestCase +{ + #[Test] + #[DataProvider('providerForImperialAreaPromotion')] + public function itShouldPromoteImperialArea(string $unit, string $input, string $expected): void + { + $formatter = new ImperialAreaFormatter($unit); + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForImperialAreaPromotion(): array + { + return [ + 'square inches to square feet' => ['in²', '144', '1ft²'], + 'square feet to acres' => ['ft²', '43560', '1ac'], + 'acres to square miles' => ['ac', '640', '1mi²'], + 'negative square feet to acres' => ['ft²', '-43560', '-1ac'], + 'zero keeps base unit' => ['yd²', '0', '0yd²'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new ImperialAreaFormatter('ft²'); + $actual = $formatter->format($input); + + self::assertSame($input, $actual); + } + + /** @return array */ + public static function providerForNonNumericInput(): array + { + return [ + 'empty string' => [''], + 'text' => ['abc'], + 'mixed text and numbers' => ['123abc'], + 'multiple decimals' => ['1.2.3'], + ]; + } + + #[Test] + public function itShouldThrowExceptionWhenUnitIsInvalid(): void + { + $this->expectException(InvalidFormatterException::class); + + new ImperialAreaFormatter('m2'); + } +} diff --git a/tests/Unit/ImperialLengthFormatterTest.php b/tests/Unit/ImperialLengthFormatterTest.php new file mode 100644 index 0000000..0a81b49 --- /dev/null +++ b/tests/Unit/ImperialLengthFormatterTest.php @@ -0,0 +1,74 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Test\Unit; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Respect\StringFormatter\ImperialLengthFormatter; +use Respect\StringFormatter\InvalidFormatterException; + +#[CoversClass(ImperialLengthFormatter::class)] +final class ImperialLengthFormatterTest extends TestCase +{ + #[Test] + #[DataProvider('providerForImperialLengthPromotion')] + public function itShouldPromoteImperialLength(string $unit, string $input, string $expected): void + { + $formatter = new ImperialLengthFormatter($unit); + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForImperialLengthPromotion(): array + { + return [ + 'inches to feet' => ['in', '12', '1ft'], + 'inches to yards' => ['in', '36', '1yd'], + 'inches to miles' => ['in', '63360', '1mi'], + 'feet to miles' => ['ft', '5280', '1mi'], + 'negative inches to feet' => ['in', '-12', '-1ft'], + 'zero keeps base unit' => ['yd', '0', '0yd'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new ImperialLengthFormatter('in'); + $actual = $formatter->format($input); + + self::assertSame($input, $actual); + } + + /** @return array */ + public static function providerForNonNumericInput(): array + { + return [ + 'empty string' => [''], + 'text' => ['abc'], + 'mixed text and numbers' => ['123abc'], + 'multiple decimals' => ['1.2.3'], + ]; + } + + #[Test] + public function itShouldThrowExceptionWhenUnitIsInvalid(): void + { + $this->expectException(InvalidFormatterException::class); + + new ImperialLengthFormatter('cm'); + } +} diff --git a/tests/Unit/ImperialMassFormatterTest.php b/tests/Unit/ImperialMassFormatterTest.php new file mode 100644 index 0000000..213f8e8 --- /dev/null +++ b/tests/Unit/ImperialMassFormatterTest.php @@ -0,0 +1,74 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Test\Unit; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Respect\StringFormatter\ImperialMassFormatter; +use Respect\StringFormatter\InvalidFormatterException; + +#[CoversClass(ImperialMassFormatter::class)] +final class ImperialMassFormatterTest extends TestCase +{ + #[Test] + #[DataProvider('providerForImperialMassPromotion')] + public function itShouldPromoteImperialMass(string $unit, string $input, string $expected): void + { + $formatter = new ImperialMassFormatter($unit); + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForImperialMassPromotion(): array + { + return [ + 'ounces to pounds' => ['oz', '16', '1lb'], + 'pounds to stones' => ['lb', '14', '1st'], + 'pounds to long tons' => ['lb', '2240', '1ton'], + 'pounds to ounces (decimal input)' => ['lb', '0.5', '8oz'], + 'negative ounces to pounds' => ['oz', '-16', '-1lb'], + 'zero keeps base unit' => ['st', '0', '0st'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new ImperialMassFormatter('lb'); + $actual = $formatter->format($input); + + self::assertSame($input, $actual); + } + + /** @return array */ + public static function providerForNonNumericInput(): array + { + return [ + 'empty string' => [''], + 'text' => ['abc'], + 'mixed text and numbers' => ['123abc'], + 'multiple decimals' => ['1.2.3'], + ]; + } + + #[Test] + public function itShouldThrowExceptionWhenUnitIsInvalid(): void + { + $this->expectException(InvalidFormatterException::class); + + new ImperialMassFormatter('kg'); + } +} diff --git a/tests/Unit/MassFormatterTest.php b/tests/Unit/MassFormatterTest.php new file mode 100644 index 0000000..e9e8dda --- /dev/null +++ b/tests/Unit/MassFormatterTest.php @@ -0,0 +1,77 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Test\Unit; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Respect\StringFormatter\InvalidFormatterException; +use Respect\StringFormatter\MassFormatter; + +#[CoversClass(MassFormatter::class)] +final class MassFormatterTest extends TestCase +{ + #[Test] + #[DataProvider('providerForMassPromotion')] + public function itShouldPromoteMass(string $unit, string $input, string $expected): void + { + $formatter = new MassFormatter($unit); + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForMassPromotion(): array + { + return [ + 'grams to kilograms' => ['g', '1000', '1kg'], + 'grams to milligrams' => ['g', '0.001', '1mg'], + 'kilograms to tonnes' => ['kg', '1000', '1t'], + 'milligrams to grams' => ['mg', '1000', '1g'], + 'negative mass' => ['g', '-1000', '-1kg'], + 'zero keeps base unit' => ['g', '0', '0g'], + 'no rounding applied' => ['g', '1.23000', '1.23g'], + 'scientific notation supported' => ['g', '1e6', '1t'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new MassFormatter('g'); + $actual = $formatter->format($input); + + self::assertSame($input, $actual); + } + + /** @return array */ + public static function providerForNonNumericInput(): array + { + return [ + 'empty string' => [''], + 'text' => ['abc'], + 'mixed text and numbers' => ['123abc'], + 'multiple decimals' => ['1.2.3'], + ]; + } + + #[Test] + public function itShouldThrowExceptionWhenUnitIsInvalid(): void + { + $this->expectException(InvalidFormatterException::class); + $this->expectExceptionMessage('Unsupported metric mass unit'); + + new MassFormatter('invalid'); + } +} diff --git a/tests/Unit/MetricFormatterTest.php b/tests/Unit/MetricFormatterTest.php new file mode 100644 index 0000000..ad69f0e --- /dev/null +++ b/tests/Unit/MetricFormatterTest.php @@ -0,0 +1,83 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Test\Unit; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Respect\StringFormatter\InvalidFormatterException; +use Respect\StringFormatter\MetricFormatter; + +#[CoversClass(MetricFormatter::class)] +final class MetricFormatterTest extends TestCase +{ + #[Test] + #[DataProvider('providerForMetricLengthPromotion')] + public function itShouldPromoteMetricLength(string $unit, string $input, string $expected): void + { + $formatter = new MetricFormatter($unit); + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForMetricLengthPromotion(): array + { + return [ + 'example 1000m to km' => ['m', '1000', '1km'], + 'example 0.1m to cm' => ['m', '0.1', '10cm'], + + 'meters to millimeters' => ['m', '0.001', '1mm'], + 'too small stays smallest' => ['m', '0.0009', '0.9mm'], + 'meters stays meters under 1000' => ['m', '999.999', '999.999m'], + 'negative meters to km' => ['m', '-1000', '-1km'], + 'zero keeps base unit' => ['m', '0', '0m'], + + 'centimeters to meters' => ['cm', '100', '1m'], + 'millimeters to kilometers' => ['mm', '1000000', '1km'], + + 'scientific notation supported' => ['m', '1e6', '1000km'], + 'no rounding applied' => ['m', '1.234500', '1.2345m'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new MetricFormatter('m'); + $actual = $formatter->format($input); + + self::assertSame($input, $actual); + } + + /** @return array */ + public static function providerForNonNumericInput(): array + { + return [ + 'empty string' => [''], + 'text' => ['abc'], + 'mixed text and numbers' => ['123abc'], + 'multiple decimals' => ['1.2.3'], + ]; + } + + #[Test] + public function itShouldThrowExceptionWhenUnitIsInvalid(): void + { + $this->expectException(InvalidFormatterException::class); + $this->expectExceptionMessage('Unsupported metric length unit'); + + new MetricFormatter('invalid'); + } +} diff --git a/tests/Unit/TimeFormatterTest.php b/tests/Unit/TimeFormatterTest.php new file mode 100644 index 0000000..079e231 --- /dev/null +++ b/tests/Unit/TimeFormatterTest.php @@ -0,0 +1,78 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Test\Unit; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Respect\StringFormatter\InvalidFormatterException; +use Respect\StringFormatter\TimeFormatter; + +#[CoversClass(TimeFormatter::class)] +final class TimeFormatterTest extends TestCase +{ + #[Test] + #[DataProvider('providerForTimePromotion')] + public function itShouldPromoteTime(string $unit, string $input, string $expected): void + { + $formatter = new TimeFormatter($unit); + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForTimePromotion(): array + { + return [ + 'seconds to minutes' => ['s', '60', '1min'], + 'seconds to hours' => ['s', '3600', '1h'], + 'seconds to days' => ['s', '86400', '1d'], + 'seconds to weeks' => ['s', '604800', '1w'], + 'seconds to months' => ['s', '2628000', '1mo'], + 'seconds to years' => ['s', '31536000', '1y'], + 'seconds to milliseconds' => ['s', '0.001', '1ms'], + 'seconds to microseconds (scientific notation)' => ['s', '1e-6', '1us'], + 'negative seconds to minutes' => ['s', '-60', '-1min'], + 'zero keeps base unit' => ['ms', '0', '0ms'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new TimeFormatter('s'); + $actual = $formatter->format($input); + + self::assertSame($input, $actual); + } + + /** @return array */ + public static function providerForNonNumericInput(): array + { + return [ + 'empty string' => [''], + 'text' => ['abc'], + 'mixed text and numbers' => ['123abc'], + 'multiple decimals' => ['1.2.3'], + ]; + } + + #[Test] + public function itShouldThrowExceptionWhenUnitIsInvalid(): void + { + $this->expectException(InvalidFormatterException::class); + + new TimeFormatter('month'); + } +}