rcas 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. metadata +206 -0
@@ -0,0 +1,443 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module RCAS
5
+ # Integer arithmetic beyond what Ruby ships with: prime factorization,
6
+ # primality, divisors, Euler's totient, modular inverses and the Chinese
7
+ # remainder theorem.
8
+ #
9
+ # factor(360) # => 2**3*3**2*5
10
+ # isprime(2**61 - 1) # => true
11
+ # divisors(12) # => [1, 2, 3, 4, 6, 12]
12
+ # chrem([2, 3], [3, 5]) # => 8
13
+ #
14
+ # Ruby's Integer already provides gcd, lcm, pow(e, m), Integer.sqrt and
15
+ # bit_length, and Integer#prime? for numbers below about 3e24. Above that
16
+ # its prime? falls back to trial division, so primality here is a
17
+ # Miller-Rabin test (deterministic below 3.3e24, probabilistic with 24
18
+ # bases beyond) and factoring uses trial division by small primes
19
+ # followed by Pollard-Brent rho.
20
+ #
21
+ # Sources (keys refer to MANUAL.md, section Sources): Miller-Rabin
22
+ # [Mil76], [Rab80b], [Knu98, §4.5.4, Algorithm P]; the 13-base bound
23
+ # 3.3e24 [SW17]; Pollard rho [Pol75] with Brent's cycle finding [Bre80],
24
+ # [Knu98, §4.5.4, Algorithm B]; Chinese remainder theorem and modular
25
+ # inverse [Coh93, §1.3], [Knu98, §4.3.2, §4.5.2]; totient [HW08, §5.5].
26
+ module NumberTheory
27
+ module_function
28
+
29
+ # The primes below 1000, by sieve: enough to divide out the small
30
+ # factors before Miller-Rabin and rho take over. (`require "prime"` is
31
+ # trial division all the way up and cost 27 ms of the load.)
32
+ SMALL_PRIMES = begin
33
+ sieve = Array.new(1000, true)
34
+ sieve[0] = sieve[1] = false
35
+ (2..31).each { |i| (i * i).step(999, i) { |j| sieve[j] = false } if sieve[i] }
36
+ sieve.each_index.select { |i| sieve[i] }.freeze
37
+ end
38
+
39
+ # Above this many bits a composite cofactor is left alone when the
40
+ # caller did not ask for a factorization in the first place.
41
+ EASY_BITS = 64
42
+
43
+ # Bases that make Miller-Rabin deterministic below 3 317 044 064 679 887 385 961 981.
44
+ DETERMINISTIC_BASES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41].freeze
45
+ DETERMINISTIC_LIMIT = 3_317_044_064_679_887_385_961_981
46
+
47
+ # ---- factorization ----------------------------------------------------
48
+
49
+ # Prime factorization of an integer or rational as an IntegerFactorization.
50
+ def factor(n)
51
+ n = integer_or_rational(n, "factor")
52
+ raise ArgumentError, "factor: 0 has no prime factorization" if n.zero?
53
+ unit = n.negative? ? -1 : 1
54
+ if n.is_a?(Rational)
55
+ num = prime_division(n.numerator.abs)
56
+ den = prime_division(n.denominator).map { |p, e| [p, -e] }
57
+ IntegerFactorization.new(unit, (num + den).sort)
58
+ else
59
+ IntegerFactorization.new(unit, prime_division(n.abs))
60
+ end
61
+ end
62
+
63
+ # The primes in ascending order, without end: what `Prime.each` was.
64
+ def each_prime
65
+ SMALL_PRIMES.each { |p| yield p }
66
+ p = SMALL_PRIMES.last
67
+ loop { yield(p = nextprime(p)) }
68
+ end
69
+
70
+ # [[prime, multiplicity], ...] in ascending order; [] for 1.
71
+ # With hard: false a large composite cofactor is not pursued and the
72
+ # answer is nil instead: that is for callers that are only tidying a
73
+ # number they were handed - sqrt(n), log(n) - and must not disappear
74
+ # into a factorization nobody asked for.
75
+ def prime_division(n, hard: true)
76
+ raise ArgumentError, "prime_division: expected a positive integer, got #{n.inspect}" unless n.is_a?(Integer) && n.positive?
77
+ counts = Hash.new(0)
78
+ SMALL_PRIMES.each do |p|
79
+ break if p * p > n
80
+ while (n % p).zero?
81
+ counts[p] += 1
82
+ n /= p
83
+ end
84
+ end
85
+ if n > 1
86
+ if n < SMALL_PRIMES.last**2 || prime?(n)
87
+ counts[n] += 1
88
+ else
89
+ return nil if !hard && n.bit_length > EASY_BITS
90
+ split_composite(n, counts)
91
+ end
92
+ end
93
+ counts.sort
94
+ end
95
+
96
+ def split_composite(n, counts)
97
+ stack = [n]
98
+ until stack.empty?
99
+ m = stack.pop
100
+ if prime?(m)
101
+ counts[m] += 1
102
+ elsif (r = Integer.sqrt(m)) && r * r == m
103
+ stack.push(r, r)
104
+ else
105
+ d = rho(m)
106
+ stack.push(d, m / d)
107
+ end
108
+ end
109
+ end
110
+
111
+ # Pollard-Brent rho: a nontrivial factor of a composite n.
112
+ def rho(n)
113
+ return 2 if n.even?
114
+ c = 1
115
+ loop do
116
+ y = 2
117
+ m = 128
118
+ g = r = q = 1
119
+ x = ys = y
120
+ while g == 1
121
+ x = y
122
+ r.times { y = (y * y + c) % n }
123
+ k = 0
124
+ while k < r && g == 1
125
+ ys = y
126
+ [m, r - k].min.times do
127
+ y = (y * y + c) % n
128
+ q = q * (x - y).abs % n
129
+ end
130
+ g = q.gcd(n)
131
+ k += m
132
+ end
133
+ r *= 2
134
+ end
135
+ if g == n
136
+ loop do
137
+ ys = (ys * ys + c) % n
138
+ g = (x - ys).abs.gcd(n)
139
+ break if g > 1
140
+ end
141
+ end
142
+ return g if g != n
143
+ c += 1
144
+ end
145
+ end
146
+
147
+ # ---- primes -----------------------------------------------------------
148
+
149
+ # Miller-Rabin; exact below DETERMINISTIC_LIMIT, a strong probable-prime
150
+ # test with 24 bases above it.
151
+ def prime?(n)
152
+ n = integer_or_rational(n, "isprime")
153
+ return false unless n.is_a?(Integer) && n >= 2
154
+ SMALL_PRIMES.each do |p|
155
+ return n == p if (n % p).zero?
156
+ end
157
+ return true if n < SMALL_PRIMES.last**2
158
+ bases = n < DETERMINISTIC_LIMIT ? DETERMINISTIC_BASES : SMALL_PRIMES.first(24)
159
+ d = n - 1
160
+ s = 0
161
+ while d.even?
162
+ d >>= 1
163
+ s += 1
164
+ end
165
+ bases.all? { |a| strong_probable_prime?(n, a, d, s) }
166
+ end
167
+
168
+ def strong_probable_prime?(n, a, d, s)
169
+ x = a.pow(d, n)
170
+ return true if x == 1 || x == n - 1
171
+ (s - 1).times do
172
+ x = x.pow(2, n)
173
+ return true if x == n - 1
174
+ end
175
+ false
176
+ end
177
+
178
+ # The smallest prime greater than n.
179
+ def nextprime(n)
180
+ n = integer_or_rational(n, "nextprime").floor
181
+ return 2 if n < 2
182
+ candidate = n.even? ? n + 1 : n + 2
183
+ candidate += 2 until prime?(candidate)
184
+ candidate
185
+ end
186
+
187
+ # The largest prime smaller than n.
188
+ def prevprime(n)
189
+ n = integer_or_rational(n, "prevprime").ceil
190
+ raise ArgumentError, "prevprime: there is no prime below #{n}" if n <= 2
191
+ return 2 if n == 3
192
+ candidate = n.even? ? n - 1 : n - 2
193
+ candidate -= 2 until prime?(candidate)
194
+ candidate
195
+ end
196
+
197
+ # ---- divisors and multiplicative functions ----------------------------
198
+
199
+ # All positive divisors of n in ascending order.
200
+ def divisors(n)
201
+ n = integer_or_rational(n, "divisors")
202
+ raise ArgumentError, "divisors: expected a nonzero integer, got #{n.inspect}" unless n.is_a?(Integer) && !n.zero?
203
+ prime_division(n.abs).reduce([1]) do |list, (p, e)|
204
+ list.flat_map { |d| (0..e).map { |k| d * p**k } }
205
+ end.sort
206
+ end
207
+
208
+ # Euler's totient: the number of integers in 1..n coprime to n.
209
+ def totient(n)
210
+ n = integer_or_rational(n, "totient")
211
+ raise ArgumentError, "totient: expected a positive integer, got #{n.inspect}" unless n.is_a?(Integer) && n.positive?
212
+ prime_division(n).reduce(1) { |acc, (p, e)| acc * p**(e - 1) * (p - 1) }
213
+ end
214
+
215
+ # ---- modular arithmetic -----------------------------------------------
216
+
217
+ # The inverse of a modulo m, in 0...m.
218
+ def invmod(a, m)
219
+ raise ArgumentError, "invmod: the modulus must be a positive integer" unless m.is_a?(Integer) && m.positive?
220
+ a = a % m
221
+ g, x, = extended_gcd(a, m)
222
+ raise ZeroDivisionError, "#{a} has no inverse modulo #{m} (gcd #{g})" unless g == 1
223
+ x % m
224
+ end
225
+
226
+ # [g, s, t] with s*a + t*b = g = gcd(a, b).
227
+ def extended_gcd(a, b)
228
+ s0, s1, t0, t1 = 1, 0, 0, 1
229
+ until b.zero?
230
+ q, r = a.divmod(b)
231
+ a, b = b, r
232
+ s0, s1 = s1, s0 - q * s1
233
+ t0, t1 = t1, t0 - q * t1
234
+ end
235
+ a.negative? ? [-a, -s0, -t0] : [a, s0, t0]
236
+ end
237
+
238
+ # Chinese remainder theorem: the smallest x >= 0 with x = r_i (mod m_i)
239
+ # for every i. Moduli need not be coprime; an inconsistent system raises.
240
+ # Solutions of f(x) = 0 mod m, as the residues in 0...m. A linear
241
+ # congruence goes through the extended Euclidean algorithm, anything else
242
+ # is tested residue by residue, which is what a first course does.
243
+ def congruence(f, x, m)
244
+ m = integer_or_rational(m, "congruence").to_i
245
+ raise ArgumentError, "congruence: the modulus must be positive" unless m.positive?
246
+ x = Expression.lift(x)
247
+ coefficients = Solve.polynomial_coefficients(Solve.to_zero(f).simplify, x)
248
+ raise ArgumentError, "congruence: #{f} is not a polynomial in #{x}" if coefficients.nil?
249
+ values = coefficients.map { |c| integer_or_rational(c, "congruence").to_i }
250
+ return linear_congruence(values, m) if values.size == 2
251
+ raise ArgumentError, "congruence: the modulus #{m} is too large to search" if m > 100_000
252
+ (0...m).select { |r| horner(values, r, m).zero? }
253
+ end
254
+
255
+ def horner(coefficients, r, m) = coefficients.reverse.reduce(0) { |acc, c| (acc * r + c) % m }
256
+
257
+ # a*x + b = 0 mod m has gcd(a, m) solutions when that gcd divides b.
258
+ def linear_congruence(coefficients, m)
259
+ b, a = coefficients
260
+ g = a.gcd(m)
261
+ return [] unless (-b % m % g).zero?
262
+ step = m / g
263
+ first = (invmod(a / g, step) * (-b / g)) % step
264
+ (0...g).map { |i| (first + i * step) % m }
265
+ end
266
+
267
+ # The Legendre symbol (a/p): 0, 1 when a is a square modulo the odd prime
268
+ # p, and -1 when it is not (Euler's criterion).
269
+ def legendre(a, p)
270
+ a = integer_or_rational(a, "legendre").to_i
271
+ p = integer_or_rational(p, "legendre").to_i
272
+ raise ArgumentError, "legendre: p must be an odd prime" unless p > 2 && prime?(p)
273
+ value = a.pow((p - 1) / 2, p)
274
+ value > 1 ? value - p : value
275
+ end
276
+
277
+ # The Jacobi symbol, the Legendre symbol extended to odd composite n.
278
+ def jacobi(a, n)
279
+ a = integer_or_rational(a, "jacobi").to_i
280
+ n = integer_or_rational(n, "jacobi").to_i
281
+ raise ArgumentError, "jacobi: n must be positive and odd" unless n.positive? && n.odd?
282
+ a %= n
283
+ result = 1
284
+ while a != 0
285
+ while a.even?
286
+ a /= 2
287
+ result = -result if [3, 5].include?(n % 8)
288
+ end
289
+ a, n = n, a
290
+ result = -result if a % 4 == 3 && n % 4 == 3
291
+ a %= n
292
+ end
293
+ n == 1 ? result : 0
294
+ end
295
+
296
+ # The multiplicative order of a modulo m: the least k > 0 with a**k = 1.
297
+ def order(a, m)
298
+ a = integer_or_rational(a, "order").to_i
299
+ m = integer_or_rational(m, "order").to_i
300
+ raise ArgumentError, "order: a and m must be coprime" unless a.gcd(m) == 1
301
+ phi = totient(m)
302
+ divisors(phi).find { |d| a.pow(d, m) == 1 }
303
+ end
304
+
305
+ # A generator of the multiplicative group modulo m, when one exists.
306
+ def primitive_root(m)
307
+ m = integer_or_rational(m, "primitive_root").to_i
308
+ phi = totient(m)
309
+ # from 1: the unit group mod 2 is {1}, and 1 generates it (A8)
310
+ candidate = (1...m).find { |a| a.gcd(m) == 1 && order(a, m) == phi }
311
+ raise ArgumentError, "primitive_root: there is none modulo #{m}" if candidate.nil? && m > 1
312
+ m == 1 ? 0 : candidate
313
+ end
314
+
315
+ # The continued fraction [a0; a1, a2, ...] of a rational or a real number.
316
+ def continued_fraction(value, terms = 10)
317
+ value = value.simplify if value.is_a?(Expression)
318
+ value = value.value if value.is_a?(Num)
319
+ return fraction_terms(Rational(value), terms) if value.is_a?(Integer) || value.is_a?(Rational)
320
+ return fraction_terms(value, terms) if value.is_a?(Float)
321
+ certified_terms(Expression.lift(value), terms)
322
+ end
323
+
324
+ def fraction_terms(x, terms)
325
+ out = []
326
+ terms.times do
327
+ whole = x.floor
328
+ out << whole
329
+ rest = x - whole
330
+ break if rest.zero? || (rest.is_a?(Float) && rest.abs < 1e-12)
331
+ x = 1 / rest
332
+ end
333
+ out
334
+ end
335
+
336
+ # An irrational constant's terms from its digits, and only the terms
337
+ # two precisions agree on: a Float gave sqrt(1000001) wrong from the
338
+ # third term and pi from the fourteenth (third review, A6).
339
+ def certified_terms(expr, terms)
340
+ digits = 20 + 8 * terms
341
+ 4.times do
342
+ coarse = fraction_terms(Precision.evalf(expr, digits, certify: false).value.to_r, terms + 2)
343
+ fine = fraction_terms(Precision.evalf(expr, 2 * digits, certify: false).value.to_r, terms + 2)
344
+ common = coarse.zip(fine).take_while { |a, b| a == b }.map(&:first)
345
+ return common.first(terms) if common.size > terms || (common.size == terms && coarse.size == fine.size)
346
+ digits *= 2
347
+ end
348
+ raise ArgumentError, "continued_fraction: #{expr} could not be expanded to #{terms} terms"
349
+ end
350
+
351
+ # The fractions [a0], [a0; a1], ... of a continued fraction, the best
352
+ # rational approximations of the number.
353
+ def convergents(value, terms = 10)
354
+ coefficients = value.is_a?(Array) ? value : continued_fraction(value, terms)
355
+ previous = [1, 0] # numerator, denominator of the fraction before
356
+ current = [coefficients.first, 1]
357
+ out = [Rational(current[0], current[1])]
358
+ coefficients.drop(1).each do |a|
359
+ nxt = [a * current[0] + previous[0], a * current[1] + previous[1]]
360
+ previous = current
361
+ current = nxt
362
+ out << Rational(current[0], current[1])
363
+ end
364
+ out
365
+ end
366
+
367
+ def chrem(residues, moduli)
368
+ raise ArgumentError, "chrem: give as many residues as moduli" unless residues.size == moduli.size && !moduli.empty?
369
+ x = 0
370
+ m = 1
371
+ residues.zip(moduli).each do |r, mod|
372
+ mod = mod.value if mod.is_a?(Num)
373
+ raise ArgumentError, "chrem: the moduli must be positive integers" unless mod.is_a?(Integer) && mod.positive?
374
+ r = residue(r, mod)
375
+ g, s, = extended_gcd(m, mod)
376
+ diff = r - x
377
+ raise ArgumentError, "chrem: no solution, #{x} mod #{m} and #{r} mod #{mod} are incompatible" unless (diff % g).zero?
378
+ lcm = m / g * mod
379
+ x = (x + m * ((diff / g) * s % (mod / g))) % lcm
380
+ m = lcm
381
+ end
382
+ x
383
+ end
384
+
385
+ # An Integer, the rcas number holding one, or an element of GF(mod) -
386
+ # what a determinant over GF(p) is (it was a NoMethodError, 24 Sept 2026).
387
+ def residue(r, mod)
388
+ r = r.value if r.is_a?(Num)
389
+ if r.is_a?(Mod)
390
+ raise ArgumentError, "chrem: #{r.value} is a residue modulo #{r.p}, not modulo #{mod}" unless r.p == mod
391
+ r = r.value
392
+ end
393
+ raise ArgumentError, "chrem: the residues must be integers, got #{r.inspect}" unless r.is_a?(Integer)
394
+ r
395
+ end
396
+
397
+ def integer_or_rational(n, name)
398
+ n = n.value if n.is_a?(Num)
399
+ return n if n.is_a?(Integer) || n.is_a?(Rational)
400
+ raise ArgumentError, "#{name}: expected an integer, got #{n.inspect}"
401
+ end
402
+ end
403
+
404
+ # The result of factoring an integer or rational: a unit (1 or -1) and
405
+ # [prime, exponent] pairs; rationals carry negative exponents.
406
+ class IntegerFactorization
407
+ include Enumerable
408
+
409
+ attr_reader :unit, :factors
410
+
411
+ def initialize(unit, factors)
412
+ @unit = unit
413
+ @factors = factors.freeze
414
+ freeze
415
+ end
416
+
417
+ def each(&block) = factors.each(&block)
418
+ def size = factors.size
419
+ def [](i) = factors[i]
420
+ def prime? = factors.size == 1 && factors.first.last == 1 && unit == 1
421
+ def primes = factors.map(&:first)
422
+
423
+ # Multiply back out: the Integer or Rational.
424
+ def expand
425
+ factors.reduce(unit) { |acc, (p, e)| acc * Rational(p)**e }.then { |v| v.is_a?(Rational) && v.denominator == 1 ? v.numerator : v }
426
+ end
427
+
428
+ def to_expr
429
+ up = factors.select { |_, e| e.positive? }.map { |p, e| Simplify.power_node(Num.new(p), e) }
430
+ down = factors.select { |_, e| e.negative? }.map { |p, e| Simplify.power_node(Num.new(p), -e) }
431
+ body = up.empty? ? Num.new(1) : Simplify.product_node(up)
432
+ body = Div.new(body, Simplify.product_node(down)) unless down.empty?
433
+ unit == -1 ? Neg.new(body) : body
434
+ end
435
+
436
+ def to_s = to_expr.to_s
437
+ alias inspect to_s
438
+
439
+ def to_latex(wrap: nil) = LaTeX.of(to_expr, wrap: wrap)
440
+
441
+ def ==(other) = other.is_a?(IntegerFactorization) && other.unit == unit && other.factors == factors
442
+ end
443
+ end