@wzo/calc 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@wzo/calc",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "Precision math + number formatting — zero runtime dependencies, BigInt internally",
6
+ "author": "nowo",
7
+ "license": "MIT",
8
+ "homepage": "https://nowo.github.io/calc",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/nowo/calc.git",
12
+ "directory": "packages/main"
13
+ },
14
+ "bugs": "https://github.com/nowo/calc/issues",
15
+ "keywords": [
16
+ "calc",
17
+ "precision",
18
+ "decimal",
19
+ "bignumber",
20
+ "format"
21
+ ],
22
+ "sideEffects": false,
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./dist/index.d.mts",
30
+ "default": "./dist/index.mjs"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.cts",
34
+ "default": "./dist/index.cjs"
35
+ }
36
+ }
37
+ },
38
+ "main": "./dist/index.cjs",
39
+ "module": "./dist/index.mjs",
40
+ "types": "./dist/index.d.mts",
41
+ "files": [
42
+ "README.zh.md",
43
+ "dist",
44
+ "src"
45
+ ],
46
+ "engines": {
47
+ "node": ">=16.6"
48
+ },
49
+ "devDependencies": {
50
+ "a-calc": "^3.0.0",
51
+ "changelogen": "^0.6.2",
52
+ "decimal.js": "^10.6.0",
53
+ "mathjs": "^15.2.0",
54
+ "tsdown": "^0.18.0",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^3.2.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "dev": "tsdown --watch",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest",
64
+ "bench": "vitest bench --run",
65
+ "release": "changelogen --release --push"
66
+ }
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ // @wzo/calc — precision math + number formatting
2
+ // Zero runtime dependencies; all precision arithmetic uses BigInt internally
3
+
4
+ export { calcAvg, calcMax, calcMin, calcSum } from './utils/aggregate'
5
+ export { calc, fmt } from './utils/calc'
6
+
7
+ export type { ICalcOptions, IDebugInfo } from './utils/calc'
8
+ export { chainAdd, chainDiv, chainMul, chainSub } from './utils/chain'
9
+
10
+ export type { IChain } from './utils/chain'
11
+ export { getConfig, resetConfig, setConfig } from './utils/config'
12
+
13
+ export type { IGlobalConfig, IPrecisionOption } from './utils/config'
14
+
15
+ export type { IFmtOptions, IFormat, Rounding } from './utils/format'
16
+
17
+ // Advanced usage: direct access to precision primitives
18
+ export {
19
+ abs,
20
+ cmp,
21
+ neg,
22
+ parse,
23
+ div as rawDiv, // division with explicit precision parameter (for add/sub/mul use addStr/subStr/mulStr)
24
+ roundBanker,
25
+ roundCeil,
26
+ roundHalfUp,
27
+ truncate,
28
+ } from './utils/precision'
29
+
30
+ export type { IDecimal } from './utils/precision'
31
+ export { add, addStr, div, divStr, mul, mulStr, sub, subStr } from './utils/standalone'
@@ -0,0 +1,109 @@
1
+ // Aggregation: calcSum / calcAvg / calcMax / calcMin
2
+ // Usage: calcSum("price", [{ price: 10 }, { price: 20 }]) → "30"
3
+ // calcSum([1, 2, 3]) → "6" (direct array form)
4
+
5
+ import type { IGlobalConfig, IPrecisionOption } from './config'
6
+ import { configWithPrecision } from './config'
7
+ import { add as addStr, cmp, div as divStr } from './precision'
8
+
9
+ type Val = string | number | bigint
10
+ // Values may be null / undefined: backends often return null/undefined values; pickValues skips them (the type reflects this design)
11
+ type Item = Record<string, Val | null | undefined>
12
+
13
+ const pickValues = (keyOrArr: string | Val[], list?: Item[]): string[] => {
14
+ let raw: Array<Val | null | undefined>
15
+ if (Array.isArray(keyOrArr)) {
16
+ raw = keyOrArr
17
+ } else {
18
+ if (!list) throw new Error('list is required when keyOrArr is a field name')
19
+ raw = list.map(item => item[keyOrArr])
20
+ }
21
+ // Skip null / undefined (backends often return empty values) to avoid parse errors from String(null)
22
+ return raw.filter(v => v != null).map(v => String(v))
23
+ }
24
+
25
+ /** Internal sum core — invalid values throw and propagate to the caller. */
26
+ const sumOf = (values: string[]): string => {
27
+ if (values.length === 0) return '0'
28
+ let sum = values[0]!
29
+ for (let i = 1; i < values.length; i++) sum = addStr(sum, values[i]!)
30
+ return sum
31
+ }
32
+
33
+ /**
34
+ * Computes the sum. Accepts two call forms: a direct value array, or a field name with an array of objects.
35
+ *
36
+ * @param keyOrArr Value array (`[1, 2, 3]`) or the field name to sum (`'price'`)
37
+ * @param list Array of objects, required when the first argument is a field name
38
+ * @returns Total sum (`string`, high precision)
39
+ * @example
40
+ * calcSum([1, 2, 3]) // '6'
41
+ * calcSum('price', [{ price: 10 }, { price: 20 }]) // '30'
42
+ */
43
+ export const calcSum = (keyOrArr: string | Val[], list?: Item[]): string => sumOf(pickValues(keyOrArr, list))
44
+
45
+ /** Computes the average with the given config precision (shared by the default {@link calcAvg} export and per-call precision entry points). */
46
+ export const calcAvgWith = (cfg: IGlobalConfig, keyOrArr: string | Val[], list?: Item[]): string => {
47
+ const values = pickValues(keyOrArr, list)
48
+ if (values.length === 0) return '0'
49
+ return divStr(sumOf(values), String(values.length), cfg._precision)
50
+ }
51
+
52
+ /**
53
+ * Computes the average (sum / count). Returns `'0'` for an empty collection.
54
+ *
55
+ * Precision defaults to the global `_precision`; pass `{ _precision }` as the **last** argument
56
+ * to override it for this call only (does not affect the global config).
57
+ *
58
+ * The first argument is either a value array (`[1,2,3]`) or a field name (`'price'`
59
+ * used together with an array of objects as the second argument).
60
+ *
61
+ * @returns Average value (`string`)
62
+ * @example
63
+ * calcAvg([1, 2, 3]) // '2'
64
+ * calcAvg('score', [{ score: 80 }, { score: 90 }]) // '85'
65
+ * calcAvg([10, 20, 25], { _precision: 2 }) // '18.33'
66
+ */
67
+ export function calcAvg(arr: Val[], opt?: IPrecisionOption): string
68
+ export function calcAvg(key: string, list: Item[], opt?: IPrecisionOption): string
69
+ export function calcAvg(keyOrArr: string | Val[], listOrOpt?: Item[] | IPrecisionOption, opt?: IPrecisionOption): string {
70
+ // Array form: second argument is opt; field-name form: second argument is list, third is opt
71
+ const isFieldForm = Array.isArray(listOrOpt)
72
+ const list = isFieldForm ? listOrOpt : undefined
73
+ const o = isFieldForm ? opt : listOrOpt
74
+ return calcAvgWith(configWithPrecision(o), keyOrArr, list)
75
+ }
76
+
77
+ /**
78
+ * Returns the maximum value (numeric comparison, not lexicographic).
79
+ *
80
+ * @param keyOrArr Value array or field name
81
+ * @param list Array of objects, required when the first argument is a field name
82
+ * @returns Maximum value (`string`); returns `'0'` for an empty collection
83
+ * @example
84
+ * calcMax([3, 10, 2]) // '10'
85
+ */
86
+ export const calcMax = (keyOrArr: string | Val[], list?: Item[]): string => {
87
+ const values = pickValues(keyOrArr, list)
88
+ if (values.length === 0) return '0'
89
+ let max = values[0]!
90
+ for (let i = 1; i < values.length; i++) if (cmp(values[i]!, max) > 0) max = values[i]!
91
+ return max
92
+ }
93
+
94
+ /**
95
+ * Returns the minimum value (numeric comparison).
96
+ *
97
+ * @param keyOrArr Value array or field name
98
+ * @param list Array of objects, required when the first argument is a field name
99
+ * @returns Minimum value (`string`); returns `'0'` for an empty collection
100
+ * @example
101
+ * calcMin([3, 10, 2]) // '2'
102
+ */
103
+ export const calcMin = (keyOrArr: string | Val[], list?: Item[]): string => {
104
+ const values = pickValues(keyOrArr, list)
105
+ if (values.length === 0) return '0'
106
+ let min = values[0]!
107
+ for (let i = 1; i < values.length; i++) if (cmp(values[i]!, min) < 0) min = values[i]!
108
+ return min
109
+ }
@@ -0,0 +1,100 @@
1
+ // Entry point calc(): arithmetic expression evaluation → (optional) formatting
2
+
3
+ import type { IGlobalConfig } from './config'
4
+ import type { IFormat } from './format'
5
+ import type { IEvalContext } from './parser'
6
+ import { getConfig } from './config'
7
+ import { formatValue, normalizeFormat } from './format'
8
+ import { evaluate } from './parser'
9
+
10
+ /** Control options for {@link calc} (all prefixed with `_`; variables are not supported — embed values via template interpolation) */
11
+ export interface ICalcOptions {
12
+ /** true ⇒ enable unit mode (% is treated as a unit marker rather than division by 100; the result retains the % suffix) */
13
+ _unit?: boolean
14
+ /** Explicit format: an {@link IFormat} options object */
15
+ _fmt?: IFormat
16
+ /** Division precision (defaults to the global `_precision`) */
17
+ _precision?: number
18
+ /**
19
+ * Step-by-step evaluation debug:
20
+ * - `true` ⇒ prints to console
21
+ * - pass a function ⇒ receives {@link IDebugInfo} via callback, no console output
22
+ */
23
+ _debug?: boolean | ((info: IDebugInfo) => void)
24
+ }
25
+
26
+ /** Evaluation info collected by `_debug` */
27
+ export interface IDebugInfo {
28
+ /** The original expression */
29
+ expr: string
30
+ /** Step-by-step evaluation trace (one entry per step) */
31
+ steps: string[]
32
+ /** Raw result before formatting */
33
+ value: string
34
+ /** Final returned value */
35
+ result: string | number
36
+ }
37
+
38
+ const RE_HAS_PERCENT = /%/
39
+
40
+ const emitDebug = (info: IDebugInfo, debug: true | ((info: IDebugInfo) => void)): void => {
41
+ if (typeof debug === 'function') {
42
+ debug(info)
43
+ return
44
+ }
45
+ // eslint-disable-next-line no-console
46
+ console.log(
47
+ `[calc] ${info.expr}\n${
48
+ info.steps.length ? `${info.steps.map(s => ` · ${s}`).join('\n')}\n` : ''
49
+ } = ${info.result}`,
50
+ )
51
+ }
52
+
53
+ /** Evaluate with an explicit config (the default export {@link calc} delegates to this). */
54
+ export function calcWith(cfg: IGlobalConfig, expr: string, options: ICalcOptions = {}): string | number {
55
+ const steps = options._debug ? [] as string[] : undefined
56
+ const ctx: IEvalContext = {
57
+ unit: !!options._unit,
58
+ precision: options._precision ?? cfg._precision,
59
+ trace: steps,
60
+ }
61
+ const value = evaluate(expr, ctx)
62
+
63
+ // Unit mode: if the expression contains %, append % to the result (only when no explicit format is set)
64
+ const exprHasPercent = options._unit && RE_HAS_PERCENT.test(expr)
65
+
66
+ const fmtSpec = options._fmt || cfg._fmt
67
+ const result = !fmtSpec
68
+ ? (exprHasPercent ? `${value}%` : value)
69
+ : formatValue(value, normalizeFormat(fmtSpec))
70
+
71
+ if (options._debug) emitDebug({ expr, steps: steps!, value, result }, options._debug)
72
+ return result
73
+ }
74
+
75
+ /**
76
+ * Main entry point: evaluates an arithmetic expression string and optionally formats the output.
77
+ *
78
+ * Expressions are pure arithmetic: the four basic operations, parentheses, and math functions
79
+ * (`max`/`min`/`clamp`…). **Variables are not supported** — embed values directly in the
80
+ * expression via template interpolation: `` calc(`${price} * ${qty}`) ``.
81
+ * Formatting is specified via `options._fmt` (an {@link IFormat} object).
82
+ *
83
+ * `calc` is designed for **computation**: on error (invalid expression, etc.) it **throws
84
+ * directly** and the caller is responsible for handling it.
85
+ * For **display** scenarios that need a fallback on error, use {@link fmt} instead
86
+ * (same API and supports arithmetic, but returns `_error` on failure rather than throwing).
87
+ *
88
+ * @param expr Arithmetic expression, e.g. `'1 + 2 * 3'`, `'(1 + 2) / 3'`
89
+ * @param options Control options (see {@link ICalcOptions})
90
+ * @returns Computation result (`string`; `number` when `_fmt.output` is `'number'`)
91
+ * @throws Throws when the expression is invalid
92
+ * @example
93
+ * calc('1 + 2 * 3') // '7'
94
+ * calc(`${price} * ${qty}`) // use template interpolation instead of variables
95
+ * calc('1 + 2', { _fmt: { decimals: 2 } }) // '3.00'
96
+ */
97
+ export const calc = (expr: string, options: ICalcOptions = {}): string | number => calcWith(getConfig(), expr, options)
98
+
99
+ /** Direct formatting */
100
+ export { fmt } from './format'
@@ -0,0 +1,127 @@
1
+ // Chaining API: chainAdd/chainSub/chainMul/chainDiv → returns an object supporting further .add().sub().mul().div() calls
2
+ // Terminate: call with () or (formatStr) to obtain the final string/number result
3
+ // Errors (invalid arguments, etc.) are thrown directly and must be handled by the caller
4
+
5
+ import type { IGlobalConfig, IPrecisionOption } from './config'
6
+ import type { IFormat } from './format'
7
+ import { configWithPrecision, getConfig } from './config'
8
+ import { formatValue, normalizeFormat } from './format'
9
+ import { add, div, mul, sub } from './precision'
10
+
11
+ type Val = string | number | bigint
12
+
13
+ /** Extracts a per-call precision option from the end of a variadic argument list (last element is an object ⇒ treated as {@link IPrecisionOption}). */
14
+ const splitPrecision = (args: Array<Val | IPrecisionOption>): [Val[], IPrecisionOption?] => {
15
+ const last = args.at(-1)
16
+ if (last != null && typeof last === 'object') {
17
+ return [args.slice(0, -1) as Val[], last as IPrecisionOption]
18
+ }
19
+ return [args as Val[], undefined]
20
+ }
21
+
22
+ /**
23
+ * A chaining computation object. Every arithmetic method returns itself for further chaining;
24
+ * calling it as a function terminates the chain and returns the result.
25
+ */
26
+ export interface IChain {
27
+ /** Continues accumulating additions; returns itself for chaining. */
28
+ add: (...args: Val[]) => IChain
29
+ /** Continues accumulating subtractions; returns itself. */
30
+ sub: (...args: Val[]) => IChain
31
+ /** Continues accumulating multiplications; returns itself. */
32
+ mul: (...args: Val[]) => IChain
33
+ /** Continues accumulating divisions (uses the instance's `_precision`); returns itself. */
34
+ div: (...args: Val[]) => IChain
35
+ /**
36
+ * Terminates the chain and retrieves the result: no argument ⇒ returns the current value (`string`);
37
+ * pass an {@link IFormat} object ⇒ returns `string | number` according to the formatting rules.
38
+ */
39
+ (format?: IFormat): string | number
40
+ }
41
+
42
+ const makeChain = (cfg: IGlobalConfig, initial: string): IChain => {
43
+ let value = initial
44
+ const fn = ((format?: IFormat): string | number => {
45
+ if (!format) return value
46
+ return formatValue(value, normalizeFormat(format))
47
+ }) as IChain
48
+
49
+ fn.add = (...args: Val[]) => {
50
+ for (const a of args) value = add(value, a)
51
+ return fn
52
+ }
53
+ fn.sub = (...args: Val[]) => {
54
+ for (const a of args) value = sub(value, a)
55
+ return fn
56
+ }
57
+ fn.mul = (...args: Val[]) => {
58
+ for (const a of args) value = mul(value, a)
59
+ return fn
60
+ }
61
+ fn.div = (...args: Val[]) => {
62
+ for (const a of args) value = div(value, a, cfg._precision)
63
+ return fn
64
+ }
65
+ return fn
66
+ }
67
+
68
+ const reduceWith = (op: (a: string, b: Val) => string, args: Val[]): string => {
69
+ if (args.length === 0) return '0'
70
+ let r = String(args[0])
71
+ for (let i = 1; i < args.length; i++) r = op(r, args[i]!)
72
+ return r
73
+ }
74
+
75
+ // ── Internal implementations that accept a config (shared by default exports and per-call precision entry points) ──
76
+ export const chainAddWith = (cfg: IGlobalConfig, ...args: Val[]): IChain => makeChain(cfg, reduceWith(add, args))
77
+ export const chainSubWith = (cfg: IGlobalConfig, ...args: Val[]): IChain => makeChain(cfg, reduceWith(sub, args))
78
+ export const chainMulWith = (cfg: IGlobalConfig, ...args: Val[]): IChain => makeChain(cfg, reduceWith(mul, args))
79
+ export const chainDivWith = (cfg: IGlobalConfig, ...args: Val[]): IChain => makeChain(cfg, reduceWith((a, b) => div(a, b, cfg._precision), args))
80
+
81
+ /**
82
+ * Starts a chaining computation with addition (`a + b + c ...`).
83
+ *
84
+ * @param args Initial values to accumulate via addition
85
+ * @returns An {@link IChain} supporting further `.add().sub().mul().div()` calls
86
+ * @example
87
+ * chainAdd(1, 2).mul(3)() // '9'
88
+ * chainAdd(1, 2).mul(3)({ decimals: 2 }) // '9.00'
89
+ */
90
+ export const chainAdd = (...args: Val[]): IChain => chainAddWith(getConfig(), ...args)
91
+ /**
92
+ * Starts a chaining computation with subtraction (`a - b - c ...`).
93
+ *
94
+ * @param args Initial minuend and subtrahends
95
+ * @returns An {@link IChain} supporting further chaining
96
+ * @example
97
+ * chainSub(10, 1, 2)() // '7'
98
+ */
99
+ export const chainSub = (...args: Val[]): IChain => chainSubWith(getConfig(), ...args)
100
+ /**
101
+ * Starts a chaining computation with multiplication (`a * b * c ...`).
102
+ *
103
+ * @param args Initial factors to multiply together
104
+ * @returns An {@link IChain} supporting further chaining
105
+ * @example
106
+ * chainMul(2, 3).add(4)() // '10'
107
+ */
108
+ export const chainMul = (...args: Val[]): IChain => chainMulWith(getConfig(), ...args)
109
+ /**
110
+ * Starts a chaining computation with division (`a / b / c ...`).
111
+ *
112
+ * Precision defaults to the global `_precision`; pass `{ _precision }` as the **last** argument
113
+ * to override the division precision for this chain (including subsequent `.div()` calls)
114
+ * without affecting the global config.
115
+ *
116
+ * @param args Initial dividend and divisors, with an optional {@link IPrecisionOption} at the end
117
+ * @returns An {@link IChain} supporting further chaining
118
+ * @example
119
+ * chainDiv(100, 4)() // '25'
120
+ * chainDiv(100, 3, { _precision: 5 })() // '33.33333'
121
+ */
122
+ export function chainDiv(...args: Val[]): IChain
123
+ export function chainDiv(...args: [...Val[], IPrecisionOption]): IChain
124
+ export function chainDiv(...args: Array<Val | IPrecisionOption>): IChain {
125
+ const [values, opt] = splitPrecision(args)
126
+ return chainDivWith(configWithPrecision(opt), ...values)
127
+ }
@@ -0,0 +1,59 @@
1
+ // Global config: error fallback, default format, division precision
2
+
3
+ import type { IFormat } from './format'
4
+
5
+ export interface IGlobalConfig {
6
+ /** Fallback value on error: defaults to `'-'`; set to `0` to return `'0'` on error. */
7
+ _error: string | number
8
+ /** Global default format ({@link IFormat} object). */
9
+ _fmt: IFormat | undefined
10
+ /** Division precision (maximum decimal places). */
11
+ _precision: number
12
+ }
13
+
14
+ /** Default configuration values. */
15
+ export const DEFAULT_CONFIG: IGlobalConfig = {
16
+ _error: '-',
17
+ _fmt: undefined,
18
+ _precision: 50,
19
+ }
20
+
21
+ /** Per-call precision override: optional last argument accepted by `div` / `divStr` / `chainDiv` / `calcAvg`. */
22
+ export interface IPrecisionOption {
23
+ /** Division precision for this call, overrides the global `_precision` (does not affect the global config). */
24
+ _precision?: number
25
+ }
26
+
27
+ const config: IGlobalConfig = { ...DEFAULT_CONFIG }
28
+
29
+ /**
30
+ * Partially updates the global configuration (only the provided fields are overwritten).
31
+ *
32
+ * @param patch Configuration fields to update
33
+ * @example
34
+ * setConfig({ _precision: 10, _fmt: { decimals: 2 } })
35
+ */
36
+ export const setConfig = (patch: Partial<IGlobalConfig>): void => {
37
+ Object.assign(config, patch)
38
+ }
39
+
40
+ /**
41
+ * Resets the global configuration to its default values.
42
+ *
43
+ * @example
44
+ * resetConfig()
45
+ */
46
+ export const resetConfig = (): void => {
47
+ Object.assign(config, DEFAULT_CONFIG)
48
+ }
49
+
50
+ /**
51
+ * Returns the current global configuration object (by reference — do not mutate directly; use {@link setConfig}).
52
+ *
53
+ * @returns The current {@link IGlobalConfig}
54
+ */
55
+ export const getConfig = (): IGlobalConfig => config
56
+
57
+ /** Returns the global config; if `_precision` is provided, returns a copy with that field overridden without mutating the global singleton. */
58
+ export const configWithPrecision = (opt?: IPrecisionOption): IGlobalConfig =>
59
+ opt?._precision != null ? { ...config, _precision: opt._precision } : config