calculus 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/calculus.gemspec +6 -1
- data/lib/calculus.rb +7 -0
- data/lib/calculus/expression.rb +74 -6
- data/lib/calculus/latex.rb +18 -0
- data/lib/calculus/parser.rb +53 -0
- data/lib/calculus/version.rb +1 -1
- metadata +7 -6
data/calculus.gemspec
CHANGED
@@ -10,7 +10,12 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.email = ["sergey.avseyev@gmail.com"]
|
11
11
|
s.homepage = "http://avsej.net/calculus"
|
12
12
|
s.summary = %q{A ruby parser for TeX equations}
|
13
|
-
s.description = %q{A ruby parser for TeX equations. It parses equations to postfix (reverse polish) notation and can build abstract syntax tree (AST). Also it can render images via latex.}
|
13
|
+
s.description = %q{A ruby parser for TeX equations. It parses equations to postfix (reverse polish) notation and can build abstract syntax tree (AST). Also it can render images via latex. Requres modern ruby 1.9.x due to because of using advanced oniguruma regex engine}
|
14
|
+
|
15
|
+
s.has_rdoc = true
|
16
|
+
s.rdoc_options = ['--main', 'README.rdoc']
|
17
|
+
|
18
|
+
s.required_ruby_version = '>= 1.9'
|
14
19
|
|
15
20
|
s.rubyforge_project = "calculus"
|
16
21
|
|
data/lib/calculus.rb
CHANGED
@@ -4,7 +4,14 @@ require 'calculus/latex'
|
|
4
4
|
require 'calculus/expression'
|
5
5
|
|
6
6
|
module Calculus
|
7
|
+
# Raised when parser encounter invalid character
|
7
8
|
class ParserError < Exception; end
|
9
|
+
|
10
|
+
# Raised when <tt>Expression</tt> detects during calculation that
|
11
|
+
# there are unbound variables presented
|
8
12
|
class UnboundVariableError < Exception; end
|
13
|
+
|
14
|
+
# Raised when <tt>LaTeX</tt> mixin detect missing binaries for images
|
15
|
+
# rendering.
|
9
16
|
class CommandNotFoundError < Exception; end
|
10
17
|
end
|
data/lib/calculus/expression.rb
CHANGED
@@ -2,15 +2,48 @@ require 'digest/sha1'
|
|
2
2
|
|
3
3
|
module Calculus
|
4
4
|
|
5
|
+
# This class represent some expression and optionaly transform it to
|
6
|
+
# the postfix notation for later analysis.
|
7
|
+
#
|
8
|
+
# Expression can introduce variables which could be substituted later
|
9
|
+
#
|
10
|
+
# exp = Expression.new("x + 2 * 4")
|
11
|
+
# exp.to_s #=> "x + 2 * 4"
|
12
|
+
# exp.calculate # raises UnboundVariableError
|
13
|
+
# exp["x"] = 5
|
14
|
+
# exp.to_s #=> "5 + 2 * 4"
|
15
|
+
# exp.calculate #=> 40
|
16
|
+
#
|
5
17
|
class Expression
|
6
18
|
include Latex
|
7
19
|
|
20
|
+
# Represents unique identifier of expression. Should be changed when
|
21
|
+
# some variables are binding
|
8
22
|
attr_reader :sha1
|
23
|
+
|
24
|
+
# Source expression string
|
9
25
|
attr_reader :source
|
10
26
|
|
27
|
+
# Array with postfix notation of expression
|
11
28
|
attr_reader :postfix_notation
|
12
29
|
alias :rpn :postfix_notation
|
13
30
|
|
31
|
+
# Initialize instance with given string expression.
|
32
|
+
#
|
33
|
+
# It is possible to skip parser.
|
34
|
+
#
|
35
|
+
# # raises Calculus::ParserError: Invalid character...
|
36
|
+
# x = Expression.new("\sum_{i=1}^n \omega_i \x_i")
|
37
|
+
# # just stores source string and allows rendering to PNG
|
38
|
+
# x = Expression.new("\sum_{i=1}^n \omega_i \x_i", :parse => false)
|
39
|
+
# x.parsed? #=> false
|
40
|
+
#
|
41
|
+
# It raises ArgumentError if there are more than one equal sign
|
42
|
+
# because if you need to represent the system of equations you
|
43
|
+
# should you two instances of <tt>Expression</tt> class and no
|
44
|
+
# equals sign for just calculation.
|
45
|
+
#
|
46
|
+
# Also it initializes SHA1 fingerprint of particular expression
|
14
47
|
def initialize(source, options = {})
|
15
48
|
options = {:parse => true}.merge(options)
|
16
49
|
@source = source
|
@@ -20,30 +53,46 @@ module Calculus
|
|
20
53
|
update_sha1
|
21
54
|
end
|
22
55
|
|
56
|
+
# Returns <tt>true</tt> when postfix notation has been built
|
23
57
|
def parsed?
|
24
58
|
!@postfix_notation.empty?
|
25
59
|
end
|
26
60
|
|
61
|
+
# Returns <tt>true</tt> if there equals sign presented
|
62
|
+
def equation?
|
63
|
+
@postfix_notation.include?(:eql)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns array of strings with variable names
|
27
67
|
def variables
|
28
68
|
@variables.keys
|
29
69
|
end
|
30
70
|
|
71
|
+
# Returns array of variables which have nil value
|
31
72
|
def unbound_variables
|
32
73
|
@variables.keys.select{|k| @variables[k].nil?}
|
33
74
|
end
|
34
75
|
|
76
|
+
# Getter for given variable.
|
77
|
+
# Raises an <tt>Argument</tt> exception when there no such variable.
|
35
78
|
def [](name)
|
36
79
|
raise ArgumentError, "No such variable defined: #{name}" unless @variables.keys.include?(name)
|
37
80
|
@variables[name]
|
38
81
|
end
|
39
82
|
|
83
|
+
# Setter for given variable.
|
84
|
+
# Raises an <tt>Argument</tt> exception when there no such variable.
|
40
85
|
def []=(name, value)
|
41
86
|
raise ArgumentError, "No such variable defined: #{name}" unless @variables.keys.include?(name)
|
42
87
|
@variables[name] = value
|
43
88
|
update_sha1
|
44
89
|
end
|
45
90
|
|
46
|
-
|
91
|
+
# Perform traverse along postfix notation. Yields <tt>operation</tt>
|
92
|
+
# with <tt>left</tt> and <tt>right</tt> operands and the latest
|
93
|
+
# argument is the current state of <tt>stack</tt> (*note* you can
|
94
|
+
# amend this stack from outside)
|
95
|
+
def traverse(&block) # :yields: operation, left, right, stack
|
47
96
|
stack = []
|
48
97
|
@postfix_notation.each do |node|
|
49
98
|
case node
|
@@ -59,6 +108,16 @@ module Calculus
|
|
59
108
|
stack.pop
|
60
109
|
end
|
61
110
|
|
111
|
+
# Traverse postfix notation and calculate the actual value of
|
112
|
+
# expression. Raises <tt>NotImplementedError</tt> when equation
|
113
|
+
# detected (currently it cannot solve equation) and
|
114
|
+
# <tt>UnboundVariableError</tt> if there unbound variables found.
|
115
|
+
#
|
116
|
+
# *Note* that there some rounding errors here in root operation
|
117
|
+
# because of general approach to calculate it:
|
118
|
+
#
|
119
|
+
# 1000 ** (1.0 / 3) #=> 9.999999999999998
|
120
|
+
#
|
62
121
|
def calculate
|
63
122
|
raise NotImplementedError, "Equation detected. This class can't calculate equations yet." if equation?
|
64
123
|
raise UnboundVariableError, "Can't calculate. Unbound variables found: #{unbound_variables.join(', ')}" unless unbound_variables.empty?
|
@@ -75,10 +134,12 @@ module Calculus
|
|
75
134
|
end
|
76
135
|
end
|
77
136
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
137
|
+
# Builds abstract syntax tree (AST) as alternative expression
|
138
|
+
# notation. Return nested array where first member is an operation
|
139
|
+
# and the other operands
|
140
|
+
#
|
141
|
+
# Calculus::Expression.new("x + 2 * 4").ast #=> [:plus, "x", [:mul, 2, 4]]
|
142
|
+
#
|
82
143
|
def abstract_syntax_tree
|
83
144
|
traverse do |operation, left, right, stack|
|
84
145
|
[operation, left, right]
|
@@ -86,6 +147,8 @@ module Calculus
|
|
86
147
|
end
|
87
148
|
alias :ast :abstract_syntax_tree
|
88
149
|
|
150
|
+
# Returns string representation of expression. Substitutes bound
|
151
|
+
# variables.
|
89
152
|
def to_s
|
90
153
|
result = source.dup
|
91
154
|
(variables - unbound_variables).each do |var|
|
@@ -97,16 +160,21 @@ module Calculus
|
|
97
160
|
result
|
98
161
|
end
|
99
162
|
|
100
|
-
def inspect
|
163
|
+
def inspect # :nodoc:
|
101
164
|
"#<Expression:#{@sha1} postfix_notation=#{@postfix_notation.inspect} variables=#{@variables.inspect}>"
|
102
165
|
end
|
103
166
|
|
104
167
|
protected
|
105
168
|
|
169
|
+
# Extracts variables from postfix notation and returns <tt>Hash</tt>
|
170
|
+
# object with keys corresponding to variables and nil initial
|
171
|
+
# values.
|
106
172
|
def extract_variables
|
107
173
|
@postfix_notation.select{|node| node.kind_of? String}.inject({}){|h, v| h[v] = nil; h}
|
108
174
|
end
|
109
175
|
|
176
|
+
# Update SHA1 fingerprint. Used during initialization and when
|
177
|
+
# variables is bounding.
|
110
178
|
def update_sha1
|
111
179
|
@sha1 = Digest::SHA1.hexdigest([@postfix_notation, @variables].map(&:inspect).join('-'))
|
112
180
|
end
|
data/lib/calculus/latex.rb
CHANGED
@@ -2,8 +2,13 @@ require 'tmpdir'
|
|
2
2
|
|
3
3
|
module Calculus
|
4
4
|
|
5
|
+
# Renders expression to PNG image using <tt>latex<tt> and
|
6
|
+
# <tt>dvipng</tt>
|
5
7
|
module Latex
|
6
8
|
|
9
|
+
# Basic latex template which use packages <tt>amsmath</tt> and
|
10
|
+
# <tt>amssymb</tt> from standard distributive and set off expression
|
11
|
+
# with <tt>$$</tt>.
|
7
12
|
TEMPLATE = <<-EOT.gsub(/^\s+/, '')
|
8
13
|
\\documentclass{article}
|
9
14
|
\\usepackage{amsmath,amssymb}
|
@@ -13,6 +18,16 @@ module Calculus
|
|
13
18
|
\\end{document}
|
14
19
|
EOT
|
15
20
|
|
21
|
+
# Render image from source expression string. It is possible to pass
|
22
|
+
# <tt>background</tt> color (default: <tt>'White'</tt>) and
|
23
|
+
# <tt>density</tt> (default: <tt>700</tt>). See <tt>dvipng(1)</tt>
|
24
|
+
# page for details.
|
25
|
+
#
|
26
|
+
# Raises <tt>CommandNotFound</tt> exception when some tools not
|
27
|
+
# available.
|
28
|
+
#
|
29
|
+
# Returns path to images. *Note* that caller should take care about
|
30
|
+
# this file.
|
16
31
|
def to_png(background = 'White', density = 700)
|
17
32
|
raise CommandNotFoundError, "Required commands missing: #{missing_commands.join(', ')} in PATH. (#{ENV['PATH']})" unless missing_commands.empty?
|
18
33
|
|
@@ -29,6 +44,8 @@ module Calculus
|
|
29
44
|
File.unlink("#{sha1}.dvi") if File.exists?("#{sha1}.dvi")
|
30
45
|
end
|
31
46
|
|
47
|
+
# Check LaTeX toolchain availability and returns array of missing
|
48
|
+
# tools
|
32
49
|
def missing_commands
|
33
50
|
commands = []
|
34
51
|
commands << "latex" unless can_run?("latex -v")
|
@@ -38,6 +55,7 @@ module Calculus
|
|
38
55
|
|
39
56
|
protected
|
40
57
|
|
58
|
+
# Trial command and check if return code is zero
|
41
59
|
def can_run?(command)
|
42
60
|
`#{command} 2>&1`
|
43
61
|
$?.exitstatus.zero?
|
data/lib/calculus/parser.rb
CHANGED
@@ -2,15 +2,62 @@ require 'strscan'
|
|
2
2
|
|
3
3
|
module Calculus
|
4
4
|
|
5
|
+
# Parses string with expression or equation and builds postfix
|
6
|
+
# notation. It supprorts following operators (ordered by precedence
|
7
|
+
# from the highest to the lowest):
|
8
|
+
#
|
9
|
+
# +:sqrt+, +:exp+:: root and exponent operations. Could be written as
|
10
|
+
# <tt>\sqrt[degree]{radix}</tt> and <tt>x^y</tt>.
|
11
|
+
# +:div+, +:mul+:: division and multiplication. There are set of
|
12
|
+
# syntaxes accepted. To make division operator you
|
13
|
+
# can use <tt>num/denum</tt> or
|
14
|
+
# <tt>\frac{num}{denum}</tt>. For multiplication
|
15
|
+
# there accepted <tt>*</tt> and also two TeX
|
16
|
+
# symbols: <tt>\cdot</tt> and <tt>\times</tt>.
|
17
|
+
# +:plus+, +:minus+:: summation and substraction. Here you can use
|
18
|
+
# plain <tt>+</tt> and <tt>-</tt>
|
19
|
+
# +:eql+:: equals sign it has the lowest priority so it to
|
20
|
+
# be calculated in last turn.
|
21
|
+
#
|
22
|
+
# Also it is possible to use parentheses for grouping. There are plain
|
23
|
+
# <tt>(</tt>, <tt>)</tt> acceptable and also <tt>\(</tt>, <tt>\)</tt>
|
24
|
+
# which are differ only for latex diplay. Parser doesn't distinguish
|
25
|
+
# these two styles so you could give expression with visually
|
26
|
+
# unbalanced parentheses (matter only for image generation. Consider
|
27
|
+
# the example:
|
28
|
+
#
|
29
|
+
# Parser.new("(2 + 3) * 4").parse #=> [2, 3, :plus, 4, :mul]
|
30
|
+
# Parser.new("(2 + 3\) * 4").parse #=> [2, 3, :plus, 4, :mul]
|
31
|
+
#
|
32
|
+
# This two examples will yield the same notation, but make issue
|
33
|
+
# during display.
|
34
|
+
#
|
35
|
+
# Numbers could be given as a floats and as a integer
|
36
|
+
#
|
37
|
+
# Parser.new("3 + 4.0 * 4.5e-10") #=> [3, 4.0, 4.5e-10, :mul, :plus]
|
38
|
+
#
|
39
|
+
# Symbols could be just alpha-numeric values with optional subscript
|
40
|
+
# index
|
41
|
+
#
|
42
|
+
# Parser.new("x_3 + y * E") #=> ["x_3", "y", "E", :mul, :plus]
|
43
|
+
#
|
5
44
|
class Parser < StringScanner
|
6
45
|
attr_accessor :operators
|
7
46
|
|
47
|
+
# Initialize parser with given source string. It could simple
|
48
|
+
# (native expression like <tt>2 + 3 * (4 / 3)</tt>, but also in TeX
|
49
|
+
# style <tt>2 + 3 \cdot \frac{4}{3}.
|
8
50
|
def initialize(source)
|
9
51
|
@operators = {:sqrt => 3, :exp => 3, :div => 2, :mul => 2, :plus => 1, :minus => 1, :eql => 0}
|
10
52
|
|
11
53
|
super(source.dup)
|
12
54
|
end
|
13
55
|
|
56
|
+
# Run parse cycle. It builds postfix notation (aka reverse polish
|
57
|
+
# notation). Returns array with operations with operands.
|
58
|
+
#
|
59
|
+
# Parser.new("2 + 3 * 4").parse #=> [2, 3, 4, :mul, :plus]
|
60
|
+
# Parser.new("(\frac{2}{3} + 3) * 4").parse #=> [2, 3, :div, 3, :plus, 4, :mul]
|
14
61
|
def parse
|
15
62
|
exp = []
|
16
63
|
stack = []
|
@@ -37,6 +84,12 @@ module Calculus
|
|
37
84
|
exp
|
38
85
|
end
|
39
86
|
|
87
|
+
# Fetch next token from source string. Skips any whitespaces
|
88
|
+
# matching regexp <tt>/\s+/</tt> and returs <tt>nil</tt> at when
|
89
|
+
# meet the end of string.
|
90
|
+
#
|
91
|
+
# Raises <tt>ParseError</tt> when encounter invalid character
|
92
|
+
# sequence.
|
40
93
|
def fetch_token
|
41
94
|
skip(/\s+/)
|
42
95
|
return nil if(eos?)
|
data/lib/calculus/version.rb
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: calculus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.1.
|
5
|
+
version: 0.1.4
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Sergey Avseyev
|
@@ -10,11 +10,11 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-05-
|
13
|
+
date: 2011-05-13 00:00:00 +03:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|
17
|
-
description: A ruby parser for TeX equations. It parses equations to postfix (reverse polish) notation and can build abstract syntax tree (AST). Also it can render images via latex.
|
17
|
+
description: A ruby parser for TeX equations. It parses equations to postfix (reverse polish) notation and can build abstract syntax tree (AST). Also it can render images via latex. Requres modern ruby 1.9.x due to because of using advanced oniguruma regex engine
|
18
18
|
email:
|
19
19
|
- sergey.avseyev@gmail.com
|
20
20
|
executables: []
|
@@ -42,8 +42,9 @@ homepage: http://avsej.net/calculus
|
|
42
42
|
licenses: []
|
43
43
|
|
44
44
|
post_install_message:
|
45
|
-
rdoc_options:
|
46
|
-
|
45
|
+
rdoc_options:
|
46
|
+
- --main
|
47
|
+
- README.rdoc
|
47
48
|
require_paths:
|
48
49
|
- lib
|
49
50
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -51,7 +52,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
52
|
requirements:
|
52
53
|
- - ">="
|
53
54
|
- !ruby/object:Gem::Version
|
54
|
-
version: "
|
55
|
+
version: "1.9"
|
55
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
57
|
none: false
|
57
58
|
requirements:
|