js-precision
v0.3.0
Published
Frontend high-precision decimal library powered by Rust + WebAssembly with strict-by-default immutable APIs.
Maintainers
Readme
js-precision
面向前端业务的高精度十进制计算库,基于 Rust + WebAssembly,底层使用 rust_decimal。
A frontend-oriented high-precision decimal library powered by Rust + WebAssembly, built on top of rust_decimal.
npm i js-precision中文
开发自测
# Rust 单测 / 集成测试
npm run test:rust
# wasm 运行测试(Node 环境)
npm run test:wasm:node快速上手
import { Precise, RoundingMode } from "js-precision";
const result = new Precise("0.1")
.add("0.2")
.div("3")
.withPrecision(8)
.withRounding(RoundingMode.HalfUp)
.toFixed();
console.log(result); // "0.10000000"strict 模式下,非法输入会抛错而不是静默兜底。所有运算返回新实例,不修改原值。
核心设计
| 特性 | 说明 |
| --- | --- |
| 严格输入 | 非法字符串 / NaN / Infinity 直接抛错 |
| 不可变链式 | add/sub/mul/div 均返回新 Precise 实例 |
| 精度可配置 | withPrecision(n) 设置默认输出位数(0–28) |
| 舍入可选 | Trunk(截断)/ HalfUp(四舍五入)/ HalfEven(银行家舍入) |
| 宽松模式 | *Loose 系列:非法输入按 0 或 1 兜底,不抛错 |
构造
// strict(推荐)
new Precise("123.45")
Precise.fromString("1e-7")
Precise.fromNumber(1.5) // ⚠️ 已有浮点误差无法修复,金融场景优先用字符串
// loose(容错场景)
Precise.fromStringLoose("bad") // → 0
Precise.fromNumberLoose(NaN) // → 0
// 分转元
Precise.fromCents("12345") // → 123.45(scale 默认 2)
Precise.fromCents("12345", 3) // → 12.345
Precise.fromCentsLoose("bad") // → 0,不抛错四则运算
const base = new Precise("100");
// strict(推荐)
base.add("20") // 120
base.sub("5") // 95
base.mul("1.1") // 110
base.div("3") // 33.333... div("0") 会抛错
// 语义别名(strict)
base.plus("20")
base.minus("5")
base.times("1.1")
base.dividedBy("3")
// loose(非法输入不抛错)
base.addLoose("abc") // 加 0 → 100
base.subLoose("abc") // 减 0 → 100
base.mulLoose("abc") // 乘 1 → 100 (注意:不是乘 0)
base.divLoose("0") // 除数为 0 → 返回原值 100链式调用不改变原值:
const a = new Precise("10"); const b = a.add("5"); // b = 15 a.toString(); // 仍然是 "10"
比较
const p = new Precise("10.5");
p.eq("10.5") // true
p.lt("11") // true
p.lte("10.5") // true
p.gt("10") // true
p.gte("10.5") // true比较基于高精度 Decimal,非法参数同样抛错。
聚合
const values = ["1.2", 2.3, "3.4"];
Precise.sum(values).toString() // "6.9"
Precise.avg(values).withPrecision(4).toFixed() // "2.3000"
Precise.min(values).toString() // "1.2"
Precise.max(values).toString() // "3.4"空数组行为: sum / avg / min / max 传入空数组均会抛错。
精度与输出
const amount = new Precise("1234567.891")
.withPrecision(2)
.withRounding(RoundingMode.HalfUp);
amount.toFixed() // "1234567.89"
amount.toDisplay(2, true) // "1,234,567.89"
amount.toDisplayWith({ grouping: true }) // "1,234,567.89"(使用实例精度)
amount.toDisplayWith({ grouping: true, separator: "_" }) // "1_234_567.89"
amount.toString() // "1234567.891"(原始值,不补零)
amount.toNumber() // 1234567.891(可能丢精度)
amount.toCents() // "123456789"(元转分,scale 默认 2)toFixed vs toString:
| 方法 | 补零 | 受 withPrecision 影响 |
| --- | --- | --- |
| toFixed() | ✅ | ✅ |
| toString() | ❌ | ❌ |
舍入模式:
| 模式 | 说明 | 1.235 保留 2 位 |
| --- | --- | --- |
| RoundingMode.Trunk | 向 0 截断(默认) | 1.23 |
| RoundingMode.HalfUp | 四舍五入 | 1.24 |
| RoundingMode.HalfEven | 银行家舍入 | 1.24(4 为偶数) |
完整示例:订单结算
import { Precise, RoundingMode } from "js-precision";
const items = [
{ qty: "2", unitPrice: "199.90" },
{ qty: "1", unitPrice: "88.50" },
{ qty: "3", unitPrice: "12.34" },
];
try {
// 行小计求和
const subtotal = Precise.sum(
items.map((it) => new Precise(it.qty).mul(it.unitPrice).toString())
);
// 九五折 + 6% 税
const payable = subtotal
.mul("0.95")
.mul("1.06")
.withPrecision(2)
.withRounding(RoundingMode.HalfUp);
console.log(payable.toDisplayWith({ grouping: true })); // "533.78"
} catch (err) {
console.error("计算失败:", err);
}错误处理
所有 strict API 在以下情况抛错:
- 非法字符串(
"abc"、""、"1.2.3") - 非有限数(
NaN、Infinity) - 除以零(
div("0")) - 空数组聚合(
sum/avg/min/max([])) fromCents传入非整数
try {
new Precise(userInput).div(rate).withPrecision(2).toFixed();
} catch (err) {
// 统一在边界捕获,内部链式无需逐步判断
}v1 升级说明见 MIGRATION_V1_TO_V2.md
English
Local Validation
# Rust unit/integration tests
npm run test:rust
# wasm runtime tests (Node)
npm run test:wasm:nodeQuick Start
import { Precise, RoundingMode } from "js-precision";
const result = new Precise("0.1")
.add("0.2")
.div("3")
.withPrecision(8)
.withRounding(RoundingMode.HalfUp)
.toFixed();
console.log(result); // "0.10000000"In strict mode, invalid input throws instead of silently falling back. All arithmetic methods return new instances (immutable style).
Design Highlights
| Feature | Description |
| --- | --- |
| Strict Input | Invalid strings / NaN / Infinity throw errors |
| Immutable Chaining | add/sub/mul/div always return new Precise instances |
| Configurable Precision | withPrecision(n) controls default output scale (0–28) |
| Selectable Rounding | Trunk / HalfUp / HalfEven |
| Loose Mode | *Loose APIs use explicit fallback (0 or 1) without throwing |
Construction
// strict (recommended)
new Precise("123.45")
Precise.fromString("1e-7")
Precise.fromNumber(1.5) // binary floating-point precision is already decided in JS
// loose
Precise.fromStringLoose("bad") // -> 0
Precise.fromNumberLoose(NaN) // -> 0
// cents -> decimal
Precise.fromCents("12345") // -> 123.45 (default scale = 2)
Precise.fromCents("12345", 3) // -> 12.345
Precise.fromCentsLoose("bad") // -> 0Arithmetic
const base = new Precise("100");
// strict
base.add("20")
base.sub("5")
base.mul("1.1")
base.div("3") // div("0") throws
// strict aliases
base.plus("20")
base.minus("5")
base.times("1.1")
base.dividedBy("3")
// loose
base.addLoose("abc") // add 0
base.subLoose("abc") // sub 0
base.mulLoose("abc") // mul 1 (keeps original value)
base.divLoose("0") // keep original valueImmutable behavior:
const a = new Precise("10"); const b = a.add("5"); a.toString(); // still "10"
Comparison
const p = new Precise("10.5");
p.eq("10.5")
p.lt("11")
p.lte("10.5")
p.gt("10")
p.gte("10.5")All comparisons are based on high-precision decimal values and validate inputs strictly.
Aggregation
const values = ["1.2", 2.3, "3.4"];
Precise.sum(values).toString() // "6.9"
Precise.avg(values).withPrecision(4).toFixed() // "2.3000"
Precise.min(values).toString() // "1.2"
Precise.max(values).toString() // "3.4"Empty arrays: sum/avg/min/max([]) all throw.
Precision and Output
const amount = new Precise("1234567.891")
.withPrecision(2)
.withRounding(RoundingMode.HalfUp);
amount.toFixed() // "1234567.89"
amount.toDisplay(2, true) // "1,234,567.89"
amount.toDisplayWith({ grouping: true }) // "1,234,567.89"
amount.toDisplayWith({ grouping: true, separator: "_" }) // "1_234_567.89"
amount.toString() // "1234567.891"
amount.toNumber() // 1234567.891 (precision may be lost)
amount.toCents() // "123456789"toFixed vs toString:
| Method | Zero-padding | Affected by withPrecision |
| --- | --- | --- |
| toFixed() | ✅ | ✅ |
| toString() | ❌ | ❌ |
Rounding modes:
| Mode | Description | 1.235 with 2 digits |
| --- | --- | --- |
| RoundingMode.Trunk | Truncate toward 0 (default) | 1.23 |
| RoundingMode.HalfUp | Round half away from 0 | 1.24 |
| RoundingMode.HalfEven | Bankers rounding | 1.24 |
Full Example: Order Settlement
import { Precise, RoundingMode } from "js-precision";
const items = [
{ qty: "2", unitPrice: "199.90" },
{ qty: "1", unitPrice: "88.50" },
{ qty: "3", unitPrice: "12.34" },
];
try {
const subtotal = Precise.sum(
items.map((it) => new Precise(it.qty).mul(it.unitPrice).toString())
);
const payable = subtotal
.mul("0.95")
.mul("1.06")
.withPrecision(2)
.withRounding(RoundingMode.HalfUp);
console.log(payable.toDisplayWith({ grouping: true }));
} catch (err) {
console.error("calculation failed:", err);
}Error Handling
Strict APIs throw on:
- Invalid decimal strings (
"abc","","1.2.3") - Non-finite numbers (
NaN,Infinity) - Division by zero (
div("0")) - Empty array aggregations (
sum/avg/min/max([])) - Non-integer input for
fromCents
try {
new Precise(userInput).div(rate).withPrecision(2).toFixed();
} catch (err) {
// Catch once at boundaries
}For v1 migration details, see MIGRATION_V1_TO_V2.md.
