Univerto uses a fraction-based internal representation to maintain precision throughout the conversion process, avoiding the common floating-point errors that plague JavaScript arithmetic.
The Floating Point Problem
JavaScript uses IEEE 754 double-precision floating-point numbers, which can lead to unexpected results:
// Classic floating-point error
console.log(0.1 + 0.2) // 0.30000000000000004 ❌
// Multiplication errors
console.log(0.1 * 3) // 0.30000000000000004 ❌
// Division precision loss
console.log(1 / 3) // 0.3333333333333333 (truncated)
These errors compound during unit conversions, especially when dealing with complex conversion factors:
// Without Univerto - potential precision issues
const meters = (5 * 1609.344) // miles to meters
const feet = meters * 3.28084 // meters to feet
// Multiple floating-point operations = compounding errors
Fraction-Based Representation
Univerto solves this by representing all values and conversion factors as fractions (rational numbers) internally.
The Rational Class
At the core of Univerto’s precision system is the Rational class:
class Rational {
private numerator: number
private denominator: number
constructor(numerator: number, denominator: number) {
this.numerator = numerator
this.denominator = denominator
}
multiply(other: Rational) {
return new Rational(
this.numerator * other.numerator,
this.denominator * other.denominator
)
}
divide(other: Rational) {
return new Rational(
this.numerator * other.denominator,
this.denominator * other.numerator
)
}
toNumber() {
return this.numerator / this.denominator
}
}
The full implementation can be found in ~/workspace/source/src/features/core/utils/rational/index.ts:1-42
How It Works
Instead of storing 0.333..., Univerto stores 1/3 as { numerator: 1, denominator: 3 }.
Example: Time Conversion
Let’s trace how Univerto converts 1 hour to milliseconds:
import { TIME_UNIT, TimeUnitConverter } from 'univerto/time'
const ms = TimeUnitConverter.from(1, TIME_UNIT.HOUR)
.to(TIME_UNIT.MILLISECOND)
.convert()
Internal process:
- The input value (1) is converted to a Rational:
{ numerator: 1, denominator: 1 }
- The hour scale factor is:
{ numerator: 3600000, denominator: 1 }
- The millisecond scale factor is:
{ numerator: 1, denominator: 1 }
- Division creates the conversion factor:
hour.divide(millisecond) = { numerator: 3600000, denominator: 1 }
- Multiplication with input:
{ numerator: 1, denominator: 1 } × { numerator: 3600000, denominator: 1 }
= { numerator: 3600000, denominator: 1 }
- Final conversion to number:
3600000 / 1 = 3600000 ✓
Maintaining Precision During Conversions
All arithmetic operations use fraction math, not floating-point math:
function convertToRational() {
const from = scale[fromUnit] // Rational
const to = scale[targetUnit] // Rational
const fromRational = Rational.fromNumber(fromQuantity) // Rational
const factor = from.divide(to) // Rational division
return fromRational.multiply(factor) // Rational multiplication
}
Only the final step converts to a JavaScript number:
function convert() {
const result = convertToRational() // All operations in Rational
return result.toNumber() // Single conversion to float
}
This means:
- No intermediate rounding errors
- No accumulation of floating-point imprecision
- Exact arithmetic until the very end
Scale Definitions
Each unit category defines its units as rational numbers relative to a base unit:
// Time units defined as fractions
const MS = new Rational(1, 1) // Base unit: millisecond
const SEC = scaleUnit(MS, 1000) // 1000ms = 1s
const MIN = scaleUnit(SEC, 60) // 60s = 1min
const HOUR = scaleUnit(MIN, 60) // 60min = 1hr
const DAY = scaleUnit(HOUR, 24) // 24hr = 1day
// Nanosecond requires division
const NS = scaleUnit(MS, 1, 1000) // 1ms = 1000ns
The scaleUnit helper creates new Rational instances by multiplying:
function scaleUnit(base: Rational, numerator: number, denominator: number = 1) {
return base.multiply(new Rational(numerator, denominator))
}
See ~/workspace/source/src/features/core/utils/scale-unit/index.ts:3-5 and ~/workspace/source/src/features/time/constants/time-scale.ts:6-32
Precision Benefits in Action
Example 1: Complex Time Conversion
import { TIME_UNIT, TimeUnitConverter } from 'univerto/time'
// Convert 1 nanosecond to hours
const hours = TimeUnitConverter.from(1, TIME_UNIT.NANOSECOND)
.to(TIME_UNIT.HOUR)
.convert()
// Internally: (1/1) × (1/1000000000) × (3600000/1)^-1
// Precise fraction arithmetic throughout
Example 2: Avoiding Common Pitfalls
import { LENGTH_UNIT, LengthUnitConverter } from 'univerto/length'
// Traditional JS (with errors):
const traditionalResult = (1 / 3) * 3 // 0.9999999999999999
// Univerto (precise):
const fraction = LengthUnitConverter.from(1, LENGTH_UNIT.METER)
.to(LENGTH_UNIT.CENTIMETER)
.convertToFraction()
// Exact fraction until you need the decimal
When Precision Matters Most
- Scientific calculations: Precise measurements and conversions
- Financial applications: Currency conversions, billing
- Engineering: CAD/CAM systems, simulations
- Data processing: Large-scale conversions where errors compound
Accessing the Fraction Directly
If you need to maintain precision beyond Univerto’s conversion, use convertToFraction():
const fraction = TimeUnitConverter.from(1, TIME_UNIT.HOUR)
.to(TIME_UNIT.MILLISECOND)
.convertToFraction()
console.log(fraction) // { numerator: 3600000, denominator: 1 }
You can then pass this fraction to arbitrary-precision libraries for further processing. See High-Precision Arithmetic for details.
Implementation Reference
The precision system is implemented across several core files:
Rational class: ~/workspace/source/src/features/core/utils/rational/index.ts:1-42
- Conversion logic:
~/workspace/source/src/features/core/utils/create-precise-converter/index.ts:10-30
- Scale utilities:
~/workspace/source/src/features/core/utils/scale-unit/index.ts:3-5