npm package: @silyze/decimal
Description: Decimal (fraction) number library
Author: Mihail Mojsoski
Latest version: 1.0.0

# @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 / div` with 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

```bash
npm i @silyze/decimal
# or
pnpm add @silyze/decimal
# or
yarn add @silyze/decimal
```

---

## Quick start

```ts
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. `Decimal` stores 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 via `lossy = true`.
- **Bigint-powered.** No overflow in intermediate integer steps (limited by available memory/time).

---

## Concepts & Behavior

### Representation

- Internals: two private `bigint` fields `#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 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:

```ts
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**.

- `withPrefix` adds `0x` for base-16 or `0b` for base-2 when formatting integers.

Examples:

```ts
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 `denominator` is a power of ten → uses precise fixed-point conversion.
  - Otherwise → `Number(n) / Number(d)` (IEEE-754 rounding).

- `toJSON()` returns `toString()`; 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

```ts
export type NumericLike = Decimal | bigint | number | string;
export type decimal = Decimal; // alias
```

### Construction

```ts
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")` for `Number.NaN`/±`Infinity`
  - `SyntaxError` for malformed strings (invalid digits, multiple slashes, etc.)

### Arithmetic (exact)

```ts
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

```ts
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)
```

- `pow` with negative integer exponents returns the exact reciprocal if defined.

### Comparison & Utilities

```ts
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

```ts
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

```ts
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)

```ts
// 12345 cents → 123.45
Decimal.from(12345n, 100n).toString(); // "123.45"
```

### Binary/hex work

```ts
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 `gcd` to keep numerators/denominators small.
- All integer ops are `bigint`; performance scales with operand size.
  If you repeatedly format with `lossy = 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 using `toString()` with `lossy = false`.
- **Always-fixed JSON**: serialize with `x.toString(10, true)` to force a fixed-point decimal with up to 34 fractional digits.
- **Numbers**: use `+x` or `Number(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 | string`
  - `decimal` alias

- 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.