jimeneztdavid/scaled-int

A value object for scaled integers (e.g. money cents) with safe arithmetic and configurable rounding.

Maintainers

Package info

github.com/jimeneztdavid/scaled-int

pkg:composer/jimeneztdavid/scaled-int

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.1.0 2026-01-26 14:49 UTC

This package is auto-updated.

Last update: 2026-03-26 15:21:46 UTC


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 InvalidArgumentException is 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:

  • UP
  • DOWN
  • HALF_UP
  • HALF_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