PHPackages                             asika/better-units - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. asika/better-units

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

asika/better-units
==================

A modern and intuitive unit conversion tool that allows you to convert units of measurement.

0.2.0(1mo ago)2311↓33.3%2MITPHPPHP &gt;=8.4.1CI passing

Since Aug 8Pushed 1mo agoCompare

[ Source](https://github.com/asika32764/better-units)[ Packagist](https://packagist.org/packages/asika/better-units)[ RSS](/packages/asika-better-units/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (8)Versions (6)Used By (2)

BetterUnits - A Better PHP Measurement Converter
================================================

[](#betterunits---a-better-php-measurement-converter)

[![GitHub Actions Workflow Status](https://camo.githubusercontent.com/fb89d55e27e534f859f484601530394ad4c7a3a85d99e33a38c514e8797b807e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6173696b6133323736342f6265747465722d756e6974732f63692e796d6c3f7374796c653d666f722d7468652d6261646765)](https://camo.githubusercontent.com/fb89d55e27e534f859f484601530394ad4c7a3a85d99e33a38c514e8797b807e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6173696b6133323736342f6265747465722d756e6974732f63692e796d6c3f7374796c653d666f722d7468652d6261646765)[![Packagist Version](https://camo.githubusercontent.com/d67eebe0602e7d9240835c183bbb159cbcd269efc052fcc387f72bd5639ac5fa/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6173696b612f6265747465722d756e6974733f7374796c653d666f722d7468652d6261646765)](https://packagist.org/packages/asika/better-units)[![Packagist Downloads](https://camo.githubusercontent.com/e094c6c1b5c197d527cca1b3cce11a6a2cf84b30b541db883b3048ab73462e10/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6173696b612f6265747465722d756e6974733f7374796c653d666f722d7468652d6261646765)](https://packagist.org/packages/asika/better-units)

BetterUnits is a modern and intuitive unit conversion tool that allows you to convert between various units of measurement. It supports a wide range of categories including length, weight, temperature, volume, and more.

- [BetterUnits - A Better PHP Measurement Converter](#betterunits---a-better-php-measurement-converter)
    - [Installation](#installation)
    - [Available Measurements](#available-measurements)
    - [Getting Started](#getting-started)
        - [How to Use This Package](#how-to-use-this-package)
        - [How to Create Measurement Object](#how-to-create-measurement-object)
        - [Rounding Mode \[!important\]](#rounding-mode-important)
        - [Create By Strings](#create-by-strings)
        - [Set or Parse Values deferred](#set-or-parse-values-deferred)
    - [Unit Conversion](#unit-conversion)
        - [Output Values](#output-values)
        - [convertTo() Method](#convertto-method)
        - [convertToExact() Method](#converttoexact-method)
        - [Precision Control](#precision-control)
    - [Units](#units)
    - [Formatting](#formatting)
        - [`format()`](#format)
        - [`humanize()`](#humanize)
        - [Default Formatting Handler](#default-formatting-handler)
        - [`serialize()`](#serialize)
        - [`serializeCallback()`](#serializecallback)
    - [Unit Management](#unit-management)
        - [Restrict Available Units](#restrict-available-units)
        - [Customizing or Adding Units](#customizing-or-adding-units)
        - [Changing Conversion Rates](#changing-conversion-rates)
        - [Other Unit Adjustments](#other-unit-adjustments)
    - [Get the Unit Closest to 1](#get-the-unit-closest-to-1)
    - [Modifying the Content of a Measurement](#modifying-the-content-of-a-measurement)
        - [Operations](#operations)
    - [Compound Measurement](#compound-measurement)
        - [Indeterminate Scales](#indeterminate-scales)
        - [Predefined Units](#predefined-units)
    - [Creating Your Own Measurement](#creating-your-own-measurement)
        - [Dynamic Measurement](#dynamic-measurement)
    - [Contribution](#contribution)

Installation
------------

[](#installation)

This package requires PHP `8.4.1` up.

```
composer require asika/better-units
```

Compatibility with `brick/math` is as follows:

Version`brick/math` version0.2.x`>=0.14.2`0.1.2`0.9 ~ 0.14`0.1.x`*`Available Measurements
----------------------

[](#available-measurements)

- [Measurements List](./docs)
    - Basic Measurement
        - [Area](./docs/area.md)
        - [Duration](./docs/duration.md)
        - [Energy](./docs/energy.md)
        - [FileSize](./docs/filesize.md)
        - [Length](./docs/length.md)
        - [Volume](./docs/volume.md)
        - [Weight](./docs/weight.md)
    - Compound Measurement
        - [Speed](./docs/speed.md)
        - [Bitrate](./docs/bitrate.md)

Getting Started
---------------

[](#getting-started)

### How to Use This Package

[](#how-to-use-this-package)

This package provides a simple and intuitive way to store and convert measurement values. You can use it to store and convert various units of measurement, such as time, length, weight, temperature, and more, and pass them between objects and functions.

In the past, when your objects needed to accept a measurement value, you might have used `int` or `float` to represent the value. However, this approach has some issues:

- You cannot ensure the unit of the value is correct, which may lead to unit errors.
- You cannot ensure the precision of the value, which may result in precision loss.
- You cannot ensure the range of the value, which may cause overflow issues.
- You cannot ensure the format of the value, which may lead to formatting errors.

For example, the following function calculates the total listening time, but the developer cannot determine the unit of the `$duration` parameter. It could be seconds, minutes, or even hours, which may lead to calculation errors.

```
function calcListenTime(int $duration): string {
    return sprintf('%.2f hours', $duration / 3600);
}

// What unit is this? Could be seconds, minutes, or hours
calcListenTime(3600);
```

By using Measurement objects, the function itself does not need to handle unit details, as the Measurement object will automatically manage unit conversion and precision issues. Developers only need to pass the measurement object into the function, and the function will automatically convert it to the required unit and format.

```
function calcListenTime(Duration $duration): string {
    return $duration->toHours(scale: 4)->format(suffix: ' hours');
}

calcListenTime(4575); // "1.2694 hours"
```

### How to Create Measurement Object

[](#how-to-create-measurement-object)

You can create a Measurement as follows. Each Measurement has its own default unit. For example, the default unit of `Duration` is `seconds`, so when you create a `Duration` object directly, the input value will be stored in `seconds`.

You can immediately convert it to other units, such as `minutes` or `hours`. This package uses the [brick/math](https://github.com/brick/math) for mathematical operations, so the returned value will be a `BigDecimal` object.

```
use Asika\BetterUnits\Duration;

$duration = new Duration(600); // 600 seconds

// Get raw value in seconds
$duration->value; // BigDecimal(600)

// Convert to minutes value
$duration->toMinutes(); // BigDecimal(10)

(string) $duration->toMinutes()->minus(2); // "8"
```

You can also specify the initial unit, as shown in the following examples. The unit can be specified using class constants or English abbreviations like `minutes` or `min`. (For detailed available units, please refer to the [documentation](./docs) of each Measurement.)

```
$duration = new Duration(60, Duration::UNIT_MINUTES); // 10 minutes
$duration = new Duration(60, 'minutes'); // 10 minutes
$duration = new Duration(60, 'min'); // 10 minutes

// Get raw value in minutes
$duration->value; // BigDecimal(60)

// Convert to hour value
$duration->toHours(); // BigDecimal(1)
```

### Rounding Mode \[!important\]

[](#rounding-mode-important)

When the conversion result includes decimals, the default rounding mode is "round down". Therefore, when converting smaller units to larger units, if the value is insufficient to round up, such as converting seconds to hours or months, the result may directly be `0`. This is the expected behavior.

You can add the precision parameter `scale: int` to specify the number of decimal places. Additionally, you can use the `roundingMode: enum` parameter from [brick/math](https://github.com/brick/math) to change the rounding rule.

```
$duration->toHours(); // BigDecimal(0)
$duration->toHours(scale: 5); // BigDecimal(0.16666)

$duration->toHours(scale: 1, roundingMode: \Brick\Math\RoundingMode::HalfUp); // BigDecimal(0.17)
```

### Create By Strings

[](#create-by-strings)

BetterUnits lets you create conversion objects using strings, making unit conversions easier.

When parsing strings, the smallest supported unit is used first, then converted to the default unit. For example, the smallest unit for `Duration` is `femtoseconds`, and the default unit is `seconds`. So, when you create a `Duration`object from a string, it is first parsed into `femtoseconds` and then converted to `seconds`.

This ensures the original value, including decimals, is preserved. More details on value conversion and normalization will follow.

```
$duration = \Asika\BetterUnits\Duration::parse('10hours 5minutes 30seconds 50ms 100ns 300fs');

$duration->value; // BigDecimal(36330.0500001000003)
```

If you want to parse a string and convert it to another unit, you can add a second `asUnit` parameter.

```
$duration = Duration::parse(
    '10hours 5minutes 30seconds 50ms 100ns 300fs',
    asUnit: Duration::UNIT_MINUTES
)->value; // BigDecimal(605.500833335000005)

// Peek the current unit of this measurement
echo $duration->unit; // "minutes"
```

Similarly, all functions that involve unit conversion can include the `scale` and `roundingMode` parameters to control the number of decimal places and rounding rules.

```
$duration = Duration::parse(
    '10hours 5minutes 30seconds 50ms 100ns 300fs',
    asUnit: Duration::UNIT_MINUTES,
    scale: 3,
    roundingMode: \Brick\Math\RoundingMode::HALF_UP
)->value; // BigDecimal(605.501)
```

The `from()` method is a more general-purpose function. If a string is passed as a parameter, it will parse the string. If a number is passed, it will directly create a measurement object with the value.

```
$duration = Duration::from('100seconds');
$duration = Duration::from('3 years 50days 5hours 30minutes', scale: 4, roundingMode: RoundingMode::HALF_UP);
$duration = Duration::from(1200); // 1200 seconds
$duration = Duration::from(500, Duration::UNIT_MINUTES);
```

The `parseToValue()` method is a more convenient way to parse a string and immediately get the value in the default unit.

```
$value = Duration::parseToValue(
    '10hours 5minutes 30seconds 50ms 100ns 300fs',
    asUnit: Duration::UNIT_MINUTES,
    scale: 3,
    roundingMode: \Brick\Math\RoundingMode::HALF_UP
); // BigDecimal(605.501)
```

There are also `tryParse()`, `tryFrom()` and `tryParseToValue()` methods that return `null` if value is `null`, instead of throwing an TypeError.

```
$value: string|null = ...;

Duration::tryFrom($value); // NULL or Duration
Duration::tryParse($value); // NULL or Duration
Duration::tryParseToValue($value); // NULL or Duration
```

### Set or Parse Values deferred

[](#set-or-parse-values-deferred)

Sometimes, you may want to configure the Measurement object first and then parse the value later. You can create an empty Measurement object and set value or parse value later, all Measurement objects are **immutable**, so you must assign the result to a new variable.

```
use Asika\BetterUnits\Duration;

$duration = new Duration()
    ->withTropicalCalendar();

$duration = $duration->with(123, 'hours'); // Set value and units
// OR
$duration = $duration->withParse('123hours'/*, $scale, $roundingMode*/);
// OR
$duration = $duration->withFrom('123hours'/*, $scale, $roundingMode*/);
```

Unit Conversion
---------------

[](#unit-conversion)

BetterUnits provides two ways to convert units: one retains the `Measurement` object after conversion, and the other outputs the value directly.

### Output Values

[](#output-values)

Use the `to()` or `toXxx()` methods to convert units and output values. All values will be `BigDecimal` objects.

```
$duration->to(Duration::UNIT_MINUTES); // Use constants
$duration->to('months'); // Months
$duration->to('y'); // Year with shortcut

// Or preset methods
$duration->toMinutes(); // Minutes
$duration->toHours(); // Hours
$duration->toDays(); // Days
```

If you just want to get the current value, you can directly use the `value` property, which will return a `BigDecimal`object.

```
$duration->value; // BigDecimal(600)
```

### convertTo() Method

[](#convertto-method)

The `convertTo()` method allows you to convert units while maintaining the `Measurement` object with chaining operations. All modifications to the `Measurement` object are **immutable**, so you must assign the result to a new variable.

```
// Signatures
public function convertTo(
    string $toUnit,
    int|null|false $scale = null,
    RoundingMode $roundingMode = RoundingMode::Down
): static {
```

Important

Additionally, when converting smaller units to larger ones, precision may be lost. Be sure to manually set the `scale` and `roundingMode` parameters as needed for the conversion.

```
$seconds = new Duration(600, 's'); // 600 seconds

// Immutable
$minutes = $seconds->convertTo(Duration::UNIT_MINUTES);

// $seconds still 600 seconds
$seconds->value; // BigDecimal(600)
$minutes->value; // BigDecimal(10)

// Control the precision
$hours = $seconds->convertTo(Duration::UNIT_HOURS, scale: 2, roundingMode: RoundingMode::HALF_UP);

$hours->value; // BigDecimal(0.17)
```

### convertToExact() Method

[](#converttoexact-method)

`convertToExact()` is similar to `convertTo()`, but has no `scale` and `roundingMode` arguments. It will keep all decimal places and remove trailing zeros during conversion. This is useful when you want to maintain the highest precision during unit conversion.

Important

But be cautious when using this method, for example, converting `1` second to minutes will yield `0.016666666666666666...`. In such cases, the `convertToExact()` may throw an exception due to precision loss. Therefore, it is not recommended to use thie method for general unit conversions.

### Precision Control

[](#precision-control)

For safety reasons, BetterUnits uses `RoundingMode::DOWN` from [brick/math](https://github.com/brick/math)as the default rounding rule during unit conversions, discarding all decimal places. This means that even `59` seconds will be converted to `0` minutes.

Important

If you use the `convertTo()` method, all discarded digits during the conversion process will be removed and cannot be restored, resulting in precision loss. The following example demonstrates this situation:

```
$duration = new Duration(59, 's')
    ->convertTo(Duration::UNIT_MINUTES) // 0 minutes
    ->convertTo(Duration::UNIT_SECONDS); // 0 seconds

$duration->value; // BigDecimal(0) - All precision lost
```

During the conversion process, if indefinite-length decimals are allowed, unexpected minor precision loss may occur, and engineers or users may not notice it at all.

Therefore, this package requires developers to consciously specify precision and rounding rules to ensure that any precision loss during the conversion process is anticipated and controlled.

If you want to specify precision and rounding rules, you can add the `scale` and `roundingMode` parameters during conversion to manually control the range of precision loss.

```
$duration = new Duration(59, 's')
    ->convertTo(Duration::UNIT_MINUTES, scale: 8) // 0.98333333 minutes
    ->convertTo(Duration::UNIT_SECONDS, scale: 8); // 58.9999998 seconds

// Back to seconds
$duration->value; // BigDecimal(58.9999998)
```

The following example shows how to use high precision when converting to larger units and apply half-up rounding when converting to smaller units. This ensures that the original value in seconds can be accurately restored.

```
new Duration(59, 's')
    ->convertTo(Duration::UNIT_MINUTES, 8, RoundingMode::HALF_UP) // 0.98333333 minutes
    ->convertTo(Duration::UNIT_SECONDS, 0, RoundingMode::HALF_UP);

// BigDecimal(59)
```

Units
-----

[](#units)

BetterUnits can represent units using constants or English unit strings. For example, with `Duration`, the supported units include:

- `Duration::UNIT_FEMTOSECONDS` (fs, femtoseconds, femtosecond)
- `Duration::UNIT_PICOSECONDS` (ps, picoseconds, picosecond)
- `Duration::UNIT_NANOSECONDS` (ns, nanoseconds, nanosecond)
- `Duration::UNIT_MICROSECONDS` (μs, us, microseconds, microsecond)
- `Duration::UNIT_MILLISECONDS` (ms, milliseconds, millisecond)
- `Duration::UNIT_SECONDS` (s, sec, seconds, second)
- `Duration::UNIT_MINUTES` (min, m, minutes, minute)
- `Duration::UNIT_HOURS` (h, hour, hours)
- `Duration::UNIT_DAYS` (d, day, days)
- `Duration::UNIT_WEEKS` (w, week, weeks)
- `Duration::UNIT_MONTHS` (mo, month, months)
- `Duration::UNIT_YEARS` (y, year, years)

Any part that allows inputting units for conversion or parsing strings can use these constants or strings to represent units.

It does not matter if there is a space between the unit and the value. For example, `2hours`, `2 hours`, `2hr`, and `2 hr` are all acceptable formats.

Depending on the Measurement, singular and plural forms like `year` and `years`are usually interchangeable (except for some units where singular and plural forms have specific differences, as defined by the Measurement).

Below are examples of input for parsing:

```
\Asika\BetterUnits\Duration::parse('10 hours 5 minutes 30 seconds 50ms 100ns 300fs');
\Asika\BetterUnits\Duration::parse('3y 2mo 1w 2d 3h 4min 5s 6ms 7μs 8ns 9fs');
```

Formatting
----------

[](#formatting)

Measurement provides several functions to display formatted strings. These functions are generally available for all Measurement types. For now, we will use `Duration` as an example.

### `format()`

[](#format)

The `format()` method formats the current value based on its unit. By default, it appends the unit's original string as a suffix directly to the value.

```
$duration = new Duration(59, 's');

$duration->value; // BigDecimal(59)

$duration->format(); // "59seconds"
```

The first parameter, `suffix`, specifies the suffix format for the output. This parameter can be:

- A plain string, used as the suffix.
- A string containing `%s`, which will be used as a `sprintf` template.
- A `Closure` that receives the value and object during execution and returns a string. This is useful for integrating with frameworks like i18n.
    - Format: `Closure(BigDecimal $value, string $unit, AbstractMeasurement $measurement): string`

```
$duration->format(); // "59seconds"

$duration->format(suffix: ' SEC'); // "59 SEC"

// Use for localization, this is "seconds" in Chinese.
$duration->format(suffix: '秒'); // "59秒"

// Use template string
$duration->format(suffix: 'The Timeout is: %s'); // "The Timeout is: 59"

// Closure
$duration->format(
    function (BigDecimal $value, string $unit, AbstractMeasurement $measurement): string {
        // Integrate with i18n or other frameworks
        return Translator::trans('app.timeout.unit.seconds', value: $value->toScale(2), unit: $unit);
    }
); // "Timeout: 59.00 seconds"
```

`format()` can specify the output unit and will immediately convert and display the value in that unit. Since it formats into a single unit, precision and rounding rules must also be considered.

```
$duration = new Duration(59, 's');

$duration->format(unit: Duration::UNIT_MINUTES); // 0minutes

$duration->format(unit: Duration::UNIT_MINUTES, scale: 8); // "0.98333333minutes"
```

If you have already set the precision and rounding rules when parsing values or using the `convertTo()` method, you don't need to specify the `scale` when calling `format()`. It will use the current precision settings to display the value.

```
new Duration(59, 's')
    ->convertTo(Duration::UNIT_MINUTES, scale: 8) // The scale will save into the measurement
    ->format(); // "0.98333333minutes"
```

### `humanize()`

[](#humanize)

`humanize()` is used to convert the current unit into a more readable format. It automatically breaks down the value into units displayed from largest to smallest.

```
$duration = Duration::parse('162231024996102500ns');

echo $duration->humanize();
// 5years 1month 3weeks 1day 5hours 46minutes 24seconds 996milliseconds 102microseconds 500nanoseconds
```

This is ideal for presenting final statistics to end-users. Below is an example where we show the total music playback time for this month to the end-user:

```
$seconds = 465718;
$totalPlaySeconds = Duration::from($seconds, 's');
echo $totalPlaySeconds->humanize(); // 5days 9hours 21minutes 58seconds
```

The first parameter, `formats`, can accept a `Closure` to control the formatting logic for all units. This is particularly useful for integrating with frameworks like i18n.
The second parameter, `divider`, allows you to specify the separator between units, with the default being a space.

```
$totalPlaySeconds->humanize(
    formats: fn(BigDecimal $value, string $unit) => $value . ' ' . strtoupper($unit),
    divider: ' / '
);
// 5 DAYS / 9 HOURS / 21 MINUTES / 58 SECONDS
```

But we usually only need to display up to hours and do not need to convert hours into days. We can provide a unit array to the first parameter `formats` to control the units we want to display.

```
echo $totalPlaySeconds->humanize(
    formats: [
        Duration::UNIT_HOURS,
        Duration::UNIT_MINUTES,
        Duration::UNIT_SECONDS,
    ],
    divider: ', '
);
// 129hours, 21minutes, 58seconds
```

If `formats` is an array, you can also provide string or Closure for formatting. Below is string text example, also, you can use `%s` as template for the value:

```
$format = fn(\Brick\Math\BigDecimal $value) => str_pad((string) $value, 2, '0', STR_PAD_LEFT);
echo $totalPlaySeconds->humanize(
    formats: [
        Duration::UNIT_HOURS => '시간', // OR '%s시간'
        Duration::UNIT_MINUTES => '분', // OR '%s분'
        Duration::UNIT_SECONDS => '초', // OR '%s초'
    ],
    divider: ':'
);
// 129시간:21분:58초
```

Below is an `Closure` example of using a simplified time expression, which is suitable for displaying durations in media players:

```
$format = fn(\Brick\Math\BigDecimal $value) => str_pad((string) $value, 2, '0', STR_PAD_LEFT);
echo $totalPlaySeconds->humanize(
    formats: [
        Duration::UNIT_HOURS => $format,
        Duration::UNIT_MINUTES => $format,
        Duration::UNIT_SECONDS => $format,
    ],
    divider: ':'
);
// 129:21:58
```

The `humanize()` method has an `options` parameter that supports two directives:

`OPTION_NO_FALLBACK`: Controls whether to display units with a value of 0 when the total value is 0.

```
$duration = new Duration(0, 's');

echo $duration->humanize(); // "0seconds"
echo $duration->humanize(options: Duration::OPTION_NO_FALLBACK); // ""
```

`OPTION_KEEP_ZERO` controls whether units with a value of 0 should be displayed.

```
$duration = new Duration(1000500, 's');
echo $duration->humanize(); // "1week 4days 13hours 55minutes"

echo $duration->humanize(options: Duration::OPTION_KEEP_ZERO);
// 0years 0months 1week 4days 13hours 55minutes 0seconds 0milliseconds
// 0microseconds 0nanoseconds 0picoseconds 0femtoseconds
```

### Default Formatting Handler

[](#default-formatting-handler)

A `Measurement` can register a default formatting handler. When `format()` or `humanize()` is called without specifying formatting parameters, this handler will be used. The example below demonstrates a handler that changes the suffix based on whether the value is singular or plural. Note that the `suffixFormatter` and the `format()` handler parameters are different. The first parameter is the default suffix, and the second parameter is the unit value, which can be used to make necessary adjustments.

```
$measurement = $measurement->withSuffixFormatter(
    function (string $suffix, BigDecimal $value, string $unit, Duration $measurement): string {
        if ($value->isEqualTo(1)) {
            $suffix = StrNormalizer::singularize($suffix);
        } else {
            $suffix = StrNormalizer::pluralize($suffix);
        }

        return $value . ' ' . $suffix;
    }
);
```

### `serialize()`

[](#serialize)

`serialize()` is similar to `humanize()`, but it does not allow customizing the format string. It converts a `Measurement` object into a serializable string, making it convenient for storing in a database or cache. You can use the `parse()` method to convert the string back into a `Measurement` object.

```
$duration = new Duration(1000500, 's');
$serialized = echo $duration->serialize(); // 1week 4days 13hours 55minutes

$newDuration = Duration::parse($serialized);

$duration->value->equals($newDuration->value); // TRUE
```

serialize() can also specify the output unit, allowing the value to be serialized directly into a specific unit.

```
$duration = new Duration(1000500, 's');
echo $duration->serialize(
    [
        Duration::UNIT_HOURS,
        Duration::UNIT_MINUTES,
    ]
); // 277hours 55minutes
```

Note that `serialize()` does not support decimals. It is recommended to serialize using the smallest representable unit to avoid precision loss.

```
$duration = new Duration(1000500, 's');
echo $duration->serialize(
    [
        Duration::UNIT_FEMTOSECONDS,
    ]
);
// 1000500000000000000000femtoseconds
```

### `serializeCallback()`

[](#serializecallback)

`serializeCallback()` is a powerful tool that allows you to customize the serialized string format, integrate with frameworks for translation, or display data in a user-friendly way.

This function accepts a `Closure` with two parameters: `Closure(AbstractMeasurement $remainder, array $sortedUnits): string`. The first parameter is the `Measurement` object converted to the atomUnit, and the second parameter is an array of units and values sorted by their conversion rates.

Below is an example using `Duration`:

```
$duration = new Duration(1000500, 's');
echo $duration = $duration->serializeCallback(
    function (Duration $remainder, array $sortedUnits) {
        $text = [];

        foreach ($sortedUnits as $unit => $ratio) {
            [$extracted, $remainder] = $remainder->withExtract($unit);

            if ($extracted->isZero()) {
                continue;
            }

            // You don't need to set $scale parameter here, all extracted values are integer.
            $text[] = $extracted->format();

            if ($remainder->isZero()) {
                break; // [Optional] No more remainder, stop here
            }
        }

        return implode(' ', $text);
    }
); // 1week 4days 13hours 55minutes
```

`$sortedUnits` is a sorted array, arranged from the largest unit to the smallest based on their conversion rates. This allows extracting values starting from the largest unit. If the remaining value is not enough to extract a full unit, it will be passed to the next smaller unit for extraction, continuing until the smallest atomic unit is reached. All extracted values will be integers, as any remainder will be carried over to the next unit, so you don't need to worry about precision issues during formatting.

The `withExtract()` method extracts the value of a specified unit from a `Measurement` object and returns a tuple `[extracted, remainder]`. For example, if the largest unit is `year`, it will try to extract the integer value of `year`into a separate `Measurement` object called `extracted`, while the remaining fractional value will be stored in `remainder`. The `remainder` is then passed to the next iteration for `months` extraction, and this process continues until the `remainder` becomes 0 or all units have been processed.

Thanks to the powerful extraction capability of `withExtract()`, you can fully customize the list of units to serialize. The units don't need to be consecutive (but you must ensure the units are ordered correctly by size).

```
$duration = new Duration(6000500, 's');
echo $duration = $duration->serializeCallback(
    function (Duration $remainder) {
        $text = [];

        $units = [
            Duration::UNIT_MONTHS,
            // We ignore weeks and days
            Duration::UNIT_HOURS,
            Duration::UNIT_MINUTES,
            Duration::UNIT_SECONDS,
        ];

        foreach ($units as $unit) {
            [$extracted, $remainder] = $remainder->withExtract($unit);

            if ($extracted->isZero()) {
                continue;
            }

            $text[] = $extracted->format();
        }

        return implode(' ', $text);
    }
); // 2months 206hours 20seconds
```

Note: If the current unit of your `Measurement` is smaller than the smallest unit you are serializing, precision loss may occur. This is because `withExtract()` only extracts the integer part, and the remaining decimal part will be discarded. Alternatively, you can manually output the final `remainder` as a decimal string.

Unit Management
---------------

[](#unit-management)

Each `Measurement` has several unit-related settings. Here's a brief introduction:

- `$measurement->atomUnit`: The smallest indivisible unit of the `Measurement`, such as `femtoseconds` for `Duration`.
- `$measurement->baseUnit`: The base unit for exchange rates, where the ratio is `1`. For example, `seconds` for `Duration`.
- `$measurement->defaultUnit`: The default unit used when creating a `Measurement` if no unit is specified. This is usually the same as `baseUnit` but not always. For example, `seconds` for `Duration`.
- `$measurement->unit`: The current unit of the `Measurement`, which can be manually specified during creation or changed using the `convertTo()` method.

When using the `parse()` method to parse a string, all `Measurement` values are automatically converted to the `atomUnit` first, then to the `defaultUnit` or the specified unit.

### Restrict Available Units

[](#restrict-available-units)

Sometimes, you may want to limit the units a `Measurement` can handle. For example, you might want `Duration` to ignore the `weeks` unit or restrict `FileSize` to only use byte-based units.

You can use the `withAvailableUnits()` method to restrict the available units. This ensures that only the specified units can be used for conversions and outputs.

```
$duration = $duration->withAvailableUnits(
    [
        Duration::UNIT_SECONDS,
        Duration::UNIT_MINUTES,
        Duration::UNIT_HOURS,
        Duration::UNIT_DAYS,
    ]
);
$duration = $duration->withParse('3 days 5 hours 30 minutes');

$duration = $duration->withParse('2 years 3 days'); // Exception: Unknown unit "years"
```

Each Measurement has commonly used units, which can be found in its documentation or by checking the constants defined in the Measurement class.

### Customizing or Adding Units

[](#customizing-or-adding-units)

Measurement supports customizing or adding new units. You can use the `withAddedUnitExchangeRate()` method to add a new unit, which will be included in the list of available units for the Measurement. The rate of the unit is based on the unit defined as `1` in the Measurement. For example, the base unit for `Duration`is `seconds`, with a rate of `1`. We can try adding a `centuries` unit and set its rate to `3153600000` seconds (the number of seconds in 100 years).

```
$duration = new Duration()->withAddedUnitExchangeRate('centuries', 3_153_600_000);

$duration->withParse('350years')
    ->format(unit: 'centuries', scale: 1); // "3.5centuries"
```

To make the `centuries` unit recognize various abbreviations, we can use the `withUnitNormalizer()` method to set a unit normalizer. This allows `centuries` to support abbreviations like `century`, `c`, etc. This normalizer is an additional feature and will not override the behavior of built-in units.

```
$duration = $duration->withUnitNormalizer(
    function (string $unit): string {
        return match ($unit) {
            'centuries', 'century', 'cent', 'cents', 'c' => 'centuries',
            default => $unit,
        };
    }
);
```

If you want the `Measurement` to be serializable, you can use a callable pointing to a static function as the normalizer. This avoids issues where closures cannot be serialized. (Alternatively, you can consider using [laravel/serializable-closure](https://github.com/laravel/serializable-closure).)

```
$duration = $duration->withUnitNormalizer(
    [MyCenturiesHelper::class, 'normalizeUnit'] // 靜態函式 normalizeUnit
);
```

If you want to dynamically set the number of seconds in `centuries`, you can use any unit for conversion. For example, we can calculate it based on the rate of years.

```
$duration = new Duration();
$yearRate = $duration->getUnitExchangeRate(Duration::UNIT_YEARS);
$duration = $duration->withAddedUnitExchangeRate(
    'centuries',
     $yearRate->multipliedBy(100)
);
```

Note

Note this value is an approximation. The actual number of seconds in a year may vary depending on the calendar system. For more details, please refer to the documentation of `Duration`.

### Changing Conversion Rates

[](#changing-conversion-rates)

Each `Measurement` has a different base unit that represents `1`. For example, the base unit for `Duration` is `seconds`, while for `FileSize`, it is `bytes`.

The `unitExchanges` for `Duration` look like this:

```
    protected array $unitExchanges = [
        self::UNIT_FEMTOSECONDS => 1e-15,
        self::UNIT_PICOSECONDS => 1e-12,
        self::UNIT_NANOSECONDS => 1e-9,
        self::UNIT_MICROSECONDS => 1e-6,
        self::UNIT_MILLISECONDS => 1e-3,
        self::UNIT_SECONDS => 1.0,
        self::UNIT_MINUTES => 60.0,
        self::UNIT_HOURS => 3600.0,
        self::UNIT_DAYS => 86400.0,
        self::UNIT_WEEKS => 604800.0,
        self::UNIT_MONTHS => self::MONTH_SECONDS_COMMON,
        self::UNIT_YEARS => self::YEAR_SECONDS_COMMON,
    ]
```

For certain reasons, if you need to change the conversion rate of the base unit, you can use the `withUnitExchangeRate()` method to set a new base unit rate. Below is an example where the rate of femtoseconds is set to `1`, making it the new base unit. This function will reset all available units, allowing you to add or remove units as needed.

```
$d->withUnitExchanges(
    [
        Duration::UNIT_FEMTOSECONDS => 1.0,
        Duration::UNIT_PICOSECONDS => 1000.0,
        Duration::UNIT_NANOSECONDS => 1_000_000.0,
        Duration::UNIT_MICROSECONDS => 1_000_000_000.0,
        Duration::UNIT_MILLISECONDS => 1_000_000_000_000.0,
        Duration::UNIT_SECONDS => 1_000_000_000_000_000.0,
        Duration::UNIT_MINUTES => 60_000_000_000_000_000.0,
        Duration::UNIT_HOURS => 3_600_000_000_000_000_000.0,
        Duration::UNIT_DAYS => 86_400_000_000_000_000_000.0,
        Duration::UNIT_WEEKS => 604_800_000_000_000_000_000.0,
        Duration::UNIT_MONTHS => 2_592_000_000_000_000_000_000.0, // 30 days
        Duration::UNIT_YEARS => 31_536_000_000_000_000_000_000.0, // 365 days
    ],
    atomUnit: Duration::UNIT_FEMTOSECONDS,
    defaultUnit: Duration::UNIT_SECONDS
);
```

Since the subsequent exchange rates might exceed the integer limit, it is recommended to represent them as strings or floating-point numbers. The `unitExchanges` property can accept formats such as `int`, `float`, `string`, or `BigDecimal`. These values will later be unified into `BigDecimal` for consistent calculations.

This function requires you to explicitly redefine both the `atomUnit` and `defaultUnit` because the units and exchange rates in a `Measurement` are closely related.

The `defaultUnit` does not necessarily have to be the same as the `baseUnit`. It is used as the default unit when creating a `Measurement` without specifying a unit.

You can also use the `withAddedUnitExchangeRate()` method to add new units or the `withoutUnitExchangeRate()` method to remove units without affecting the existing ones.

### Other Unit Adjustments

[](#other-unit-adjustments)

Each type of measurement unit has its own settings, which can be used to adjust the behavior or calculation logic of the units.
For example, `Duration` can configure calendar rules to calculate the number of seconds in a year or a month.

```
$duration = new \Asika\BetterUnits\Duration();
$duration = $duration->withAnomalisticCalendar(); // Use Anomalistic Calendar for year/month calculations

// you must parse values after setting calendar
$duration->withParse('1 year')->toSeconds(); // 31556952 seconds (Anomalistic year)
```

Alternatively, `FileSize` supports both IS and IEC unit standards, allowing you to configure which standard to use for unit calculations.

```
$fs = new \Asika\BetterUnits\FileSize();
$fs = $fs->withOnlyBytesBinary(); // Use only binary bytes (IEC) for calculations (KiB, MiB, GiB, etc.)

$fs->withParse('100KiB'); // OK
$fs->withParse('100KB'); // ERROR: Unknown base unit: KB
```

For more detailed configuration methods, please refer to the documentation for each measurement unit.

Get the Unit Closest to 1
-------------------------

[](#get-the-unit-closest-to-1)

The `nearest()` method in Measurement allows you to find the unit closest to 1. This method calculates the most human-readable unit based on the current value and unit ratio.

```
$fs = \Asika\BetterUnits\FileSize::from('8500KiB');
$fs = $fs->withOnlyBytesBinary(); // Use only binary bytes (IEC) for calculations

$nearest = $fs->nearest(scale: 2, RoundingMode::HALF_UP)->format(); // 8.31MiB
```

Modifying the Content of a Measurement
--------------------------------------

[](#modifying-the-content-of-a-measurement)

Measurement objects are immutable, meaning that any operation on a Measurement will return a new Measurement object without modifying the original one.

We provide a series of methods to modify the content of a Measurement. If you want to change the value and unit of a Measurement, you can use the `with()` method. This method changes the value and unit without performing any conversion.

```
$measurement = $measurement->with(100, 'seconds'); // Returns a new Measurement with 100 seconds
```

If you provide a `BigDecimal` with a specific scale, the `Measurement` object will retain this scale. This ensures that precision is preserved during subsequent conversions and formatting.

```
$measurement = $measurement->with(BigDecimal::of(100.25), 'hours');

$measurement->format(); // "100.25hours"
```

If you only want to change the value while keeping the unit, or change the unit while keeping the value, you can use the `withValue()` or `withUnit()` methods.

```
$measurement = \Asika\BetterUnits\Duration::from(100, 'seconds');

$measurement->withValue(300); // Returns a new Duration with 300 seconds, keep unit as seconds
$measurement->withUnit(Duration::UNIT_HOURS); // Returns a new Duration with unit hours, keep value as 300
```

### Operations

[](#operations)

Measurement objects support basic arithmetic operations such as addition, subtraction, multiplication, and division. The values used in these operations can be `BigNumber`, numbers, or strings.

```
$new = $measurement->plus(100); // Returns a new Measurement with value + 100
$new = $measurement->minus(50.0); // Returns a new Measurement with value - 50
$new = $measurement->multipliedBy('2'); // Returns a new Measurement with value * 2
$new = $measurement->dividedBy(BigNumber::of(2)); // Returns a new Measurement with value / 2
```

The `plus()` and `minus()` methods can accept another Measurement object for calculations. They will automatically convert the unit to match the original Measurement. However, you must manually specify the precision to avoid precision loss after conversion. Additionally, the default `roundingMode` for addition and subtraction is `UNNECESSARY`, so it is highly recommended to explicitly specify the `RoundingMode` to prevent errors.

```
$measurement = new Duration(120, 'seconds'); // 120 seconds
$new = $measurement->plus(new Duration(2, 'minutes'), scale: 2, RoundingMode::HALF_UP); // Returns a new Duration with 240 seconds
$new = $measurement->minus(new Duration(2500, 'ms'), scale: 2, RoundingMode::HALF_UP); // Returns a new Duration with 117.5 seconds
```

If you need to perform more complex calculations, you can directly access the `value` property, which is a `BigDecimal`object. You can use `BigDecimal` methods for calculations and then create a new Measurement object using the `with()` or `withValue()` methods. These methods also accept a `Closure` as a parameter, allowing for more flexible calculations.

```
// Returns a new Measurement with value / 2
$measurement = $measurement->with(
    $measurement->value->dividedBy(2, scale: 2, RoundingMode::UP)
);

// Calculate by a Closure
$measurement = $measurement->with(
    fn (BigDecimal $value, string $unit, $measurementObject) => $measurement->value->power()
);
```

Compound Measurement
--------------------

[](#compound-measurement)

Some Measurements require combining multiple units, referred to as `num` (numerator) and `deno` (denominator), representing the units in the numerator and denominator.

For example, Speed requires both distance and time, making it a Compound Measurement composed of `Length` (numerator) and `Duration` (denominator). When expressing the unit of Speed, it will be the unit of `Length` divided by the unit of `Duration`, such as `m/s` or `km/h`.

```
$speed = Speed::from('100 km/h'); // 100 kilometers per hour
$speed->convertTo('m/s', scale: 4); // 27.7777m/s
```

### Indeterminate Scales

[](#indeterminate-scales)

Since this library always converts to the smallest atom unit first before converting to the target unit, and the Compound Measurement object may perform multiple conversions in 2-3 steps internally, there might be cases where the number of decimal places cannot be determined, leading to unexpected precision loss.

To prevent such issues, Compound Measurement uses a default scale of `99` decimal places for internal conversions. If your unit conversion process exceeds this decimal scale, you can try increasing the `intermediateScale` to ensure accurate calculations, or decreasing the `intermediateScale` value to improve calculation speed.

```
// Set higher
$compoundMeasurement = $compoundMeasurement->withIntermediateScale(299);

// Set lower
$compoundMeasurement = $compoundMeasurement->withIntermediateScale(20);
```

Here we demonstrate a case with `Speed` object, where an insufficient `intermediateScale` causes unexpected errors:

```
$mps = new Speed()
    ->withIntermediateScale(20)
    ->withParse('1kph')
    ->toMps(scale: 10);

echo $mps;
// Expect: 0.2777777777
// Actual: 0.27777
```

Now we set intermediateScale to a higher value to ensure precision

```
$mps = new Speed()
    ->withIntermediateScale(99)
    ->withParse('1kph')
    ->toMps(scale: 10);

show((string) $mps);
// Good: 0.2777777777
```

### Predefined Units

[](#predefined-units)

Each Compound Measurement has some predefined units, which are commonly used international standard unit names, such as:

- `kph` (km/h, kilometers per hour)
- `mph` (miles per hour)
- `mps` (m/s, meters per second)
- `knots` (knots, nautical miles per hour)

These units can be directly used in the `from()` or `convertTo()` methods, making it convenient to create or convert Compound Measurements.

```
$speed = Speed::from('100 kph'); // 100 kilometers per hour
$speed->convertTo('mps', scale: 4); // 27.7777m/s
```

Creating Your Own Measurement
-----------------------------

[](#creating-your-own-measurement)

Here is a simple example. To create a custom Measurement, you need to extend the `AbstractBasicMeasurement` class.
There are three required properties to implement:
`$atomUnit` represents the smallest indivisible unit,
`$defaultUnit` is the default unit,
and `$unitExchanges` defines the conversion rates between units.

Make sure to include at least one base unit with a rate of `1`, as some calculations may fail without it.

The `normalizeUnit()` method is optional. It is used to convert input unit strings into supported units and is called during string parsing or unit conversion.

```
class ScreenMeasurement extends AbstractBasicMeasurement
{
    public const string UNIT_PX = 'px';
    public const string UNIT_PT = 'pt';
    public const string UNIT_EM = 'em';
    public const string UNIT_REM = 'rem';

    public string $atomUnit = self::UNIT_PX;

    public string $defaultUnit = self::UNIT_PX;

    protected array $unitExchanges = [
        self::UNIT_PX => 1.0,
        self::UNIT_PT => 1.3333333333, // 1pt = 1/72 inch, 1px = 96/72 inch
        self::UNIT_EM => 16.0, // Assuming 1em = 16px
        self::UNIT_REM => 16.0, // Assuming 1rem = 16px
    ];

    protected function normalizeUnit(string $unit): string
    {
        return match (strtolower($unit)) {
            'px', 'pixel', 'pixels' => self::UNIT_PX,
            'pt', 'point' => self::UNIT_PT,
            'em', 'em quad' => self::UNIT_EM,
            'rem', 'root em' => self::UNIT_REM,
            default => $unit,
        };
    }
}
```

### Dynamic Measurement

[](#dynamic-measurement)

You can use `DynamicMeasurement` to create a dynamic Measurement that allows you to set units and exchange rates at runtime.

Below is an example of a dynamic currency conversion Measurement. This is useful for e-commerce systems where exchange rates and currencies can be configured dynamically.

```
use Asika\BetterUnits\DynamicMeasurement;

$currency = new DynamicMeasurement(
    atomUnit: 'USD',
    defaultUnit: 'USD',
    // Example exchange rate
    unitExchanges: [
        'TWD' => $dailyExchangeRate->getRate('TWD'), // 0.33
        'CNY' => $dailyExchangeRate->getRate('CNY'), // 0.15
        'JPY' => $dailyExchangeRate->getRate('JPY'), // 0.007
        'USD' => 1.0,
        'EUR' => $dailyExchangeRate->getRate('EUR'), // 1.1
        'GBP' => $dailyExchangeRate->getRate('GBP'), // 1.3
    ]
);
$currency = $currency->withUnitNormalizer(
    fn(string $unit): string => match (strtolower($unit)) {
        'usd' => 'USD',
        'eur' => 'EUR',
        'gbp' => 'GBP',
        'cny' => 'CNY',
        'twd' => 'TWD',
        'jpy' => 'JPY',
        default => $unit,
    }
);

$currency = $currency->withParse('100USD')
    ->convertTo('EUR', scale: 2);

echo $currency->format(); // 90.9EUR
```

Contribution
------------

[](#contribution)

If you find any errors and know how to fix them, feel free to open a Pull Request. This will help us improve the fixing faster.

Since I cannot precisely verify all unit conversion rates, if you find any incorrect conversion rates in code or documentation, please make sure to include reference sources in the Issue or Pull Request.

###  Health Score

43

—

FairBetter than 91% of packages

Maintenance88

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~74 days

Total

4

Last Release

58d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1639206?v=4)[Simon Asika](/maintainers/asika32764)[@asika32764](https://github.com/asika32764)

---

Top Contributors

[![asika32764](https://avatars.githubusercontent.com/u/1639206?v=4)](https://github.com/asika32764 "asika32764 (45 commits)")

---

Tags

php-measurementphp-unitsunit-converterunitsunits-converterunits-measures-converterunits-of-measurementunitconversionconverterunitsmeasurementunit-conversionmeasurephp units

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/asika-better-units/health.svg)

```
[![Health](https://phpackages.com/badges/asika-better-units/health.svg)](https://phpackages.com/packages/asika-better-units)
```

###  Alternatives

[php-unit-conversion/php-unit-conversion

A fully PSR-4 compatible PHP library for converting between standard units of measure.

681.2M1](/packages/php-unit-conversion-php-unit-conversion)[nmarfurt/measurements

A PHP library for representing and converting dimensional units of measure.

17170.2k1](/packages/nmarfurt-measurements)[zman/zman

A Jewish date converter and helper.

186.4k](/packages/zman-zman)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
