From e02fed5187097624024c228e2320096f3c1bded3 Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Fri, 10 Jul 2026 20:51:47 +0400 Subject: [PATCH 1/5] feat: add speed, data, force, energy, power, pressure, frequency, angle, and more base units --- src/Unit/Name.php | 182 +++++++++++++++++++++---- src/Unit/bootstrap.php | 81 ++++++++++- tests/unit_families/unit_families.phpt | 28 ++++ 3 files changed, 260 insertions(+), 31 deletions(-) create mode 100644 tests/unit_families/unit_families.phpt diff --git a/src/Unit/Name.php b/src/Unit/Name.php index 5f5a7f2..0f6ef63 100644 --- a/src/Unit/Name.php +++ b/src/Unit/Name.php @@ -4,39 +4,106 @@ enum Name: string { - // Length units - case MM = 'mm'; // millimeter - case CM = 'cm'; // centimeter - case M = 'm'; // meter - case KM = 'km'; // kilometer - case INCH = 'in'; // inch - case FT = 'ft'; // foot - - // Mass units - case G = 'g'; // gram - case KG = 'kg'; // kilogram - case MG = 'mg'; // milligram - - // Time units - case S = 's'; // second - case MIN = 'min'; // minute - case H = 'h'; // hour - - // Area units - case M2 = 'm²'; + // Length + case MM = 'mm'; + case CM = 'cm'; + case M = 'm'; + case KM = 'km'; + case INCH = 'in'; + case FT = 'ft'; + case YD = 'yd'; + case MI = 'mi'; + + // Mass + case MG = 'mg'; + case G = 'g'; + case KG = 'kg'; + case TON = 't'; + case LB = 'lb'; + case OZ = 'oz'; + + // Time + case MS = 'ms'; + case S = 's'; + case MIN = 'min'; + case H = 'h'; + case DAY = 'd'; + case WK = 'wk'; + + // Area case CM2 = 'cm²'; + case M2 = 'm²'; + case KM2 = 'km²'; + case FT2 = 'ft²'; + case HA = 'ha'; + case ACRE = 'acre'; - // Volume units - case L = 'L'; // liter - case ML = 'mL'; // milliliter + // Volume + case ML = 'mL'; + case L = 'L'; case M3 = 'm³'; + case GAL = 'gal'; - // Temperature units (if supported) + // Temperature case C = '°C'; case F = '°F'; case K = 'K'; - // Helper: full unit name + // Speed + case MPS = 'm/s'; + case KMH = 'km/h'; + case MPH = 'mph'; + case KNOT = 'kn'; + + // Data + case BIT = 'bit'; + case BYTE = 'B'; + case KB = 'KB'; + case MB = 'MB'; + case GB = 'GB'; + case TB = 'TB'; + case KIB = 'KiB'; + case MIB = 'MiB'; + case GIB = 'GiB'; + case TIB = 'TiB'; + + // Force + case N = 'N'; + case KN = 'kN'; + + // Energy + case J = 'J'; + case KJ = 'kJ'; + case CAL = 'cal'; + case KCAL = 'kcal'; + case WH = 'Wh'; + case KWH = 'kWh'; + + // Power + case W = 'W'; + case KW = 'kW'; + case HP = 'hp'; + + // Pressure + case PA = 'Pa'; + case KPA = 'kPa'; + case BAR = 'bar'; + case ATM = 'atm'; + case PSI = 'psi'; + + // Frequency + case HZ = 'Hz'; + case KHZ = 'kHz'; + case MHZ = 'MHz'; + case GHZ = 'GHz'; + + // Angle + case RAD = 'rad'; + case DEG = 'deg'; + case GRAD = 'grad'; + case TURN = 'turn'; + + /** Human-readable name for the unit. */ public function label(): string { return match ($this) { @@ -46,25 +113,84 @@ public function label(): string self::KM => 'Kilometer', self::INCH => 'Inch', self::FT => 'Foot', + self::YD => 'Yard', + self::MI => 'Mile', + self::MG => 'Milligram', self::G => 'Gram', self::KG => 'Kilogram', - self::MG => 'Milligram', + self::TON => 'Tonne', + self::LB => 'Pound', + self::OZ => 'Ounce', + self::MS => 'Millisecond', self::S => 'Second', self::MIN => 'Minute', self::H => 'Hour', + self::DAY => 'Day', + self::WK => 'Week', - self::M2 => 'Square Meter', self::CM2 => 'Square Centimeter', + self::M2 => 'Square Meter', + self::KM2 => 'Square Kilometer', + self::FT2 => 'Square Foot', + self::HA => 'Hectare', + self::ACRE => 'Acre', - self::L => 'Liter', self::ML => 'Milliliter', + self::L => 'Liter', self::M3 => 'Cubic Meter', + self::GAL => 'Gallon (US)', self::C => 'Celsius', self::F => 'Fahrenheit', self::K => 'Kelvin', + + self::MPS => 'Meter per second', + self::KMH => 'Kilometer per hour', + self::MPH => 'Mile per hour', + self::KNOT => 'Knot', + + self::BIT => 'Bit', + self::BYTE => 'Byte', + self::KB => 'Kilobyte', + self::MB => 'Megabyte', + self::GB => 'Gigabyte', + self::TB => 'Terabyte', + self::KIB => 'Kibibyte', + self::MIB => 'Mebibyte', + self::GIB => 'Gibibyte', + self::TIB => 'Tebibyte', + + self::N => 'Newton', + self::KN => 'Kilonewton', + + self::J => 'Joule', + self::KJ => 'Kilojoule', + self::CAL => 'Calorie', + self::KCAL => 'Kilocalorie', + self::WH => 'Watt-hour', + self::KWH => 'Kilowatt-hour', + + self::W => 'Watt', + self::KW => 'Kilowatt', + self::HP => 'Horsepower', + + self::PA => 'Pascal', + self::KPA => 'Kilopascal', + self::BAR => 'Bar', + self::ATM => 'Atmosphere', + self::PSI => 'Pound per square inch', + + self::HZ => 'Hertz', + self::KHZ => 'Kilohertz', + self::MHZ => 'Megahertz', + self::GHZ => 'Gigahertz', + + self::RAD => 'Radian', + self::DEG => 'Degree', + self::GRAD => 'Gradian', + self::TURN => 'Turn', }; } } diff --git a/src/Unit/bootstrap.php b/src/Unit/bootstrap.php index f763c54..6f2f7c4 100644 --- a/src/Unit/bootstrap.php +++ b/src/Unit/bootstrap.php @@ -23,6 +23,14 @@ $area = new Dimension(['L' => 2]); $volume = new Dimension(['L' => 3]); $temperature = new Dimension(['Θ' => 1]); +$speed = new Dimension(['L' => 1, 'T' => -1]); +$data = new Dimension(['B' => 1]); +$force = new Dimension(['M' => 1, 'L' => 1, 'T' => -2]); +$energy = new Dimension(['M' => 1, 'L' => 2, 'T' => -2]); +$power = new Dimension(['M' => 1, 'L' => 2, 'T' => -3]); +$pressure = new Dimension(['M' => 1, 'L' => -1, 'T' => -2]); +$frequency = new Dimension(['T' => -1]); +$angle = new Dimension(['A' => 1]); // Length (base unit: metre) $register(Name::MM, 0.001, $length); @@ -31,27 +39,94 @@ $register(Name::KM, 1000.0, $length); $register(Name::INCH, 0.0254, $length); $register(Name::FT, 0.3048, $length); +$register(Name::YD, 0.9144, $length); +$register(Name::MI, 1609.344, $length); // Mass (base unit: kilogram) $register(Name::MG, 1e-6, $mass); $register(Name::G, 0.001, $mass); $register(Name::KG, 1.0, $mass); +$register(Name::TON, 1000.0, $mass); +$register(Name::LB, 0.45359237, $mass); +$register(Name::OZ, 0.028349523125, $mass); // Time (base unit: second) +$register(Name::MS, 0.001, $time); $register(Name::S, 1.0, $time); $register(Name::MIN, 60.0, $time); $register(Name::H, 3600.0, $time); +$register(Name::DAY, 86400.0, $time); +$register(Name::WK, 604800.0, $time); // Area (base unit: square metre) -$register(Name::M2, 1.0, $area); $register(Name::CM2, 0.0001, $area); +$register(Name::M2, 1.0, $area); +$register(Name::KM2, 1e6, $area); +$register(Name::FT2, 0.09290304, $area); +$register(Name::HA, 10000.0, $area); +$register(Name::ACRE, 4046.8564224, $area); // Volume (base unit: cubic metre) -$register(Name::M3, 1.0, $volume); -$register(Name::L, 0.001, $volume); $register(Name::ML, 1e-6, $volume); +$register(Name::L, 0.001, $volume); +$register(Name::M3, 1.0, $volume); +$register(Name::GAL, 0.003785411784, $volume); // Temperature (base unit: kelvin) — affine scales use factor + offset. $register(Name::K, 1.0, $temperature, 0.0); $register(Name::C, 1.0, $temperature, 273.15); $register(Name::F, 5.0 / 9.0, $temperature, 273.15 - (32.0 * 5.0 / 9.0)); + +// Speed (base unit: metre per second) +$register(Name::MPS, 1.0, $speed); +$register(Name::KMH, 1000.0 / 3600.0, $speed); +$register(Name::MPH, 0.44704, $speed); +$register(Name::KNOT, 1852.0 / 3600.0, $speed); + +// Data (base unit: byte) +$register(Name::BIT, 0.125, $data); +$register(Name::BYTE, 1.0, $data); +$register(Name::KB, 1e3, $data); +$register(Name::MB, 1e6, $data); +$register(Name::GB, 1e9, $data); +$register(Name::TB, 1e12, $data); +$register(Name::KIB, 1024.0, $data); +$register(Name::MIB, 1024.0 ** 2, $data); +$register(Name::GIB, 1024.0 ** 3, $data); +$register(Name::TIB, 1024.0 ** 4, $data); + +// Force (base unit: newton) +$register(Name::N, 1.0, $force); +$register(Name::KN, 1000.0, $force); + +// Energy (base unit: joule) +$register(Name::J, 1.0, $energy); +$register(Name::KJ, 1000.0, $energy); +$register(Name::CAL, 4.184, $energy); +$register(Name::KCAL, 4184.0, $energy); +$register(Name::WH, 3600.0, $energy); +$register(Name::KWH, 3.6e6, $energy); + +// Power (base unit: watt) +$register(Name::W, 1.0, $power); +$register(Name::KW, 1000.0, $power); +$register(Name::HP, 745.6998715822702, $power); + +// Pressure (base unit: pascal) +$register(Name::PA, 1.0, $pressure); +$register(Name::KPA, 1000.0, $pressure); +$register(Name::BAR, 1e5, $pressure); +$register(Name::ATM, 101325.0, $pressure); +$register(Name::PSI, 6894.757293168361, $pressure); + +// Frequency (base unit: hertz) +$register(Name::HZ, 1.0, $frequency); +$register(Name::KHZ, 1e3, $frequency); +$register(Name::MHZ, 1e6, $frequency); +$register(Name::GHZ, 1e9, $frequency); + +// Angle (base unit: radian) +$register(Name::RAD, 1.0, $angle); +$register(Name::DEG, M_PI / 180.0, $angle); +$register(Name::GRAD, M_PI / 200.0, $angle); +$register(Name::TURN, 2.0 * M_PI, $angle); diff --git a/tests/unit_families/unit_families.phpt b/tests/unit_families/unit_families.phpt new file mode 100644 index 0000000..0182369 --- /dev/null +++ b/tests/unit_families/unit_families.phpt @@ -0,0 +1,28 @@ +--TEST-- +Conversions across the extended unit families (speed, data, energy, pressure, angle) +--FILE-- +to('mph')->format(2) . "\n"; +echo Quantity::of(1, 'GB')->to('MB') . "\n"; +echo Quantity::of(1, 'GiB')->to('MiB') . "\n"; +echo Quantity::of(1, 'kWh')->to('J') . "\n"; +echo Quantity::of(1, 'mi')->to('km') . "\n"; +echo Quantity::of(1, 'atm')->to('psi')->format(3) . "\n"; +echo Quantity::of(180, 'deg')->to('rad')->format(5) . "\n"; +echo Quantity::of(1, 'hp')->to('W')->format(2) . "\n"; +echo Quantity::of(1, 'lb')->to('g')->format(2) . "\n"; +?> +--EXPECT-- +62.14 mph +1000 MB +1024 MiB +3600000 J +1.609344 km +14.696 psi +3.14159 rad +745.70 W +453.59 g From e196d8210d2938726d417d715cbe00734ad69aa6 Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Fri, 10 Jul 2026 20:52:44 +0400 Subject: [PATCH 2/5] feat: add Quantity::parse() for human-readable and multi-segment strings --- src/Unit/Quantity.php | 25 +++++++++++++++++++++++++ tests/parse/parse.phpt | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tests/parse/parse.phpt diff --git a/src/Unit/Quantity.php b/src/Unit/Quantity.php index acdae2a..bc74d79 100644 --- a/src/Unit/Quantity.php +++ b/src/Unit/Quantity.php @@ -34,6 +34,31 @@ public static function of(float $value, string $symbol, ?int $precision = null): return self::from($value, $symbol, $precision); } + /** + * Parse a human-readable string into a Quantity. + * + * Supports single values ("100 km/h", "-40 °C") and multi-segment inputs of + * the same dimension ("5 ft 3 in"), which are summed. The result is expressed + * in the first segment's unit. + * + * @throws \InvalidArgumentException on empty input, unknown units, or mixed dimensions. + */ + public static function parse(string $input, ?int $precision = null): self + { + if (!preg_match_all('/([+-]?\d+(?:\.\d+)?)\s*([^\s\d]+)/u', $input, $matches, PREG_SET_ORDER)) { + throw new \InvalidArgumentException("Could not parse a quantity from: \"{$input}\"."); + } + + $result = null; + foreach ($matches as [, $value, $symbol]) { + $part = self::from((float) $value, $symbol); + $result = $result === null ? $part : $result->add($part); + } + + /** @var self $result */ + return $precision === null ? $result : $result->withPrecision($precision); + } + public function convertTo(string $symbol): self { $target = UnitRegistry::get($symbol); diff --git a/tests/parse/parse.phpt b/tests/parse/parse.phpt new file mode 100644 index 0000000..3893960 --- /dev/null +++ b/tests/parse/parse.phpt @@ -0,0 +1,32 @@ +--TEST-- +Quantity::parse() handles single, compound, and multi-segment strings +--FILE-- +to('mph')->format(2) . "\n"; +echo Quantity::parse('-40 °C')->to('°F')->format(1) . "\n"; +echo Quantity::parse('5 ft 3 in')->to('cm')->format(2) . "\n"; +echo Quantity::parse('2.5 kg')->to('g') . "\n"; + +try { + Quantity::parse('hello'); +} catch (\InvalidArgumentException $e) { + echo $e->getMessage() . "\n"; +} + +try { + Quantity::parse('5 m 3 kg'); +} catch (\InvalidArgumentException $e) { + echo $e->getMessage() . "\n"; +} +?> +--EXPECT-- +62.14 mph +-40.0 °F +160.02 cm +2500 g +Could not parse a quantity from: "hello". +Cannot add: units are dimensionally incompatible. From cbfafc0cb73fe19ab7797ed97ac74045beb7d66f Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Fri, 10 Jul 2026 20:54:06 +0400 Subject: [PATCH 3/5] feat: add Quantity::humanize() to auto-select the most readable unit --- src/Unit/Quantity.php | 54 ++++++++++++++++++++++++++++++++++++ tests/humanize/humanize.phpt | 26 +++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 tests/humanize/humanize.phpt diff --git a/src/Unit/Quantity.php b/src/Unit/Quantity.php index bc74d79..e99446d 100644 --- a/src/Unit/Quantity.php +++ b/src/Unit/Quantity.php @@ -123,6 +123,60 @@ public function divide(Quantity $other): self return new self($value, $newUnit, $this->precision); } + /** + * Convert to the most readable unit in the same family (e.g. 1500 m -> 1.5 km). + * + * Picks the largest ladder unit whose value stays >= 1. Returns the quantity + * unchanged when it has no ladder or uses an affine scale (e.g. temperature). + */ + public function humanize(): self + { + if ($this->unit->offset !== 0.0) { + return $this; + } + + foreach (self::humanizeLadders() as $symbols) { + if (!UnitRegistry::has($symbols[0])) { + continue; + } + if (!UnitRegistry::get($symbols[0])->dimension->equals($this->unit->dimension)) { + continue; + } + + $base = $this->toBase(); + $chosen = $symbols[0]; + foreach ($symbols as $symbol) { + $candidate = UnitRegistry::get($symbol); + if (abs($base / $candidate->factor) >= 1.0) { + $chosen = $symbol; + } else { + break; + } + } + + return $this->convertTo($chosen); + } + + return $this; + } + + /** + * Ladders of unit symbols (smallest to largest) used by {@see self::humanize()}. + * + * @return list> + */ + private static function humanizeLadders(): array + { + return [ + ['mm', 'cm', 'm', 'km'], + ['mg', 'g', 'kg', 't'], + ['ms', 's', 'min', 'h', 'd'], + ['mL', 'L', 'm³'], + ['cm²', 'm²', 'km²'], + ['B', 'KB', 'MB', 'GB', 'TB'], + ]; + } + /** Compares two quantities after converting to a common base. */ public function equals(Quantity $other, float $epsilon = 1e-9): bool { diff --git a/tests/humanize/humanize.phpt b/tests/humanize/humanize.phpt new file mode 100644 index 0000000..5cc7c3b --- /dev/null +++ b/tests/humanize/humanize.phpt @@ -0,0 +1,26 @@ +--TEST-- +Quantity::humanize() picks the most readable unit +--FILE-- +humanize() . "\n"; +echo Quantity::of(999, 'm')->humanize() . "\n"; +echo Quantity::of(0.5, 'mm')->humanize() . "\n"; +echo Quantity::of(5400, 's')->humanize() . "\n"; +echo Quantity::of(1500, 'B')->humanize() . "\n"; +echo Quantity::of(2500000, 'B')->humanize()->format(2) . "\n"; +echo Quantity::of(2500, 'g')->humanize() . "\n"; +echo Quantity::of(25, '°C')->humanize() . "\n"; +?> +--EXPECT-- +1.5 km +999 m +0.5 mm +1.5 h +1.5 KB +2.50 MB +2.5 kg +25 °C From e2c28dea457669d185452c9dafafec8583a1599a Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Fri, 10 Jul 2026 20:58:05 +0400 Subject: [PATCH 4/5] feat: add Laravel service provider and Eloquent AsQuantity cast --- composer.json | 14 ++++++- src/Laravel/AsQuantity.php | 57 +++++++++++++++++++++++++++++ src/Laravel/UnitServiceProvider.php | 39 ++++++++++++++++++++ tests/laravel/cast.phpt | 42 +++++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/Laravel/AsQuantity.php create mode 100644 src/Laravel/UnitServiceProvider.php create mode 100644 tests/laravel/cast.phpt diff --git a/composer.json b/composer.json index b8d1164..dfedf87 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ }, "autoload": { "psr-4": { + "KhaledAlam\\Unit\\Laravel\\": "src/Laravel", "KhaledAlam\\Unit\\": "src/Unit" }, "files": [ @@ -46,7 +47,11 @@ "require-dev": { "phpunit/phpunit": "^11.4", "phpstan/phpstan": "^2.0", - "friendsofphp/php-cs-fixer": "^3.64" + "friendsofphp/php-cs-fixer": "^3.64", + "illuminate/database": "^11.0 || ^12.0" + }, + "suggest": { + "illuminate/database": "For the Laravel service provider and Eloquent 'AsQuantity' cast." }, "scripts": { "test": "phpunit --configuration phpunit.xml.dist --testsuite=unit", @@ -54,6 +59,13 @@ "cs": "php-cs-fixer fix --dry-run --diff", "cs-fix": "php-cs-fixer fix" }, + "extra": { + "laravel": { + "providers": [ + "KhaledAlam\\Unit\\Laravel\\UnitServiceProvider" + ] + } + }, "config": { "sort-packages": true }, diff --git a/src/Laravel/AsQuantity.php b/src/Laravel/AsQuantity.php new file mode 100644 index 0000000..6b080b2 --- /dev/null +++ b/src/Laravel/AsQuantity.php @@ -0,0 +1,57 @@ + AsQuantity::class]; + * } + * + * @implements CastsAttributes + */ +final class AsQuantity implements CastsAttributes +{ + /** + * @param array $attributes + */ + public function get(Model $model, string $key, mixed $value, array $attributes): ?Quantity + { + if ($value instanceof Quantity) { + return $value; + } + + return is_string($value) && $value !== '' ? Quantity::parse($value) : null; + } + + /** + * @param array $attributes + * @return array + */ + public function set(Model $model, string $key, mixed $value, array $attributes): array + { + if ($value === null) { + return [$key => null]; + } + + if ($value instanceof Quantity) { + return [$key => (string) $value]; + } + + if (is_string($value)) { + return [$key => (string) Quantity::parse($value)]; + } + + throw new \InvalidArgumentException( + 'AsQuantity expects a Quantity or string, got ' . get_debug_type($value) . '.' + ); + } +} diff --git a/src/Laravel/UnitServiceProvider.php b/src/Laravel/UnitServiceProvider.php new file mode 100644 index 0000000..697a3b6 --- /dev/null +++ b/src/Laravel/UnitServiceProvider.php @@ -0,0 +1,39 @@ + [ + * new \KhaledAlam\Unit\Unit('yard', 'yd', 0.9144, new \KhaledAlam\Unit\Dimension(['L' => 1])), + * ], + * ]; + */ +final class UnitServiceProvider extends ServiceProvider +{ + public function boot(): void + { + /** @var Repository $config */ + $config = $this->app->make('config'); + + $units = $config->get('unit.units', []); + + foreach (is_array($units) ? $units : [] as $unit) { + if ($unit instanceof Unit) { + UnitRegistry::register($unit); + } + } + } +} diff --git a/tests/laravel/cast.phpt b/tests/laravel/cast.phpt new file mode 100644 index 0000000..40eff3d --- /dev/null +++ b/tests/laravel/cast.phpt @@ -0,0 +1,42 @@ +--TEST-- +Laravel AsQuantity Eloquent cast round-trips quantities +--SKIPIF-- + +--FILE-- + AsQuantity::class]; + } +}; + +$model->distance = Quantity::of(2.5, 'km'); +echo $model->getAttributes()['distance'] . "\n"; // stored string +echo $model->distance->to('m') . "\n"; // hydrated + converted + +$model->distance = '5 ft 3 in'; +echo $model->getAttributes()['distance'] . "\n"; // parsed + stored +echo $model->distance->humanize()->format(4) . "\n"; // hydrated + humanized + +$model->distance = null; +var_dump($model->distance); +?> +--EXPECT-- +2.5 km +2500 m +5.25 ft +1.6002 m +NULL From 68495636134d0ca3927abcd81917f5e6c4a76d33 Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Fri, 10 Jul 2026 20:59:54 +0400 Subject: [PATCH 5/5] docs: document parse, humanize, new units, and Laravel; expand live demo and examples --- CHANGELOG.md | 12 ++++++ README.md | 70 ++++++++++++++++++++++++++++++--- docs/index.html | 26 ++++++++---- examples/parse-and-humanize.php | 20 ++++++++++ 4 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 examples/parse-and-humanize.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c415930..155fdab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- **String parsing** — `Quantity::parse('100 km/h')`, `Quantity::parse('5 ft 3 in')` + (multi-segment inputs of the same dimension are summed). +- **`Quantity::humanize()`** — auto-selects the most readable unit (e.g. `1500 m` → `1.5 km`). +- **Many more units** — speed (`km/h`, `mph`, `kn`), data (`KB`…`TiB`, `bit`), force (`N`), + energy (`J`, `kWh`, `cal`), power (`W`, `hp`), pressure (`Pa`, `bar`, `psi`), frequency + (`Hz`…`GHz`), angle (`rad`, `deg`), plus more length/mass/time/area/volume units. +- **Laravel integration** — auto-discovered `UnitServiceProvider` and an Eloquent + `AsQuantity` cast; register custom units via `config/unit.php`. + ## [1.1.0] - 2026-07-10 ### Added diff --git a/README.md b/README.md index 6ef2953..d0e9cef 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,22 @@ echo Quantity::of(32, '°F')->to('°C')->format(1); // "0.0 °C" echo Quantity::of(25, '°C')->to('K')->format(2); // "298.15 K" ``` +### Parsing strings + +```php +echo Quantity::parse('100 km/h')->to('mph')->format(2); // "62.14 mph" +echo Quantity::parse('-40 °C')->to('°F')->format(1); // "-40.0 °F" +echo Quantity::parse('5 ft 3 in')->to('cm')->format(2); // "160.02 cm" (segments summed) +``` + +### Humanize (auto-pick the best unit) + +```php +echo Quantity::of(1500, 'm')->humanize(); // "1.5 km" +echo Quantity::of(2500000, 'B')->humanize(); // "2.5 MB" +echo Quantity::of(5400, 's')->humanize(); // "1.5 h" +``` + ### Comparison ```php @@ -152,17 +168,61 @@ Quantity::of(5, 'kg')->add(Quantity::of(3, 's')); // InvalidArgumentException | Dimension | Units | | --- | --- | -| Length | `mm`, `cm`, `m`, `km`, `in`, `ft` | -| Mass | `mg`, `g`, `kg` | -| Time | `s`, `min`, `h` | -| Area | `cm²`, `m²` | -| Volume | `mL`, `L`, `m³` | +| Length | `mm`, `cm`, `m`, `km`, `in`, `ft`, `yd`, `mi` | +| Mass | `mg`, `g`, `kg`, `t`, `lb`, `oz` | +| Time | `ms`, `s`, `min`, `h`, `d`, `wk` | +| Area | `cm²`, `m²`, `km²`, `ft²`, `ha`, `acre` | +| Volume | `mL`, `L`, `m³`, `gal` | | Temperature | `°C`, `°F`, `K` | +| Speed | `m/s`, `km/h`, `mph`, `kn` | +| Data | `bit`, `B`, `KB`, `MB`, `GB`, `TB`, `KiB`, `MiB`, `GiB`, `TiB` | +| Force | `N`, `kN` | +| Energy | `J`, `kJ`, `cal`, `kcal`, `Wh`, `kWh` | +| Power | `W`, `kW`, `hp` | +| Pressure | `Pa`, `kPa`, `bar`, `atm`, `psi` | +| Frequency | `Hz`, `kHz`, `MHz`, `GHz` | +| Angle | `rad`, `deg`, `grad`, `turn` | Need more? [Open an issue](https://github.com/khaledalam/unit/issues) or register your own. --- +## Laravel integration + +The package ships a service provider (auto-discovered) and an Eloquent cast. Store a +quantity in a single column and get a `Quantity` back on access: + +```php +use Illuminate\Database\Eloquent\Model; +use KhaledAlam\Unit\Laravel\AsQuantity; + +class Parcel extends Model +{ + protected function casts(): array + { + return ['weight' => AsQuantity::class]; + } +} + +$parcel->weight = Quantity::of(2.5, 'kg'); // stored as "2.5 kg" +$parcel->weight->to('g'); // 2500 g +$parcel->weight = '5 ft 3 in'; // strings are parsed on the way in +``` + +Register app-specific units via `config/unit.php`: + +```php +return [ + 'units' => [ + new \KhaledAlam\Unit\Unit('furlong', 'fur', 201.168, new \KhaledAlam\Unit\Dimension(['L' => 1])), + ], +]; +``` + +> Requires `illuminate/database` (already present in any Laravel app). + +--- + ## Examples Runnable scripts live in [`examples/`](examples): diff --git a/docs/index.html b/docs/index.html index 5401653..6f627bd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -172,14 +172,26 @@

Install