@jarenjs/core 0.9.2 → 0.34.0

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.
Files changed (121) hide show
  1. package/ARCHITECTURE.md +229 -26
  2. package/README.md +64 -10
  3. package/dist/types/array.d.ts +13 -0
  4. package/dist/types/cache.d.ts +166 -0
  5. package/dist/types/chunk.d.ts +102 -0
  6. package/dist/types/color.d.ts +64 -0
  7. package/dist/types/convert/convert.d.ts +33 -0
  8. package/dist/types/convert/currency.d.ts +47 -0
  9. package/dist/types/convert/index.d.ts +9 -0
  10. package/dist/types/convert/registry.d.ts +55 -0
  11. package/dist/types/dates/civil.d.ts +146 -0
  12. package/dist/types/dates/duration.d.ts +83 -0
  13. package/dist/types/dates/format.d.ts +59 -0
  14. package/dist/types/dates/index.d.ts +4 -0
  15. package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
  16. package/dist/types/errors.d.ts +77 -0
  17. package/dist/types/finance/amortization.d.ts +51 -0
  18. package/dist/types/finance/bond.d.ts +45 -0
  19. package/dist/types/finance/cashflow.d.ts +44 -0
  20. package/dist/types/finance/depreciation.d.ts +26 -0
  21. package/dist/types/finance/index.d.ts +16 -0
  22. package/dist/types/finance/indicators.d.ts +76 -0
  23. package/dist/types/finance/interest.d.ts +44 -0
  24. package/dist/types/finance/returns.d.ts +43 -0
  25. package/dist/types/finance/tvm.d.ts +50 -0
  26. package/dist/types/geo/angle.d.ts +4 -0
  27. package/dist/types/geo/bbox.d.ts +32 -0
  28. package/dist/types/geo/distance.d.ts +67 -0
  29. package/dist/types/geo/geohash.d.ts +63 -0
  30. package/dist/types/geo/geojson.d.ts +108 -0
  31. package/dist/types/geo/index-tree.d.ts +52 -0
  32. package/dist/types/geo/index.d.ts +11 -0
  33. package/dist/types/geo/mercator.d.ts +57 -0
  34. package/dist/types/geo/predicates.d.ts +44 -0
  35. package/dist/types/geo/ring.d.ts +80 -0
  36. package/dist/types/geo/simplify.d.ts +31 -0
  37. package/dist/types/geo/valid.d.ts +24 -0
  38. package/dist/types/geo/wkt.d.ts +16 -0
  39. package/dist/types/math/float64.d.ts +126 -3
  40. package/dist/types/math/format.d.ts +58 -0
  41. package/dist/types/math/index.d.ts +5 -0
  42. package/dist/types/math/mat4.d.ts +60 -0
  43. package/dist/types/math/project.d.ts +54 -0
  44. package/dist/types/math/solve.d.ts +67 -0
  45. package/dist/types/math/word.d.ts +82 -0
  46. package/dist/types/message.d.ts +52 -0
  47. package/dist/types/object.d.ts +151 -0
  48. package/dist/types/scan.d.ts +30 -0
  49. package/dist/types/schema.d.ts +24 -0
  50. package/dist/types/string.d.ts +96 -0
  51. package/dist/types/text/base64.d.ts +6 -4
  52. package/dist/types/text/email.d.ts +0 -1
  53. package/dist/types/text/host.d.ts +39 -8
  54. package/dist/types/text/i18n.d.ts +13 -1
  55. package/dist/types/text/index.d.ts +1 -0
  56. package/dist/types/text/misc.d.ts +15 -1
  57. package/dist/types/text/punycode.d.ts +45 -85
  58. package/docs/CONVERT.md +45 -0
  59. package/docs/DATES.md +78 -0
  60. package/docs/FINANCE.md +59 -0
  61. package/docs/GEO.md +114 -0
  62. package/docs/MATH.md +75 -0
  63. package/package.json +57 -4
  64. package/src/array.js +16 -0
  65. package/src/cache.js +206 -0
  66. package/src/chunk.js +159 -0
  67. package/src/color.js +125 -0
  68. package/src/convert/convert.js +59 -0
  69. package/src/convert/currency.js +74 -0
  70. package/src/convert/index.js +11 -0
  71. package/src/convert/registry.js +213 -0
  72. package/src/dates/civil.js +360 -0
  73. package/src/dates/duration.js +225 -0
  74. package/src/dates/format.js +238 -0
  75. package/src/dates/index.js +30 -0
  76. package/src/dates/rfc3339.js +621 -0
  77. package/src/errors.js +102 -0
  78. package/src/finance/amortization.js +50 -0
  79. package/src/finance/bond.js +100 -0
  80. package/src/finance/cashflow.js +122 -0
  81. package/src/finance/depreciation.js +54 -0
  82. package/src/finance/index.js +27 -0
  83. package/src/finance/indicators.js +207 -0
  84. package/src/finance/interest.js +63 -0
  85. package/src/finance/returns.js +90 -0
  86. package/src/finance/tvm.js +94 -0
  87. package/src/function.js +0 -2
  88. package/src/geo/angle.js +12 -0
  89. package/src/geo/bbox.js +80 -0
  90. package/src/geo/distance.js +155 -0
  91. package/src/geo/geohash.js +224 -0
  92. package/src/geo/geojson.js +361 -0
  93. package/src/geo/index-tree.js +294 -0
  94. package/src/geo/index.js +52 -0
  95. package/src/geo/mercator.js +124 -0
  96. package/src/geo/predicates.js +313 -0
  97. package/src/geo/ring.js +182 -0
  98. package/src/geo/simplify.js +130 -0
  99. package/src/geo/valid.js +142 -0
  100. package/src/geo/wkt.js +262 -0
  101. package/src/math/float64.js +228 -11
  102. package/src/math/format.js +157 -0
  103. package/src/math/index.js +5 -0
  104. package/src/math/mat4.js +131 -0
  105. package/src/math/project.js +49 -0
  106. package/src/math/solve.js +112 -0
  107. package/src/math/word.js +177 -0
  108. package/src/message.js +119 -0
  109. package/src/object.js +329 -0
  110. package/src/scan.js +42 -0
  111. package/src/schema.js +37 -0
  112. package/src/string.js +189 -0
  113. package/src/text/base64.js +15 -52
  114. package/src/text/email.js +0 -5
  115. package/src/text/host.js +515 -140
  116. package/src/text/i18n.js +13 -6
  117. package/src/text/identifiers.js +1 -1
  118. package/src/text/index.js +1 -0
  119. package/src/text/misc.js +81 -5
  120. package/src/text/punycode.js +235 -323
  121. package/src/dates.js +0 -371
@@ -0,0 +1,157 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Number formatting and parsing. The
4
+ * one place the suite formats a number for display: notation control
5
+ * (auto/fixed/sci/eng), thousands grouping and radix output, plus a
6
+ * radix-aware `parseNumber`. Radix I/O delegates to `math/word.js` so
7
+ * base conversion is never re-implemented.
8
+ */
9
+
10
+ import { toBase, fromBase } from './word.js';
11
+
12
+ /**
13
+ * @typedef {object} FormatOptions
14
+ * @property {'auto'|'fixed'|'sci'|'eng'} [notation] default 'auto'
15
+ * @property {number} [precision] significant/decimal digits (notation-dependent)
16
+ * @property {boolean|string} [group] thousands separator: true → ',', or a custom string
17
+ * @property {number} [radix] 2..36; when not 10, `value` is formatted as an integer word
18
+ * @property {string} [decimal] decimal mark, default '.'
19
+ */
20
+
21
+ /**
22
+ * Insert a grouping separator every three digits of an integer string
23
+ * (sign-aware).
24
+ * @param {string} intPart
25
+ * @param {string} sep
26
+ * @returns {string}
27
+ */
28
+ function groupInteger(intPart, sep) {
29
+ let neg = '';
30
+ let s = intPart;
31
+ if (s.startsWith('-')) { neg = '-'; s = s.slice(1); }
32
+ const parts = [];
33
+ for (let i = s.length; i > 0; i -= 3) {
34
+ parts.unshift(s.slice(Math.max(0, i - 3), i));
35
+ }
36
+ return neg + parts.join(sep);
37
+ }
38
+
39
+ /**
40
+ * Engineering notation: mantissa in [1, 1000) times 10^(3k).
41
+ * @param {number} value
42
+ * @param {number} precision
43
+ * @returns {string}
44
+ */
45
+ function toEngineering(value, precision) {
46
+ if (value === 0) return '0e+0';
47
+ const exp = Math.floor(Math.log10(Math.abs(value)));
48
+ const engExp = Math.floor(exp / 3) * 3;
49
+ const mantissa = value / Math.pow(10, engExp);
50
+ const m = precision >= 0 ? mantissa.toFixed(precision) : String(mantissa);
51
+ return `${m}e${engExp >= 0 ? '+' : ''}${engExp}`;
52
+ }
53
+
54
+ /**
55
+ * Format a number for display.
56
+ *
57
+ * `notation: 'auto'` (the default) is NOT a drop-in for a fixed-unit
58
+ * readout: the auto branch escapes to exponential once
59
+ * `abs(value) >= 1e21` or `abs(value) < 1e-6`, so
60
+ * `formatNumber(v, { precision: 3 })` is not interchangeable with a
61
+ * plain `Number(v.toPrecision(3))`. A caller that appends its own unit
62
+ * - `"0.0509 ms"` - wants the value never to become `5.09e-5`, and must
63
+ * either pass `notation: 'fixed'` or keep its own rounder. See
64
+ * packages/core/docs/MATH.md.
65
+ * @param {number} value
66
+ * @param {FormatOptions} [opts]
67
+ * @returns {string}
68
+ */
69
+ export function formatNumber(value, opts = {}) {
70
+ value = +value;
71
+ if (Number.isNaN(value)) return 'NaN';
72
+ if (value === Infinity) return '∞';
73
+ if (value === -Infinity) return '-∞';
74
+
75
+ const notation = opts.notation ?? 'auto';
76
+ const sep = opts.group === true ? ',' : (typeof opts.group === 'string' ? opts.group : '');
77
+ const decimal = opts.decimal ?? '.';
78
+
79
+ // Radix output: integer word in the requested base.
80
+ if (opts.radix !== undefined && opts.radix !== 10) {
81
+ let str = toBase(Math.trunc(value), opts.radix, { upper: true });
82
+ if (sep) str = groupInteger(str, sep);
83
+ return str;
84
+ }
85
+
86
+ let out;
87
+ if (notation === 'fixed') {
88
+ out = value.toFixed(opts.precision ?? 2);
89
+ }
90
+ else if (notation === 'sci') {
91
+ out = value.toExponential(opts.precision ?? undefined);
92
+ }
93
+ else if (notation === 'eng') {
94
+ out = toEngineering(value, opts.precision ?? 3);
95
+ }
96
+ else {
97
+ // auto: plain decimal for human-scale magnitudes, else scientific.
98
+ const abs = Math.abs(value);
99
+ if (value !== 0 && (abs >= 1e21 || abs < 1e-6)) {
100
+ out = opts.precision !== undefined ? value.toExponential(opts.precision) : value.toExponential();
101
+ }
102
+ else if (opts.precision !== undefined) {
103
+ out = String(Number(value.toPrecision(opts.precision)));
104
+ }
105
+ else {
106
+ out = String(value);
107
+ }
108
+ }
109
+
110
+ if (sep || decimal !== '.') {
111
+ const eIdx = out.search(/[eE]/);
112
+ const exp = eIdx >= 0 ? out.slice(eIdx) : '';
113
+ const mant = eIdx >= 0 ? out.slice(0, eIdx) : out;
114
+ const dot = mant.indexOf('.');
115
+ const intPart = dot >= 0 ? mant.slice(0, dot) : mant;
116
+ const frac = dot >= 0 ? mant.slice(dot + 1) : '';
117
+ out = (sep ? groupInteger(intPart, sep) : intPart)
118
+ + (frac ? decimal + frac : '')
119
+ + exp;
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /**
125
+ * Parse a numeric string, radix-aware. Recognizes `0x`/`0o`/`0b`
126
+ * prefixes, an explicit `radix` option, grouping separators (`,`/`_`/
127
+ * space) and base-10 scientific notation. Returns `NaN` on failure
128
+ * rather than throwing.
129
+ *
130
+ * @param {string} str
131
+ * @param {{ radix?: number }} [opts]
132
+ * @returns {number}
133
+ */
134
+ export function parseNumber(str, opts = {}) {
135
+ if (typeof str !== 'number') str = String(str);
136
+ else return +str;
137
+ let s = str.trim();
138
+ if (s === '') return NaN;
139
+ const lower = s.toLowerCase();
140
+ if (lower === 'nan') return NaN;
141
+ if (lower === '∞' || lower === 'infinity' || lower === '+infinity') return Infinity;
142
+ if (lower === '-∞' || lower === '-infinity') return -Infinity;
143
+
144
+ const prefixed = /^[+-]?0[xob]/i.test(s);
145
+ if (prefixed || (opts.radix !== undefined && opts.radix !== 10)) {
146
+ try {
147
+ return Number(fromBase(s, opts.radix ?? 10));
148
+ }
149
+ catch {
150
+ return NaN;
151
+ }
152
+ }
153
+ // Base 10: strip grouping, keep the numeric grammar.
154
+ const cleaned = s.replace(/[,_ ]/g, '');
155
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(cleaned)) return NaN;
156
+ return Number(cleaned);
157
+ }
package/src/math/index.js CHANGED
@@ -3,3 +3,8 @@ export * from './vec2i32.js';
3
3
  export * from './float64.js';
4
4
  export * from './vec2f64.js';
5
5
  export * from './vec3f64.js';
6
+ export * from './word.js';
7
+ export * from './solve.js';
8
+ export * from './mat4.js';
9
+ export * from './project.js';
10
+ export * from './format.js';
@@ -0,0 +1,131 @@
1
+ //@ts-check
2
+ /**
3
+ * @file A minimal 4×4 matrix kernel for the x·y·z plotter and
4
+ * any future 3D consumer (Mermaid 3D is a candidate — see ROADMAP).
5
+ *
6
+ * Matrices are `Float64Array(16)` in **column-major** order (the WebGL /
7
+ * glMatrix convention): element `m[col * 4 + row]`. Every factory returns
8
+ * a fresh array; `multiply` composes right-to-left like linear algebra
9
+ * (`multiply(a, b)` applies `b` then `a` to a column vector).
10
+ */
11
+
12
+ /** @typedef {Float64Array} Mat4Array */
13
+
14
+ export class Mat4 {
15
+ /** @returns {Mat4Array} the 4×4 identity. */
16
+ static identity() {
17
+ const m = new Float64Array(16);
18
+ m[0] = 1; m[5] = 1; m[10] = 1; m[15] = 1;
19
+ return m;
20
+ }
21
+
22
+ /**
23
+ * `a · b` (column-major). Neither operand is mutated.
24
+ * @param {Mat4Array} a @param {Mat4Array} b
25
+ * @returns {Mat4Array}
26
+ */
27
+ static multiply(a, b) {
28
+ const out = new Float64Array(16);
29
+ for (let col = 0; col < 4; col++) {
30
+ for (let row = 0; row < 4; row++) {
31
+ let s = 0;
32
+ for (let k = 0; k < 4; k++) {
33
+ s += a[k * 4 + row] * b[col * 4 + k];
34
+ }
35
+ out[col * 4 + row] = s;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ /** @param {number} rad @returns {Mat4Array} */
42
+ static rotationX(rad) {
43
+ const c = Math.cos(rad); const s = Math.sin(rad);
44
+ const m = Mat4.identity();
45
+ m[5] = c; m[6] = s; m[9] = -s; m[10] = c;
46
+ return m;
47
+ }
48
+
49
+ /** @param {number} rad @returns {Mat4Array} */
50
+ static rotationY(rad) {
51
+ const c = Math.cos(rad); const s = Math.sin(rad);
52
+ const m = Mat4.identity();
53
+ m[0] = c; m[2] = -s; m[8] = s; m[10] = c;
54
+ return m;
55
+ }
56
+
57
+ /** @param {number} rad @returns {Mat4Array} */
58
+ static rotationZ(rad) {
59
+ const c = Math.cos(rad); const s = Math.sin(rad);
60
+ const m = Mat4.identity();
61
+ m[0] = c; m[1] = s; m[4] = -s; m[5] = c;
62
+ return m;
63
+ }
64
+
65
+ /** @param {number} tx @param {number} ty @param {number} tz @returns {Mat4Array} */
66
+ static translation(tx, ty, tz) {
67
+ const m = Mat4.identity();
68
+ m[12] = tx; m[13] = ty; m[14] = tz;
69
+ return m;
70
+ }
71
+
72
+ /** @param {number} sx @param {number} sy @param {number} sz @returns {Mat4Array} */
73
+ static scaling(sx, sy, sz) {
74
+ const m = new Float64Array(16);
75
+ m[0] = sx; m[5] = sy; m[10] = sz; m[15] = 1;
76
+ return m;
77
+ }
78
+
79
+ /**
80
+ * Orthographic projection.
81
+ * @param {number} l @param {number} r @param {number} b @param {number} t
82
+ * @param {number} n @param {number} f
83
+ * @returns {Mat4Array}
84
+ */
85
+ static ortho(l, r, b, t, n, f) {
86
+ const m = new Float64Array(16);
87
+ m[0] = 2 / (r - l);
88
+ m[5] = 2 / (t - b);
89
+ m[10] = -2 / (f - n);
90
+ m[12] = -(r + l) / (r - l);
91
+ m[13] = -(t + b) / (t - b);
92
+ m[14] = -(f + n) / (f - n);
93
+ m[15] = 1;
94
+ return m;
95
+ }
96
+
97
+ /**
98
+ * Perspective projection (right-handed, clip space z in [-1, 1]).
99
+ * @param {number} fovy vertical field of view, radians
100
+ * @param {number} aspect width / height
101
+ * @param {number} n near plane (> 0)
102
+ * @param {number} f far plane
103
+ * @returns {Mat4Array}
104
+ */
105
+ static perspective(fovy, aspect, n, f) {
106
+ const t = 1 / Math.tan(fovy / 2);
107
+ const m = new Float64Array(16);
108
+ m[0] = t / aspect;
109
+ m[5] = t;
110
+ m[10] = (f + n) / (n - f);
111
+ m[11] = -1;
112
+ m[14] = (2 * f * n) / (n - f);
113
+ return m;
114
+ }
115
+
116
+ /**
117
+ * Transform a 3D point (implicit w = 1) by `m`, returning the
118
+ * perspective-divided `{ x, y, z, w }` (w is the pre-divide clip w, so
119
+ * callers can reject points behind the camera).
120
+ * @param {Mat4Array} m @param {number} x @param {number} y @param {number} z
121
+ * @returns {{ x: number, y: number, z: number, w: number }}
122
+ */
123
+ static transformPoint(m, x, y, z) {
124
+ const cx = m[0] * x + m[4] * y + m[8] * z + m[12];
125
+ const cy = m[1] * x + m[5] * y + m[9] * z + m[13];
126
+ const cz = m[2] * x + m[6] * y + m[10] * z + m[14];
127
+ const cw = m[3] * x + m[7] * y + m[11] * z + m[15];
128
+ const w = cw === 0 ? 1 : cw;
129
+ return { x: cx / w, y: cy / w, z: cz / w, w: cw };
130
+ }
131
+ }
@@ -0,0 +1,49 @@
1
+ //@ts-check
2
+ /**
3
+ * @file 3D → 2D projection helpers, built on `Mat4` and the
4
+ * existing `Vec3f64`. Used by the x·y·z plotter to turn a rotated grid
5
+ * of surface points into screen coordinates and to shade faces.
6
+ */
7
+
8
+ import { Mat4 } from './mat4.js';
9
+ import { Vec3f64 } from './vec3f64.js';
10
+
11
+ /**
12
+ * @typedef {object} Viewport
13
+ * @property {number} x left edge in pixels
14
+ * @property {number} y top edge in pixels
15
+ * @property {number} width
16
+ * @property {number} height
17
+ */
18
+
19
+ /**
20
+ * Project a 3D point through `mat` (model·view·projection) and map the
21
+ * resulting normalized device coordinates into `viewport` pixels. The Y
22
+ * axis is flipped so that +Y is up in world space but down in screen
23
+ * space, as SVG expects.
24
+ *
25
+ * @param {{ x: number, y: number, z: number } | import('./vec3f64.js').Vec3f64} point3
26
+ * @param {import('./mat4.js').Mat4Array} mat
27
+ * @param {Viewport} viewport
28
+ * @returns {{ x: number, y: number, z: number, w: number }}
29
+ */
30
+ export function project3dTo2d(point3, mat, viewport) {
31
+ const clip = Mat4.transformPoint(mat, +point3.x, +point3.y, +point3.z);
32
+ const sx = viewport.x + (clip.x * 0.5 + 0.5) * viewport.width;
33
+ const sy = viewport.y + (0.5 - clip.y * 0.5) * viewport.height;
34
+ return { x: sx, y: sy, z: clip.z, w: clip.w };
35
+ }
36
+
37
+ /**
38
+ * The (un-normalized) surface normal of triangle `a→b→c`, right-hand
39
+ * winding. Returns a `Vec3f64`.
40
+ * @param {import('./vec3f64.js').Vec3f64} a
41
+ * @param {import('./vec3f64.js').Vec3f64} b
42
+ * @param {import('./vec3f64.js').Vec3f64} c
43
+ * @returns {import('./vec3f64.js').Vec3f64}
44
+ */
45
+ export function surfaceNormal(a, b, c) {
46
+ const ab = Vec3f64.sub(b, a);
47
+ const ac = Vec3f64.sub(c, a);
48
+ return Vec3f64.cross(ab, ac);
49
+ }
@@ -0,0 +1,112 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Generic, domain-free numeric root finders. No finance
4
+ * or geometry lives here; `@jarenjs/core/finance` builds NPV/IRR/rate on
5
+ * top of these, the calculator's equation helpers on top of them too.
6
+ *
7
+ * Each solver returns a `{ root, iterations, converged }` record instead
8
+ * of throwing on non-convergence — callers decide how to react (finance
9
+ * falls back from Newton to bisection, for example).
10
+ */
11
+
12
+ /**
13
+ * @typedef {object} SolveResult
14
+ * @property {number} root the best estimate found
15
+ * @property {number} iterations iterations actually run
16
+ * @property {boolean} converged whether `tol` was reached
17
+ */
18
+
19
+ const DEFAULT_TOL = 1e-10;
20
+ const DEFAULT_MAX_ITER = 100;
21
+
22
+ /**
23
+ * Newton–Raphson using an explicit derivative. Falls back to reporting
24
+ * non-convergence (rather than diverging silently) when the derivative
25
+ * vanishes or the step explodes.
26
+ *
27
+ * @param {(x: number) => number} f
28
+ * @param {(x: number) => number} df
29
+ * @param {number} x0
30
+ * @param {{ tol?: number, maxIter?: number }} [opts]
31
+ * @returns {SolveResult}
32
+ */
33
+ export function newtonRaphson(f, df, x0, opts = {}) {
34
+ const tol = opts.tol ?? DEFAULT_TOL;
35
+ const maxIter = opts.maxIter ?? DEFAULT_MAX_ITER;
36
+ let x = +x0;
37
+ for (let i = 1; i <= maxIter; i++) {
38
+ const fx = +f(x);
39
+ if (Math.abs(fx) <= tol) return { root: x, iterations: i, converged: true };
40
+ const dfx = +df(x);
41
+ if (dfx === 0 || !isFinite(dfx)) return { root: x, iterations: i, converged: false };
42
+ const next = x - fx / dfx;
43
+ if (!isFinite(next)) return { root: x, iterations: i, converged: false };
44
+ if (Math.abs(next - x) <= tol) return { root: next, iterations: i, converged: true };
45
+ x = next;
46
+ }
47
+ return { root: x, iterations: maxIter, converged: false };
48
+ }
49
+
50
+ /**
51
+ * Bisection on a sign-changing bracket `[a, b]`. Guaranteed to converge
52
+ * when `f(a)` and `f(b)` straddle a root; returns `converged: false` if
53
+ * the bracket does not (rather than throwing).
54
+ *
55
+ * @param {(x: number) => number} f
56
+ * @param {number} a
57
+ * @param {number} b
58
+ * @param {{ tol?: number, maxIter?: number }} [opts]
59
+ * @returns {SolveResult}
60
+ */
61
+ export function bisect(f, a, b, opts = {}) {
62
+ const tol = opts.tol ?? DEFAULT_TOL;
63
+ const maxIter = opts.maxIter ?? DEFAULT_MAX_ITER;
64
+ let lo = +a;
65
+ let hi = +b;
66
+ let flo = +f(lo);
67
+ let fhi = +f(hi);
68
+ if (flo === 0) return { root: lo, iterations: 0, converged: true };
69
+ if (fhi === 0) return { root: hi, iterations: 0, converged: true };
70
+ if (flo * fhi > 0) return { root: (lo + hi) / 2, iterations: 0, converged: false };
71
+ let mid = (lo + hi) / 2;
72
+ for (let i = 1; i <= maxIter; i++) {
73
+ mid = (lo + hi) / 2;
74
+ const fmid = +f(mid);
75
+ if (Math.abs(fmid) <= tol || (hi - lo) / 2 <= tol) {
76
+ return { root: mid, iterations: i, converged: true };
77
+ }
78
+ if (flo * fmid < 0) { hi = mid; fhi = fmid; }
79
+ else { lo = mid; flo = fmid; }
80
+ }
81
+ return { root: mid, iterations: maxIter, converged: false };
82
+ }
83
+
84
+ /**
85
+ * The secant method (Newton without an explicit derivative). Needs two
86
+ * starting points.
87
+ *
88
+ * @param {(x: number) => number} f
89
+ * @param {number} x0
90
+ * @param {number} x1
91
+ * @param {{ tol?: number, maxIter?: number }} [opts]
92
+ * @returns {SolveResult}
93
+ */
94
+ export function secant(f, x0, x1, opts = {}) {
95
+ const tol = opts.tol ?? DEFAULT_TOL;
96
+ const maxIter = opts.maxIter ?? DEFAULT_MAX_ITER;
97
+ let a = +x0;
98
+ let b = +x1;
99
+ let fa = +f(a);
100
+ let fb = +f(b);
101
+ for (let i = 1; i <= maxIter; i++) {
102
+ if (Math.abs(fb) <= tol) return { root: b, iterations: i, converged: true };
103
+ const denom = fb - fa;
104
+ if (denom === 0 || !isFinite(denom)) return { root: b, iterations: i, converged: false };
105
+ const next = b - fb * (b - a) / denom;
106
+ if (!isFinite(next)) return { root: b, iterations: i, converged: false };
107
+ if (Math.abs(next - b) <= tol) return { root: next, iterations: i, converged: true };
108
+ a = b; fa = fb;
109
+ b = next; fb = +f(b);
110
+ }
111
+ return { root: b, iterations: maxIter, converged: false };
112
+ }
@@ -0,0 +1,177 @@
1
+ //@ts-check
2
+ /**
3
+ * @file BigInt fixed-width word math. The programmer-calculator
4
+ * kernel: two's-complement integers of a fixed bit width (8/16/32/64),
5
+ * signed or unsigned, with the bitwise/shift/rotate operators and
6
+ * radix-string I/O a HEX/DEC/OCT/BIN calculator needs.
7
+ *
8
+ * Everything is native `BigInt` — zero dependencies, exact at any width.
9
+ * Values crossing the module boundary are plain `BigInt`s already reduced
10
+ * to the requested word (mask + sign-extend), so operators compose
11
+ * without re-normalizing.
12
+ */
13
+
14
+ /** Supported word sizes, in bits. */
15
+ export const WORD_BITS = Object.freeze([8, 16, 32, 64]);
16
+
17
+ /**
18
+ * The unsigned mask for a word of `bits` bits: `2**bits - 1`.
19
+ * @param {number} bits
20
+ * @returns {bigint}
21
+ */
22
+ export function wordMask(bits) {
23
+ return (1n << BigInt(bits | 0)) - 1n;
24
+ }
25
+
26
+ /**
27
+ * Reduce any integer to a fixed-width word. Unsigned words wrap into
28
+ * `[0, 2**bits)`; signed words are the two's-complement interpretation,
29
+ * so the high bit becomes the sign and the result lands in
30
+ * `[-2**(bits-1), 2**(bits-1))`.
31
+ *
32
+ * @param {bigint|number|string} value
33
+ * @param {number} [bits]
34
+ * @param {boolean} [signed]
35
+ * @returns {bigint}
36
+ */
37
+ export function toWord(value, bits = 32, signed = false) {
38
+ const b = BigInt(bits | 0);
39
+ const mask = (1n << b) - 1n;
40
+ let v = (typeof value === 'bigint' ? value : BigInt(Math.trunc(Number(value)))) & mask;
41
+ if (signed) {
42
+ const signBit = 1n << (b - 1n);
43
+ if ((v & signBit) !== 0n) v -= (1n << b);
44
+ }
45
+ return v;
46
+ }
47
+
48
+ /** @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed] */
49
+ export function wAnd(a, b, bits = 32, signed = false) {
50
+ return toWord(toWord(a, bits, false) & toWord(b, bits, false), bits, signed);
51
+ }
52
+ /** @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed] */
53
+ export function wOr(a, b, bits = 32, signed = false) {
54
+ return toWord(toWord(a, bits, false) | toWord(b, bits, false), bits, signed);
55
+ }
56
+ /** @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed] */
57
+ export function wXor(a, b, bits = 32, signed = false) {
58
+ return toWord(toWord(a, bits, false) ^ toWord(b, bits, false), bits, signed);
59
+ }
60
+ /** @param {bigint} a @param {number} [bits] @param {boolean} [signed] */
61
+ export function wNot(a, bits = 32, signed = false) {
62
+ return toWord(~toWord(a, bits, false), bits, signed);
63
+ }
64
+ /** @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed] */
65
+ export function wShl(a, n, bits = 32, signed = false) {
66
+ return toWord(toWord(a, bits, false) << (BigInt(n) % BigInt(bits)), bits, signed);
67
+ }
68
+ /**
69
+ * Logical right shift (zero fill) over the unsigned representation, then
70
+ * reinterpreted per `signed`.
71
+ * @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed]
72
+ */
73
+ export function wShr(a, n, bits = 32, signed = false) {
74
+ return toWord(toWord(a, bits, false) >> (BigInt(n) % BigInt(bits)), bits, signed);
75
+ }
76
+ /** Rotate left. @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed] */
77
+ export function wRol(a, n, bits = 32, signed = false) {
78
+ const b = BigInt(bits | 0);
79
+ const u = toWord(a, bits, false);
80
+ const s = ((BigInt(n) % b) + b) % b;
81
+ const mask = (1n << b) - 1n;
82
+ return toWord(((u << s) | (u >> (b - s))) & mask, bits, signed);
83
+ }
84
+ /** Rotate right. @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed] */
85
+ export function wRor(a, n, bits = 32, signed = false) {
86
+ const b = BigInt(bits | 0);
87
+ const u = toWord(a, bits, false);
88
+ const s = ((BigInt(n) % b) + b) % b;
89
+ const mask = (1n << b) - 1n;
90
+ return toWord(((u >> s) | (u << (b - s))) & mask, bits, signed);
91
+ }
92
+ /**
93
+ * Euclidean-flavoured modulo that follows the sign of the divisor,
94
+ * reduced back into the word.
95
+ * @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed]
96
+ */
97
+ export function wMod(a, b, bits = 32, signed = false) {
98
+ const x = toWord(a, bits, signed);
99
+ const y = toWord(b, bits, signed);
100
+ if (y === 0n) throw new RangeError('wMod: division by zero');
101
+ return toWord(x % y, bits, signed);
102
+ }
103
+
104
+ /** @param {number} radix @returns {string} */
105
+ function radixDigits(radix) {
106
+ return '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, radix);
107
+ }
108
+
109
+ /**
110
+ * Format an integer as a radix string (no prefix). Groups digits with an
111
+ * optional separator and left-pads to `pad` digits.
112
+ *
113
+ * @param {bigint|number} int
114
+ * @param {number} [radix] 2..36
115
+ * @param {{ group?: number, sep?: string, pad?: number, upper?: boolean }} [opts]
116
+ * @returns {string}
117
+ */
118
+ export function toBase(int, radix = 10, opts = {}) {
119
+ const r = radix | 0;
120
+ if (r < 2 || r > 36) throw new RangeError(`toBase: radix ${radix} out of range 2..36`);
121
+ let v = typeof int === 'bigint' ? int : BigInt(Math.trunc(Number(int)));
122
+ const neg = v < 0n;
123
+ if (neg) v = -v;
124
+ const R = BigInt(r);
125
+ const digits = radixDigits(r);
126
+ let out = v === 0n ? '0' : '';
127
+ while (v > 0n) {
128
+ out = digits[Number(v % R)] + out;
129
+ v /= R;
130
+ }
131
+ if (opts.pad && out.length < opts.pad) out = out.padStart(opts.pad, '0');
132
+ if (opts.group && opts.group > 0) {
133
+ const sep = opts.sep ?? ' ';
134
+ const parts = [];
135
+ for (let i = out.length; i > 0; i -= opts.group) {
136
+ parts.unshift(out.slice(Math.max(0, i - opts.group), i));
137
+ }
138
+ out = parts.join(sep);
139
+ }
140
+ if (opts.upper) out = out.toUpperCase();
141
+ return neg ? '-' + out : out;
142
+ }
143
+
144
+ /**
145
+ * Parse a radix string back to a BigInt. Tolerates group separators
146
+ * (space/underscore), a leading sign, and an optional `0x`/`0o`/`0b`
147
+ * prefix (which must agree with `radix` when both are given).
148
+ *
149
+ * @param {string} str
150
+ * @param {number} [radix] 2..36
151
+ * @returns {bigint}
152
+ */
153
+ export function fromBase(str, radix = 10) {
154
+ let s = String(str).trim().replace(/[ _]/g, '');
155
+ let neg = false;
156
+ if (s.startsWith('-')) { neg = true; s = s.slice(1); }
157
+ else if (s.startsWith('+')) s = s.slice(1);
158
+ const m = /^0([xob])/i.exec(s);
159
+ if (m) {
160
+ const pfx = m[1].toLowerCase();
161
+ const pfxRadix = pfx === 'x' ? 16 : pfx === 'o' ? 8 : 2;
162
+ radix = pfxRadix;
163
+ s = s.slice(2);
164
+ }
165
+ const r = radix | 0;
166
+ if (r < 2 || r > 36) throw new RangeError(`fromBase: radix ${radix} out of range 2..36`);
167
+ if (s.length === 0) throw new SyntaxError('fromBase: empty digit string');
168
+ const R = BigInt(r);
169
+ const digits = radixDigits(r);
170
+ let acc = 0n;
171
+ for (const ch of s.toLowerCase()) {
172
+ const d = digits.indexOf(ch);
173
+ if (d < 0) throw new SyntaxError(`fromBase: '${ch}' is not a base-${r} digit`);
174
+ acc = acc * R + BigInt(d);
175
+ }
176
+ return neg ? -acc : acc;
177
+ }