pimath 0.2.14 → 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 +850 -1879
  2. package/dist/pimath.js.map +1 -1
  3. package/package.json +1 -1
  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,360 +1,501 @@
1
- import {Fraction} from "./fraction"
2
- import type {IExpression, InputValue, IPiMathObject} from "../pimath.interface"
3
- import {stripParenthesis} from "../helpers"
4
- import {Numeric} from "../numeric"
5
- import {InvalidArgumentError, MathError, ParseError} from "../errors"
6
-
7
- export class Root implements IPiMathObject<Root>, IExpression<Root> {
8
- #factor: Fraction
9
- #index: number
10
- #radical: Fraction
11
- #withSign = false
12
-
13
- constructor(value?: InputValue<Root | Fraction>) {
14
- this.#index = 2
15
- this.#factor = new Fraction().zero()
16
- this.#radical = new Fraction().zero()
17
-
18
- if (value) {
19
- this.parse(value)
20
- }
21
-
22
-
23
- return this
24
- }
25
-
26
- parse(value: InputValue<Root | Fraction>): this {
27
- // should be able to parse "display" roots.
28
- // sqrt12 or sqrt(12) or sqrt(12/5) => [2, 12 or 12/5, 1]
29
- // root(n)10 or root(n)(10) or root(n)(10/3) => [n, 10 or 10/3, 1]
30
- // 7sqrt12 or 7sqrt(12) or 7sqrt(12/5) => [2, 12 or 12/5, 7]
31
- // 7root(n)10 or 7root(n)(10) or 7root(n)(10/3) => [n, 10 or 10/3, 7]
32
-
33
- if (value instanceof Root) {
34
- this.index = value.index
35
- this.radical = value.radical.clone()
36
- this.factor = value.factor.clone()
37
-
38
- return this
39
- }
40
-
41
- if (value instanceof Fraction) {
42
- this.index = 2
43
- this.factor = value.clone()
44
- this.radical.one()
45
-
46
- return this
47
- }
48
-
49
- if (typeof value === "string") {
50
- if (value.includes('sqrt')) {
51
- return this.#parse_sqrt(value)
52
- }
53
-
54
- if (value.includes('root')) {
55
- return this.#parse_root(value)
56
- }
57
- }
58
-
59
- this.index = 2
60
- this.factor = new Fraction(value)
61
- this.radical.one()
62
-
63
- return this
64
- }
65
-
66
- clone(): Root {
67
- return new Root().from(this.index, this.radical, this.factor)
68
- }
69
-
70
- get tex(): string {
71
- // b \sqrt{}
72
- // b
73
- // \sqrt{}
74
- // -\sqrt{}
75
-
76
- // Force the plus sign.
77
- const plus = this.#withSign && this.factor.isPositive() ? '+' : ''
78
- const space = (plus && this.#factor.numerator===1) ? ' ' : ''
79
-
80
- if (this.value === 0) return `${plus}0`
81
-
82
- const den = this.#makeTeXLine(this.#factor.denominator, this.#radical.denominator)
83
- const num = this.#makeTeXLine(this.#factor.numerator, this.#radical.numerator) ?? '1'
84
-
85
- if (den === null) return `${plus}${space}${num}`
86
-
87
- return `${plus}${space}\\frac{ ${num} }{ ${den} }`
88
- }
89
-
90
- get display(): string {
91
- // force the '+' sign if asked
92
- const plus = this.#withSign && this.factor.isPositive() ? '+' : ''
93
-
94
- if (this.value === 0) return `${plus}0`
95
-
96
- const den = this.#makeDisplayLine(this.#factor.denominator, this.#radical.denominator, true)
97
- const num = this.#makeDisplayLine(this.#factor.numerator, this.#radical.numerator, den !== null) ?? '1'
98
-
99
- if (den === null) return `${plus}${num}`
100
-
101
- return `${plus}${num}/${den}`
102
- }
103
-
104
- add(value: InputValue<Root>): this {
105
- // start by reducing
106
- this.reduce()
107
- const rt = new Root(value).reduce()
108
-
109
- if (
110
- this.index !== rt.index ||
111
- !this.radical.isEqual(rt.radical)
112
- ) {
113
- throw new MathError("Add can only be done with two same index and radical")
114
- }
115
-
116
- this.factor.add(rt.factor)
117
-
118
- return this
119
- }
120
-
121
- divide(value: InputValue<Root>): this {
122
- return this.multiply(new Root(value).inverse())
123
- }
124
-
125
- get factor(): Fraction {
126
- return this.#factor
127
- }
128
-
129
- set factor(value: Fraction) {
130
- this.#factor = value
131
- }
132
-
133
- from(index: number, radical: InputValue<Fraction>, factor?: InputValue<Fraction>): this {
134
- // set the index
135
- this.index = index
136
-
137
- // set the radical
138
- this.radical = new Fraction(radical)
139
-
140
- // set the factor if any
141
- this.factor = factor ? new Fraction(factor) : new Fraction().one()
142
-
143
- return this
144
- }
145
-
146
- /**
147
- * convert to root(index)(radical), without factor
148
- */
149
- group(): this {
150
- this.radical.multiply(this.factor.pow(this.index))
151
- this.factor.one()
152
-
153
- return this
154
- }
155
-
156
- get index(): number {
157
- return this.#index
158
- }
159
-
160
- set index(value: number) {
161
- if (!Number.isSafeInteger(value) || value <= 0) {
162
- throw new InvalidArgumentError("Index must be a strictly positive integer.")
163
- }
164
- this.#index = value
165
- }
166
-
167
- get indexAsPow(): Fraction {
168
- return new Fraction(this.index).inverse()
169
- }
170
-
171
- inverse(): this {
172
- this.factor.inverse()
173
- this.radical.inverse()
174
- return this
175
- }
176
-
177
- isEqual(root: Root): boolean {
178
- return this.value === root.value
179
- }
180
-
181
- isOne(): boolean {
182
- return this.factor.isOne() && this.radical.isOne()
183
- }
184
-
185
- isRational(): boolean {
186
- const reduced = this.clone().reduce()
187
-
188
- if (reduced.radical.isOne()) return reduced.factor.exact
189
-
190
- if (reduced.index === 1) return reduced.factor.exact && reduced.radical.exact
191
-
192
- return false
193
- }
194
-
195
- isZero(): boolean {
196
- return this.factor.isZero() || this.radical.isZero()
197
- }
198
-
199
- multiply(value: InputValue<Root>): this {
200
- const rt = new Root(value)
201
-
202
- this.factor.multiply(rt.factor)
203
-
204
- if (this.index === rt.index) {
205
- this.radical.multiply(rt.radical)
206
- return this
207
- }
208
-
209
- if (this.radical.isEqual(rt.radical)) {
210
- const F = this.indexAsPow.add(rt.indexAsPow).reduce()
211
- this.index = F.denominator
212
- this.radical = this.radical.pow(F.numerator)
213
- return this
214
- }
215
-
216
- throw new MathError('Multiply can only be done if radical or index as equals.')
217
- }
218
-
219
- one(): this {
220
- this.radical.one()
221
- this.factor.one()
222
-
223
- return this
224
- }
225
-
226
- opposite(): this {
227
- this.factor.opposite()
228
- return this
229
- }
230
-
231
- pow(value: number): this {
232
- this.factor.pow(value)
233
-
234
- const g = Numeric.gcd(this.index, value)
235
- this.index = this.index / g
236
- this.radical.pow(value / g)
237
-
238
- return this
239
- }
240
-
241
- get radical(): Fraction {
242
- return this.#radical
243
- }
244
-
245
- set radical(value: Fraction) {
246
- this.#radical = value
247
- }
248
-
249
- reduce(): this {
250
- // 1. extract roots
251
- // 2. avoid roots at denominator.
252
-
253
- // Remove the roots at denominator
254
- if (this.radical.isRational()) {
255
- const den = this.radical.denominator
256
- this.radical.denominator = 1
257
- this.radical.numerator *= den
258
-
259
- this.factor.divide(den)
260
- }
261
-
262
- // Extract the greatest root from.
263
- const greatest = Numeric.greatestPower(this.radical.value, this.index)
264
-
265
- this.factor.multiply(Math.pow(greatest, 1 / this.index))
266
- this.radical.divide(greatest)
267
-
268
- return this
269
- }
270
-
271
- root(value: number): this {
272
- this.group()
273
- this.index = this.index * value
274
- return this
275
- }
276
-
277
- sqrt(): this {
278
- return this.root(2)
279
- }
280
-
281
- subtract(value: InputValue<Root>): this {
282
- const rt = new Root(value)
283
-
284
- return this.add(rt.opposite())
285
- }
286
-
287
- public get value(): number {
288
- return Numeric.numberCorrection(this.factor.value * Math.pow(this.radical.value, 1 / this.index))
289
- }
290
-
291
- get withSign(): this {
292
- this.#withSign = true
293
- return this
294
- }
295
-
296
- get withoutSign(): this {
297
- this.#withSign = false
298
- return this
299
- }
300
-
301
- zero(): this {
302
- this.radical.zero()
303
- this.factor.zero()
304
- return this
305
- }
306
-
307
- #makeDisplayLine(a: number, b: number, wrap: boolean): string | null {
308
- const rad = this.#index === 2 ? `sqrt` : `root(${this.#index})`
309
-
310
- const formatted: string[] = [
311
- (a === 1 || a === 0) ? null : a===-1 ? '-' : a.toString(),
312
- (b !== 1 && b !== 0) ? `${rad}(${b})` : null,
313
- ].filter(x => x !== null)
314
-
315
- if (formatted.length === 0) return null
316
-
317
- if (wrap && formatted.length===2) return `(${formatted.join("")})`
318
-
319
- return formatted.join("")
320
- }
321
-
322
- #makeTeXLine(a: number, b: number): string | null {
323
- const rad = this.#index === 2 ? `\\sqrt` : `\\sqrt[ ${this.#index} ]`
324
-
325
- const formatted: string[] = [
326
- (a === 1 || a === 0) ? null : a===-1 ? '-' : a.toString(),
327
- (b !== 1 && b !== 0) ? `${rad}{ ${b} }` : null,
328
- ].filter(x => x !== null)
329
-
330
- if (formatted.length === 0) return null
331
-
332
- return formatted.join(" ")
333
- }
334
-
335
- #parse_root(value: string): this {
336
- // Format: [factor]root([index])[radical] or [factor]root([index])([radical])
337
- const match = /^(.*?)root\((\d+)\)\(?([^)]+)/.exec(value)
338
- if (!match) throw new ParseError(`Invalid root format: "${value}"`)
339
-
340
- const [, factor, index, radical] = match
341
- this.index = +index
342
- this.radical = new Fraction(radical)
343
- this.factor = factor === '' ? new Fraction().one() : new Fraction(factor.trim())
344
-
345
- return this
346
- }
347
-
348
- #parse_sqrt(value: string): this {
349
- // value = asqrtb or a sqrt(b)
350
- const [factor, radical] = value.split('sqrt')
351
-
352
- this.index = 2
353
-
354
- this.radical = new Fraction(stripParenthesis(radical))
355
-
356
- this.factor = factor === '' ? new Fraction().one() : new Fraction(factor)
357
-
358
- return this
359
- }
1
+ import {Fraction} from "./fraction"
2
+ import type {IExpression, InputValue, IPiMathObject} from "../pimath.interface"
3
+ import {Numeric} from "../numeric"
4
+ import {InvalidArgumentError, MathError, ParseError} from "../errors"
5
+
6
+ export class Root implements IPiMathObject<Root>, IExpression<Root> {
7
+ #factor: Fraction
8
+ #index: number
9
+ #radical: Fraction
10
+ #withSign = false
11
+
12
+ constructor(value?: InputValue<Root | Fraction>) {
13
+ this.#index = 2
14
+ this.#factor = new Fraction().zero()
15
+ this.#radical = new Fraction().zero()
16
+
17
+ // Test `!== undefined`, not truthiness: 0 and "" are legitimate inputs,
18
+ // and a NaN must be rejected by parse (boundary strictness), not silently
19
+ // swallowed into a zero root.
20
+ if (value !== undefined) {
21
+ this.parse(value)
22
+ }
23
+
24
+
25
+ return this
26
+ }
27
+
28
+ // Router: inspect the input type and delegate to the matching builder.
29
+ // A same-type (Root) input goes through copy(); every other form has its
30
+ // own from*** / #fromScalar. Construction logic is never inlined here.
31
+ parse(value: InputValue<Root | Fraction>): this {
32
+ // should be able to parse "display" roots.
33
+ // sqrt12 or sqrt(12) or sqrt(12/5) => [2, 12 or 12/5, 1]
34
+ // root(n)10 or root(n)(10) or root(n)(10/3) => [n, 10 or 10/3, 1]
35
+ // 7sqrt12 or 7sqrt(12) or 7sqrt(12/5) => [2, 12 or 12/5, 7]
36
+ // 7root(n)10 or 7root(n)(10) or 7root(n)(10/3) => [n, 10 or 10/3, 7]
37
+
38
+ if (value instanceof Root) {
39
+ return this.copy(value)
40
+ }
41
+
42
+ if (value instanceof Fraction) {
43
+ return this.fromFraction(value)
44
+ }
45
+
46
+ if (typeof value === "string") {
47
+ return this.fromString(value)
48
+ }
49
+
50
+ // Remaining scalar (number): build the factor at Root's boundary.
51
+ return this.#fromScalar(value)
52
+ }
53
+
54
+ // Copy the mathematical state of another Root (index + radical + factor).
55
+ // Like Fraction.copy, it deliberately does NOT transfer the display state
56
+ // (#withSign): parse() routes a Root input here, so the receiver keeps its
57
+ // own formatting. Use clone() for a full duplicate.
58
+ copy(value: Root): this {
59
+ this.index = value.index
60
+ this.radical = value.radical.clone()
61
+ this.factor = value.factor.clone()
62
+
63
+ return this
64
+ }
65
+
66
+ fromFraction(value: Fraction): this {
67
+ this.index = 2
68
+ this.factor = value.clone()
69
+ this.radical.one()
70
+
71
+ return this
72
+ }
73
+
74
+ fromString(value: string): this {
75
+ if (value.includes('sqrt')) {
76
+ return this.#parse_sqrt(value)
77
+ }
78
+
79
+ if (value.includes('root')) {
80
+ return this.#parse_root(value)
81
+ }
82
+
83
+ // Plain fraction string: no radical, delegate to the scalar builder.
84
+ return this.#fromScalar(value)
85
+ }
86
+
87
+ // Shared scalar builder (number or plain fraction string) → factor·√[2]{1}.
88
+ // Strictness is enforced at Root's boundary: Fraction keeps a permissive
89
+ // NaN state (its invalid()), but a Root refuses to build on an invalid
90
+ // factor, so a malformed input throws instead of yielding a NaN root.
91
+ #fromScalar(value: string | number): this {
92
+ const factor = new Fraction(value)
93
+ if (factor.isNaN()) {
94
+ throw new ParseError(`Cannot parse "${value}" as a root or fraction`)
95
+ }
96
+
97
+ this.index = 2
98
+ this.factor = factor
99
+ this.radical.one()
100
+
101
+ return this
102
+ }
103
+
104
+ clone(): Root {
105
+ const R = new Root().from(this.index, this.radical, this.factor)
106
+ R.#withSign = this.#withSign
107
+ return R
108
+ }
109
+
110
+ get tex(): string {
111
+ // b \sqrt{}
112
+ // b
113
+ // \sqrt{}
114
+ // -\sqrt{}
115
+
116
+ // Force the plus sign.
117
+ const plus = this.#withSign && this.factor.isPositive() ? '+' : ''
118
+ const space = (plus && this.#factor.numerator===1) ? ' ' : ''
119
+
120
+ if (this.isZero()) return `${plus}0`
121
+
122
+ const den = this.#makeTeXLine(this.#factor.denominator, this.#radical.denominator)
123
+ const num = this.#makeTeXLine(this.#factor.numerator, this.#radical.numerator) ?? '1'
124
+
125
+ if (den === null) return `${plus}${space}${num}`
126
+
127
+ return `${plus}${space}\\frac{ ${num} }{ ${den} }`
128
+ }
129
+
130
+ get display(): string {
131
+ // force the '+' sign if asked
132
+ const plus = this.#withSign && this.factor.isPositive() ? '+' : ''
133
+
134
+ if (this.isZero()) return `${plus}0`
135
+
136
+ const den = this.#makeDisplayLine(this.#factor.denominator, this.#radical.denominator, true)
137
+ const num = this.#makeDisplayLine(this.#factor.numerator, this.#radical.numerator, den !== null) ?? '1'
138
+
139
+ if (den === null) return `${plus}${num}`
140
+
141
+ return `${plus}${num}/${den}`
142
+ }
143
+
144
+ add(value: InputValue<Root>): this {
145
+ return this.#addSub(value, false)
146
+ }
147
+
148
+ divide(value: InputValue<Root>): this {
149
+ return this.multiply(new Root(value).inverse())
150
+ }
151
+
152
+ get factor(): Fraction {
153
+ return this.#factor
154
+ }
155
+
156
+ set factor(value: Fraction) {
157
+ this.#factor = value
158
+ }
159
+
160
+ from(index: number, radical: InputValue<Fraction>, factor?: InputValue<Fraction>): this {
161
+ // set the index (validates: strictly positive integer)
162
+ this.index = index
163
+
164
+ // set the radical; reject an invalid (NaN) primitive at Root's boundary (A6).
165
+ const rad = new Fraction(radical)
166
+ if (rad.isNaN()) {
167
+ throw new ParseError(`Cannot build a root from an invalid radical: "${radical}"`)
168
+ }
169
+ this.radical = rad
170
+
171
+ // set the factor if provided; 0 is a valid factor, only `undefined` means omitted.
172
+ if (factor === undefined) {
173
+ this.factor = new Fraction().one()
174
+ } else {
175
+ const f = new Fraction(factor)
176
+ if (f.isNaN()) {
177
+ throw new ParseError(`Cannot build a root from an invalid factor: "${factor}"`)
178
+ }
179
+ this.factor = f
180
+ }
181
+
182
+ return this
183
+ }
184
+
185
+ /**
186
+ * convert to root(index)(radical), without factor
187
+ */
188
+ group(): this {
189
+ this.radical.multiply(this.factor.pow(this.index))
190
+ this.factor.one()
191
+
192
+ return this
193
+ }
194
+
195
+ get index(): number {
196
+ return this.#index
197
+ }
198
+
199
+ set index(value: number) {
200
+ if (!Number.isSafeInteger(value) || value <= 0) {
201
+ throw new InvalidArgumentError("Index must be a strictly positive integer.")
202
+ }
203
+ this.#index = value
204
+ }
205
+
206
+ get indexAsPow(): Fraction {
207
+ return new Fraction(this.index).inverse()
208
+ }
209
+
210
+ inverse(): this {
211
+ this.factor.inverse()
212
+ this.radical.inverse()
213
+ return this
214
+ }
215
+
216
+ isEqual(root: Root): boolean {
217
+ if (this.isZero() && root.isZero()) {
218
+ return true
219
+ }
220
+
221
+ // Even roots of a negative radical are not real; they compare to nothing.
222
+ if (!this.#isReal() || !root.#isReal()) {
223
+ return false
224
+ }
225
+
226
+ if (this.#valueSign() !== root.#valueSign()) {
227
+ return false
228
+ }
229
+
230
+ // Compare |value| exactly by raising to a common multiple L of both
231
+ // indices: (|f| * |radical|^(1/i))^L = |f|^L * |radical|^(L/i), an exact
232
+ // Fraction (no floating point). The radical sign is handled above.
233
+ const L = Numeric.lcm(this.index, root.index)
234
+
235
+ const a = this.factor.clone().abs().pow(L)
236
+ .multiply(this.radical.clone().abs().pow(L / this.index))
237
+ const b = root.factor.clone().abs().pow(L)
238
+ .multiply(root.radical.clone().abs().pow(L / root.index))
239
+
240
+ return a.isEqual(b)
241
+ }
242
+
243
+ #isReal(): boolean {
244
+ return !(this.radical.isNegative() && this.index % 2 === 0)
245
+ }
246
+
247
+ #valueSign(): 1 | -1 {
248
+ const radicalSign = this.index % 2 === 1 ? this.radical.sign() : 1
249
+ return (this.factor.sign() * radicalSign) as 1 | -1
250
+ }
251
+
252
+ isOne(): boolean {
253
+ return this.factor.isOne() && this.radical.isOne()
254
+ }
255
+
256
+ isRational(): boolean {
257
+ const reduced = this.clone().reduce()
258
+
259
+ if (reduced.radical.isOne()) return reduced.factor.exact
260
+
261
+ if (reduced.index === 1) return reduced.factor.exact && reduced.radical.exact
262
+
263
+ return false
264
+ }
265
+
266
+ isZero(): boolean {
267
+ return this.factor.isZero() || this.radical.isZero()
268
+ }
269
+
270
+ multiply(value: InputValue<Root>): this {
271
+ // Compute on a clone and commit only on success, so a failed multiply
272
+ // leaves the receiver untouched (same atomic discipline as add).
273
+ const a = this.clone()
274
+ const rt = new Root(value)
275
+
276
+ if (a.index !== rt.index && !a.radical.isEqual(rt.radical)) {
277
+ throw new MathError('Multiply can only be done if radical or index as equals.')
278
+ }
279
+
280
+ a.factor.multiply(rt.factor)
281
+
282
+ if (a.index === rt.index) {
283
+ a.radical.multiply(rt.radical)
284
+ } else {
285
+ // same radical, different index: combine the fractional exponents.
286
+ const F = a.indexAsPow.add(rt.indexAsPow).reduce()
287
+ a.index = F.denominator
288
+ a.radical = a.radical.pow(F.numerator)
289
+ }
290
+
291
+ return this.parse(a)
292
+ }
293
+
294
+ one(): this {
295
+ this.radical.one()
296
+ this.factor.one()
297
+
298
+ return this
299
+ }
300
+
301
+ opposite(): this {
302
+ this.factor.opposite()
303
+ return this
304
+ }
305
+
306
+ pow(value: number): this {
307
+ this.factor.pow(value)
308
+
309
+ const g = Numeric.gcd(this.index, value)
310
+ this.index = this.index / g
311
+ this.radical.pow(value / g)
312
+
313
+ return this
314
+ }
315
+
316
+ get radical(): Fraction {
317
+ return this.#radical
318
+ }
319
+
320
+ set radical(value: Fraction) {
321
+ this.#radical = value
322
+ }
323
+
324
+ reduce(): this {
325
+ // 1. extract roots
326
+ // 2. avoid roots at denominator.
327
+
328
+ // Remove the root from the denominator:
329
+ // √[n]{num/den} = √[n]{num · den^(n-1)} / den
330
+ if (this.radical.hasFractionalPart()) {
331
+ const num = this.radical.numerator
332
+ const den = this.radical.denominator
333
+
334
+ this.radical = new Fraction(num * den ** (this.index - 1))
335
+ this.factor.divide(den)
336
+ }
337
+
338
+ // Extract the greatest root from.
339
+ const greatest = Numeric.greatestPower(this.radical.value, this.index)
340
+
341
+ this.factor.multiply(Numeric.nthRoot(greatest, this.index))
342
+ this.radical.divide(greatest)
343
+
344
+ return this
345
+ }
346
+
347
+ root(value: number): this {
348
+ this.group()
349
+ this.index = this.index * value
350
+ return this
351
+ }
352
+
353
+ sqrt(): this {
354
+ return this.root(2)
355
+ }
356
+
357
+ subtract(value: InputValue<Root>): this {
358
+ return this.#addSub(value, true)
359
+ }
360
+
361
+ // Shared, atomic add/subtract: reduce both operands to a common radical,
362
+ // then combine the factors. subtract no longer routes through
363
+ // add(opposite()), so the error message names both operations instead of
364
+ // leaking "Add". A failed op leaves the receiver untouched (clones + parse).
365
+ #addSub(value: InputValue<Root>, isSubtract: boolean): this {
366
+ const a = this.clone().reduce()
367
+ const b = new Root(value).reduce()
368
+
369
+ if (a.index !== b.index || !a.radical.isEqual(b.radical)) {
370
+ throw new MathError("Roots must share the same index and radical to be added or subtracted")
371
+ }
372
+
373
+ if (isSubtract) {
374
+ a.factor.subtract(b.factor)
375
+ } else {
376
+ a.factor.add(b.factor)
377
+ }
378
+
379
+ return this.parse(a)
380
+ }
381
+
382
+ get value(): number {
383
+ const radical = this.radical.value
384
+
385
+ if (radical < 0) {
386
+ // Even root of a negative is not real.
387
+ if (this.index % 2 === 0) {
388
+ return NaN
389
+ }
390
+ // Odd root of a negative is real and negative.
391
+ return Numeric.numberCorrection(this.factor.value * -Math.pow(-radical, 1 / this.index))
392
+ }
393
+
394
+ return Numeric.numberCorrection(this.factor.value * Math.pow(radical, 1 / this.index))
395
+ }
396
+
397
+ get withSign(): this {
398
+ this.#withSign = true
399
+ return this
400
+ }
401
+
402
+ get withoutSign(): this {
403
+ this.#withSign = false
404
+ return this
405
+ }
406
+
407
+ zero(): this {
408
+ this.radical.zero()
409
+ this.factor.zero()
410
+ return this
411
+ }
412
+
413
+ #makeDisplayLine(a: number, b: number, wrap: boolean): string | null {
414
+ const rad = this.#index === 2 ? `sqrt` : `root(${this.#index})`
415
+ return this.#makeLine(a, b, {radical: v => `${rad}(${v})`, separator: '', wrap})
416
+ }
417
+
418
+ #makeTeXLine(a: number, b: number): string | null {
419
+ const rad = this.#index === 2 ? `\\sqrt` : `\\sqrt[ ${this.#index} ]`
420
+ return this.#makeLine(a, b, {radical: v => `${rad}{ ${v} }`, separator: ' '})
421
+ }
422
+
423
+ // Shared skeleton for both display and TeX lines: applies the factor rule,
424
+ // drops empty parts, optionally wraps a two-part result and joins. The
425
+ // caller supplies the radical formatting and separator, so this core stays
426
+ // agnostic of the output format.
427
+ #makeLine(a: number, b: number, options: {
428
+ radical: (radicand: number) => string
429
+ separator: string
430
+ wrap?: boolean
431
+ }): string | null {
432
+ const factorPart = (a === 1 || a === 0) ? null : a === -1 ? '-' : a.toString()
433
+ const radicalPart = (b !== 1 && b !== 0) ? options.radical(b) : null
434
+
435
+ const formatted: string[] = [factorPart, radicalPart].filter(x => x !== null)
436
+
437
+ if (formatted.length === 0) return null
438
+
439
+ if (options.wrap && formatted.length === 2) return `(${formatted.join(options.separator)})`
440
+
441
+ return formatted.join(options.separator)
442
+ }
443
+
444
+ #parse_root(value: string): this {
445
+ // Format: [factor]root([index])[radical] or [factor]root([index])([radical])
446
+ const match = /^(.*?)root\((\d+)\)\(?([^)]+)/.exec(value)
447
+ if (!match) throw new ParseError(`Invalid root format: "${value}"`)
448
+
449
+ const [, factor, index, radical] = match
450
+ this.index = +index
451
+ this.radical = new Fraction(radical)
452
+ this.factor = factor === '' ? new Fraction().one() : new Fraction(factor.trim())
453
+
454
+ return this
455
+ }
456
+
457
+ #parse_sqrt(value: string): this {
458
+ // sqrt binds the immediately following integer; a fractional radical must
459
+ // be parenthesised. Leading N or N/D and a trailing /D are ordinary factor
460
+ // arithmetic (operator precedence): 12sqrt2/7 == 12/7sqrt(2) == (12/7)√2.
461
+
462
+ // Round-trip of display's fractional output: (Nsqrt(R))/D
463
+ const wrapped = /^\((.+)\)\/(\d+)$/.exec(value)
464
+ if (wrapped) {
465
+ this.#parse_sqrt(wrapped[1])
466
+ this.factor.divide(+wrapped[2])
467
+ return this
468
+ }
469
+
470
+ const re = /^([+-]?\d*(?:\/\d+)?)sqrt(?:\((-?\d+(?:\/\d+)?)\)|(\d+))(?:\/(\d+))?$/
471
+ const match = re.exec(value)
472
+ if (!match) throw new ParseError(`Invalid sqrt format: "${value}"`)
473
+
474
+ // Alternation / optional groups are undefined at runtime; `.at()` is typed
475
+ // `string | undefined`, unlike indexed access which TS types as `string`.
476
+ const rawFactor = match[1]
477
+ const radicalParen = match.at(2)
478
+ const radicalBare = match.at(3)
479
+ const denominator = match.at(4)
480
+
481
+ this.index = 2
482
+ this.radical = new Fraction(radicalParen ?? radicalBare ?? '')
483
+
484
+ let factor: Fraction
485
+ if (rawFactor === '' || rawFactor === '+' || rawFactor === '-') {
486
+ factor = new Fraction(rawFactor === '-' ? -1 : 1)
487
+ } else if (/^[+-]?\d+(?:\/\d+)?$/.test(rawFactor)) {
488
+ factor = new Fraction(rawFactor)
489
+ } else {
490
+ throw new ParseError(`Invalid sqrt format: "${value}"`)
491
+ }
492
+
493
+ if (denominator !== undefined) {
494
+ factor.divide(+denominator)
495
+ }
496
+
497
+ this.factor = factor
498
+
499
+ return this
500
+ }
360
501
  }