gracio 1.0.0 → 1.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/dist/examples/benchmark.js +94 -0
- package/dist/examples/collision-example.js +28 -0
- package/dist/examples/memory-audit.js +71 -0
- package/dist/examples/precision-demo.js +17 -0
- package/dist/examples/validate-precision.js +79 -0
- package/dist/gracio.bundle.js +1 -0
- package/dist/precise-calculator.bundle.js +1 -0
- package/dist/src/audit-util.js +39 -0
- package/dist/src/calc-number.js +415 -0
- package/dist/src/constants.js +32 -0
- package/dist/src/demo.js +55 -0
- package/dist/src/gracio.js +415 -0
- package/dist/src/index.js +1 -0
- package/dist/src/lexer.js +51 -0
- package/dist/src/library-entry.js +4 -0
- package/dist/src/parser.js +187 -0
- package/dist/src/types.js +1 -0
- package/dist/test/advanced-arithmetic.test.js +63 -0
- package/dist/test/basic-arithmetic.test.js +40 -0
- package/dist/test/precision-comparison.test.js +40 -0
- package/dist/test/src/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
const SMALL_PRIMES = [
|
|
2
|
+
2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n, 41n, 43n, 47n, 53n, 59n, 61n, 67n, 71n,
|
|
3
|
+
73n, 79n, 83n, 89n, 97n, 101n, 103n, 107n, 109n, 113n, 127n, 131n, 137n, 139n, 149n, 151n,
|
|
4
|
+
157n, 163n, 167n, 173n, 179n, 181n, 191n, 193n, 197n, 199n, 211n, 223n, 227n, 229n, 233n,
|
|
5
|
+
239n, 241n, 251n, 257n, 263n, 269n, 271n, 277n, 281n, 283n, 293n, 307n, 311n, 313n, 317n,
|
|
6
|
+
331n, 337n, 347n, 349n, 353n, 359n, 367n, 373n, 379n, 383n, 389n, 397n, 401n, 409n, 419n,
|
|
7
|
+
421n, 431n, 433n, 439n, 443n, 449n, 457n, 461n, 463n, 467n, 479n, 487n, 491n, 499n
|
|
8
|
+
];
|
|
9
|
+
export class Gracio {
|
|
10
|
+
numerator;
|
|
11
|
+
denominator;
|
|
12
|
+
precisionLimit; // Max decimal digits of precision to maintain
|
|
13
|
+
static MIN_SIMPLIFY_THRESHOLD = 10n ** 20n;
|
|
14
|
+
lastSimplifiedDigits = 0;
|
|
15
|
+
constructor(numerator, denominator = 1n, precisionLimit) {
|
|
16
|
+
if (denominator === 0n)
|
|
17
|
+
throw new Error("Denominator cannot be zero");
|
|
18
|
+
// Ensure the denominator is always positive
|
|
19
|
+
if (denominator < 0n) {
|
|
20
|
+
numerator *= -1n;
|
|
21
|
+
denominator *= -1n;
|
|
22
|
+
}
|
|
23
|
+
this.numerator = numerator;
|
|
24
|
+
this.denominator = denominator;
|
|
25
|
+
this.precisionLimit = precisionLimit;
|
|
26
|
+
this.simplify();
|
|
27
|
+
}
|
|
28
|
+
static fromInt(value) {
|
|
29
|
+
return new Gracio(BigInt(value), 1n);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a Gracio instance from a floating-point representation.
|
|
33
|
+
* Uses string parsing to ensure "human-intuitive" exactness (e.g., "0.1" -> 1/10).
|
|
34
|
+
*/
|
|
35
|
+
static fromFloat(value) {
|
|
36
|
+
const s = String(value).trim().toLowerCase();
|
|
37
|
+
if (s === 'nan' || s === 'infinity' || s === '-infinity') {
|
|
38
|
+
throw new Error("Cannot represent NaN or Infinity as a ratio");
|
|
39
|
+
}
|
|
40
|
+
// Handle scientific notation: e.g., 1.23e-4
|
|
41
|
+
const eIndex = s.indexOf('e');
|
|
42
|
+
if (eIndex !== -1) {
|
|
43
|
+
const coefficientStr = s.substring(0, eIndex);
|
|
44
|
+
const exponentStr = s.substring(eIndex + 1);
|
|
45
|
+
const exponent = parseInt(exponentStr, 10);
|
|
46
|
+
if (isNaN(exponent))
|
|
47
|
+
throw new Error(`Invalid exponent in float: ${s}`);
|
|
48
|
+
const coeff = Gracio.fromFloat(coefficientStr);
|
|
49
|
+
const scaleNum = exponent >= 0 ? 10n ** BigInt(exponent) : 1n;
|
|
50
|
+
const scaleDen = exponent < 0 ? 10n ** BigInt(-exponent) : 1n;
|
|
51
|
+
return new Gracio(coeff.numerator * scaleNum, coeff.denominator * scaleDen);
|
|
52
|
+
}
|
|
53
|
+
// Handle standard decimal notation: e.g., 123.456
|
|
54
|
+
const dotIndex = s.indexOf('.');
|
|
55
|
+
if (dotIndex === -1) {
|
|
56
|
+
return new Gracio(BigInt(s), 1n);
|
|
57
|
+
}
|
|
58
|
+
const wholePart = s.substring(0, dotIndex);
|
|
59
|
+
const fractionPart = s.substring(dotIndex + 1);
|
|
60
|
+
// Combine parts into a single integer (e.g., "123" + "456" -> 123456)
|
|
61
|
+
// We must handle signs carefully: if wholePart is "-0", the sign still matters.
|
|
62
|
+
const isNegative = s.startsWith('-');
|
|
63
|
+
const cleanWhole = wholePart.replace('-', '');
|
|
64
|
+
const combinedDigits = cleanWhole + fractionPart;
|
|
65
|
+
const numerator = BigInt(combinedDigits || '0');
|
|
66
|
+
const denominator = 10n ** BigInt(fractionPart.length);
|
|
67
|
+
return new Gracio(isNegative ? -numerator : numerator, denominator);
|
|
68
|
+
}
|
|
69
|
+
abs(value) {
|
|
70
|
+
return value < 0n ? -value : value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Creates a deep copy of the current number.
|
|
74
|
+
* Use this when you need to perform operations without mutating the original object.
|
|
75
|
+
*/
|
|
76
|
+
clone() {
|
|
77
|
+
return new Gracio(this.numerator, this.denominator);
|
|
78
|
+
}
|
|
79
|
+
lastSimplifiedBits = 0;
|
|
80
|
+
shouldSimplify() {
|
|
81
|
+
const currentDigits = this.abs(this.numerator).toString().length;
|
|
82
|
+
// Simplify if we've crossed the minimum threshold AND grown by at least 20 digits since last simplification
|
|
83
|
+
return this.abs(this.numerator) > Gracio.MIN_SIMPLIFY_THRESHOLD &&
|
|
84
|
+
currentDigits > (this.lastSimplifiedDigits + 20);
|
|
85
|
+
}
|
|
86
|
+
checkPrecisionLimit() {
|
|
87
|
+
if (this.precisionLimit !== undefined) {
|
|
88
|
+
const currentDigits = this.abs(this.denominator).toString().length;
|
|
89
|
+
// Trigger approximation when growth exceeds 2x the limit to avoid jittery performance
|
|
90
|
+
if (currentDigits > this.precisionLimit * 2) {
|
|
91
|
+
this.pureApproximate(this.precisionLimit);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
simplify() {
|
|
96
|
+
const start = performance.now();
|
|
97
|
+
let a = this.abs(this.numerator);
|
|
98
|
+
let b = this.abs(this.denominator);
|
|
99
|
+
if (a === 0n)
|
|
100
|
+
return this;
|
|
101
|
+
if (b === 0n)
|
|
102
|
+
throw new Error("Denominator cannot be zero");
|
|
103
|
+
// Trial Division: Prune small prime factors first to reduce BigInt magnitude
|
|
104
|
+
for (const p of SMALL_PRIMES) {
|
|
105
|
+
while (a % p === 0n && b % p === 0n) {
|
|
106
|
+
a /= p;
|
|
107
|
+
b /= p;
|
|
108
|
+
this.numerator /= p;
|
|
109
|
+
this.denominator /= p; // denominator is always positive
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Final Binary GCD for any remaining larger factors
|
|
113
|
+
const gcd = this.calculateGCD(a, b);
|
|
114
|
+
if (gcd > 1n) {
|
|
115
|
+
this.numerator /= gcd;
|
|
116
|
+
this.denominator /= gcd;
|
|
117
|
+
}
|
|
118
|
+
const end = performance.now();
|
|
119
|
+
if (end - start > 10) { // Log if simplification takes more than 10ms
|
|
120
|
+
console.log(`[Simplify] Took ${(end - start).toFixed(2)}ms | Digits: ${a.toString().length}`);
|
|
121
|
+
}
|
|
122
|
+
this.lastSimplifiedDigits = this.abs(this.numerator).toString().length;
|
|
123
|
+
this.lastSimplifiedBits = this.abs(this.numerator).toString(2).length;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
// --- Mutable Operations (Modifies current object and returns 'this' for chaining) ---
|
|
127
|
+
add(other) {
|
|
128
|
+
if (this.denominator === other.denominator) {
|
|
129
|
+
this.numerator += other.numerator;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.numerator = this.numerator * other.denominator + other.numerator * this.denominator;
|
|
133
|
+
this.denominator = this.denominator * other.denominator;
|
|
134
|
+
}
|
|
135
|
+
if (this.shouldSimplify())
|
|
136
|
+
this.simplify();
|
|
137
|
+
this.checkPrecisionLimit();
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
subtract(other) {
|
|
141
|
+
if (this.denominator === other.denominator) {
|
|
142
|
+
this.numerator -= other.numerator;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.numerator = this.numerator * other.denominator - other.numerator * this.denominator;
|
|
146
|
+
this.denominator = this.denominator * other.denominator;
|
|
147
|
+
}
|
|
148
|
+
if (this.shouldSimplify())
|
|
149
|
+
this.simplify();
|
|
150
|
+
this.checkPrecisionLimit();
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
multiply(other) {
|
|
154
|
+
const n1 = this.abs(this.numerator);
|
|
155
|
+
const d1 = this.abs(this.denominator);
|
|
156
|
+
const n2 = this.abs(other.numerator);
|
|
157
|
+
const d2 = this.abs(other.denominator);
|
|
158
|
+
// ASYMMETRIC FAST PATH: If at least one operand is small, we can afford raw multiplication.
|
|
159
|
+
// We only cross-simplify when BOTH numbers are large to prevent explosive digit growth.
|
|
160
|
+
const SMALL_THRESHOLD = 18446744073709551616n; // 2^64
|
|
161
|
+
if (n1 < SMALL_THRESHOLD || d1 < SMALL_THRESHOLD || n2 < SMALL_THRESHOLD || d2 < SMALL_THRESHOLD) {
|
|
162
|
+
this.numerator *= other.numerator;
|
|
163
|
+
this.denominator *= other.denominator;
|
|
164
|
+
if (this.shouldSimplify())
|
|
165
|
+
this.simplify();
|
|
166
|
+
this.checkPrecisionLimit();
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
// CROSS-SIMPLIFICATION: Prevents explosive growth for very large BigInts.
|
|
170
|
+
const g1 = this.calculateGCD(n1, d2);
|
|
171
|
+
const g2 = this.calculateGCD(n2, d1);
|
|
172
|
+
this.numerator = (this.numerator / g1) * (other.numerator / g2);
|
|
173
|
+
this.denominator = (this.denominator / g2) * (other.denominator / g1);
|
|
174
|
+
this.checkPrecisionLimit();
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
divide(other) {
|
|
178
|
+
if (other.numerator === 0n)
|
|
179
|
+
throw new Error("Cannot divide by zero");
|
|
180
|
+
const n1 = this.abs(this.numerator);
|
|
181
|
+
const d1 = this.abs(this.denominator);
|
|
182
|
+
const n2 = this.abs(other.numerator);
|
|
183
|
+
const d2 = this.abs(other.denominator);
|
|
184
|
+
// ASYMMETRIC FAST PATH: If at least one operand is small, avoid expensive GCDs.
|
|
185
|
+
const SMALL_THRESHOLD = 18446744073709551616n; // 2^64
|
|
186
|
+
if (n1 < SMALL_THRESHOLD || d1 < SMALL_THRESHOLD || n2 < SMALL_THRESHOLD || d2 < SMALL_THRESHOLD) {
|
|
187
|
+
this.numerator *= other.denominator;
|
|
188
|
+
this.denominator *= other.numerator;
|
|
189
|
+
if (this.denominator < 0n) {
|
|
190
|
+
this.numerator *= -1n;
|
|
191
|
+
this.denominator *= -1n;
|
|
192
|
+
}
|
|
193
|
+
if (this.shouldSimplify())
|
|
194
|
+
this.simplify();
|
|
195
|
+
this.checkPrecisionLimit();
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
// CROSS-SIMPLIFICATION: multiply by reciprocal with GCD pruning.
|
|
199
|
+
const g1 = this.calculateGCD(n1, n2); // numerator1 vs reciprocalDenominator (other.numerator)
|
|
200
|
+
const g2 = this.calculateGCD(d2, d1); // reciprocalNumerator (other.denominator) vs denominator1
|
|
201
|
+
this.numerator = (this.numerator / g1) * (d2 / g2);
|
|
202
|
+
this.denominator = (this.denominator / g2) * (n2 / g1);
|
|
203
|
+
if (this.denominator < 0n) {
|
|
204
|
+
this.numerator *= -1n;
|
|
205
|
+
this.denominator *= -1n;
|
|
206
|
+
}
|
|
207
|
+
this.checkPrecisionLimit();
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
calculateGCD(a, b) {
|
|
211
|
+
let n = a;
|
|
212
|
+
let d = b;
|
|
213
|
+
if (n === 0n)
|
|
214
|
+
return d;
|
|
215
|
+
if (d === 0n)
|
|
216
|
+
return n;
|
|
217
|
+
let shift = 0n;
|
|
218
|
+
while (((n | d) & 1n) === 0n) {
|
|
219
|
+
n >>= 1n;
|
|
220
|
+
d >>= 1n;
|
|
221
|
+
shift++;
|
|
222
|
+
}
|
|
223
|
+
while ((n & 1n) === 0n)
|
|
224
|
+
n >>= 1n;
|
|
225
|
+
do {
|
|
226
|
+
while ((d & 1n) === 0n)
|
|
227
|
+
d >>= 1n;
|
|
228
|
+
if (n > d) {
|
|
229
|
+
let t = n;
|
|
230
|
+
n = d;
|
|
231
|
+
d = t;
|
|
232
|
+
}
|
|
233
|
+
d = d - n;
|
|
234
|
+
} while (d !== 0n);
|
|
235
|
+
return n << shift;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Raises the number to the power of the given exponent.
|
|
239
|
+
* Handles negative exponents by inverting the ratio.
|
|
240
|
+
*/
|
|
241
|
+
pow(exponent) {
|
|
242
|
+
if (exponent === 0n) {
|
|
243
|
+
this.numerator = 1n;
|
|
244
|
+
this.denominator = 1n;
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
const absExp = exponent < 0n ? -exponent : exponent;
|
|
248
|
+
let newNum = this.numerator ** absExp;
|
|
249
|
+
let newDen = this.denominator ** absExp;
|
|
250
|
+
if (exponent < 0n) {
|
|
251
|
+
// Swap numerator and denominator for negative exponents
|
|
252
|
+
[newNum, newDen] = [newDen, newNum];
|
|
253
|
+
}
|
|
254
|
+
this.numerator = newNum;
|
|
255
|
+
this.denominator = newDen;
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
toFloat() {
|
|
259
|
+
this.simplify();
|
|
260
|
+
const n = this.abs(this.numerator);
|
|
261
|
+
const d = this.abs(this.denominator);
|
|
262
|
+
if (n === 0n)
|
|
263
|
+
return 0;
|
|
264
|
+
// Avoid overflow by scaling down if numbers exceed the safe range for Number (approx 1024 bits)
|
|
265
|
+
const nBits = n.toString(2).length;
|
|
266
|
+
const dBits = d.toString(2).length;
|
|
267
|
+
const maxBits = Math.max(nBits, dBits);
|
|
268
|
+
if (maxBits > 1000) {
|
|
269
|
+
const nShift = BigInt(nBits - 60);
|
|
270
|
+
const dShift = BigInt(dBits - 60);
|
|
271
|
+
// Scale both independently to fit in double-precision floats, then adjust by the difference in exponents
|
|
272
|
+
const nSmall = Number(this.numerator >> nShift);
|
|
273
|
+
const dSmall = Number(this.denominator >> dShift);
|
|
274
|
+
return (nSmall / dSmall) * Math.pow(2, nBits - dBits);
|
|
275
|
+
}
|
|
276
|
+
return Number(this.numerator) / Number(this.denominator);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Approximates the current ratio to a simpler fraction that maintains
|
|
280
|
+
* precision up to the specified number of decimal places.
|
|
281
|
+
* Uses Continued Fraction convergents to find the best rational approximation.
|
|
282
|
+
*/
|
|
283
|
+
/**
|
|
284
|
+
* Approximates the current ratio to a simpler fraction using continued fractions.
|
|
285
|
+
* @param digits Maximum number of digits allowed in the denominator.
|
|
286
|
+
*/
|
|
287
|
+
approximate(digits = 15) {
|
|
288
|
+
return this.pureApproximate(digits);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Pure rational approximation without converting to float.
|
|
292
|
+
* Finds the best convergent where the denominator does not exceed maxDenominatorDigits.
|
|
293
|
+
*/
|
|
294
|
+
pureApproximate(maxDenominatorDigits = 50) {
|
|
295
|
+
// console.log(`[PrecisionLimit] Approximating to ${maxDenominatorDigits} digits...`);
|
|
296
|
+
let a = this.abs(this.numerator);
|
|
297
|
+
let b = this.abs(this.denominator);
|
|
298
|
+
const originalSign = (this.numerator < 0n) !== (this.denominator < 0n);
|
|
299
|
+
if (a === 0n)
|
|
300
|
+
return this;
|
|
301
|
+
let h_prev2 = 0n, h_prev1 = 1n;
|
|
302
|
+
let k_prev2 = 1n, k_prev1 = 0n;
|
|
303
|
+
while (b !== 0n) {
|
|
304
|
+
const q = a / b;
|
|
305
|
+
const r = a % b;
|
|
306
|
+
const h = q * h_prev1 + h_prev2;
|
|
307
|
+
const k = q * k_prev1 + k_prev2;
|
|
308
|
+
// If the new denominator exceeds our digit limit, stop and use the last valid convergent.
|
|
309
|
+
if (k.toString().length > maxDenominatorDigits) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
h_prev2 = h_prev1;
|
|
313
|
+
h_prev1 = h;
|
|
314
|
+
k_prev2 = k_prev1;
|
|
315
|
+
k_prev1 = k;
|
|
316
|
+
a = b;
|
|
317
|
+
b = r;
|
|
318
|
+
}
|
|
319
|
+
this.numerator = originalSign ? -h_prev1 : h_prev1;
|
|
320
|
+
this.denominator = k_prev1;
|
|
321
|
+
// Convergents are already in simplest form, no need to simplify().
|
|
322
|
+
return this;
|
|
323
|
+
}
|
|
324
|
+
toString() {
|
|
325
|
+
this.simplify();
|
|
326
|
+
return `${this.numerator}/${this.denominator}`;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Calculates the n-th root of a number.
|
|
330
|
+
* 1. Checks for perfect powers to return exact ratios.
|
|
331
|
+
* 2. Uses continued fractions for high-precision rational approximations of irrationals.
|
|
332
|
+
*/
|
|
333
|
+
static root(index, value) {
|
|
334
|
+
console.log(`[Debug] Calculating root(${index}, ${value.toString()})`);
|
|
335
|
+
if (index === 0n)
|
|
336
|
+
throw new Error("Root index cannot be zero");
|
|
337
|
+
if (index === 1n)
|
|
338
|
+
return value.clone();
|
|
339
|
+
const n = value.numerator;
|
|
340
|
+
const d = value.denominator;
|
|
341
|
+
// --- Step 1: Perfect Power Check ---
|
|
342
|
+
const rootN = this.integerRoot(n, index);
|
|
343
|
+
const rootD = this.integerRoot(d, index);
|
|
344
|
+
if (rootN !== null && rootD !== null) {
|
|
345
|
+
return new Gracio(rootN, rootD);
|
|
346
|
+
}
|
|
347
|
+
// --- Step 2: Continued Fraction Approximation ---
|
|
348
|
+
const target = value.toFloat();
|
|
349
|
+
const rootVal = Math.pow(target, 1 / Number(index));
|
|
350
|
+
let p0 = 0n, q0 = 1n; // Convergent -1
|
|
351
|
+
let p1 = 1n, q1 = 0n; // Not quite standard, let's use the iterative approach
|
|
352
|
+
// Standard Continued Fraction algorithm for a float x
|
|
353
|
+
let x = rootVal;
|
|
354
|
+
let a = Math.floor(x);
|
|
355
|
+
let h_prev2 = 0n, h_prev1 = 1n;
|
|
356
|
+
let k_prev2 = 1n, k_prev1 = 0n;
|
|
357
|
+
// We iterate for a few steps to get a very high precision convergent
|
|
358
|
+
for (let i = 0; i < 15; i++) {
|
|
359
|
+
const h = BigInt(Math.floor(x)) * h_prev1 + h_prev2;
|
|
360
|
+
const k = BigInt(Math.floor(x)) * k_prev1 + k_prev2;
|
|
361
|
+
h_prev2 = h_prev1;
|
|
362
|
+
h_prev1 = h;
|
|
363
|
+
k_prev2 = k_prev1;
|
|
364
|
+
k_prev1 = k;
|
|
365
|
+
if (x - Math.floor(x) === 0)
|
|
366
|
+
break;
|
|
367
|
+
x = 1 / (x - Math.floor(x));
|
|
368
|
+
}
|
|
369
|
+
return new Gracio(h_prev1, k_prev1);
|
|
370
|
+
}
|
|
371
|
+
static integerRoot(value, index) {
|
|
372
|
+
if (value === 0n)
|
|
373
|
+
return 0n;
|
|
374
|
+
if (value < 0n && index % 2n === 0n)
|
|
375
|
+
return null; // Even root of negative
|
|
376
|
+
const isNegative = value < 0n;
|
|
377
|
+
const absVal = value < 0n ? -value : value;
|
|
378
|
+
// Fast-path: Prime Sieve. If a prime divides absVal, it must divide it at least 'index' times.
|
|
379
|
+
for (const p of SMALL_PRIMES) {
|
|
380
|
+
if (absVal % p === 0n) {
|
|
381
|
+
let count = 0;
|
|
382
|
+
let temp = absVal;
|
|
383
|
+
while (temp % p === 0n) {
|
|
384
|
+
temp /= p;
|
|
385
|
+
count++;
|
|
386
|
+
}
|
|
387
|
+
if (count % Number(index) !== 0)
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
let low = 1n;
|
|
392
|
+
let high = absVal;
|
|
393
|
+
// Optimization: for index >= 2, the root is at most sqrt(absVal).
|
|
394
|
+
// For very large numbers, this significantly narrows the search space.
|
|
395
|
+
if (index >= 2n) {
|
|
396
|
+
// We can't easily get a starting 'high' without Math.sqrt,
|
|
397
|
+
// but we can use a smaller bound for common cases if needed.
|
|
398
|
+
}
|
|
399
|
+
while (low <= high) {
|
|
400
|
+
const mid = (low + high) / 2n;
|
|
401
|
+
if (mid === 0n) {
|
|
402
|
+
low = 1n;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const p = mid ** index;
|
|
406
|
+
if (p === absVal)
|
|
407
|
+
return isNegative ? -mid : mid;
|
|
408
|
+
if (p < absVal)
|
|
409
|
+
low = mid + 1n;
|
|
410
|
+
else
|
|
411
|
+
high = mid - 1n;
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './library-entry.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class Lexer {
|
|
2
|
+
position = 0;
|
|
3
|
+
tokenize(expression) {
|
|
4
|
+
const tokens = [];
|
|
5
|
+
while (this.position < expression.length) {
|
|
6
|
+
const char = expression[this.position];
|
|
7
|
+
// Skip whitespace
|
|
8
|
+
if (char.match(/\s/)) {
|
|
9
|
+
this.position++;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
// Match numbers (including decimals and negatives)
|
|
13
|
+
if (char.match(/[0-9.]/) || (char === '-' &&
|
|
14
|
+
(tokens.length === 0 ||
|
|
15
|
+
tokens[tokens.length - 1].type === 'operator' ||
|
|
16
|
+
tokens[tokens.length - 1].type === '('))) {
|
|
17
|
+
let numberStr = '';
|
|
18
|
+
if (char === '-') {
|
|
19
|
+
numberStr += char;
|
|
20
|
+
this.position++;
|
|
21
|
+
}
|
|
22
|
+
while (this.position < expression.length &&
|
|
23
|
+
expression[this.position].match(/[0-9.]/)) {
|
|
24
|
+
numberStr += expression[this.position];
|
|
25
|
+
this.position++;
|
|
26
|
+
}
|
|
27
|
+
tokens.push({ type: 'number', value: numberStr });
|
|
28
|
+
}
|
|
29
|
+
// Match operators
|
|
30
|
+
else if (['+', '-', '*', '/', '(', ')', '^', ','].includes(char)) {
|
|
31
|
+
const tokenType = ['(', ')'].includes(char) ? char : 'operator';
|
|
32
|
+
tokens.push({
|
|
33
|
+
type: tokenType,
|
|
34
|
+
value: char
|
|
35
|
+
});
|
|
36
|
+
this.position++;
|
|
37
|
+
}
|
|
38
|
+
// Match function names (e.g., "root")
|
|
39
|
+
else if (char.match(/[a-zA-Z]/)) {
|
|
40
|
+
let name = '';
|
|
41
|
+
while (this.position < expression.length &&
|
|
42
|
+
expression[this.position].match(/[a-zA-Z0-9]/)) {
|
|
43
|
+
name += expression[this.position];
|
|
44
|
+
this.position++;
|
|
45
|
+
}
|
|
46
|
+
tokens.push({ type: 'function', value: name });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return tokens;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Gracio } from './gracio.js';
|
|
2
|
+
export class Parser {
|
|
3
|
+
position = 0;
|
|
4
|
+
tokens;
|
|
5
|
+
logger;
|
|
6
|
+
constructor(tokens, logger) {
|
|
7
|
+
this.tokens = tokens;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
parse() {
|
|
11
|
+
const result = this.parseExpression();
|
|
12
|
+
const final = this.collapse(result);
|
|
13
|
+
return new Gracio(final.n, final.d);
|
|
14
|
+
}
|
|
15
|
+
collapse(res) {
|
|
16
|
+
if (res.type === 'ratio')
|
|
17
|
+
return { n: res.n, d: res.d };
|
|
18
|
+
// Evaluate root as a ratio using the core engine's approximation
|
|
19
|
+
const cn = new Gracio(res.value.n, res.value.d);
|
|
20
|
+
const rootRes = Gracio.root(res.index, cn);
|
|
21
|
+
return { n: rootRes.numerator, d: rootRes.denominator };
|
|
22
|
+
}
|
|
23
|
+
parseExpression() {
|
|
24
|
+
let result = this.parseTerm();
|
|
25
|
+
while (this.position < this.tokens.length &&
|
|
26
|
+
this.currentToken().type === 'operator' &&
|
|
27
|
+
['+', '-'].includes(this.currentToken().value)) {
|
|
28
|
+
const op = this.currentToken().value;
|
|
29
|
+
this.position++;
|
|
30
|
+
const term = this.parseTerm();
|
|
31
|
+
// Addition and subtraction force a collapse of any pending roots
|
|
32
|
+
const left = this.collapse(result);
|
|
33
|
+
const right = this.collapse(term);
|
|
34
|
+
if (op === '+') {
|
|
35
|
+
result = {
|
|
36
|
+
type: 'ratio',
|
|
37
|
+
n: left.n * right.d + right.n * left.d,
|
|
38
|
+
d: left.d * right.d
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
result = {
|
|
43
|
+
type: 'ratio',
|
|
44
|
+
n: left.n * right.d - right.n * left.d,
|
|
45
|
+
d: left.d * right.d
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
parseTerm() {
|
|
52
|
+
let result = this.parseExponent();
|
|
53
|
+
while (this.position < this.tokens.length &&
|
|
54
|
+
this.currentToken().type === 'operator' &&
|
|
55
|
+
['*', '/'].includes(this.currentToken().value)) {
|
|
56
|
+
const op = this.currentToken().value;
|
|
57
|
+
this.position++;
|
|
58
|
+
const factor = this.parseExponent();
|
|
59
|
+
if (op === '*') {
|
|
60
|
+
// ROOT FOLDING: if both are roots of same index, combine values
|
|
61
|
+
if (result.type === 'root' && factor.type === 'root' && result.index === factor.index) {
|
|
62
|
+
result = {
|
|
63
|
+
type: 'root',
|
|
64
|
+
index: result.index,
|
|
65
|
+
value: {
|
|
66
|
+
n: result.value.n * factor.value.n,
|
|
67
|
+
d: result.value.d * factor.value.d
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const left = this.collapse(result);
|
|
73
|
+
const right = this.collapse(factor);
|
|
74
|
+
result = {
|
|
75
|
+
type: 'ratio',
|
|
76
|
+
n: left.n * right.n,
|
|
77
|
+
d: left.d * right.d
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// ROOT FOLDING: if both are roots of same index, combine values (division)
|
|
83
|
+
if (result.type === 'root' && factor.type === 'root' && result.index === factor.index) {
|
|
84
|
+
result = {
|
|
85
|
+
type: 'root',
|
|
86
|
+
index: result.index,
|
|
87
|
+
value: {
|
|
88
|
+
n: result.value.n * factor.value.d,
|
|
89
|
+
d: result.value.d * factor.value.n
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const left = this.collapse(result);
|
|
95
|
+
const right = this.collapse(factor);
|
|
96
|
+
result = {
|
|
97
|
+
type: 'ratio',
|
|
98
|
+
n: left.n * right.d,
|
|
99
|
+
d: left.d * right.n
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
parseExponent() {
|
|
107
|
+
let result = this.parseFactor();
|
|
108
|
+
while (this.position < this.tokens.length &&
|
|
109
|
+
this.currentToken().type === 'operator' &&
|
|
110
|
+
this.currentToken().value === '^') {
|
|
111
|
+
this.position++;
|
|
112
|
+
const exponentRes = this.parseFactor();
|
|
113
|
+
// Exponents must be ratios that resolve to integers for current BigInt support
|
|
114
|
+
const expComp = this.collapse(exponentRes);
|
|
115
|
+
if (expComp.d !== 1n) {
|
|
116
|
+
throw new Error("Exponents must be integers in the current implementation");
|
|
117
|
+
}
|
|
118
|
+
const exp = expComp.n;
|
|
119
|
+
// If we are raising a root to a power, we can fold it: (root(i, v))^p = root(i, v^p)
|
|
120
|
+
if (result.type === 'root') {
|
|
121
|
+
const absExp = exp < 0n ? -exp : exp;
|
|
122
|
+
let newN = result.value.n ** absExp;
|
|
123
|
+
let newD = result.value.d ** absExp;
|
|
124
|
+
if (exp < 0n)
|
|
125
|
+
[newN, newD] = [newD, newN];
|
|
126
|
+
result = {
|
|
127
|
+
type: 'root',
|
|
128
|
+
index: result.index,
|
|
129
|
+
value: { n: newN, d: newD }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const ratio = this.collapse(result);
|
|
134
|
+
const absExp = exp < 0n ? -exp : exp;
|
|
135
|
+
let n = ratio.n ** absExp;
|
|
136
|
+
let d = ratio.d ** absExp;
|
|
137
|
+
if (exp < 0n)
|
|
138
|
+
[n, d] = [d, n];
|
|
139
|
+
result = { type: 'ratio', n, d };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
parseFactor() {
|
|
145
|
+
const token = this.currentToken();
|
|
146
|
+
if (token.type === 'number') {
|
|
147
|
+
this.position++;
|
|
148
|
+
const cn = Gracio.fromFloat(token.value);
|
|
149
|
+
return { type: 'ratio', n: cn.numerator, d: cn.denominator };
|
|
150
|
+
}
|
|
151
|
+
if (token.type === 'function' && token.value === 'root') {
|
|
152
|
+
this.position++; // skip 'root'
|
|
153
|
+
if (this.currentToken().type !== '(')
|
|
154
|
+
throw new Error("Expected '(' after root");
|
|
155
|
+
this.position++; // skip '('
|
|
156
|
+
const index = this.parseExpression();
|
|
157
|
+
if (this.currentToken().type !== 'operator' || this.currentToken().value !== ',') {
|
|
158
|
+
throw new Error("Expected ',' separating root index and value");
|
|
159
|
+
}
|
|
160
|
+
this.position++; // skip ','
|
|
161
|
+
const value = this.parseExpression();
|
|
162
|
+
if (this.currentToken().type !== ')')
|
|
163
|
+
throw new Error("Expected ')' after root arguments");
|
|
164
|
+
this.position++; // skip ')'
|
|
165
|
+
return {
|
|
166
|
+
type: 'root',
|
|
167
|
+
index: this.collapse(index).n,
|
|
168
|
+
value: this.collapse(value)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (token.type === '(') {
|
|
172
|
+
this.position++; // skip '('
|
|
173
|
+
const result = this.parseExpression();
|
|
174
|
+
if (this.currentToken().type !== ')')
|
|
175
|
+
throw new Error("Expected closing parenthesis");
|
|
176
|
+
this.position++; // skip ')'
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
throw new Error(`Unexpected token: ${JSON.stringify(token)}`);
|
|
180
|
+
}
|
|
181
|
+
currentToken() {
|
|
182
|
+
if (this.position >= this.tokens.length) {
|
|
183
|
+
throw new Error("Unexpected end of input");
|
|
184
|
+
}
|
|
185
|
+
return this.tokens[this.position];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|