jimeneztdavid / scaled-int
A value object for scaled integers (e.g. money cents) with safe arithmetic and configurable rounding.
Requires
- php: >=8.1
Requires (Dev)
- phpunit/phpunit: ^12.5
README
Why this package exists
Working with money, prices, weights, percentages, or any decimal-based value using floating-point numbers (float) is dangerous.
Rounding errors, precision loss, and unexpected comparisons are common problems in real-world applications.
ScaledInt solves this by:
- Storing values internally as integers only
- Applying a fixed scale (power of 10) to represent decimals
- Providing safe arithmetic, comparisons, and rounding
- Avoiding floating-point math entirely
This makes it ideal for:
- Money and prices
- Taxes (IVA / VAT)
- Percentages and discounts
- Measurements (kg, grams, meters)
- Any domain where precision matters
Core concept
A ScaledInt stores:
- minor → integer value (scaled)
- scale → power of 10 (10, 100, 1000, etc.)
Example with scale 100:
| Human value | Stored minor |
|---|---|
10.25 |
1025 |
5.00 |
500 |
-3.50 |
-350 |
Installation
composer require jimeneztdavid/scaled-int
Creating values
From a major (human) value
use Jimeneztdavid\ScaledInt\ScaledInt; $price = ScaledInt::fromMajor('10.25'); // scale = 100 by default
From a minor (integer) value
$price = ScaledInt::fromMinor(1025); // represents 10.25
Using a custom scale
$weight = ScaledInt::fromMajor('1.234', 1000); // 3 decimals
⚠️ Scale must be a power of 10 (10, 100, 1000, ...)
Reading values
Get minor value
$price->minor(); // 1025
Get scale
$price->scale(); // 100
Convert back to string
$price->toMajorString(); // "10.25"
echo $price; // "10.25"
Comparisons
All comparisons require the same scale.
$a = ScaledInt::fromMajor('10.00'); $b = ScaledInt::fromMajor('12.50'); $a->lessThan($b); // true $b->greaterThan($a); // true $a->equalTo($b); // false
compareTo()
Use compareTo() when you want a single method that tells you the ordering between two ScaledInt values.
- Returns -1 if this value is less than the other
- Returns 0 if both values are equal
- Returns 1 if this value is greater than the other
⚠️ Both numbers must have the same scale, otherwise an
InvalidArgumentExceptionis thrown.
Example
use Jimeneztdavid\ScaledInt\ScaledInt; $a = ScaledInt::fromMajor('10.25'); // 10.25 $b = ScaledInt::fromMajor('10.30'); // 10.30 $c = ScaledInt::fromMajor('10.25'); // 10.25 $a->compareTo($b); // -1 (a < b) $b->compareTo($a); // 1 (b > a) $a->compareTo($c); // 0 (a == c)
Arithmetic operations
Addition
$total = $a->add($b);
Subtraction
$diff = $b->subtract($a);
Multiply by integer
$double = $price->multiplyByInt(2);
Divide by integer (exact)
$half = $price->divideByIntExact(2);
Throws if the division is not exact.
Division with rounding
use Jimeneztdavid\ScaledInt\RoundingMode; $result = $price->divideByInt(3, RoundingMode::HALF_UP);
Supported modes:
UPDOWNHALF_UPHALF_EVEN
Percentages
$price = ScaledInt::fromMajor('100.00'); $iva = $price->percentOf(19, RoundingMode::HALF_UP); // 19.00
Common scenarios
Calculate IVA (VAT)
$price = ScaledInt::fromMajor('100.00'); $iva = $price->percentOf(19, RoundingMode::HALF_UP); $total = $price->add($iva); // 119.00
Extract IVA from a final price
IVA included price formula:
base = total / (1 + IVA)
Example with 19% IVA:
$total = ScaledInt::fromMajor('119.00'); $base = $total ->multiplyByInt(100) ->divideByInt(119, RoundingMode::HALF_EVEN); $iva = $total->subtract($base);
Discounts
$price = ScaledInt::fromMajor('200.00'); $discount = $price->percentOf(15, RoundingMode::HALF_UP); $final = $price->subtract($discount);
Measurements (non-money)
$weight = ScaledInt::fromMajor('2.750', 1000); // kg $double = $weight->multiplyByInt(2); // 5.500 kg
Why not floats?
0.1 + 0.2 !== 0.3 // true 😬
ScaledInt::fromMajor('0.10')->add( ScaledInt::fromMajor('0.20') )->toMajorString(); // "0.30" ✅
Design principles
- Immutable objects
- Integer-only math
- Explicit rounding
- Overflow-safe operations
- Scale consistency enforced
When to use ScaledInt
✅ Financial calculations
✅ Taxes and percentages
✅ Measurements
✅ Precise comparisons
❌ Scientific floating-point math
❌ Values requiring arbitrary precision decimals
Float vs ScaledInt
| Aspect | float |
ScaledInt |
|---|---|---|
| Precision | ❌ Imprecise (binary rounding errors) | ✅ Exact (integer math) |
| Internal representation | IEEE 754 binary floating point | Integer + fixed scale |
0.1 + 0.2 |
0.30000000000000004 😬 |
"0.30" ✅ |
| Equality comparison | ❌ Unreliable | ✅ Safe and deterministic |
| Rounding control | ❌ Implicit and inconsistent | ✅ Explicit (UP, DOWN, HALF_UP, HALF_EVEN) |
| Overflow detection | ❌ Silent | ✅ Explicit exceptions |
| Currency safety | ❌ Dangerous | ✅ Designed for money |
| Percentage calculations | ❌ Error-prone | ✅ Deterministic |
Comparisons (> < ==) |
❌ Risky | ✅ Guaranteed correctness |
| Domain intent | ❌ Generic numeric | ✅ Explicit domain modeling |
| Production suitability | ⚠️ Needs extra care | ✅ Safe by default |
License
MIT