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/dixon.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Dixon's p-adic solution of one square system A*x = b with Integer and
|
|
5
|
+
# Rational entries [Dix82].
|
|
6
|
+
#
|
|
7
|
+
# The primes of Multimodular pay a whole elimination per prime. Dixon
|
|
8
|
+
# pays for one: C = A**-1 modulo a single prime p, and then computes the
|
|
9
|
+
# solution one p-adic digit at a time, as long division does in base 10:
|
|
10
|
+
#
|
|
11
|
+
# x_i = C*r mod p, r <- (r - A*x_i)/p (r starts as b)
|
|
12
|
+
#
|
|
13
|
+
# Each step is two products of a matrix and a vector, and the division
|
|
14
|
+
# by p is exact because A*x_i = r modulo p. After k steps
|
|
15
|
+
# x_0 + x_1*p + ... + x_(k-1)*p**(k-1) is the solution modulo p**k, and
|
|
16
|
+
# rational reconstruction reads it back as fractions over one common
|
|
17
|
+
# denominator [vzGG13, §5.10]. So an elimination is paid once, and every
|
|
18
|
+
# further digit costs n**2 instead of n**3.
|
|
19
|
+
#
|
|
20
|
+
# The reconstruction is tried after 4, 8, 16, ... steps and believed only
|
|
21
|
+
# when A*x = b holds exactly, which is a proof: A is invertible (it is
|
|
22
|
+
# modulo p), so the solution is unique. Hadamard's bound says when the
|
|
23
|
+
# reconstruction must succeed - numerators and denominator are then below
|
|
24
|
+
# the square root of p**k/2 - so the loop ends.
|
|
25
|
+
module Dixon
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# The solution as Integers and Rationals, or nil when A is singular.
|
|
29
|
+
def solve(rows, b)
|
|
30
|
+
a, scales = Multimodular.integral(Multimodular.values(rows))
|
|
31
|
+
column = b.map { |e| e.is_a?(Num) ? e.value : e }.each_with_index.map { |v, i| v * scales[i] }
|
|
32
|
+
common = column.map(&:denominator).reduce(1, :lcm)
|
|
33
|
+
d, x = lift(a, column.map { |v| (v * common).to_i })
|
|
34
|
+
return nil if d.zero?
|
|
35
|
+
x.map { |v| Rational(v, d * common) }.map { |v| v.denominator == 1 ? v.numerator : v }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# [d, N] with A*N = d*b for an Integer matrix A and an Integer vector b,
|
|
39
|
+
# or [0, nil] when A is singular.
|
|
40
|
+
def lift(a, b)
|
|
41
|
+
n = a.size
|
|
42
|
+
found = inverse_modulo_a_prime(a) or return [0, nil]
|
|
43
|
+
p, c = found
|
|
44
|
+
certain = 2 * [Multimodular.cramer_bound(a, b.map { |v| [v] }), Multimodular.hadamard(a)].max**2
|
|
45
|
+
r = b.dup
|
|
46
|
+
x = Array.new(n, 0)
|
|
47
|
+
power = 1
|
|
48
|
+
attempt = 4
|
|
49
|
+
(1..).each do |step|
|
|
50
|
+
digit = c.map do |row|
|
|
51
|
+
s = 0
|
|
52
|
+
j = 0
|
|
53
|
+
while j < n
|
|
54
|
+
s += row[j] * r[j]
|
|
55
|
+
j += 1
|
|
56
|
+
end
|
|
57
|
+
s % p
|
|
58
|
+
end
|
|
59
|
+
x = x.each_with_index.map { |v, i| v + power * digit[i] }
|
|
60
|
+
power *= p
|
|
61
|
+
r = a.each_with_index.map do |row, i|
|
|
62
|
+
s = r[i]
|
|
63
|
+
j = 0
|
|
64
|
+
while j < n
|
|
65
|
+
s -= row[j] * digit[j]
|
|
66
|
+
j += 1
|
|
67
|
+
end
|
|
68
|
+
s / p
|
|
69
|
+
end
|
|
70
|
+
next unless step == attempt || power > certain
|
|
71
|
+
attempt *= 2
|
|
72
|
+
found = Multimodular.verified(a, b.map { |v| [v] }, x.map { |v| [v] }, power)
|
|
73
|
+
return [found.first, found.last.map(&:first)] if found
|
|
74
|
+
raise "Dixon: no solution past the bound, which cannot happen" if power > certain
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# [p, A**-1 mod p] for the first prime that does not divide det A, or
|
|
79
|
+
# nil when the primes tried multiply past twice Hadamard's bound: then
|
|
80
|
+
# det A is 0 modulo all of them, and so is 0.
|
|
81
|
+
def inverse_modulo_a_prime(a)
|
|
82
|
+
n = a.size
|
|
83
|
+
identity = Array.new(n) { |i| Array.new(n) { |j| i == j ? 1 : 0 } }
|
|
84
|
+
bound = 2 * Multimodular.hadamard(a)
|
|
85
|
+
modulus = 1
|
|
86
|
+
(0..).each do |i|
|
|
87
|
+
return nil if modulus > bound
|
|
88
|
+
p = Multimodular.prime(i)
|
|
89
|
+
d, inverse = Multimodular.eliminate(a, identity, p)
|
|
90
|
+
return [p, inverse] unless d.zero?
|
|
91
|
+
modulus *= p
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/rcas/docs.rb
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# What one name does, read from the source itself.
|
|
5
|
+
#
|
|
6
|
+
# doc(:factor) # the top-level function, its comment and where the manual covers it
|
|
7
|
+
# doc(:simplify) # a function that is also a method on expressions
|
|
8
|
+
# doc("ZZ") # a value: the integers
|
|
9
|
+
# doc(:Matrix) # a class, with its public methods
|
|
10
|
+
#
|
|
11
|
+
# rcas documents every public function with a comment above its `def`
|
|
12
|
+
# (see CLAUDE.md), so the help is the source and cannot drift from it.
|
|
13
|
+
# `bin/rcas-chat` shows the same text as `/help NAME`.
|
|
14
|
+
Documentation = Struct.new(:name, :kind, :signature, :lines, :also, :sections, :background, :sources, :reading) do
|
|
15
|
+
WIDTH = 74
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
out = [signature]
|
|
19
|
+
out.concat(lines.map { |l| " #{l}" })
|
|
20
|
+
out << " also: #{also}" if also
|
|
21
|
+
background&.each { |label, text| out.concat(Documentation.wrap(" #{label}: ", text)) }
|
|
22
|
+
sources&.each_with_index { |source, i| out.concat(Documentation.wrap(i.zero? ? " sources: " : " ", source)) }
|
|
23
|
+
reading&.each_with_index { |link, i| out << "#{i.zero? ? ' read: ' : ' '}#{link}" }
|
|
24
|
+
out << " manual: #{sections.join('; ')}" unless sections.nil? || sections.empty?
|
|
25
|
+
out.join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# +label+ on the first line, the rest indented under it.
|
|
29
|
+
def self.wrap(label, text)
|
|
30
|
+
indent = " " * label.size
|
|
31
|
+
lines = [label.rstrip.empty? ? label.dup : label.rstrip] # a blank label is an indent
|
|
32
|
+
text.split(/\s+/).each do |word|
|
|
33
|
+
lines << indent.dup if lines.last.size + word.size + 1 > WIDTH
|
|
34
|
+
lines[-1] = "#{lines.last}#{lines.last.end_with?(' ') ? '' : ' '}#{word}"
|
|
35
|
+
end
|
|
36
|
+
lines
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inspect = to_s
|
|
40
|
+
def pretty_print(printer) = printer.text(to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module Docs
|
|
44
|
+
MANUAL = File.expand_path("../../MANUAL.md", __dir__)
|
|
45
|
+
MAX_METHODS = 16
|
|
46
|
+
MAX_SECTIONS = 3
|
|
47
|
+
|
|
48
|
+
class NotFound < StandardError; end
|
|
49
|
+
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
# => Documentation; raises NotFound with near misses.
|
|
53
|
+
def doc(name)
|
|
54
|
+
key = name.is_a?(Symbol) || name.is_a?(String) ? name.to_s : name.to_s
|
|
55
|
+
key = key.sub(/\A[A-Z]+::/, "").delete_prefix("RCAS::")
|
|
56
|
+
found = function(key) || namespaced(key) || expression_method(key) || structure_method(key) || constant(key)
|
|
57
|
+
raise NotFound, "nothing known about #{key}#{suggestion(key)}" if found.nil?
|
|
58
|
+
found
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Near misses, compared on the part after the namespace, so that
|
|
62
|
+
# `chebyshev` finds Poly.chebyshev_t.
|
|
63
|
+
def suggestion(key)
|
|
64
|
+
target = bare(key)
|
|
65
|
+
near = names.select { |n| bare(n).start_with?(target[0, 2].to_s) || levenshtein(bare(n), target) <= 2 }
|
|
66
|
+
.sort_by { |n| levenshtein(bare(n), target) }
|
|
67
|
+
near.empty? ? "" : "; did you mean #{near.first(4).join(', ')}?"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def bare(name) = name.split(".").last.to_s
|
|
71
|
+
|
|
72
|
+
# Modules with functions of their own, addressed through their name:
|
|
73
|
+
# `Poly.legendre` is the polynomial, the bare `legendre` the symbol.
|
|
74
|
+
NAMESPACES = %w[Poly].freeze
|
|
75
|
+
|
|
76
|
+
# The structures and how one is written down, for a method that belongs
|
|
77
|
+
# to a ring or a space rather than to an expression: ZZ[x].random.
|
|
78
|
+
STRUCTURES = { "PolynomialRing" => "ZZ[x]", "FractionField" => "Frac(ZZ[x])", "MatrixSpace" => "(ZZ**[2, 2])",
|
|
79
|
+
"VectorSpace" => "(ZZ**3)", "NumberSet" => "ZZ", "FiniteField" => "GF(9)",
|
|
80
|
+
"AlgebraicField" => "QQ.adjoin(sqrt(2))" }.freeze
|
|
81
|
+
|
|
82
|
+
# Every name doc knows about.
|
|
83
|
+
def names
|
|
84
|
+
functions = Functions.instance_methods(false).map(&:to_s)
|
|
85
|
+
methods = Expression.public_instance_methods(false).map(&:to_s)
|
|
86
|
+
constants = RCAS.constants.map(&:to_s)
|
|
87
|
+
(functions + methods + constants + namespaced_names + structure_names).uniq.sort
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def structure_names
|
|
91
|
+
STRUCTURES.keys.flat_map { |name| structure_methods(name).map(&:to_s) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def structure_methods(name)
|
|
95
|
+
RCAS.const_get(name).public_instance_methods(false) - Object.instance_methods
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def namespaced_names
|
|
99
|
+
NAMESPACES.flat_map do |prefix|
|
|
100
|
+
RCAS.const_get(prefix).public_instance_methods(false).map { |name| "#{prefix}.#{name}" }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def levenshtein(a, b)
|
|
105
|
+
rows = Array.new(a.size + 1) { |i| [i] + Array.new(b.size, 0) }
|
|
106
|
+
(0..b.size).each { |j| rows[0][j] = j }
|
|
107
|
+
(1..a.size).each do |i|
|
|
108
|
+
(1..b.size).each do |j|
|
|
109
|
+
cost = a[i - 1] == b[j - 1] ? 0 : 1
|
|
110
|
+
rows[i][j] = [rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost].min
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
rows[a.size][b.size]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ---- the three kinds -------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def function(key)
|
|
119
|
+
name = key.to_sym
|
|
120
|
+
return nil unless Functions.method_defined?(name) || Functions.private_method_defined?(name)
|
|
121
|
+
info = from_source(Functions.instance_method(name), name)
|
|
122
|
+
also = []
|
|
123
|
+
also << "e.#{name}" if Expression.method_defined?(name) && !%i[in in?].include?(name)
|
|
124
|
+
also << "f.#{name} on polynomials" if Polynomial.method_defined?(name) && !Expression.method_defined?(name)
|
|
125
|
+
Documentation.new(key, :function, info[:signature], info[:lines], also.empty? ? nil : also.join(", "), sections(key), Background[key], sources_for(key), Background.reading(key))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Poly.legendre, or the bare chebyshev_t when no top-level function has
|
|
129
|
+
# that name. The documentation is keyed by the qualified name, so that
|
|
130
|
+
# Poly.legendre and the Legendre symbol can have a background each.
|
|
131
|
+
def namespaced(key)
|
|
132
|
+
prefix, name = key.sub("::", ".").split(".", 2)
|
|
133
|
+
prefix, name = [nil, prefix] if name.nil?
|
|
134
|
+
prefixes = prefix ? NAMESPACES & [prefix] : NAMESPACES
|
|
135
|
+
found = prefixes.find { |p| RCAS.const_get(p).public_method_defined?(name.to_sym) }
|
|
136
|
+
return nil if found.nil?
|
|
137
|
+
qualified = "#{found}.#{name}"
|
|
138
|
+
info = from_source(RCAS.const_get(found).instance_method(name.to_sym), name.to_sym)
|
|
139
|
+
Documentation.new(qualified, :function, "#{found}.#{info[:signature]}", info[:lines], nil,
|
|
140
|
+
sections(qualified), Background[qualified], sources_for(qualified), Background.reading(qualified))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def expression_method(key)
|
|
144
|
+
name = key.to_sym
|
|
145
|
+
return nil unless Expression.method_defined?(name)
|
|
146
|
+
info = from_source(Expression.instance_method(name), name)
|
|
147
|
+
Documentation.new(key, :method, "e.#{info[:signature]}", info[:lines], "a method on expressions", sections(key), Background[key], sources_for(key), Background.reading(key))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A method of a ring, a field or a space, shown the way one is written:
|
|
151
|
+
# ZZ[x].random. The other structures that answer it are named beside it.
|
|
152
|
+
def structure_method(key)
|
|
153
|
+
name = key.to_sym
|
|
154
|
+
homes = STRUCTURES.keys.select { |c| structure_methods(c).include?(name) }
|
|
155
|
+
return nil if homes.empty?
|
|
156
|
+
info = from_source(RCAS.const_get(homes.first).instance_method(name), name)
|
|
157
|
+
also = homes.drop(1).map { |c| "#{STRUCTURES[c]}.#{name}" }
|
|
158
|
+
Documentation.new(key, :method, "#{STRUCTURES[homes.first]}.#{info[:signature]}", info[:lines],
|
|
159
|
+
also.empty? ? nil : also.join(", "), sections(key), Background[key], sources_for(key), Background.reading(key))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def constant(key)
|
|
163
|
+
return nil unless key.match?(/\A[[:upper:]][[:alnum:]_]*\z/) && RCAS.const_defined?(key)
|
|
164
|
+
value = RCAS.const_get(key)
|
|
165
|
+
location = Object.const_source_location("RCAS::#{key}")
|
|
166
|
+
lines = location && location.first ? comment_above(location.first, location.last) : []
|
|
167
|
+
if value.is_a?(Module)
|
|
168
|
+
methods = (value.public_instance_methods(false) - Object.instance_methods).map(&:to_s).grep(/\A[a-z]/).sort
|
|
169
|
+
lines = [""] + lines unless lines.empty?
|
|
170
|
+
lines = lines.reject(&:empty?)
|
|
171
|
+
lines << "methods: #{methods.first(MAX_METHODS).join(', ')}#{methods.size > MAX_METHODS ? ', ...' : ''}" unless methods.empty?
|
|
172
|
+
Documentation.new(key, :class, "#{value} (#{value.is_a?(Class) ? 'class' : 'module'})", lines, nil, sections(key), Background[key], sources_for(key), Background.reading(key))
|
|
173
|
+
else
|
|
174
|
+
if lines.empty? # a bare constant: say what its class is for
|
|
175
|
+
where = Object.const_source_location(value.class.name.to_s)
|
|
176
|
+
lines = where && where.first ? comment_above(where.first, where.last).first(2) : []
|
|
177
|
+
end
|
|
178
|
+
lines = ["#{value} (#{value.class.name.to_s.sub('RCAS::', '')})"] + lines
|
|
179
|
+
Documentation.new(key, :value, key, lines, nil, sections(key), Background[key], sources_for(key), Background.reading(key))
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# ---- reading the source ----------------------------------------------------
|
|
184
|
+
|
|
185
|
+
# The `def` line and the comment block above it. Several defs may share
|
|
186
|
+
# one comment (degree, ldegree, lcoeff), so walk up past them.
|
|
187
|
+
def from_source(method, name)
|
|
188
|
+
file, line = method.source_location
|
|
189
|
+
return { signature: "#{name}(...)", lines: [] } if file.nil?
|
|
190
|
+
lines = File.readlines(file)
|
|
191
|
+
i = line - 1
|
|
192
|
+
i -= 1 while i.positive? && !lines[i].match?(/^\s*def\s/)
|
|
193
|
+
match = i.positive? ? lines[i].match(/def\s+(?:self\.)?(?<name>[A-Za-z_]\w*[?!=]?)\s*(?:\((?<params>.*?)\))?/) : nil
|
|
194
|
+
if match && match[:name] == name.to_s
|
|
195
|
+
signature = match[:params].to_s.strip.empty? ? name.to_s : "#{name}(#{match[:params].strip})"
|
|
196
|
+
{ signature: signature, lines: comment_above(file, i + 1, lines) }
|
|
197
|
+
else # define_method (sin, cos, ...) or an alias
|
|
198
|
+
{ signature: parameter_signature(method, name), lines: nearest_comment(lines, line - 1) }
|
|
199
|
+
end
|
|
200
|
+
rescue SystemCallError, IOError
|
|
201
|
+
{ signature: parameter_signature(method, name), lines: [] }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The comment block ending just above +line+, skipping other def lines.
|
|
205
|
+
def comment_above(file, line, lines = nil)
|
|
206
|
+
lines ||= File.readlines(file)
|
|
207
|
+
j = line - 2
|
|
208
|
+
j -= 1 while j >= 0 && lines[j].match?(/^\s*(def\s|alias\s|\z)/) && !lines[j].match?(/^\s*#/)
|
|
209
|
+
out = []
|
|
210
|
+
while j >= 0 && lines[j].match?(/^\s*#/)
|
|
211
|
+
out.unshift(lines[j].sub(/^\s*#\s?/, "").rstrip)
|
|
212
|
+
j -= 1
|
|
213
|
+
end
|
|
214
|
+
out.shift while out.first&.empty?
|
|
215
|
+
out.pop while out.last&.empty?
|
|
216
|
+
out
|
|
217
|
+
rescue SystemCallError, IOError
|
|
218
|
+
[]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The comment block closest above +from+, at most eight lines up.
|
|
222
|
+
def nearest_comment(lines, from)
|
|
223
|
+
j = from
|
|
224
|
+
j -= 1 while j.positive? && !lines[j].match?(/^\s*#/) && from - j < 8
|
|
225
|
+
return [] unless lines[j].to_s.match?(/^\s*#/)
|
|
226
|
+
out = []
|
|
227
|
+
while j >= 0 && lines[j].match?(/^\s*#/)
|
|
228
|
+
out.unshift(lines[j].sub(/^\s*#\s?/, "").rstrip)
|
|
229
|
+
j -= 1
|
|
230
|
+
end
|
|
231
|
+
out
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def parameter_signature(method, name)
|
|
235
|
+
parts = method.parameters.map do |kind, parameter|
|
|
236
|
+
case kind
|
|
237
|
+
when :req then parameter.to_s
|
|
238
|
+
when :opt then "#{parameter} = ..."
|
|
239
|
+
when :rest then "*#{parameter}"
|
|
240
|
+
when :keyreq then "#{parameter}:"
|
|
241
|
+
when :key then "#{parameter}: ..."
|
|
242
|
+
when :keyrest then "**#{parameter}"
|
|
243
|
+
when :block then "&#{parameter}"
|
|
244
|
+
end
|
|
245
|
+
end.compact
|
|
246
|
+
parts.empty? ? name.to_s : "#{name}(#{parts.join(', ')})"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# ---- sources ----------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
# "[Knu98] D. E. Knuth, The Art of Computer Programming, ..." for every
|
|
252
|
+
# source the background of this name cites, in the order they appear.
|
|
253
|
+
def sources_for(key)
|
|
254
|
+
entry = Background[key] or return []
|
|
255
|
+
cited = entry.values.join(" ").scan(/\[([A-Za-z]+\d+[a-z]?)(?:,[^\]]*)?\]/).flatten.uniq
|
|
256
|
+
cited.filter_map { |k| bibliography[k] && "[#{k}] #{bibliography[k]}" }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# { "Knu98" => "D. E. Knuth, ..." } from section 4 of the manual, so the
|
|
260
|
+
# citation lives in one place only.
|
|
261
|
+
def bibliography
|
|
262
|
+
return @bibliography if defined?(@bibliography)
|
|
263
|
+
@bibliography = {}
|
|
264
|
+
return @bibliography unless File.file?(MANUAL)
|
|
265
|
+
current = nil
|
|
266
|
+
File.foreach(MANUAL) do |line|
|
|
267
|
+
if (m = line.match(/\A- \[([A-Za-z]+\d+[a-z]?)\]\s+(.*)$/))
|
|
268
|
+
current = m[1]
|
|
269
|
+
@bibliography[current] = m[2].strip
|
|
270
|
+
elsif current && line.match?(/\A \S/)
|
|
271
|
+
@bibliography[current] = "#{@bibliography[current]} #{line.strip}"
|
|
272
|
+
else
|
|
273
|
+
current = nil
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
# the manual writes links as <https://...> autolinks; a terminal wants them bare
|
|
277
|
+
@bibliography = @bibliography.transform_values { |v| v.delete("*").gsub(/<(https?:[^>]+)>/, '\1').sub(/\.\z/, "") }
|
|
278
|
+
rescue SystemCallError, IOError
|
|
279
|
+
@bibliography = {}
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# ---- the manual ------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
# Headings of the manual sections that mention this name.
|
|
285
|
+
def sections(key)
|
|
286
|
+
return [] unless File.file?(MANUAL)
|
|
287
|
+
pattern = /(?<![A-Za-z_])#{Regexp.escape(key)}(?![A-Za-z_])/
|
|
288
|
+
found = count_mentions(pattern)
|
|
289
|
+
found = count_mentions(/(?<![A-Za-z_])#{Regexp.escape(key)}(?![A-Za-z_])/i) if found.empty? && key.match?(/\A[[:upper:]]/)
|
|
290
|
+
found.first(MAX_SECTIONS)
|
|
291
|
+
rescue SystemCallError, IOError
|
|
292
|
+
[]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Headings ordered by how often they mention the name (the loose pattern
|
|
296
|
+
# catches classes, which the prose writes in lower case).
|
|
297
|
+
# The appendices and the tables of part 2 to 5 are not sections to read:
|
|
298
|
+
# they are tracked by their own heading level, so their subsections go too.
|
|
299
|
+
SKIP = /\A[0-9]+\. (Reference|Files|Sources|License)|\AAppendix/
|
|
300
|
+
|
|
301
|
+
def count_mentions(pattern)
|
|
302
|
+
part = nil
|
|
303
|
+
heading = nil
|
|
304
|
+
counts = Hash.new(0)
|
|
305
|
+
File.foreach(MANUAL) do |line|
|
|
306
|
+
if line.start_with?("## ")
|
|
307
|
+
part = line.sub(/\A#+\s*/, "").strip
|
|
308
|
+
heading = part
|
|
309
|
+
next
|
|
310
|
+
elsif line.start_with?("###")
|
|
311
|
+
heading = line.sub(/\A#+\s*/, "").strip
|
|
312
|
+
next
|
|
313
|
+
end
|
|
314
|
+
next unless heading && line.match?(pattern)
|
|
315
|
+
next if part.to_s.match?(SKIP) || heading.match?(SKIP)
|
|
316
|
+
counts[heading] += line.scan(pattern).size
|
|
317
|
+
end
|
|
318
|
+
counts.sort_by { |_, n| -n }.map(&:first)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|