@silyze/decimal
v1.0.0
Published
Decimal (fraction) number library
Downloads
28
Readme
@silyze/decimal
A tiny, dependency-free, exact rational arithmetic library for TypeScript/JavaScript backed by bigint.
Decimal stores numbers as a fully reduced fraction N/D and gives you:
- Exact
add / sub / mul / divwith automatic fraction reduction - Deterministic string formatting across radices (2–36)
- Flexible parsing (
"1/3","1.25","1e3","0x1.fp2","1_000.50", etc.) - Safe comparisons (
eq/lt/gt,min/max) - Interoperability helpers (lossless→string, lossy→number/string on demand)
Lossy transcendental helpers (exp/log/sin/cos/tan) are included for convenience and use IEEE-754 Math.* under the hood.
Installation
npm i @silyze/decimal
# or
pnpm add @silyze/decimal
# or
yarn add @silyze/decimalQuick start
import decimal, { Decimal } from "@silyze/decimal";
// Construct from literals
const a = Decimal.from("1/3");
const b = Decimal.from("2.5"); // base-10
const c = Decimal.from("0x1.fp2"); // hex-float = (1.9375) * 2^2 = 7.75
// Exact arithmetic
const sum = a.add(b); // 1/3 + 2.5 = 17/6
const mul = sum.mul("3/7"); // 17/6 * 3/7 = 17/14
// Deterministic formatting
sum.toString(); // "17/6" (non-terminating in base 10 → fraction form)
Decimal.from("1/8").toString(); // "0.125" (terminating → decimal form)
Decimal.from("42").toString(16); // "2a"
Decimal.from("42").toString(16, false, true); // "0x2a" (with radix prefix)
// Lossy formatting when you want a fixed-point string
sum.toString(10, true); // "2.83333333333333333333333333333333" (up to 34 frac digits)
// Coercion to number (only exact if denominator is 10^k)
+Decimal.from("1/4"); // 0.25 (exact)
+Decimal.from("1/3"); // 0.3333333333333333 (IEEE-754 rounding)Why this library?
- Exactness first. Most “decimal” libraries are either base-10 decimal floating point or big decimal approximations.
Decimalstores a reduced rational fraction, preserving exactness for all rational operations. - Predictable I/O. Non-terminating expansions return
"N/D"by default. You can opt into bounded fixed-point formatting vialossy = true. - Bigint-powered. No overflow in intermediate integer steps (limited by available memory/time).
Concepts & Behavior
Representation
- Internals: two private
bigintfields#n(numerator) and#d(denominator). - Always stored in lowest terms with a positive denominator.
Parsing (via Decimal.from(value, divisor = 1, radix = 0))
Accepts:
Numbers (finite only)
Bigints
Strings with:
Fractions:
"A/B"(both sides can themselves be any accepted literal)Decimal:
"123","1.25","1e6", optional underscores for readability:"1_000.50"Radix prefixes when
radix = 0:- Hex
"0x"/"0X", Octal"0o"/"0O", Binary"0b"/"0B"
- Hex
Hex floats with binary exponent:
"0x1.fp2"(i.e., mantissa base-16, exponent base-2)
Optional leading sign
"+" | "-".Optional divisor to scale inputs exactly (useful for minor units).
Examples:
Decimal.from("1/6").toString(); // "1/6"
Decimal.from("1e3").toString(); // "1000"
Decimal.from("0x1.fp2").toString(); // "7.75"
Decimal.from("1_000_000").toString(); // "1000000"
Decimal.from("1011", 1, 2).toString(); // "11" (binary literal)
Decimal.from("7f.8", 1, 16).toString(); // "127.5"
Decimal.from("123", 100).toString(); // "1.23"Formatting (toString(radix = 10, lossy = false, withPrefix = false))
If the fractional expansion terminates in the given
radix, returns a fixed-point string (e.g.,0.125).Otherwise:
lossy = false→ returns exact"N/D".lossy = true→ returns fixed-point string with up to 34 fractional digits.
withPrefixadds0xfor base-16 or0bfor base-2 when formatting integers.
Examples:
Decimal.from("1/8").toString(10); // "0.125"
Decimal.from("1/3").toString(10); // "1/3"
Decimal.from("1/3").toString(10, true); // "0.33333333333333333333333333333333"
Decimal.from("42").toString(16, false, true); // "0x2a"Coercion & JSON
valueOf/Number(x)/ unary+x:- If
denominatoris a power of ten → uses precise fixed-point conversion. - Otherwise →
Number(n) / Number(d)(IEEE-754 rounding).
- If
toJSON()returnstoString(); non-terminating decimals serialize as"N/D"unless you format otherwise.
If you need always-fixed-point JSON, call
x.toString(10, true)yourself.
API Reference
Types
export type NumericLike = Decimal | bigint | number | string;
export type decimal = Decimal; // aliasConstruction
new Decimal(base: bigint, divisor: bigint); // internal; base/divisor
Decimal.from(value: NumericLike, divisor?: number | bigint, radix?: number): Decimal;
// Convenience factory (default export):
decimal(value: number | string, divisor?: number | bigint, radix?: number): decimal;Throws:
RangeError("... divisor cannot be 0")RangeError("non-finite number")forNumber.NaN/±InfinitySyntaxErrorfor malformed strings (invalid digits, multiple slashes, etc.)
Arithmetic (exact)
add(num: NumericLike): Decimal;
sub(num: NumericLike): Decimal;
mul(num: NumericLike): Decimal;
div(num: NumericLike): Decimal; // throws on division by zero
negate(): Decimal;All operations reduce the result to lowest terms.
Power & Elementary functions
static pow(base: NumericLike, exp: NumericLike): Decimal;
// Exact for integer exponents. For non-integer exponents falls back to Math.pow (lossy).
static exp(x: NumericLike): Decimal; // lossy (uses Math.exp)
static log(x: NumericLike, base?: NumericLike): Decimal; // lossy (uses Math.log), domain-checked
static ln(x: NumericLike): Decimal; // alias for log(x)
static sin(x: NumericLike): Decimal; // lossy (uses Math.sin)
static cos(x: NumericLike): Decimal; // lossy (uses Math.cos)
static tan(x: NumericLike): Decimal; // lossy (uses Math.tan)powwith negative integer exponents returns the exact reciprocal if defined.
Comparison & Utilities
static eq(a: NumericLike, b: NumericLike): boolean;
static lt(a: NumericLike, b: NumericLike): boolean;
static gt(a: NumericLike, b: NumericLike): boolean;
static min(a: NumericLike, b: NumericLike): Decimal; // returns one of the coerced Decimals
static max(a: NumericLike, b: NumericLike): Decimal;
static abs(x: NumericLike): Decimal;String & Number conversion
toString(radix = 10, lossy = false, withPrefix = false): string;
valueOf(): number; // see coercion notes above
toJSON(): string; // equals toString()
[Symbol.toPrimitive](hint: "number" | "string" | "default"): number | string;Examples
Exact pipelines
const price = Decimal.from("19.99"); // exact 1999/100
const qty = Decimal.from(3);
const tax = Decimal.from("21"); // 21%
const total = price.mul(qty); // 59.97 → 5997/100
const gross = total.mul(Decimal.from("1/100")).mul(tax).add(total);
// gross = total * (1 + tax/100)
gross.toString(10, true); // "72.5637..." (fixed-point, 34 digits max)Minor units (divisor)
// 12345 cents → 123.45
Decimal.from(12345n, 100n).toString(); // "123.45"Binary/hex work
Decimal.from("0b101.01").toString(); // "5.25"
Decimal.from("0x1.fp2").toString(16, false); // "7.c" (terminating in hex)
Decimal.from("0x1/0x3").toString(16, true); // "0.5555555555555555..." (fixed-point in hex if desired)Error handling
| Condition | Error type | Message (excerpt) |
| ----------------------------------------- | ----------- | ----------------------------------------------- |
| divisor === 0 in ctor/from | RangeError | divisor cannot be 0 |
| Division by zero in div / "A/B" parse | RangeError | division by zero |
| Non-finite number input | RangeError | non-finite number |
| Invalid radix (not 0 or 2..36) | RangeError | radix must be an integer in [2, 36] |
| Malformed string / invalid digit | SyntaxError | invalid mantissa / digit 'x' out of range ... |
| Hex-float exponent invalid | SyntaxError | invalid hex-float exponent |
| General powBigInt negative exponent | RangeError | powBigInt: negative exponent |
Performance notes
- Arithmetic uses fraction cross-reduction via
gcdto keep numerators/denominators small. - All integer ops are
bigint; performance scales with operand size. If you repeatedly format withlossy = true, consider caching strings if profiling shows hotspots.
Interop tips
- Always-exact JSON: keep the default
toJSON()or explicitly serialize as"N/D"for non-terminating cases by usingtoString()withlossy = false. - Always-fixed JSON: serialize with
x.toString(10, true)to force a fixed-point decimal with up to 34 fractional digits. - Numbers: use
+xorNumber(x)only if you accept IEEE-754 rounding (or if your denominator is a power of ten).
TypeScript support
Fully typed, ships with
.d.ts.Exported types:
NumericLike = Decimal | bigint | number | stringdecimalalias
Default export is the convenience factory
decimal(...).
FAQ
Q: Why does toString() sometimes return "N/D"?
A: If the fraction does not have a terminating expansion in the chosen radix (e.g., 1/3 in base-10), the default is to preserve exactness as "N/D". Use toString(radix, true) for fixed-point output.
Q: Will valueOf() always be exact?
A: Only when the denominator is a power of ten (10^k). Otherwise converting to number uses IEEE-754 and may round.
Q: Are transcendental functions exact?
A: No. exp/log/sin/cos/tan delegate to Math.* (double precision). They are provided for convenience.
