Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 65 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
14 changes: 13 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"autoload": {
"psr-4": {
"KhaledAlam\\Unit\\Laravel\\": "src/Laravel",
"KhaledAlam\\Unit\\": "src/Unit"
},
"files": [
Expand All @@ -46,14 +47,25 @@
"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",
"analyse": "phpstan analyse",
"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
},
Expand Down
26 changes: 19 additions & 7 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,26 @@ <h2>Install</h2>
<script>
// Mirror of src/Unit/bootstrap.php — factor to base, plus affine offset for temperature.
const DIMS = {
Length: { base:'m', units:{ mm:[0.001,0], cm:[0.01,0], m:[1,0], km:[1000,0], in:[0.0254,0], ft:[0.3048,0] } },
Mass: { base:'kg', units:{ mg:[1e-6,0], g:[0.001,0], kg:[1,0] } },
Time: { base:'s', units:{ s:[1,0], min:[60,0], h:[3600,0] } },
Area: { base:'m²', units:{ 'cm²':[0.0001,0], 'm²':[1,0] } },
Volume: { base:'m³', units:{ mL:[1e-6,0], L:[0.001,0], 'm³':[1,0] } },
Temperature:{ base:'K', units:{ '°C':[1,273.15], '°F':[5/9, 273.15-(32*5/9)], K:[1,0] } },
Length: { units:{ mm:[0.001,0], cm:[0.01,0], m:[1,0], km:[1000,0], in:[0.0254,0], ft:[0.3048,0], yd:[0.9144,0], mi:[1609.344,0] } },
Mass: { units:{ mg:[1e-6,0], g:[0.001,0], kg:[1,0], t:[1000,0], lb:[0.45359237,0], oz:[0.028349523125,0] } },
Time: { units:{ ms:[0.001,0], s:[1,0], min:[60,0], h:[3600,0], d:[86400,0], wk:[604800,0] } },
Area: { units:{ 'cm²':[0.0001,0], 'm²':[1,0], 'km²':[1e6,0], 'ft²':[0.09290304,0], ha:[10000,0], acre:[4046.8564224,0] } },
Volume: { units:{ mL:[1e-6,0], L:[0.001,0], 'm³':[1,0], gal:[0.003785411784,0] } },
Temperature:{ units:{ '°C':[1,273.15], '°F':[5/9, 273.15-(32*5/9)], K:[1,0] } },
Speed: { units:{ 'm/s':[1,0], 'km/h':[1000/3600,0], mph:[0.44704,0], kn:[1852/3600,0] } },
Data: { units:{ bit:[0.125,0], B:[1,0], KB:[1e3,0], MB:[1e6,0], GB:[1e9,0], TB:[1e12,0], KiB:[1024,0], MiB:[1024**2,0], GiB:[1024**3,0], TiB:[1024**4,0] } },
Force: { units:{ N:[1,0], kN:[1000,0] } },
Energy: { units:{ J:[1,0], kJ:[1000,0], cal:[4.184,0], kcal:[4184,0], Wh:[3600,0], kWh:[3.6e6,0] } },
Power: { units:{ W:[1,0], kW:[1000,0], hp:[745.6998715822702,0] } },
Pressure:{ units:{ Pa:[1,0], kPa:[1000,0], bar:[1e5,0], atm:[101325,0], psi:[6894.757293168361,0] } },
Frequency:{ units:{ Hz:[1,0], kHz:[1e3,0], MHz:[1e6,0], GHz:[1e9,0] } },
Angle: { units:{ rad:[1,0], deg:[Math.PI/180,0], grad:[Math.PI/200,0], turn:[2*Math.PI,0] } },
};
const DEFAULTS = {
Length:['m','cm'], Mass:['kg','g'], Time:['h','s'], Area:['m²','cm²'], Volume:['L','mL'],
Temperature:['°C','°F'], Speed:['km/h','mph'], Data:['GB','MB'], Force:['kN','N'],
Energy:['kWh','J'], Power:['hp','kW'], Pressure:['atm','psi'], Frequency:['GHz','MHz'], Angle:['deg','rad'],
};
const DEFAULTS = { Length:['m','cm'], Mass:['kg','g'], Time:['h','s'], Area:['m²','cm²'], Volume:['L','mL'], Temperature:['°C','°F'] };

const $ = id => document.getElementById(id);
let dim = 'Length';
Expand Down
20 changes: 20 additions & 0 deletions examples/parse-and-humanize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/**
* Parse human-readable strings and print them in the most readable unit.
*
* php examples/parse-and-humanize.php
*/

require __DIR__ . '/../vendor/autoload.php';

use KhaledAlam\Unit\Quantity;

// Parse compound and multi-segment strings
echo Quantity::parse('100 km/h')->to('mph')->format(2), "\n"; // 62.14 mph
echo Quantity::parse('5 ft 3 in')->to('cm')->format(2), "\n"; // 160.02 cm

// Let the library choose the nicest unit
echo Quantity::of(1500, 'm')->humanize(), "\n"; // 1.5 km
echo Quantity::of(2_500_000, 'B')->humanize(), "\n"; // 2.5 MB
echo Quantity::of(90, 'min')->humanize(), "\n"; // 1.5 h
57 changes: 57 additions & 0 deletions src/Laravel/AsQuantity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace KhaledAlam\Unit\Laravel;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use KhaledAlam\Unit\Quantity;

/**
* Eloquent cast that stores a {@see Quantity} as a human-readable string
* (e.g. "2 m") and hydrates it back into a Quantity on access.
*
* Usage:
* protected function casts(): array
* {
* return ['distance' => AsQuantity::class];
* }
*
* @implements CastsAttributes<Quantity, Quantity|string>
*/
final class AsQuantity implements CastsAttributes
{
/**
* @param array<string, mixed> $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<string, mixed> $attributes
* @return array<string, string|null>
*/
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) . '.'
);
}
}
39 changes: 39 additions & 0 deletions src/Laravel/UnitServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace KhaledAlam\Unit\Laravel;

use Illuminate\Contracts\Config\Repository;
use Illuminate\Support\ServiceProvider;
use KhaledAlam\Unit\Unit;
use KhaledAlam\Unit\UnitRegistry;

/**
* Registers the package with Laravel.
*
* Common units are already available (they auto-register via the package's
* Composer `autoload.files`). This provider lets an application register extra
* custom units through config, e.g.:
*
* // config/unit.php
* return [
* 'units' => [
* 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);
}
}
}
}
Loading
Loading