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.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- 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
|