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
data/lib/rcas/latex.rb ADDED
@@ -0,0 +1,556 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # Renders rcas objects as LaTeX math (no surrounding $ delimiters).
5
+ #
6
+ # ((:x + 1) / (:x - 1)).to_latex # => "\\frac{x + 1}{x - 1}"
7
+ # RCAS::QQ[:x].to_latex # => "\\mathbb{Q}[x]"
8
+ # RCAS::LaTeX.of([1, :x]) # => "\\left[1,\\; x\\right]"
9
+ # long_poly.to_latex(wrap: 60) # => "\\begin{aligned} & ... \\\\ &\\quad {} + ... \\end{aligned}"
10
+ #
11
+ # Structure follows Printer: nothing is rewritten, parentheses are added
12
+ # only where the tree needs them. A few display-only niceties are applied
13
+ # on top: implicit multiplication, \frac for division, \sqrt for exponent
14
+ # 1/2, \sin^{2}(x), e^{x}, \ln, and `a + (-1)` shown as `a - 1`.
15
+ module LaTeX
16
+ ADDITIVE = Printer::ADDITIVE
17
+ MULTIPLICATIVE = Printer::MULTIPLICATIVE
18
+ UNARY = Printer::UNARY
19
+ POWER = Printer::POWER
20
+ ATOM = Printer::ATOM
21
+
22
+ GREEK = %w[
23
+ alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi
24
+ pi rho sigma tau upsilon phi chi psi omega
25
+ Gamma Delta Theta Lambda Xi Pi Sigma Upsilon Phi Psi Omega
26
+ varepsilon vartheta varphi varrho varsigma
27
+ ].freeze
28
+
29
+ FUNCTIONS = {
30
+ sin: '\sin', cos: '\cos', tan: '\tan', atan: '\arctan',
31
+ sinh: '\sinh', cosh: '\cosh', tanh: '\tanh', log: '\ln',
32
+ asin: '\arcsin', acos: '\arccos', gamma: '\Gamma', zeta: '\zeta', arg: '\arg',
33
+ sign: '\operatorname{sgn}', erf: '\operatorname{erf}', erfc: '\operatorname{erfc}'
34
+ }.freeze
35
+
36
+ # Functions with a notation of their own, by the number of arguments.
37
+ BRACKETED = { abs: ['\left|', '\right|'], floor: ['\left\lfloor ', '\right\rfloor'], ceil: ['\left\lceil ', '\right\rceil'] }.freeze
38
+ INDEXED = { bernoulli: "B", fibonacci: "F", harmonic: "H" }.freeze
39
+
40
+ # Functions written as \sin^{2}(x) rather than \left(\sin(x)\right)^{2}.
41
+ POWER_FUNCTIONS = %i[sin cos tan sinh cosh tanh].freeze
42
+
43
+ SETS = { "NN" => '\mathbb{N}', "ZZ" => '\mathbb{Z}', "QQ" => '\mathbb{Q}', "RR" => '\mathbb{R}', "CC" => '\mathbb{C}' }.freeze
44
+
45
+ module_function
46
+
47
+ # LaTeX for anything rcas produces, plus Ruby numbers, symbols, arrays,
48
+ # hashes, booleans, nil and strings. +wrap+ is a line width in roughly
49
+ # typeset characters; long sums, products and arrays are broken into an
50
+ # aligned block that fits it (see #wrapped).
51
+ def of(obj, wrap: nil)
52
+ case obj
53
+ when Expression, Polynomial, Factorization
54
+ obj.to_latex(wrap: wrap)
55
+ when ->(o) { defined?(Equation) && o.is_a?(Equation) }
56
+ obj.to_latex(wrap: wrap)
57
+ when Domain, Vector, Matrix, VectorSpace, MatrixSpace
58
+ obj.to_latex
59
+ when Symbol then print(Var.new(obj))
60
+ when Numeric then print(Num.new(obj))
61
+ when Array then array(obj, wrap: wrap)
62
+ when Hash then table(obj)
63
+ when true, false, nil then "\\mathrm{#{obj.inspect}}"
64
+ when String then text(obj)
65
+ else
66
+ obj.respond_to?(:to_latex) ? obj.to_latex : text(obj.to_s)
67
+ end
68
+ end
69
+
70
+ # Elements side by side, or one per line when that would be too wide.
71
+ def array(list, wrap: nil)
72
+ return '\left[\,\right]' if list.empty?
73
+ items = list.map { |e| of(e, wrap: wrap) }
74
+ joined = "\\left[#{items.join(',\; ')}\\right]"
75
+ return joined if wrap.nil? || width(joined) <= wrap || items.size == 1
76
+ rows = items.each_with_index.map { |item, i| "& #{item}#{i == items.size - 1 ? '' : ','}" }
77
+ "\\left[\\begin{aligned} #{rows.join(' \\\\ ')} \\end{aligned}\\right]"
78
+ end
79
+
80
+ # { x: ZZ } reads as a membership list, anything else as a mapping.
81
+ # Not `hash`: that would replace Module#hash on the module object, and
82
+ # then any Hash keyed by RCAS::LaTeX raised.
83
+ def table(table)
84
+ return '\left\{\,\right\}' if table.empty?
85
+ pairs = table.map do |k, v|
86
+ case v
87
+ when Membership, Inequality, Equation then of(v)
88
+ when Array then v.map { |s| of(s) }.join(',\; ')
89
+ when Domain then "#{of(k)} \\in #{of(v)}"
90
+ else "#{of(k)} \\mapsto #{of(v)}"
91
+ end
92
+ end
93
+ pairs.join(',\; ')
94
+ end
95
+
96
+ def text(string)
97
+ "\\text{#{escape(string)}}"
98
+ end
99
+
100
+ def escape(string)
101
+ string.to_s.gsub(/[\\{}$&#^_%~]/) do |c|
102
+ case c
103
+ when "\\" then '\textbackslash{}'
104
+ when "^" then '\textasciicircum{}'
105
+ when "~" then '\textasciitilde{}'
106
+ else "\\#{c}"
107
+ end
108
+ end
109
+ end
110
+
111
+ # ---- expressions -------------------------------------------------------
112
+
113
+ def print(expr)
114
+ case expr
115
+ when Var then variable(expr.name)
116
+ when Num then number(expr.value)
117
+ when Neg then "-#{wrap(expr.arg, UNARY, :inner)}"
118
+ when Add, Sub then sum(expr)
119
+ when Mul then product(expr)
120
+ when Div then fraction(expr)
121
+ when Pow then power(expr)
122
+ when Fn then function(expr)
123
+ when Integral then integral(expr)
124
+ when Sum then "\\sum_{#{print(expr.var)}=#{print(expr.from)}}^{#{print(expr.to)}} #{wrap(expr.term, MULTIPLICATIVE, :right)}"
125
+ when Product then "\\prod_{#{print(expr.var)}=#{print(expr.from)}}^{#{print(expr.to)}} #{wrap(expr.term, MULTIPLICATIVE, :right)}"
126
+ when Limit then "\\lim_{#{print(expr.var)} \\to #{print(expr.point)}} #{wrap(expr.expr, MULTIPLICATIVE, :right)}"
127
+ else
128
+ return constant(expr) if defined?(Const) && expr.is_a?(Const)
129
+ return derivative(expr) if defined?(Derivative) && expr.is_a?(Derivative)
130
+ return piecewise(expr) if defined?(Piecewise) && expr.is_a?(Piecewise)
131
+ return root_of(expr) if defined?(RootOf) && expr.is_a?(RootOf)
132
+ raise ArgumentError, "don't know how to typeset #{expr.class}"
133
+ end
134
+ end
135
+
136
+ # RootOf(p, k) keeps its name: it is the k-th root of p in rcas's order
137
+ # (the real ones first, by size), and a root with no radical form has no
138
+ # better notation than that.
139
+ def root_of(root)
140
+ poly = root.poly.to_expr.subs(Var.new(root.var) => Var.new(:x))
141
+ "\\operatorname{RootOf}\\left(#{print(poly)}, #{root.index}\\right)"
142
+ end
143
+
144
+ # A function defined case by case as a cases environment.
145
+ def piecewise(pw)
146
+ rows = pw.branches.map { |cond, value| "#{print(value)} & #{Piecewises.condition_latex(cond, pw)}" }
147
+ "\\begin{cases} #{rows.join(' \\\\ ')} \\end{cases}"
148
+ end
149
+
150
+ # pi as \pi; other named constants like variables.
151
+ def constant(const)
152
+ return "\\infty" if const.name == :oo
153
+ return "\\mathrm{undefined}" if const.name == :undefined
154
+ const.name == :pi ? '\pi' : variable(const.name)
155
+ end
156
+
157
+ # d y / d x, d^{2} y / d x^{2}; a composite expression goes in parentheses.
158
+ def derivative(d)
159
+ n = d.order
160
+ top = n == 1 ? "d" : "d^{#{n}}"
161
+ bottom = n == 1 ? "d#{print(d.var)}" : "d#{print(d.var)}^{#{n}}"
162
+ if d.expr.is_a?(Var) || (defined?(Fn) && d.expr.is_a?(Fn))
163
+ "\\frac{#{top} #{print(d.expr)}}{#{bottom}}"
164
+ else
165
+ "\\frac{#{top}}{#{bottom}}\\left(#{print(d.expr)}\\right)"
166
+ end
167
+ end
168
+
169
+ # x, \alpha, x_{1}, \mathit{foo}
170
+ def variable(name)
171
+ s = name.to_s
172
+ base, index = s.match(/\A(.*?)_?(\d+)\z/)&.captures || [s, nil]
173
+ base = s if base.empty?
174
+ head =
175
+ if base.size == 1 then base
176
+ elsif GREEK.include?(base) then "\\#{base}"
177
+ else "\\mathit{#{base.gsub('_', '\_')}}"
178
+ end
179
+ index && base != s ? "#{head}_{#{index}}" : head
180
+ end
181
+
182
+ def number(value)
183
+ case value
184
+ when Integer then value.to_s
185
+ when Rational then value.denominator == 1 ? value.numerator.to_s : "#{'-' if value.negative?}\\frac{#{value.numerator.abs}}{#{value.denominator}}"
186
+ when Complex then complex(value)
187
+ when Float then float(value)
188
+ else value.to_s
189
+ end
190
+ end
191
+
192
+ def float(value)
193
+ return '\infty' if value == Float::INFINITY
194
+ return '-\infty' if value == -Float::INFINITY
195
+ return '\mathrm{NaN}' if value.nan?
196
+ s = value.to_s
197
+ if (m = s.match(/\A(-?[\d.]+)e([+-]?\d+)\z/))
198
+ "#{m[1]} \\times 10^{#{m[2].to_i}}"
199
+ else
200
+ s
201
+ end
202
+ end
203
+
204
+ def complex(value)
205
+ re, im = value.real, value.imaginary
206
+ imag =
207
+ case im
208
+ when 1 then "i"
209
+ when -1 then "-i"
210
+ else "#{number(im)}\\,i"
211
+ end
212
+ return imag if re.zero?
213
+ Simplify.negative?(im) ? "#{number(re)} - #{imag.delete_prefix('-')}" : "#{number(re)} + #{imag}"
214
+ end
215
+
216
+ def precedence(expr)
217
+ case expr
218
+ when Add, Sub then ADDITIVE
219
+ when Mul, Div then MULTIPLICATIVE
220
+ when Neg then UNARY
221
+ when Pow then POWER
222
+ when Num then Printer.number_precedence(expr.value)
223
+ else ATOM
224
+ end
225
+ end
226
+
227
+ # Sums and products are printed from their pieces (see #sum_pieces), so
228
+ # long left-leaning chains stay iterative.
229
+ def sum(expr)
230
+ first, rest = sum_pieces(expr)
231
+ rest.reduce(first) { |out, (op, term, _)| "#{out}#{op}#{term}" }
232
+ end
233
+
234
+ def product(expr)
235
+ first, rest = product_pieces(expr)
236
+ rest.reduce(first) { |out, (sep, part, _)| "#{out}#{sep}#{part}" }
237
+ end
238
+
239
+ # 2x and xy need nothing; x \cdot 2 and 2 \cdot 3 need a dot.
240
+ def separator(left, right)
241
+ right_numeric = right.is_a?(Num) || (right.is_a?(Pow) && right.base.is_a?(Num)) || right.is_a?(Neg)
242
+ return ' \cdot ' if right_numeric
243
+ return ' \cdot ' if left.is_a?(Num) && !left.value.is_a?(Integer) && !left.value.is_a?(Rational) && right.is_a?(Num)
244
+ " "
245
+ end
246
+
247
+ # A negative numerator puts its sign in front of the fraction.
248
+ def fraction(expr)
249
+ num = expr.left
250
+ if num.is_a?(Neg)
251
+ "-\\frac{#{print(num.arg)}}{#{print(expr.right)}}"
252
+ elsif num.is_a?(Num) && num.negative?
253
+ "-\\frac{#{number(-num.value)}}{#{print(expr.right)}}"
254
+ else
255
+ "\\frac{#{print(num)}}{#{print(expr.right)}}"
256
+ end
257
+ end
258
+
259
+ def power(expr)
260
+ base, exp = expr.base, expr.exponent
261
+ if exp.is_a?(Num) && exp.value.is_a?(Rational) && exp.value.numerator == 1 && exp.value.denominator >= 2
262
+ n = exp.value.denominator
263
+ return n == 2 ? "\\sqrt{#{print(base)}}" : "\\sqrt[#{n}]{#{print(base)}}"
264
+ end
265
+ if base.is_a?(Fn) && POWER_FUNCTIONS.include?(base.name) && base.args.size == 1 && exp.is_a?(Num) && exp.integer? && exp.value.positive?
266
+ return "#{FUNCTIONS[base.name]}^{#{print(exp)}}#{function_argument(base.args.first)}"
267
+ end
268
+ "#{wrap(base, POWER, :left)}^{#{print(exp)}}"
269
+ end
270
+
271
+ def function(expr)
272
+ args = expr.args
273
+ case expr.name
274
+ when :exp
275
+ arg = args.first
276
+ return "e" if arg.is_a?(Num) && arg.one?
277
+ return "e^{#{print(arg)}}"
278
+ when :sqrt
279
+ return "\\sqrt{#{print(args.first)}}"
280
+ when :gamma, :zeta
281
+ return "#{FUNCTIONS[expr.name]}\\left(#{print(args.first)}\\right)" if args.size == 1
282
+ when :factorial
283
+ return "#{factorial_base(args.first)}!"
284
+ when :binomial
285
+ return "\\binom{#{print(args[0])}}{#{print(args[1])}}" if args.size == 2
286
+ when :surd
287
+ return "\\sqrt[#{print(args[1])}]{#{print(args[0])}}" if args.size == 2
288
+ when :mod
289
+ return "#{wrap(args[0], MULTIPLICATIVE, :left)} \\bmod #{wrap(args[1], MULTIPLICATIVE, :right)}" if args.size == 2
290
+ when :conj
291
+ return "\\overline{#{print(args.first)}}"
292
+ when :re, :im
293
+ return "\\#{expr.name == :re ? 'Re' : 'Im'}#{function_argument(args.first)}"
294
+ when *BRACKETED.keys
295
+ open, close = BRACKETED[expr.name]
296
+ return "#{open}#{print(args.first)}#{close}"
297
+ when *INDEXED.keys
298
+ return "#{INDEXED[expr.name]}_{#{print(args.first)}}"
299
+ end
300
+ head = FUNCTIONS[expr.name] || "\\operatorname{#{escape(expr.name)}}"
301
+ return "#{head}#{function_argument(args.first)}" if args.size == 1
302
+ "#{head}\\left(#{args.map { |a| print(a) }.join(', ')}\\right)"
303
+ end
304
+
305
+ # 5!, n!, (n + 1)!
306
+ def factorial_base(arg)
307
+ atom = arg.is_a?(Var) || (arg.is_a?(Num) && !arg.value.negative? && (arg.value.is_a?(Integer) || arg.value.is_a?(Float)))
308
+ atom ? print(arg) : "\\left(#{print(arg)}\\right)"
309
+ end
310
+
311
+ def function_argument(arg)
312
+ arg.is_a?(Var) ? " #{print(arg)}" : "\\left(#{print(arg)}\\right)"
313
+ end
314
+
315
+ def integral(expr)
316
+ body = expr.integrand
317
+ inner = precedence(body) <= ADDITIVE ? "\\left(#{print(body)}\\right)" : print(body)
318
+ bounds = expr.definite? ? "_{#{print(expr.from)}}^{#{print(expr.to)}}" : ""
319
+ "\\int#{bounds} #{inner}\\, d#{print(expr.var)}"
320
+ end
321
+
322
+ # Same decisions as Printer#wrap, with \left( \right) as the parentheses.
323
+ # Division needs no parentheses anywhere since it becomes a fraction, and
324
+ # a fraction needs them only as the base of a power.
325
+ def wrap(child, parent_prec, position, parent = nil)
326
+ s = print(child)
327
+ child_prec = precedence(child)
328
+ needs =
329
+ case position
330
+ when :left
331
+ if parent_prec == POWER
332
+ !(child.is_a?(Var) || (child.is_a?(Num) && (child.value.is_a?(Integer) || child.value.is_a?(Float)) && !child.negative?) ||
333
+ (child.is_a?(Fn) && child.name != :exp))
334
+ else
335
+ child_prec < parent_prec
336
+ end
337
+ when :right
338
+ child_prec < parent_prec ||
339
+ (child_prec == parent_prec && parent && !Printer.associative_right?(parent, child) && !child.is_a?(Div)) ||
340
+ child.is_a?(Neg) || (child.is_a?(Num) && child.negative?)
341
+ when :inner
342
+ child_prec <= ADDITIVE || child.is_a?(Neg) || (child.is_a?(Num) && child.negative?)
343
+ end
344
+ needs ? "\\left(#{s}\\right)" : s
345
+ end
346
+
347
+ # ---- line breaking -----------------------------------------------------
348
+
349
+ # +expr+ broken into lines of at most +width+ typeset characters at its
350
+ # top-level sum or product, as an aligned block; unchanged when it fits.
351
+ #
352
+ # 1 + x + x^{2} + \cdots (fits)
353
+ # \begin{aligned} & 1 + x + x^{2} \\ &\quad {} + x^{3} + \cdots \end{aligned}
354
+ def wrapped(expr, width)
355
+ if expr.is_a?(Neg) && !expr.arg.is_a?(Mul)
356
+ inner = wrapped(expr.arg, width - 2)
357
+ return print(expr) unless inner.include?('\begin{aligned}')
358
+ return "-\\left(#{inner}\\right)"
359
+ end
360
+ first, rest = pieces(expr)
361
+ return print(expr) if rest.empty?
362
+ lines = [first]
363
+ rest.each do |op, term, continuation|
364
+ candidate = "#{lines.last}#{op}#{term}"
365
+ if self.width(candidate) > width && self.width(lines.last) > 0
366
+ lines << "#{continuation}#{term}"
367
+ else
368
+ lines[-1] = candidate
369
+ end
370
+ end
371
+ return lines.first if lines.size == 1
372
+ "\\begin{aligned} & #{lines.first} \\\\ #{lines.drop(1).map { |l| "&\\quad #{l}" }.join(' \\\\ ')} \\end{aligned}"
373
+ end
374
+
375
+ # => [first, [[joiner, latex, line_start], ...]] for the top-level sum or
376
+ # product of +expr+; a lone piece for anything else.
377
+ def pieces(expr)
378
+ case expr
379
+ when Add, Sub then sum_pieces(expr)
380
+ when Mul then product_pieces(expr)
381
+ when Neg
382
+ first, rest = pieces(expr.arg)
383
+ rest.empty? ? [print(expr), []] : ["-#{first}", rest]
384
+ else [print(expr), []]
385
+ end
386
+ end
387
+
388
+ # Walks the left spine like Printer#binary; each piece carries the
389
+ # joiner used inside a line and the one used at the start of a new line.
390
+ def sum_pieces(expr)
391
+ spine = []
392
+ node = expr
393
+ while node.is_a?(Add) || node.is_a?(Sub)
394
+ spine << node
395
+ node = node.left
396
+ end
397
+ first = wrap(node, ADDITIVE, :left)
398
+ rest = spine.reverse.map do |n|
399
+ right = n.right
400
+ if n.is_a?(Add) && (right.is_a?(Neg) || (right.is_a?(Num) && right.negative?))
401
+ # a + (-b) reads better as a - b
402
+ term = right.is_a?(Neg) ? wrap(right.arg, UNARY, :inner) : number(-right.value)
403
+ [" - ", term, "{} - "]
404
+ else
405
+ op = n.is_a?(Add) ? "+" : "-"
406
+ [" #{op} ", wrap(right, ADDITIVE, :right, n), "{} #{op} "]
407
+ end
408
+ end
409
+ [first, rest]
410
+ end
411
+
412
+ def product_pieces(expr)
413
+ factors = []
414
+ node = expr
415
+ while node.is_a?(Mul)
416
+ factors.unshift(node.right)
417
+ node = node.left
418
+ end
419
+ factors.unshift(node)
420
+ parts = factors.each_with_index.map { |f, i| wrap(f, MULTIPLICATIVE, i.zero? ? :left : :right, expr) }
421
+ rest = factors.each_cons(2).zip(parts.drop(1)).map { |(a, b), part| [separator(a, b), part, "{} \\times "] }
422
+ [parts.first, rest]
423
+ end
424
+
425
+ # Rough visual width of a LaTeX fragment in characters: fences and
426
+ # spacing commands vanish, a fraction is as wide as its wider half, any
427
+ # other command counts as one glyph, braces and script marks are free.
428
+ def width(latex)
429
+ s = latex.dup
430
+ s.gsub!(/\\(left|right|displaystyle|quad|qquad|begin\{[a-z*]+\}|end\{[a-z*]+\})/, "")
431
+ s.gsub!(/\[[0-9.]+em\]/, "") # the row leading of a matrix, not a glyph
432
+ s.gsub!(/\\[,;!]/, "")
433
+ 3.times { s.gsub!(/\\frac\{([^{}]*)\}\{([^{}]*)\}/) { "x" * [Regexp.last_match(1).size, Regexp.last_match(2).size].max } }
434
+ s.gsub!(/\\[a-zA-Z]+/, "M")
435
+ s.gsub!(/[{}^_&]/, "")
436
+ s.size
437
+ end
438
+
439
+ # ---- algebraic structures ----------------------------------------------
440
+
441
+ def domain(dom)
442
+ case dom
443
+ when NumberSet then SETS.fetch(dom.name) { "\\mathbb{#{escape(dom.name)}}" }
444
+ when PolynomialRing then "#{domain(dom.base)}[#{dom.vars.map { |v| variable(v) }.join(', ')}]"
445
+ when FractionField then "\\operatorname{Frac}\\left(#{domain(dom.ring)}\\right)"
446
+ else text(dom.to_s)
447
+ end
448
+ end
449
+
450
+ def matrix(rows)
451
+ return '\left[\,\right]' if rows.empty? || rows.first.empty?
452
+ cells = rows.map { |r| r.map { |e| cell(e) } }
453
+ "\\begin{pmatrix} #{cells.map { |r| r.join(' & ') }.join(leading(cells))} \\end{pmatrix}"
454
+ end
455
+
456
+ # Fractions in a matrix are set at display size so they stay legible.
457
+ def cell(entry)
458
+ s = of(entry)
459
+ s.include?('\frac') ? "\\displaystyle #{s}" : s
460
+ end
461
+
462
+ # An array sets its rows on one fixed baseline distance, which a
463
+ # display-size fraction is taller than: two of them in a column collide
464
+ # (KaTeX draws them overlapping, TeX complains). Ask for the leading they
465
+ # need, and only then - an ordinary matrix of numbers wants none.
466
+ def leading(cells)
467
+ cells.flatten.any? { |c| c.start_with?('\displaystyle') } ? ' \\\\[0.8em] ' : ' \\\\ '
468
+ end
469
+
470
+ def vector(entries)
471
+ matrix(entries.map { |e| [e] })
472
+ end
473
+ end
474
+
475
+ # Mixin for every rcas value that can be typeset: #to_latex is defined by
476
+ # the including class; #show renders it inline (see Render).
477
+ module Typeset
478
+ def to_latex(**) = raise(RCAS::Unsupported, "#{self.class} has no LaTeX form")
479
+
480
+ # Display typeset in the terminal (inline image in iTerm2).
481
+ def show(**options) = Render.show(self, **options)
482
+
483
+ # PNG bytes, or write them to +path+.
484
+ def to_png(path = nil, **options) = Render.png(self, path, **options)
485
+ end
486
+
487
+ class Expression
488
+ include Typeset
489
+ # wrap: a line width in typeset characters; long top-level sums and
490
+ # products are then broken into an aligned block.
491
+ def to_latex(wrap: nil) = wrap ? LaTeX.wrapped(self, wrap) : LaTeX.print(self)
492
+ end
493
+
494
+ class Polynomial
495
+ include Typeset
496
+ def to_latex(wrap: nil) = to_expr.to_latex(wrap: wrap)
497
+ end
498
+
499
+ class Factorization
500
+ include Typeset
501
+ def to_latex(wrap: nil) = to_expr.to_latex(wrap: wrap)
502
+ end
503
+
504
+ class Domain
505
+ include Typeset
506
+ def to_latex = LaTeX.domain(self)
507
+ end
508
+
509
+ class VectorSpace
510
+ include Typeset
511
+ def to_latex = "#{base.to_latex}^{#{dim}}"
512
+ end
513
+
514
+ class MatrixSpace
515
+ include Typeset
516
+ def to_latex = "#{base.to_latex}^{#{rows} \\times #{cols}}"
517
+ end
518
+
519
+ class Vector
520
+ include Typeset
521
+ def to_latex = LaTeX.vector(entries)
522
+ end
523
+
524
+ class Matrix
525
+ include Typeset
526
+ def to_latex = LaTeX.matrix(entries)
527
+ end
528
+
529
+ class Membership
530
+ include Typeset
531
+ end
532
+
533
+ if defined?(Equation)
534
+ class Equation
535
+ include Typeset
536
+ # lhs = rhs; with wrap: each side may break into an aligned block.
537
+ def to_latex(wrap: nil) = "#{lhs.to_latex(wrap: wrap)} = #{rhs.to_latex(wrap: wrap)}"
538
+ end
539
+ end
540
+ end
541
+
542
+ class Symbol
543
+ def to_latex = RCAS::LaTeX.variable(self)
544
+ end
545
+
546
+ class Numeric
547
+ def to_latex = RCAS::LaTeX.number(self)
548
+ end
549
+
550
+ class Array
551
+ def to_latex(wrap: nil) = RCAS::LaTeX.array(self, wrap: wrap)
552
+ end
553
+
554
+ class Hash
555
+ def to_latex = RCAS::LaTeX.table(self)
556
+ end