diff --git a/README.md b/README.md index 8a6828b..bcdd527 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,20 @@ 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 | +| ---------------------------------------------------------- | ---------------------------------------------------------------- | +| [AreaFormatter](docs/AreaFormatter.md) | Metric area promotion (mm², cm², m², a, ha, km²) | +| [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/AreaFormatter.md b/docs/AreaFormatter.md new file mode 100644 index 0000000..8cda2c9 --- /dev/null +++ b/docs/AreaFormatter.md @@ -0,0 +1,34 @@ + + +# AreaFormatter + +The `AreaFormatter` promotes metric area values between `mm²`, `cm²`, `m²`, `a`, `ha`, and `km²`. + +- Non-numeric input is returned unchanged. +- Promotion is based on magnitude. +- Output uses symbols only (no spaces), e.g. `1m²`, `2ha`. + +## Usage + +```php +use Respect\StringFormatter\AreaFormatter; + +$formatter = new AreaFormatter('m^2'); + +echo $formatter->format('10000'); +// Outputs: 1ha +``` + +## API + +### `AreaFormatter::__construct` + +- `__construct(string $unit)` + +The `$unit` is the input unit (the unit you are providing values in). + +Accepted units: `mm^2`, `cm^2`, `m^2`, `a`, `ha`, `km^2`. diff --git a/docs/ImperialAreaFormatter.md b/docs/ImperialAreaFormatter.md new file mode 100644 index 0000000..7480a59 --- /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^2'); + +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^2`, `ft^2`, `yd^2`, `ac`, `mi^2`. 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..42b5cc7 --- /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 on non-standard symbols + +- `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/AreaFormatter.php b/src/AreaFormatter.php new file mode 100644 index 0000000..4c45c3f --- /dev/null +++ b/src/AreaFormatter.php @@ -0,0 +1,45 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class AreaFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_RATIOS = [ + 'km^2' => [1_000_000, 1], + 'ha' => [10_000, 1], + 'a' => [100, 1], + 'm^2' => [1, 1], + 'cm^2' => [1, 10_000], + 'mm^2' => [1, 1_000_000], + ]; + + private const array UNIT_ALIASES = [ + 'km^2' => 'km²', + 'ha' => 'ha', + 'a' => 'a', + 'm^2' => 'm²', + 'cm^2' => 'cm²', + 'mm^2' => 'mm²', + ]; + + public function __construct(string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported area unit'); + } + + $this->unit = $unit; + } +} diff --git a/src/ImperialAreaFormatter.php b/src/ImperialAreaFormatter.php new file mode 100644 index 0000000..d08c74b --- /dev/null +++ b/src/ImperialAreaFormatter.php @@ -0,0 +1,43 @@ + + */ + +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^2' => [4_014_489_600, 1], + 'ac' => [6_272_640, 1], + 'yd^2' => [1_296, 1], + 'ft^2' => [144, 1], + 'in^2' => [1, 1], + ]; + + private const array UNIT_ALIASES = [ + 'mi^2' => 'mi²', + 'ac' => 'ac', + 'yd^2' => 'yd²', + 'ft^2' => 'ft²', + 'in^2' => 'in²', + ]; + + public function __construct(string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported imperial area unit'); + } + + $this->unit = $unit; + } +} diff --git a/src/ImperialLengthFormatter.php b/src/ImperialLengthFormatter.php new file mode 100644 index 0000000..d789e1f --- /dev/null +++ b/src/ImperialLengthFormatter.php @@ -0,0 +1,36 @@ + + */ + +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], + ]; + + private const array UNIT_ALIASES = []; + + public function __construct(string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported imperial length unit'); + } + + $this->unit = $unit; + } +} diff --git a/src/ImperialMassFormatter.php b/src/ImperialMassFormatter.php new file mode 100644 index 0000000..1c40f05 --- /dev/null +++ b/src/ImperialMassFormatter.php @@ -0,0 +1,36 @@ + + */ + +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], + ]; + + private const array UNIT_ALIASES = []; + + public function __construct(string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported imperial mass unit'); + } + + $this->unit = $unit; + } +} diff --git a/src/Internal/UnitPromoter.php b/src/Internal/UnitPromoter.php new file mode 100644 index 0000000..08f3891 --- /dev/null +++ b/src/Internal/UnitPromoter.php @@ -0,0 +1,93 @@ + + */ + +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 +{ + private readonly string $unit; + + public function format(string $input): string + { + return self::promote( + input: $input, + inputUnit: $this->unit, + ratiosToBase: self::UNIT_RATIOS, + unitAliases: self::UNIT_ALIASES, + orderedUnits: array_keys(self::UNIT_RATIOS), + smallestUnit: array_key_last(self::UNIT_RATIOS), + largestUnit: array_key_first(self::UNIT_RATIOS), + ); + } + + /** + * @param array $ratiosToBase + * @param array $unitAliases + * @param list $orderedUnits + */ + private static function promote( + string $input, + string $inputUnit, + array $ratiosToBase, + array $orderedUnits, + array $unitAliases, + string $smallestUnit, + string $largestUnit, + ): string { + if (!is_numeric($input)) { + return $input; + } + + $amount = (float) $input; + if ($amount == 0) { + return '0' . ($unitAliases[$inputUnit] ?? $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 . ($unitAliases[$bestUnit] ?? $bestUnit); + } +} diff --git a/src/MassFormatter.php b/src/MassFormatter.php new file mode 100644 index 0000000..565c656 --- /dev/null +++ b/src/MassFormatter.php @@ -0,0 +1,36 @@ + + */ + +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], + ]; + + private const array UNIT_ALIASES = []; + + public function __construct(string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported metric mass unit'); + } + + $this->unit = $unit; + } +} diff --git a/src/MetricFormatter.php b/src/MetricFormatter.php new file mode 100644 index 0000000..d42e4e2 --- /dev/null +++ b/src/MetricFormatter.php @@ -0,0 +1,36 @@ + + */ + +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], + ]; + + private const array UNIT_ALIASES = []; + + public function __construct(string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported metric length unit'); + } + + $this->unit = $unit; + } +} diff --git a/src/Mixin/Builder.php b/src/Mixin/Builder.php index e2ff60f..0f13ba4 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,32 @@ /** @mixin FormatterBuilder */ interface Builder { - public static function mask(string $range, string $replacement = '*'): Chain; + public function area(string $unit): FormatterBuilder; - public static function pattern(string $pattern): Chain; + 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 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..780ba0b 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,32 @@ interface Chain extends Formatter { + public function area(string $unit): FormatterBuilder; + + 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..ebceb87 --- /dev/null +++ b/src/TimeFormatter.php @@ -0,0 +1,45 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use Respect\StringFormatter\Internal\UnitPromoter; + +final readonly class TimeFormatter implements Formatter +{ + use UnitPromoter; + + private const array UNIT_ALIASES = []; + + 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(string $unit) + { + if (!isset(self::UNIT_RATIOS[$unit])) { + throw new InvalidFormatterException('Unsupported time unit'); + } + + $this->unit = $unit; + } +} diff --git a/tests/Unit/AreaFormatterTest.php b/tests/Unit/AreaFormatterTest.php new file mode 100644 index 0000000..bb47b41 --- /dev/null +++ b/tests/Unit/AreaFormatterTest.php @@ -0,0 +1,75 @@ + + */ + +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\AreaFormatter; +use Respect\StringFormatter\InvalidFormatterException; + +#[CoversClass(AreaFormatter::class)] +final class AreaFormatterTest extends TestCase +{ + #[Test] + #[DataProvider('providerForAreaPromotion')] + public function itShouldPromoteArea(string $unit, string $input, string $expected): void + { + $formatter = new AreaFormatter($unit); + $actual = $formatter->format($input); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForAreaPromotion(): array + { + return [ + 'square meters to ares' => ['m^2', '100', '1a'], + 'ares to hectares' => ['a', '100', '1ha'], + 'hectares to square kilometers' => ['ha', '100', '1km²'], + 'square meters to square centimeters' => ['m^2', '0.0001', '1cm²'], + 'square millimeters to square centimeters' => ['mm^2', '100', '1cm²'], + 'negative hectares to square kilometers' => ['ha', '-100', '-1km²'], + 'zero keeps base unit' => ['m^2', '0', '0m²'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new AreaFormatter('m^2'); + $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 AreaFormatter('invalid'); + } +} diff --git a/tests/Unit/ImperialAreaFormatterTest.php b/tests/Unit/ImperialAreaFormatterTest.php new file mode 100644 index 0000000..e0798e5 --- /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^2', '144', '1ft²'], + 'square feet to acres' => ['ft^2', '43560', '1ac'], + 'acres to square miles' => ['ac', '640', '1mi²'], + 'negative square feet to acres' => ['ft^2', '-43560', '-1ac'], + 'zero keeps base unit' => ['yd^2', '0', '0yd²'], + ]; + } + + #[Test] + #[DataProvider('providerForNonNumericInput')] + public function itShouldReturnInputUnchangedForNonNumericInput(string $input): void + { + $formatter = new ImperialAreaFormatter('ft^2'); + $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'); + } +}