pimath 0.2.13 → 0.2.15

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 (59) hide show
  1. package/dist/pimath.js +856 -1882
  2. package/dist/pimath.js.map +1 -1
  3. package/package.json +2 -2
  4. package/src/algebra/equation.ts +562 -558
  5. package/src/algebra/equationSolver.ts +489 -489
  6. package/src/algebra/factor.ts +396 -392
  7. package/src/algebra/linearSystem.ts +446 -442
  8. package/src/algebra/matrix.ts +515 -511
  9. package/src/algebra/monom.ts +932 -928
  10. package/src/algebra/polyFactor.ts +724 -720
  11. package/src/algebra/polynom.ts +1233 -1229
  12. package/src/coefficients/fraction.ts +907 -748
  13. package/src/coefficients/root.ts +500 -359
  14. package/src/geometry/TupleN.ts +129 -129
  15. package/src/geometry/circle.ts +455 -455
  16. package/src/geometry/line.ts +658 -654
  17. package/src/geometry/point.ts +105 -105
  18. package/src/geometry/sphere3.ts +2 -2
  19. package/src/geometry/triangle.ts +488 -488
  20. package/src/geometry/vector.ts +230 -226
  21. package/src/numeric.ts +136 -51
  22. package/src/pimath.interface.ts +2 -0
  23. package/types/algebra/equation.d.ts +1 -0
  24. package/types/algebra/equation.d.ts.map +1 -1
  25. package/types/algebra/equationSolver.d.ts.map +1 -1
  26. package/types/algebra/factor.d.ts +1 -0
  27. package/types/algebra/factor.d.ts.map +1 -1
  28. package/types/algebra/linearSystem.d.ts +1 -0
  29. package/types/algebra/linearSystem.d.ts.map +1 -1
  30. package/types/algebra/matrix.d.ts +6 -5
  31. package/types/algebra/matrix.d.ts.map +1 -1
  32. package/types/algebra/monom.d.ts +1 -0
  33. package/types/algebra/monom.d.ts.map +1 -1
  34. package/types/algebra/polyFactor.d.ts +1 -0
  35. package/types/algebra/polyFactor.d.ts.map +1 -1
  36. package/types/algebra/polynom.d.ts +1 -0
  37. package/types/algebra/polynom.d.ts.map +1 -1
  38. package/types/coefficients/fraction.d.ts +62 -53
  39. package/types/coefficients/fraction.d.ts.map +1 -1
  40. package/types/coefficients/root.d.ts +3 -0
  41. package/types/coefficients/root.d.ts.map +1 -1
  42. package/types/geometry/TupleN.d.ts +1 -1
  43. package/types/geometry/TupleN.d.ts.map +1 -1
  44. package/types/geometry/circle.d.ts.map +1 -1
  45. package/types/geometry/line.d.ts +1 -0
  46. package/types/geometry/line.d.ts.map +1 -1
  47. package/types/geometry/point.d.ts.map +1 -1
  48. package/types/geometry/sphere3.d.ts +2 -2
  49. package/types/geometry/sphere3.d.ts.map +1 -1
  50. package/types/geometry/triangle.d.ts.map +1 -1
  51. package/types/geometry/vector.d.ts +1 -0
  52. package/types/geometry/vector.d.ts.map +1 -1
  53. package/types/index.d.ts +3 -3
  54. package/types/numeric.d.ts +4 -4
  55. package/types/numeric.d.ts.map +1 -1
  56. package/types/pimath.interface.d.ts +1 -0
  57. package/types/pimath.interface.d.ts.map +1 -1
  58. package/types/primes.data.d.ts +2 -0
  59. package/types/primes.data.d.ts.map +1 -0
@@ -1,748 +1,907 @@
1
- import type {compareSign, IExpression, InputValue, IPiMathObject} from "../pimath.interface"
2
- import {Numeric} from "../numeric"
3
- import {InvalidArgumentError, MathError} from "../errors"
4
-
5
- export enum FRAC_TYPE {
6
- frac = 'frac',
7
- dfrac = 'dfrac',
8
- tfrac = 'tfrac'
9
- }
10
-
11
- /**
12
- * The fraction class make possible to handle
13
- * \\(\frac{a}{b}\\) or \\[\frac{a}{b}\\] values.
14
- */
15
-
16
- export class Fraction implements IPiMathObject<Fraction>, IExpression<Fraction> {
17
- #denominator = 1
18
- #digits = 3
19
- #exact = true
20
- #numerator = 1
21
- #type: FRAC_TYPE = FRAC_TYPE.frac
22
- #withSign = false
23
-
24
- constructor()
25
- constructor(value: InputValue<Fraction>)
26
- constructor(numerator: number, denominator: number)
27
- constructor(value?: InputValue<Fraction>, denominator?: number) {
28
-
29
- if (value !== undefined) {
30
- this.parse(value, denominator)
31
- }
32
-
33
- return this
34
- }
35
-
36
- // ------------------------------------------
37
- /**
38
- * Parse the value to get the numerator and denominator
39
- * @param value : number or string to parse to get the fraction
40
- */
41
- public parse = (value: InputValue<Fraction>, denominator?: number): this => {
42
-
43
- // A null value means a zero fraction.
44
- if (value === "") {
45
- this.#numerator = 0
46
- this.#denominator = 1
47
- return this
48
- }
49
-
50
- if(typeof value === "number" && isNaN(value)) {
51
- return this.invalid()
52
- }
53
-
54
- if(typeof value === "number" && !isFinite(value)) {
55
- this.infinite()
56
- if(value < 0) {
57
- this.opposite()
58
- }
59
- return this
60
- }
61
-
62
- if (typeof value === "string") {
63
- return this.fromString(value)
64
- }
65
-
66
- if (typeof value === "number" && denominator === undefined) {
67
- return this.fromNumber(value)
68
- }
69
-
70
- if (typeof value === "number" && typeof denominator === "number") {
71
- return this.fromNumbers(value, denominator)
72
- }
73
-
74
- if (value instanceof Fraction) {
75
- return this.copy(value)
76
- }
77
-
78
- return this
79
- }
80
-
81
- public clone = (): Fraction => {
82
- const F = new Fraction()
83
- F.numerator = this.#numerator
84
- F.denominator = this.#denominator
85
- F.exact = this.exact
86
- F.#type = this.#type
87
- F.#digits = this.#digits
88
- F.#withSign = this.#withSign
89
- return F
90
- }
91
-
92
- public copy(value: Fraction): this {
93
- this.#numerator = value.numerator
94
- this.#denominator = value.denominator
95
- this.#exact = value.exact
96
-
97
- return this
98
- }
99
-
100
- public get tex(): string {
101
- if (this.isInfinity()) {
102
- return `${this.sign() === 1 ? '+' : '-'}\\infty`
103
- }
104
-
105
- const plus = this.#withSign && this.isPositive() ? '+' : ''
106
-
107
- if (this.exact) {
108
- if (this.#denominator === 1) {
109
- return `${plus}${this.#numerator}`
110
- } else if (this.#numerator < 0) {
111
- return `-\\${this.#type}{ ${-this.#numerator} }{ ${this.#denominator} }`
112
- } else {
113
- return `${plus}\\${this.#type}{ ${this.#numerator} }{ ${this.#denominator} }`
114
- }
115
- } else {
116
- return plus + this.value.toFixed(this.#digits)
117
- }
118
- }
119
-
120
- public get display(): string {
121
- if (this.isInfinity()) {
122
- return `${this.sign() === 1 ? '+' : '-'}oo`
123
- }
124
-
125
- const plus = this.#withSign && this.isPositive() ? '+' : ''
126
-
127
- if (this.exact) {
128
- if (this.#denominator === 1) {
129
- return `${plus}${this.#numerator}`
130
- } else {
131
- return `${plus}${this.#numerator}/${this.#denominator}`
132
- }
133
- } else {
134
- return plus + this.value.toFixed(this.#digits)
135
- }
136
- }
137
-
138
- public static areEquals = (...F: Fraction[]): boolean => {
139
- return F.every(f => f.isEqual(F[0]))
140
- }
141
-
142
- public static average = (...fractions: (InputValue<Fraction>)[]): Fraction => {
143
- const M = new Fraction().zero()
144
-
145
- for (const f of fractions) {
146
- M.add(f)
147
- }
148
-
149
- M.divide(fractions.length)
150
-
151
- return M
152
- }
153
-
154
- public static isFraction(value: InputValue<Fraction>) {
155
- if (value instanceof Fraction ||
156
- (typeof value === "number" && !isNaN(value))
157
- ) {
158
- return true
159
- }
160
-
161
- if (typeof value === "string") {
162
- const parts = value.split('/')
163
-
164
- return parts.length <= 2 && parts.every(p => !isNaN(+p))
165
- }
166
-
167
- return false
168
-
169
- }
170
-
171
- public static max = (...fractions: InputValue<Fraction>[]): Fraction => {
172
- let M = new Fraction(fractions[0])
173
-
174
- for (const m of fractions) {
175
- const compare = new Fraction(m)
176
- if (compare.isGreater(M)) {
177
- M = compare.clone()
178
- }
179
- }
180
-
181
- return M
182
- }
183
-
184
- public static min = (...fractions: (InputValue<Fraction>)[]): Fraction => {
185
- let M = new Fraction(fractions[0])
186
-
187
- for (const m of fractions) {
188
- const compare = new Fraction(m)
189
- if (compare.isLesser(M)) {
190
- M = compare.clone()
191
- }
192
- }
193
-
194
- return M
195
- }
196
-
197
- public static sort = (fractions: (InputValue<Fraction>)[], reverse?: boolean): Fraction[] => {
198
- const fractionsObject: Fraction[] = fractions.map(f => f instanceof Fraction ? f : new Fraction(f))
199
-
200
- const sorted = fractionsObject.sort((a, b) => a.value - b.value)
201
-
202
- if (reverse) {
203
- sorted.reverse()
204
- }
205
-
206
- return sorted
207
- }
208
-
209
- public static toSameDenominateur(...fractions: InputValue<Fraction>[]): Fraction[] {
210
- const F = fractions.map(x => new Fraction(x))
211
- const lcm = Numeric.lcm(...F.map(x => x.denominator))
212
-
213
- F.forEach(x => x.amplify(lcm / x.denominator))
214
-
215
- return F
216
- }
217
-
218
- public static unique = (fractions: (InputValue<Fraction>)[]): Fraction[] => {
219
- const unique: Record<string, boolean> = {},
220
- distinct: Fraction[] = []
221
-
222
- fractions.forEach(x => {
223
- if (!(x instanceof Fraction)) {
224
- x = new Fraction(x)
225
- }
226
-
227
- if (!unique[x.clone().reduce().tex]) {
228
- distinct.push(x.clone())
229
- unique[x.clone().reduce().tex] = true
230
- }
231
- })
232
-
233
- return distinct
234
- }
235
-
236
- public static xMultiply = (...values: (InputValue<Fraction>)[]): Fraction => {
237
- const R = new Fraction()
238
-
239
- values.forEach(f => R.multiply(f, false))
240
-
241
- return R
242
- }
243
-
244
- public abs = (): this => {
245
- this.#numerator = Math.abs(this.#numerator)
246
- this.#denominator = Math.abs(this.#denominator)
247
- return this
248
- }
249
-
250
- public add = (F: InputValue<Fraction>): this => {
251
- if (F instanceof Fraction) {
252
- const N: number = this.#numerator,
253
- D: number = this.#denominator
254
-
255
- this.#numerator = N * F.denominator + F.numerator * D
256
- this.#denominator = D * F.denominator
257
- this.exact = this.exact && F.exact
258
- } else {
259
- return this.add(new Fraction(F))
260
- }
261
-
262
- return this.reduce()
263
- }
264
-
265
- public amplify = (k: number): this => {
266
- if (!Number.isSafeInteger(k)) {
267
- throw new InvalidArgumentError("The amplification factor must be a safe integer.")
268
- }
269
-
270
- this.#numerator *= k
271
- this.#denominator *= k
272
-
273
- return this
274
- }
275
-
276
- /**
277
- * Compare the current coefficient with another coefficient
278
- * @param F (Coefficient) The coefficient to _compare
279
- * @param sign (string| default is =): authorized values: =, <, <=, >, >= with some variations.
280
- */
281
- public compare = (F: InputValue<Fraction>, sign?: compareSign): boolean => {
282
- sign ??= '='
283
-
284
- let compareFraction: Fraction
285
- if (F instanceof Fraction) {
286
- compareFraction = F.clone()
287
- } else {
288
- compareFraction = new Fraction(F)
289
- }
290
-
291
- switch (sign) {
292
- case '>':
293
- return this.value > compareFraction.value
294
- case ">=":
295
- case "=>":
296
- case "geq":
297
- return this.value >= compareFraction.value
298
- case "<":
299
- return this.value < compareFraction.value
300
- case "<=":
301
- case "=<":
302
- case "leq":
303
- return this.value <= compareFraction.value
304
- case "=":
305
- return this.#numerator * compareFraction.denominator ===
306
- compareFraction.numerator * this.#denominator
307
- case "<>":
308
- return this.#numerator * compareFraction.denominator !==
309
- compareFraction.numerator * this.#denominator
310
- default:
311
- return false
312
- }
313
- }
314
-
315
- public get denominator(): number {
316
- return this.#denominator
317
- }
318
-
319
- public set denominator(value: number) {
320
- this.#denominator = value
321
- }
322
-
323
- public get dfrac(): this {
324
- this.#type = FRAC_TYPE.dfrac
325
- return this
326
- }
327
-
328
- digits(value: number): this {
329
- this.#digits = value
330
- return this
331
- }
332
-
333
- public divide = (F: Fraction | number): this => {
334
- const Q = new Fraction(F)
335
-
336
- if (Q.numerator === 0) {
337
- return this.infinite()
338
- }
339
-
340
- const N: number = this.#numerator,
341
- D: number = this.#denominator
342
-
343
- this.#numerator = N * Q.denominator
344
- this.#denominator = D * Q.numerator
345
-
346
- this.exact = this.exact && Q.exact
347
- return this.reduce()
348
- }
349
-
350
- get exact(): boolean {
351
- return this.#exact
352
- }
353
-
354
- set exact(value: boolean) {
355
- this.#exact = value
356
- }
357
-
358
- public get frac(): this {
359
- this.#type = FRAC_TYPE.frac
360
- return this
361
- }
362
-
363
- public fromNumber(value: number): this {
364
- if (Number.isSafeInteger(value)) {
365
- // The given value is an integer
366
- this.#numerator = value
367
- this.#denominator = 1
368
- this.#exact = true
369
- return this
370
- }
371
-
372
- // The given value is a float number
373
- // Get the number of decimals after the float sign
374
- const [, decimal] = (value.toString()).split('.')
375
- const p: number = decimal ? decimal.length : 0
376
- const power = Math.pow(10, p)
377
-
378
- this.#numerator = value * power
379
- this.#denominator = power
380
-
381
- this.#numerator = Numeric.numberCorrection(this.#numerator)
382
- this.#denominator = Numeric.numberCorrection(this.#denominator)
383
-
384
- this.reduce()
385
-
386
- // assume it's not exact if the decimal part is more than 10 decimals
387
- this.#exact = p < 10
388
-
389
- return this
390
- }
391
-
392
- public fromNumbers(numerator: number, denominator: number): this {
393
- if (Number.isSafeInteger(numerator) && Number.isSafeInteger(denominator)) {
394
- // The given value is an integer
395
- this.#numerator = numerator
396
- this.#denominator = denominator
397
- this.#exact = true
398
- return this
399
- }
400
-
401
- return this.fromNumber(numerator / denominator)
402
- }
403
-
404
- public fromPeriodic(value: string | number, length: number): this {
405
- const [, decimal] = (value.toString()).split(/[.,]/)
406
- const p: number = decimal ? decimal.length : 0
407
-
408
- if (!Number.isSafeInteger(length) || length <= 0) {
409
- throw new InvalidArgumentError("The periodic length must be a positive integer.")
410
- }
411
-
412
- if (length > p) {
413
- throw new InvalidArgumentError(`The periodic length (${length}) cannot exceed the number of decimal digits (${p}).`)
414
- }
415
-
416
- const power = Math.pow(10, p)
417
-
418
- this.#numerator = (+value) * power - Math.floor((+value) * Math.pow(10, p - length))
419
- this.#denominator = power - Math.pow(10, p - length)
420
- this.#exact = true
421
-
422
- return this
423
- }
424
-
425
- public fromString(value: string): this {
426
- // Split the string value in two parts: Numerator/Denominator
427
- const S = value.split('/').map(Number)
428
-
429
- this.#exact = true
430
-
431
- // Only one divide sign allowed
432
- if (S.length > 2) {
433
- this.#numerator = NaN
434
- return this
435
- }
436
-
437
- // Each parts must be a number
438
- if (S.some(x => isNaN(x))) {
439
- this.#numerator = NaN
440
- return this
441
- }
442
-
443
- if (S.length === 1) {
444
- // No divide sign - it's a number
445
- return this.fromNumber(+value)
446
- }
447
-
448
- // One divide signe
449
- // We check if the denominator is zero
450
- if (S[1] === 0) {
451
- return this.infinite()
452
- }
453
-
454
- this.#numerator = S[0]
455
- this.#denominator = S[1]
456
-
457
- return this
458
- }
459
-
460
- public infinite = (): this => {
461
- this.#numerator = Infinity
462
- this.#denominator = 1
463
- this.exact = true
464
- return this
465
- }
466
-
467
- public invalid = (): this => {
468
- this.#numerator = NaN
469
- this.#denominator = 1
470
- this.exact = true
471
- return this
472
- }
473
-
474
- public inverse = (): this => {
475
- const sign = this.sign()
476
- const n = Math.abs(this.#numerator)
477
- this.#numerator = Math.abs(this.#denominator) * sign
478
- this.#denominator = n
479
-
480
- return this
481
- }
482
-
483
- public isEqual = (than: Fraction | number): boolean => {
484
- return this.compare(than, '=')
485
- }
486
-
487
- public isEven = (): boolean => {
488
- return this.isRelative() && this.value % 2 === 0
489
- }
490
-
491
- public isFinite = (): boolean => {
492
- return !this.isInfinity() && !this.isNaN()
493
- }
494
-
495
- public isGeq = (than: Fraction | number): boolean => {
496
- return this.compare(than, '>=')
497
- }
498
-
499
- public isGreater = (than: Fraction | number): boolean => {
500
- return this.compare(than, '>')
501
- }
502
-
503
- public isInfinity = (): boolean => {
504
- return Math.abs(this.#numerator) === Infinity
505
- }
506
-
507
- public isLeq = (than: Fraction | number): boolean => {
508
- return this.compare(than, '<=')
509
- }
510
-
511
- public isLesser = (than: Fraction | number): boolean => {
512
- return this.compare(than, '<')
513
- }
514
-
515
- public isNaN = (): boolean => {
516
- return isNaN(this.#numerator)
517
- }
518
-
519
- public isNatural = (): boolean => {
520
- return this.isRelative() && this.isPositive()
521
- }
522
-
523
- public isNegative = (): boolean => {
524
- return this.sign() === -1
525
- }
526
-
527
- public isNotEqual = (than: Fraction | number): boolean => {
528
- return this.compare(than, '<>')
529
- }
530
-
531
- public isNotZero = (): boolean => {
532
- return this.#numerator !== 0
533
- }
534
-
535
- public isOdd = (): boolean => {
536
- return this.isRelative() && Math.abs(this.value % 2) === 1
537
- }
538
-
539
- public isOne = (): boolean => {
540
- return this.#numerator === 1 && this.#denominator === 1
541
- }
542
-
543
- public isPositive = (): boolean => {
544
- return this.sign() === 1
545
- }
546
-
547
- public isRational = (): boolean => {
548
- return this.exact && !this.isRelative()
549
- }
550
-
551
- public isReduced = (): boolean => {
552
- return Math.abs(Numeric.gcd(this.#numerator, this.#denominator)) === 1
553
- }
554
-
555
- public isRelative = (): boolean => {
556
- return this.exact && this.clone().reduce().denominator === 1
557
- }
558
-
559
- public isSquare = (): boolean => {
560
- return Math.sqrt(this.#numerator) % 1 === 0 && Math.sqrt(this.#denominator) % 1 === 0
561
- }
562
-
563
- public isStrictlyNegative = (): boolean => {
564
- return this.value < 0
565
- }
566
-
567
- public isStrictlyPositive = (): boolean => {
568
- return this.value > 0
569
- }
570
-
571
- public isUnit(): boolean {
572
- return Math.abs(this.#numerator) === 1 && this.#denominator === 1
573
- }
574
-
575
- // Mathematical operations specific to fractions
576
- public isZero = (): boolean => {
577
- return this.#numerator === 0
578
- }
579
-
580
- public multiply = (F: InputValue<Fraction>, reduce = true): this => {
581
- // Parse the value.
582
- // If it's a fraction, return a clone of it
583
- // If it's an integer, return the fraction F/1
584
- const Q = new Fraction(F)
585
-
586
- this.#numerator = this.#numerator * Q.numerator
587
- this.#denominator = this.#denominator * Q.denominator
588
-
589
- this.exact = this.exact && Q.exact
590
-
591
- return reduce ? this.reduce() : this
592
- }
593
-
594
- // ------------------------------------------
595
- public get numerator(): number {
596
- return this.#numerator
597
- }
598
-
599
- public set numerator(value: number) {
600
- this.#numerator = value
601
- }
602
-
603
- public one = (): this => {
604
- return this.fromNumber(1)
605
- }
606
-
607
- public opposite = (): this => {
608
- this.#numerator = -this.#numerator
609
- return this
610
- }
611
-
612
- public pow = (p: number | Fraction): this => {
613
- if (p instanceof Fraction) {
614
- return this.pow(p.value)
615
- }
616
-
617
- this.reduce()
618
- if (p < 0) {
619
- this.inverse()
620
- }
621
-
622
- // Check if numerator and denominator are roots of...
623
- // otherwise, convert to numeric.
624
- const absP = Math.abs(p)
625
- const powNum = Math.pow(this.#numerator, absP)
626
- const powDen = Math.pow(this.#denominator, absP)
627
- const controlNumerator = Math.floor(powNum)
628
- const controlDenominator = Math.floor(powDen)
629
-
630
- if (controlNumerator === powNum && controlDenominator === powDen) {
631
- // Exact integer result — e.g. (9/4)^0.5 = 3/2
632
- this.#numerator = controlNumerator
633
- this.#denominator = controlDenominator
634
- } else {
635
- // Non-exact result — e.g. (2/1)^0.5 = 1.414...
636
- this.#numerator = powNum / powDen
637
- this.#denominator = 1
638
- this.exact = false
639
- }
640
-
641
- return this
642
- }
643
-
644
- public reduce = (): this => {
645
- const g = Numeric.gcd(this.#numerator, this.#denominator)
646
- this.#numerator = this.#numerator / g
647
- this.#denominator = this.#denominator / g
648
-
649
- if (this.#denominator < 0) {
650
- this.#denominator = -this.#denominator
651
- this.#numerator = -this.#numerator
652
- }
653
- return this
654
- }
655
-
656
- public root = (p: number): this => {
657
-
658
- // Check if they are perfect roots..
659
- if (p === 0) {
660
- return this
661
- }
662
-
663
- // if p is not a safe integer, throw error
664
- if (!Number.isSafeInteger(p)) {
665
- throw new InvalidArgumentError("The root must be an integer.")
666
- }
667
-
668
- // If negative, inverse the fraction
669
- if (p < 0) {
670
- this.inverse()
671
- }
672
-
673
-
674
- // if the fraction is negative and the root is even, throw error
675
- if (this.isNegative() && p % 2 === 0) {
676
- throw new MathError("The root of a negative number must be odd.")
677
- }
678
-
679
- // get the sign of the fraction and make it positive
680
- const sign = this.sign()
681
- this.abs()
682
-
683
- // Reduce the fraction
684
- this.reduce()
685
-
686
- // Check if numerator and denominator are roots of...
687
- // otherwise, convert to numeric.
688
- const controlNumerator = Math.floor(Math.pow(this.#numerator, Math.abs(1 / p))),
689
- controlDenominator = Math.floor(Math.pow(this.#denominator, Math.abs(1 / p)))
690
-
691
- this.#numerator = Math.pow(this.#numerator, Math.abs(1 / p))
692
- this.#denominator = Math.pow(this.#denominator, Math.abs(1 / p))
693
-
694
- if (controlNumerator !== this.#numerator
695
- ||
696
- controlDenominator !== this.#denominator) {
697
- // The fraction is not a perfect root - make it approximative
698
- this.#numerator = this.#numerator / this.#denominator
699
- this.#denominator = 1
700
- this.exact = false
701
- }
702
-
703
- // Restore the sign
704
- this.multiply(sign)
705
-
706
- return this
707
- }
708
-
709
- public sign = (): 1 | -1 => {
710
- return (this.#numerator * this.#denominator >= 0) ? 1 : -1
711
- }
712
-
713
- public sqrt = (): this => {
714
- return this.root(2)
715
- }
716
-
717
- public subtract = (F: Fraction | number): this => {
718
- if (F instanceof Fraction) {
719
- return this.add(F.clone().opposite())
720
- } else {
721
- return this.add(-F)
722
- }
723
- }
724
-
725
- public get tfrac(): this {
726
- this.#type = FRAC_TYPE.tfrac
727
- return this
728
- }
729
-
730
- public get value(): number {
731
- const result = this.#numerator / this.#denominator
732
- return result === 0 ? 0 : result
733
- }
734
-
735
- get withSign(): this {
736
- this.#withSign = true
737
- return this
738
- }
739
-
740
- get withoutSign(): this {
741
- this.#withSign = false
742
- return this
743
- }
744
-
745
- public zero = (): this => {
746
- return this.fromNumber(0)
747
- }
748
- }
1
+ import type {compareSign, IExpression, InputValue, IPiMathObject} from "../pimath.interface"
2
+ import {Numeric} from "../numeric"
3
+ import {InvalidArgumentError, MathError} from "../errors"
4
+
5
+ export enum FRAC_TYPE {
6
+ frac = 'frac',
7
+ dfrac = 'dfrac',
8
+ tfrac = 'tfrac'
9
+ }
10
+
11
+ /**
12
+ * The fraction class make possible to handle
13
+ * \\(\frac{a}{b}\\) or \\[\frac{a}{b}\\] values.
14
+ */
15
+
16
+ export class Fraction implements IPiMathObject<Fraction>, IExpression<Fraction> {
17
+ #denominator = 1
18
+ #digits = 3
19
+ #exact = true
20
+ #numerator = 1
21
+ #type: FRAC_TYPE = FRAC_TYPE.frac
22
+ #withSign = false
23
+
24
+ constructor()
25
+ constructor(value: InputValue<Fraction>)
26
+ constructor(numerator: number, denominator: number)
27
+ constructor(value?: InputValue<Fraction>, denominator?: number) {
28
+
29
+ if (value !== undefined) {
30
+ this.parse(value, denominator)
31
+ }
32
+
33
+ return this
34
+ }
35
+
36
+ // ------------------------------------------
37
+ /**
38
+ * Parse the value to get the numerator and denominator
39
+ * @param value : number or string to parse to get the fraction
40
+ */
41
+ parse(value: InputValue<Fraction>, denominator?: number): this {
42
+
43
+ // A null value means a zero fraction.
44
+ if (value === "") {
45
+ this.#numerator = 0
46
+ this.#denominator = 1
47
+ return this
48
+ }
49
+
50
+ if (typeof value === "number" && isNaN(value)) {
51
+ return this.invalid()
52
+ }
53
+
54
+ if (typeof value === "number" && !isFinite(value)) {
55
+ this.infinite()
56
+ if (value < 0) {
57
+ this.opposite()
58
+ }
59
+ return this
60
+ }
61
+
62
+ if (typeof value === "string") {
63
+ return this.fromString(value)
64
+ }
65
+
66
+ if (typeof value === "number" && denominator === undefined) {
67
+ return this.fromNumber(value)
68
+ }
69
+
70
+ if (typeof value === "number" && typeof denominator === "number") {
71
+ return this.fromNumbers(value, denominator)
72
+ }
73
+
74
+ if (value instanceof Fraction) {
75
+ return this.copy(value)
76
+ }
77
+
78
+ return this
79
+ }
80
+
81
+ clone(): Fraction {
82
+ const F = new Fraction()
83
+ F.numerator = this.#numerator
84
+ F.denominator = this.#denominator
85
+ F.exact = this.exact
86
+ F.#type = this.#type
87
+ F.#digits = this.#digits
88
+ F.#withSign = this.#withSign
89
+ return F
90
+ }
91
+
92
+ /**
93
+ * Copy the mathematical value of another fraction into this one.
94
+ *
95
+ * NOTE: unlike {@link clone}, `copy` intentionally transfers only the
96
+ * mathematical state (`#numerator`, `#denominator`, `#exact`) and NOT the
97
+ * display state (`#type`, `#digits`, `#withSign`). This divergence is by
98
+ * design: `parse()` routes a `Fraction` input to `copy()`, so the receiving
99
+ * fraction keeps its own formatting configuration.
100
+ */
101
+ copy(value: Fraction): this {
102
+ this.#numerator = value.numerator
103
+ this.#denominator = value.denominator
104
+ this.#exact = value.exact
105
+
106
+ return this
107
+ }
108
+
109
+ get tex(): string {
110
+ if (this.isInfinity()) {
111
+ return `${this.sign() === 1 ? '+' : '-'}\\infty`
112
+ }
113
+
114
+ const plus = this.#withSign && this.isPositive() ? '+' : ''
115
+
116
+ if (this.exact) {
117
+ if (this.#denominator === 1) {
118
+ return `${plus}${this.#numerator}`
119
+ } else if (this.#numerator < 0) {
120
+ return `-\\${this.#type}{ ${-this.#numerator} }{ ${this.#denominator} }`
121
+ } else {
122
+ return `${plus}\\${this.#type}{ ${this.#numerator} }{ ${this.#denominator} }`
123
+ }
124
+ } else {
125
+ return plus + this.value.toFixed(this.#digits)
126
+ }
127
+ }
128
+
129
+ get display(): string {
130
+ if (this.isInfinity()) {
131
+ return `${this.sign() === 1 ? '+' : '-'}oo`
132
+ }
133
+
134
+ const plus = this.#withSign && this.isPositive() ? '+' : ''
135
+
136
+ if (this.exact) {
137
+ if (this.#denominator === 1) {
138
+ return `${plus}${this.#numerator}`
139
+ } else {
140
+ return `${plus}${this.#numerator}/${this.#denominator}`
141
+ }
142
+ } else {
143
+ return plus + this.value.toFixed(this.#digits)
144
+ }
145
+ }
146
+
147
+ static areEquals(...F: Fraction[]): boolean {
148
+ return F.every(f => f.isEqual(F[0]))
149
+ }
150
+
151
+ static average(...fractions: (InputValue<Fraction>)[]): Fraction {
152
+ const M = new Fraction().zero()
153
+
154
+ for (const f of fractions) {
155
+ M.add(f)
156
+ }
157
+
158
+ M.divide(fractions.length)
159
+
160
+ return M
161
+ }
162
+
163
+ static isFraction(value: InputValue<Fraction>) {
164
+ if (value instanceof Fraction ||
165
+ (typeof value === "number" && !isNaN(value))
166
+ ) {
167
+ return true
168
+ }
169
+
170
+ if (typeof value === "string") {
171
+ const parts = value.split('/')
172
+
173
+ return parts.length <= 2 && parts.every(p => !isNaN(+p))
174
+ }
175
+
176
+ return false
177
+
178
+ }
179
+
180
+ static max(...fractions: InputValue<Fraction>[]): Fraction {
181
+ let M = new Fraction(fractions[0])
182
+
183
+ for (const m of fractions) {
184
+ const compare = new Fraction(m)
185
+ if (compare.isGreater(M)) {
186
+ M = compare.clone()
187
+ }
188
+ }
189
+
190
+ return M
191
+ }
192
+
193
+ static min(...fractions: (InputValue<Fraction>)[]): Fraction {
194
+ let M = new Fraction(fractions[0])
195
+
196
+ for (const m of fractions) {
197
+ const compare = new Fraction(m)
198
+ if (compare.isLesser(M)) {
199
+ M = compare.clone()
200
+ }
201
+ }
202
+
203
+ return M
204
+ }
205
+
206
+ static sort(fractions: (InputValue<Fraction>)[], reverse?: boolean): Fraction[] {
207
+ const fractionsObject: Fraction[] = fractions.map(f => f instanceof Fraction ? f : new Fraction(f))
208
+
209
+ const sorted = fractionsObject.sort((a, b) => a.value - b.value)
210
+
211
+ if (reverse) {
212
+ sorted.reverse()
213
+ }
214
+
215
+ return sorted
216
+ }
217
+
218
+ static toSameDenominateur(...fractions: InputValue<Fraction>[]): Fraction[] {
219
+ const F = fractions.map(x => new Fraction(x))
220
+ const lcm = Numeric.lcm(...F.map(x => x.denominator))
221
+
222
+ F.forEach(x => x.amplify(lcm / x.denominator))
223
+
224
+ return F
225
+ }
226
+
227
+ static unique(fractions: (InputValue<Fraction>)[]): Fraction[] {
228
+ const unique: Record<string, boolean> = {}
229
+ const distinct: Fraction[] = []
230
+
231
+ fractions.forEach(x => {
232
+ if (!(x instanceof Fraction)) {
233
+ x = new Fraction(x)
234
+ }
235
+
236
+ if (!unique[x.clone().reduce().tex]) {
237
+ distinct.push(x.clone())
238
+ unique[x.clone().reduce().tex] = true
239
+ }
240
+ })
241
+
242
+ return distinct
243
+ }
244
+
245
+ static xMultiply(...values: (InputValue<Fraction>)[]): Fraction {
246
+ const R = new Fraction()
247
+
248
+ values.forEach(f => R.multiply(f, false))
249
+
250
+ return R
251
+ }
252
+
253
+ abs(): this {
254
+ this.#numerator = Math.abs(this.#numerator)
255
+ this.#denominator = Math.abs(this.#denominator)
256
+ return this
257
+ }
258
+
259
+ add(F: InputValue<Fraction>): this {
260
+ if (F instanceof Fraction) {
261
+ const N: number = this.#numerator
262
+ const D: number = this.#denominator
263
+
264
+ this.#numerator = N * F.denominator + F.numerator * D
265
+ this.#denominator = D * F.denominator
266
+ this.exact = this.exact && F.exact
267
+ } else {
268
+ return this.add(new Fraction(F))
269
+ }
270
+
271
+ return this.reduce()
272
+ }
273
+
274
+ amplify(k: number): this {
275
+ if (!Number.isSafeInteger(k)) {
276
+ throw new InvalidArgumentError("The amplification factor must be a safe integer.")
277
+ }
278
+
279
+ if (k === 0) {
280
+ throw new InvalidArgumentError("The amplification factor cannot be zero.")
281
+ }
282
+
283
+ this.#numerator *= k
284
+ this.#denominator *= k
285
+
286
+ return this
287
+ }
288
+
289
+ /**
290
+ * Compare the current coefficient with another coefficient
291
+ * @param F (Coefficient) The coefficient to _compare
292
+ * @param sign (string| default is =): authorized values: =, <, <=, >, >= with some variations.
293
+ */
294
+ compare(F: InputValue<Fraction>, sign?: compareSign): boolean {
295
+ sign ??= '='
296
+
297
+ let compareFraction: Fraction
298
+ if (F instanceof Fraction) {
299
+ compareFraction = F.clone()
300
+ } else {
301
+ compareFraction = new Fraction(F)
302
+ }
303
+
304
+ switch (sign) {
305
+ case '>':
306
+ return this.value > compareFraction.value
307
+ case ">=":
308
+ case "=>":
309
+ case "geq":
310
+ return this.value >= compareFraction.value
311
+ case "<":
312
+ return this.value < compareFraction.value
313
+ case "<=":
314
+ case "=<":
315
+ case "leq":
316
+ return this.value <= compareFraction.value
317
+ case "=":
318
+ return this.#numerator * compareFraction.denominator ===
319
+ compareFraction.numerator * this.#denominator
320
+ case "<>":
321
+ return this.#numerator * compareFraction.denominator !==
322
+ compareFraction.numerator * this.#denominator
323
+ default:
324
+ return false
325
+ }
326
+ }
327
+
328
+ get denominator(): number {
329
+ return this.#denominator
330
+ }
331
+
332
+ set denominator(value: number) {
333
+ this.#denominator = value
334
+ }
335
+
336
+ get asDfrac(): this {
337
+ this.#type = FRAC_TYPE.dfrac
338
+ return this
339
+ }
340
+
341
+ digits(value: number): this {
342
+ this.#digits = value
343
+ return this
344
+ }
345
+
346
+ divide(F: Fraction | number): this {
347
+ const Q = new Fraction(F)
348
+
349
+ if (Q.numerator === 0) {
350
+ // Division by zero yields infinity; preserve the dividend's sign
351
+ // so that e.g. (-3) / 0 evaluates to -Infinity, not +Infinity.
352
+ const s = this.sign()
353
+ this.infinite()
354
+ return s === -1 ? this.opposite() : this
355
+ }
356
+
357
+ const N: number = this.#numerator
358
+ const D: number = this.#denominator
359
+
360
+ this.#numerator = N * Q.denominator
361
+ this.#denominator = D * Q.numerator
362
+
363
+ this.exact = this.exact && Q.exact
364
+ return this.reduce()
365
+ }
366
+
367
+ get exact(): boolean {
368
+ return this.#exact
369
+ }
370
+
371
+ set exact(value: boolean) {
372
+ this.#exact = value
373
+ }
374
+
375
+ get asFrac(): this {
376
+ this.#type = FRAC_TYPE.frac
377
+ return this
378
+ }
379
+
380
+ fromNumber(value: number): this {
381
+ if (Number.isSafeInteger(value)) {
382
+ // The given value is an integer
383
+ this.#numerator = value
384
+ this.#denominator = 1
385
+ this.#exact = true
386
+ return this
387
+ }
388
+
389
+ // Exponential notation (e.g. 1e-7, 1e21) is not supported: toString()
390
+ // would break the "integer numerator / integer denominator" invariant.
391
+ // Fail explicitly rather than silently producing a corrupted fraction.
392
+ if (value.toString().includes('e') || value.toString().includes('E')) {
393
+ throw new InvalidArgumentError(`Exponential notation is not supported: ${value}`)
394
+ }
395
+
396
+ // The given value is a float number
397
+ // Get the number of decimals after the float sign
398
+ const [, decimal] = (value.toString()).split('.')
399
+ const p: number = decimal ? decimal.length : 0
400
+ const power = Math.pow(10, p)
401
+
402
+ this.#numerator = value * power
403
+ this.#denominator = power
404
+
405
+ this.#numerator = Numeric.numberCorrection(this.#numerator)
406
+ this.#denominator = Numeric.numberCorrection(this.#denominator)
407
+
408
+ this.reduce()
409
+
410
+ // assume it's not exact if the decimal part is more than 10 decimals
411
+ this.#exact = p < 10
412
+
413
+ return this
414
+ }
415
+
416
+ fromNumbers(numerator: number, denominator: number): this {
417
+ if (denominator === 0) {
418
+ // Single, centralized handling of a zero denominator:
419
+ // x/0 → signed infinity, and 0/0 → invalid (NaN).
420
+ if (numerator === 0) {
421
+ return this.invalid()
422
+ }
423
+ return numerator < 0 ? this.infinite().opposite() : this.infinite()
424
+ }
425
+
426
+ if (Number.isSafeInteger(numerator) && Number.isSafeInteger(denominator)) {
427
+ // The given value is an integer
428
+ this.#numerator = numerator
429
+ this.#denominator = denominator
430
+ this.#exact = true
431
+ return this
432
+ }
433
+
434
+ return this.fromNumber(numerator / denominator)
435
+ }
436
+
437
+ fromPeriodic(value: string | number, length: number): this {
438
+ const [, decimal] = (value.toString()).split(/[.,]/)
439
+ const p: number = decimal ? decimal.length : 0
440
+
441
+ if (!Number.isSafeInteger(length) || length <= 0) {
442
+ throw new InvalidArgumentError("The periodic length must be a positive integer.")
443
+ }
444
+
445
+ if (length > p) {
446
+ throw new InvalidArgumentError(`The periodic length (${length}) cannot exceed the number of decimal digits (${p}).`)
447
+ }
448
+
449
+ const power = Math.pow(10, p)
450
+ const shift = Math.pow(10, p - length)
451
+
452
+ // Handle the sign separately: Math.floor rounds toward -Infinity,
453
+ // which would corrupt the truncation for negative values.
454
+ const numericValue = +value
455
+ const sign = numericValue < 0 ? -1 : 1
456
+ const absValue = Math.abs(numericValue)
457
+
458
+ this.#numerator = sign * Math.round(absValue * power - Math.floor(absValue * shift))
459
+ this.#denominator = power - shift
460
+ this.#exact = true
461
+
462
+ return this.reduce()
463
+ }
464
+
465
+ fromString(value: string): this {
466
+ // Periodic decimal notation, e.g. "2.31(25)" = 2.31252525...
467
+ const periodicMatch = /^([+-]?)(\d*)\.(\d*)\((\d+)\)$/.exec(value)
468
+ if (periodicMatch) {
469
+ const [, sign, intPart, nonPeriodic, period] = periodicMatch
470
+ const absDecimal = `${intPart || '0'}.${nonPeriodic}${period}`
471
+
472
+ this.fromPeriodic(absDecimal, period.length)
473
+ if (sign === '-') {
474
+ this.opposite()
475
+ }
476
+ return this.reduce()
477
+ }
478
+
479
+ // A string using parentheses that did not match the notation is malformed.
480
+ if (value.includes('(') || value.includes(')')) {
481
+ return this.invalid()
482
+ }
483
+
484
+ // Split the string value in two parts: Numerator/Denominator
485
+ const S = value.split('/').map(Number)
486
+
487
+ this.#exact = true
488
+
489
+ // Only one divide sign allowed
490
+ if (S.length > 2) {
491
+ this.#numerator = NaN
492
+ return this
493
+ }
494
+
495
+ // Each parts must be a number
496
+ if (S.some(x => isNaN(x))) {
497
+ this.#numerator = NaN
498
+ return this
499
+ }
500
+
501
+ if (S.length === 1) {
502
+ // No divide sign - it's a number
503
+ return this.fromNumber(+value)
504
+ }
505
+
506
+ // One divide sign: delegate to fromNumbers so the zero-denominator case
507
+ // is handled at a single place (see fromNumbers).
508
+ return this.fromNumbers(S[0], S[1])
509
+ }
510
+
511
+ infinite(): this {
512
+ this.#numerator = Infinity
513
+ this.#denominator = 1
514
+ this.exact = true
515
+ return this
516
+ }
517
+
518
+ invalid(): this {
519
+ this.#numerator = NaN
520
+ this.#denominator = 1
521
+ this.exact = true
522
+ return this
523
+ }
524
+
525
+ inverse(): this {
526
+ const sign = this.sign()
527
+ const n = Math.abs(this.#numerator)
528
+ this.#numerator = Math.abs(this.#denominator) * sign
529
+ this.#denominator = n
530
+
531
+ return this
532
+ }
533
+
534
+ isEqual(than: Fraction | number): boolean {
535
+ return this.compare(than, '=')
536
+ }
537
+
538
+ isEven(): boolean {
539
+ return this.isRelative() && this.value % 2 === 0
540
+ }
541
+
542
+ isFinite(): boolean {
543
+ return !this.isInfinity() && !this.isNaN()
544
+ }
545
+
546
+ isGeq(than: Fraction | number): boolean {
547
+ return this.compare(than, '>=')
548
+ }
549
+
550
+ isGreater(than: Fraction | number): boolean {
551
+ return this.compare(than, '>')
552
+ }
553
+
554
+ isInfinity(): boolean {
555
+ return Math.abs(this.#numerator) === Infinity
556
+ }
557
+
558
+ isLeq(than: Fraction | number): boolean {
559
+ return this.compare(than, '<=')
560
+ }
561
+
562
+ isLesser(than: Fraction | number): boolean {
563
+ return this.compare(than, '<')
564
+ }
565
+
566
+ isNaN(): boolean {
567
+ return isNaN(this.#numerator)
568
+ }
569
+
570
+ isNatural(): boolean {
571
+ return this.isRelative() && this.isPositive()
572
+ }
573
+
574
+ isNegative(): boolean {
575
+ return this.sign() === -1
576
+ }
577
+
578
+ isNotEqual(than: Fraction | number): boolean {
579
+ return this.compare(than, '<>')
580
+ }
581
+
582
+ isNotZero(): boolean {
583
+ return this.#numerator !== 0
584
+ }
585
+
586
+ isOdd(): boolean {
587
+ return this.isRelative() && Math.abs(this.value % 2) === 1
588
+ }
589
+
590
+ isOne(): boolean {
591
+ return this.#numerator === 1 && this.#denominator === 1
592
+ }
593
+
594
+ isPeriodic(): boolean {
595
+ if (!this.exact || !this.isFinite()) {
596
+ return false
597
+ }
598
+
599
+ let d = Math.abs(this.clone().reduce().denominator)
600
+ while (d % 2 === 0) {
601
+ d /= 2
602
+ }
603
+ while (d % 5 === 0) {
604
+ d /= 5
605
+ }
606
+
607
+ return d !== 1
608
+ }
609
+
610
+ isPositive(): boolean {
611
+ return this.sign() === 1
612
+ }
613
+
614
+ /**
615
+ * True when the fraction is an exact rational number: a finite, exact
616
+ * numerator/denominator. Integers are included (4 = 4/1 is rational).
617
+ * Approximate (non-exact) fractions and ±Infinity / NaN are not rational.
618
+ */
619
+ isRational(): boolean {
620
+ return this.exact && this.isFinite()
621
+ }
622
+
623
+ /**
624
+ * True when the fraction is exact and NOT an integer, i.e. it has a non-zero
625
+ * fractional part (e.g. 3/2). Use {@link isRelative} to test for an integer.
626
+ * (This was the former behaviour of {@link isRational}.)
627
+ */
628
+ hasFractionalPart(): boolean {
629
+ return this.exact && !this.isRelative()
630
+ }
631
+
632
+ isReduced(): boolean {
633
+ return Math.abs(Numeric.gcd(this.#numerator, this.#denominator)) === 1
634
+ }
635
+
636
+ /**
637
+ * True when the fraction represents a relative integer, i.e. its reduced
638
+ * denominator equals 1 (e.g. 6/3 = 2). See {@link hasFractionalPart} for the
639
+ * exact non-integer case.
640
+ */
641
+ isRelative(): boolean {
642
+ return this.exact && this.clone().reduce().denominator === 1
643
+ }
644
+
645
+ isSquare(): boolean {
646
+ const f = this.clone().reduce()
647
+
648
+ // A negative value can never be a perfect square.
649
+ if (f.isNegative()) {
650
+ return false
651
+ }
652
+
653
+ const n = Math.abs(f.numerator)
654
+ const d = Math.abs(f.denominator)
655
+ const rootN = Math.round(Math.sqrt(n))
656
+ const rootD = Math.round(Math.sqrt(d))
657
+
658
+ return rootN * rootN === n && rootD * rootD === d
659
+ }
660
+
661
+ isStrictlyNegative(): boolean {
662
+ return this.value < 0
663
+ }
664
+
665
+ isStrictlyPositive(): boolean {
666
+ return this.value > 0
667
+ }
668
+
669
+ isUnit(): boolean {
670
+ return Math.abs(this.#numerator) === 1 && this.#denominator === 1
671
+ }
672
+
673
+ // Mathematical operations specific to fractions
674
+ isZero(): boolean {
675
+ return this.#numerator === 0
676
+ }
677
+
678
+ multiply(F: InputValue<Fraction>, reduce = true): this {
679
+ // Parse the value.
680
+ // If it's a fraction, return a clone of it
681
+ // If it's an integer, return the fraction F/1
682
+ const Q = new Fraction(F)
683
+
684
+ this.#numerator = this.#numerator * Q.numerator
685
+ this.#denominator = this.#denominator * Q.denominator
686
+
687
+ this.exact = this.exact && Q.exact
688
+
689
+ return reduce ? this.reduce() : this
690
+ }
691
+
692
+ // ------------------------------------------
693
+ get numerator(): number {
694
+ return this.#numerator
695
+ }
696
+
697
+ set numerator(value: number) {
698
+ this.#numerator = value
699
+ }
700
+
701
+ one(): this {
702
+ return this.fromNumber(1)
703
+ }
704
+
705
+ opposite(): this {
706
+ this.#numerator = -this.#numerator
707
+ return this
708
+ }
709
+
710
+ pow(p: number | Fraction): this {
711
+ if (p instanceof Fraction) {
712
+ return this.pow(p.value)
713
+ }
714
+
715
+ this.reduce()
716
+ if (p < 0) {
717
+ this.inverse()
718
+ }
719
+
720
+ const absP = Math.abs(p)
721
+
722
+ if (Number.isInteger(absP)) {
723
+ // Integer exponent: the result is always an exact rational.
724
+ // Round to absorb floating-point noise on large powers.
725
+ this.#numerator = Math.round(Math.pow(this.#numerator, absP))
726
+ this.#denominator = Math.round(Math.pow(this.#denominator, absP))
727
+ return this
728
+ }
729
+
730
+ // Fractional exponent (e.g. 0.5): exact only when both parts are
731
+ // perfect powers. Round the candidate, then verify by raising it back
732
+ // through the inverse exponent — no arbitrary epsilon involved.
733
+ const powNum = Math.round(Math.pow(this.#numerator, absP))
734
+ const powDen = Math.round(Math.pow(this.#denominator, absP))
735
+
736
+ if (Math.round(Math.pow(powNum, 1 / absP)) === this.#numerator &&
737
+ Math.round(Math.pow(powDen, 1 / absP)) === this.#denominator) {
738
+ // Exact result — e.g. (9/4)^0.5 = 3/2
739
+ this.#numerator = powNum
740
+ this.#denominator = powDen
741
+ } else {
742
+ // Non-exact result — e.g. (2/1)^0.5 = 1.414...
743
+ this.#numerator = Math.pow(this.#numerator, absP) / Math.pow(this.#denominator, absP)
744
+ this.#denominator = 1
745
+ this.exact = false
746
+ }
747
+
748
+ return this
749
+ }
750
+
751
+ reduce(): this {
752
+ const g = Numeric.gcd(this.#numerator, this.#denominator)
753
+ this.#numerator = this.#numerator / g
754
+ this.#denominator = this.#denominator / g
755
+
756
+ if (this.#denominator < 0) {
757
+ this.#denominator = -this.#denominator
758
+ this.#numerator = -this.#numerator
759
+ }
760
+ return this
761
+ }
762
+
763
+ root(p: number): this {
764
+
765
+ // Check if they are perfect roots..
766
+ if (p === 0) {
767
+ return this
768
+ }
769
+
770
+ // if p is not a safe integer, throw error
771
+ if (!Number.isSafeInteger(p)) {
772
+ throw new InvalidArgumentError("The root must be an integer.")
773
+ }
774
+
775
+ // Validate before any mutation so a rejected root leaves the receiver
776
+ // intact (atomicity). inverse() preserves the sign, so testing
777
+ // isNegative() here is equivalent to testing it after the inversion below.
778
+ if (this.isNegative() && p % 2 === 0) {
779
+ throw new MathError("The root of a negative number must be odd.")
780
+ }
781
+
782
+ // If negative exponent, inverse the fraction
783
+ if (p < 0) {
784
+ this.inverse()
785
+ }
786
+
787
+ // get the sign of the fraction and make it positive
788
+ const sign = this.sign()
789
+ this.abs()
790
+
791
+ // Reduce the fraction
792
+ this.reduce()
793
+
794
+ // The root degree magnitude (the sign of p was already handled above).
795
+ const q = Math.abs(p)
796
+
797
+ // Round the candidate root, then verify by raising it back to the
798
+ // integer power q. Since q is an integer, that check is exact — this
799
+ // catches perfect roots (e.g. ∛27 = 3) that a plain floor would miss.
800
+ const rootNum = Math.round(Math.pow(this.#numerator, 1 / q))
801
+ const rootDen = Math.round(Math.pow(this.#denominator, 1 / q))
802
+
803
+ if (Math.pow(rootNum, q) === this.#numerator && Math.pow(rootDen, q) === this.#denominator) {
804
+ // Perfect root
805
+ this.#numerator = rootNum
806
+ this.#denominator = rootDen
807
+ } else {
808
+ // The fraction is not a perfect root - make it approximative
809
+ this.#numerator = Math.pow(this.#numerator, 1 / q) / Math.pow(this.#denominator, 1 / q)
810
+ this.#denominator = 1
811
+ this.exact = false
812
+ }
813
+
814
+ // Restore the sign
815
+ this.multiply(sign)
816
+
817
+ return this
818
+ }
819
+
820
+ /**
821
+ * Sign of the fraction, as +1 or -1.
822
+ *
823
+ * NOTE (assumed): zero is classified as positive (`sign()` returns 1).
824
+ * Consequently `isPositive()` and `isNatural()` are true for zero, while
825
+ * `isNegative()` is false.
826
+ */
827
+ sign(): 1 | -1 {
828
+ return (this.#numerator * this.#denominator >= 0) ? 1 : -1
829
+ }
830
+
831
+ sqrt(): this {
832
+ return this.root(2)
833
+ }
834
+
835
+ subtract(F: Fraction | number): this {
836
+ if (F instanceof Fraction) {
837
+ return this.add(F.clone().opposite())
838
+ } else {
839
+ return this.add(-F)
840
+ }
841
+ }
842
+
843
+ get asTfrac(): this {
844
+ this.#type = FRAC_TYPE.tfrac
845
+ return this
846
+ }
847
+
848
+ toRepeatingDecimal(): {
849
+ sign: 1 | -1
850
+ integerPart: string
851
+ nonPeriodic: string
852
+ periodic: string
853
+ periodLength: number
854
+ } {
855
+ if (!this.exact || !this.isFinite()) {
856
+ throw new InvalidArgumentError("Cannot decompose a non-exact or infinite fraction.")
857
+ }
858
+
859
+ const f = this.clone().reduce()
860
+ const sign = f.sign()
861
+ const n = Math.abs(f.numerator)
862
+ const d = Math.abs(f.denominator)
863
+
864
+ const integerPart = Math.floor(n / d).toString()
865
+ let remainder = n % d
866
+
867
+ const digits: number[] = []
868
+ const seen = new Map<number, number>() // remainder -> index in digits
869
+ let periodStart = -1
870
+
871
+ while (remainder !== 0) {
872
+ const seenIndex = seen.get(remainder)
873
+ if (seenIndex !== undefined) {
874
+ periodStart = seenIndex
875
+ break
876
+ }
877
+ seen.set(remainder, digits.length)
878
+ remainder *= 10
879
+ digits.push(Math.floor(remainder / d))
880
+ remainder %= d
881
+ }
882
+
883
+ const nonPeriodic = (periodStart === -1 ? digits : digits.slice(0, periodStart)).join('')
884
+ const periodic = periodStart === -1 ? '' : digits.slice(periodStart).join('')
885
+
886
+ return {sign, integerPart, nonPeriodic, periodic, periodLength: periodic.length}
887
+ }
888
+
889
+ get value(): number {
890
+ const result = this.#numerator / this.#denominator
891
+ return result === 0 ? 0 : result
892
+ }
893
+
894
+ get withSign(): this {
895
+ this.#withSign = true
896
+ return this
897
+ }
898
+
899
+ get withoutSign(): this {
900
+ this.#withSign = false
901
+ return this
902
+ }
903
+
904
+ zero(): this {
905
+ return this.fromNumber(0)
906
+ }
907
+ }