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: 32

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

1.2.0 2026-05-29 00:55 UTC

This package is auto-updated.

Last update: 2026-06-03 21:18:54 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)

min() and max()

Use min() to get the smaller value and max() to get the larger value.

use Jimeneztdavid\ScaledInt\ScaledInt;

$a = ScaledInt::fromMajor('10.25');
$b = ScaledInt::fromMajor('10.30');

$a->min($b); // 10.25
$a->max($b); // 10.30

Both values must have the same scale.

between()

Use between() to check whether a value is inside an inclusive range.

use Jimeneztdavid\ScaledInt\ScaledInt;

$value = ScaledInt::fromMajor('15.00');
$min = ScaledInt::fromMajor('10.00');
$max = ScaledInt::fromMajor('20.00');

$value->between($min, $max); // true

Both bounds and the value must have the same scale.

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
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