calculus 0.1.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.
@@ -0,0 +1,10 @@
1
+ *.bundle
2
+ *.gem
3
+ *.o
4
+ *.so
5
+ .bundle
6
+ Gemfile.lock
7
+ ext/**/Makefile
8
+ ext/**/mkmf.log
9
+ pkg/*
10
+ tmp/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.9.2@calculus --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in calculus.gemspec
4
+ gemspec
@@ -0,0 +1,41 @@
1
+ == Calculus
2
+
3
+ Calculus is utility library which allow to parse some subset of latex equations and store them in {Postfix notation}[http://en.wikipedia.org/wiki/Reverse_Polish_notation] It also allows translate it to {Abstract syntax tree}[http://en.wikipedia.org/wiki/Abstract_syntax_tree] and calculate (implemented for simple expressions).
4
+
5
+ == Installation
6
+
7
+ gem install calculus
8
+
9
+ == Examples
10
+
11
+ 001:0> require 'calculus'
12
+ true
13
+ 002:0> exp = Calculus::Expression.new("2 + 3 * x")
14
+ #<Expression:f46e77a9377ed2d5a9da768496a7e1c20be51bfe postfix_notation=[2, 3, "x", :mul, :plus] variables={"x"=>nil}>
15
+ 003:0> exp.postfix_notation
16
+ [2, 3, "x", :mul, :plus]
17
+ 004:0> exp.abstract_syntax_tree
18
+ [:plus, 2, [:mul, 3, "x"]]
19
+ 005:0> exp.variables
20
+ ["x"]
21
+ 006:0> exp.unbound_variables
22
+ ["x"]
23
+ 007:0> exp["x"] = 5
24
+ 5
25
+ 008:0> exp.unbound_variables
26
+ []
27
+ 009:0> exp.calculate
28
+ 17
29
+
30
+ You can also render expression to PNG image if you have <tt>latex</tt> and <tt>dvipng</tt> installed.
31
+
32
+ 010:0> Calculus::Expression.new("2 + 3 \\cdot x").to_png
33
+ "/tmp/d20110512-16457-dhxt71/f46e77a9377ed2d5a9da768496a7e1c20be51bfe.png"
34
+
35
+ {2 + 3 \cdot x}[http://files.avsej.net/expression.png]
36
+
37
+ Don't forget to cleanup file after using.
38
+
39
+ == Hacking
40
+
41
+ Just fork and pull request.
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new do |test|
6
+ test.pattern = 'test/*_test.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "calculus/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "calculus"
7
+ s.version = Calculus::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Sergey Avseyev"]
10
+ s.email = ["sergey.avseyev@gmail.com"]
11
+ s.homepage = "http://avsej.net/calculus"
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.}
14
+
15
+ s.rubyforge_project = "calculus"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
@@ -0,0 +1,10 @@
1
+ require 'calculus/version'
2
+ require 'calculus/parser'
3
+ require 'calculus/latex'
4
+ require 'calculus/expression'
5
+
6
+ module Calculus
7
+ class ParserError < Exception; end
8
+ class UnboundVariableError < Exception; end
9
+ class CommandNotFoundError < Exception; end
10
+ end
@@ -0,0 +1,102 @@
1
+ require 'digest/sha1'
2
+
3
+ module Calculus
4
+
5
+ class Expression
6
+ include Latex
7
+
8
+ attr_reader :sha1
9
+ attr_reader :source
10
+
11
+ attr_reader :postfix_notation
12
+ alias :rpn :postfix_notation
13
+
14
+ def initialize(source)
15
+ @postfix_notation = Parser.new(@source = source).parse
16
+ @variables = extract_variables
17
+ update_sha1
18
+ end
19
+
20
+ def variables
21
+ @variables.keys
22
+ end
23
+
24
+ def unbound_variables
25
+ @variables.keys.select{|k| @variables[k].nil?}
26
+ end
27
+
28
+ def [](name)
29
+ raise ArgumentError, "No such variable defined: #{name}" unless @variables.keys.include?(name)
30
+ @variables[name]
31
+ end
32
+
33
+ def []=(name, value)
34
+ raise ArgumentError, "No such variable defined: #{name}" unless @variables.keys.include?(name)
35
+ @variables[name] = value
36
+ update_sha1
37
+ end
38
+
39
+ def traverse(&block)
40
+ stack = []
41
+ @postfix_notation.each do |node|
42
+ case node
43
+ when Symbol
44
+ operation, right, left = node, stack.pop, stack.pop
45
+ stack.push(yield(operation, left, right, stack))
46
+ when Numeric
47
+ stack.push(node)
48
+ when String
49
+ stack.push(@variables[node] || node)
50
+ end
51
+ end
52
+ stack.pop
53
+ end
54
+
55
+ def calculate
56
+ raise NotImplementedError, "Equation detected. This class can't calculate equations yet." if equation?
57
+ raise UnboundVariableError, "Can't calculate. Unbound variables found: #{unbound_variables.join(', ')}" unless unbound_variables.empty?
58
+
59
+ traverse do |operation, left, right, stack|
60
+ case operation
61
+ when :sqrt then left ** (1.0 / right) # could cause some rounding errors
62
+ when :exp then left ** right
63
+ when :plus then left + right
64
+ when :minus then left - right
65
+ when :mul then left * right
66
+ when :div then left / right
67
+ end
68
+ end
69
+ end
70
+
71
+ def equation?
72
+ @postfix_notation.include?(:eql)
73
+ end
74
+
75
+ def abstract_syntax_tree
76
+ traverse do |operation, left, right, stack|
77
+ [operation, left, right]
78
+ end
79
+ end
80
+ alias :ast :abstract_syntax_tree
81
+
82
+ def to_s
83
+ source
84
+ end
85
+
86
+ def inspect
87
+ "#<Expression:#{@sha1} postfix_notation=#{@postfix_notation.inspect} variables=#{@variables.inspect}>"
88
+ end
89
+
90
+ protected
91
+
92
+ def extract_variables
93
+ @postfix_notation.select{|node| node.kind_of? String}.inject({}){|h, v| h[v] = nil; h}
94
+ end
95
+
96
+ def update_sha1
97
+ @sha1 = Digest::SHA1.hexdigest([@postfix_notation, @variables].map(&:inspect).join('-'))
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,47 @@
1
+ require 'tmpdir'
2
+
3
+ module Calculus
4
+
5
+ module Latex
6
+
7
+ TEMPLATE = <<-EOT.gsub(/^\s+/, '')
8
+ \\documentclass{article}
9
+ \\usepackage{amsmath,amssymb}
10
+ \\begin{document}
11
+ \\thispagestyle{empty}
12
+ $$ # $$
13
+ \\end{document}
14
+ EOT
15
+
16
+ def to_png(density = 700)
17
+ raise CommandNotFoundError, "Required commands missing: #{missing_commands.join(', ')} in PATH. (#{ENV['PATH']})" unless missing_commands.empty?
18
+
19
+ temp_path = Dir.mktmpdir
20
+ Dir.chdir(temp_path) do
21
+ File.open("#{sha1}.tex", 'w') do |f|
22
+ f.write(TEMPLATE.sub('#', self.to_s))
23
+ end
24
+ `latex -interaction=nonstopmode #{sha1}.tex && dvipng -q -T tight -bg White -D #{density.to_i} -o #{sha1}.png #{sha1}.dvi`
25
+ end
26
+ return File.join(temp_path, "#{sha1}.png") if $?.exitstatus.zero?
27
+ ensure
28
+ File.unlink("#{sha1}.tex") if File.exists?("#{sha1}.tex")
29
+ File.unlink("#{sha1}.dvi") if File.exists?("#{sha1}.dvi")
30
+ end
31
+
32
+ def missing_commands
33
+ commands = []
34
+ commands << "latex" unless can_run?("latex -v")
35
+ commands << "dvipng" unless can_run?("dvipng -v")
36
+ commands
37
+ end
38
+
39
+ protected
40
+
41
+ def can_run?(command)
42
+ `#{command} 2>&1`
43
+ $?.exitstatus.zero?
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,91 @@
1
+ require 'strscan'
2
+
3
+ module Calculus
4
+
5
+ class Parser < StringScanner
6
+ attr_accessor :operators
7
+
8
+ def initialize(source)
9
+ @operators = {:sqrt => 3, :exp => 3, :div => 2, :mul => 2, :plus => 1, :minus => 1, :eql => 0}
10
+
11
+ super(source)
12
+ end
13
+
14
+ def parse
15
+ exp = []
16
+ stack = []
17
+ while true
18
+ case token = fetch_token
19
+ when :open
20
+ stack.push(token)
21
+ when :close
22
+ exp << stack.pop while operators.keys.include?(stack.last)
23
+ stack.pop if stack.last == :open
24
+ when :plus, :minus, :mul, :div, :exp, :sqrt, :eql
25
+ exp << stack.pop while operators.keys.include?(stack.last) && operators[stack.last] >= operators[token]
26
+ stack.push(token)
27
+ when Numeric, String
28
+ exp << token
29
+ when nil
30
+ break
31
+ else
32
+ raise ArgumentError, "Unexpected symbol: #{token.inspect}"
33
+ end
34
+ end
35
+ exp << stack.pop while stack.last && stack.last != :open
36
+ raise ArgumentError, "Missing closing parentheses: #{stack.join(', ')}" unless stack.empty?
37
+ exp
38
+ end
39
+
40
+ def fetch_token
41
+ skip(/\s+/)
42
+ return nil if(eos?)
43
+
44
+ token = nil
45
+ scanning = true
46
+ while(scanning)
47
+ scanning = false
48
+ token = case
49
+ when scan(/=/)
50
+ :eql
51
+ when scan(/\*|\\times|\\cdot/)
52
+ :mul
53
+ when scan(/\\frac\s*(?<num>\{(?:(?>[^{}])|\g<num>)*\})\s*(?<denom>\{(?:(?>[^{}])|\g<denom>)*\})/)
54
+ num, denom = [self[1], self[2]].map{|v| v.gsub(/^{|}$/, '')}
55
+ string[pos, 0] = "(#{num}) / (#{denom}) "
56
+ scanning = true
57
+ when scan(/\//)
58
+ :div
59
+ when scan(/\+/)
60
+ :plus
61
+ when scan(/\^/)
62
+ :exp
63
+ when scan(/-/)
64
+ :minus
65
+ when scan(/sqrt/)
66
+ :sqrt
67
+ when scan(/\\sqrt\s*(?<deg>\[(?:(?>[^\[\]])|\g<deg>)*\])?\s*(?<rad>\{(?:(?>[^{}])|\g<rad>)*\})/)
68
+ deg = (self[1] || "2").gsub(/^\[|\]$/, '')
69
+ rad = self[2].gsub(/^{|}$/, '')
70
+ string[pos, 0] = "(#{rad}) sqrt (#{deg}) "
71
+ scanning = true
72
+ when scan(/\(|\\left\(/)
73
+ :open
74
+ when scan(/\)|\\right\)/)
75
+ :close
76
+ when scan(/[\-\+]? [0-9]+ ((e[\-\+]?[0-9]+)| (\.[0-9]+(e[\-\+]?[0-9]+)?))/x)
77
+ matched.to_f
78
+ when scan(/[\-\+]?[0-9]+/)
79
+ matched.to_i
80
+ when scan(/([a-z0-9]+(?>_[a-z0-9]+)?)/i)
81
+ matched
82
+ else
83
+ raise ParserError, "Invalid character at position #{pos} near '#{peek(20)}'."
84
+ end
85
+ end
86
+
87
+ return token
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Calculus
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,93 @@
1
+ require 'minitest/autorun'
2
+ require 'calculus'
3
+
4
+ class TestExpression < MiniTest::Unit::TestCase
5
+
6
+ def test_that_it_extract_variables_properly
7
+ assert_equal ["x", "y"], expression("x + 2^x = y").variables
8
+ end
9
+
10
+ def test_that_it_empty_variables_array_if_they_are_absent
11
+ assert_equal [], expression("4 + 2^3 = 12").variables
12
+ end
13
+
14
+ def test_that_variables_can_be_read_using_square_brackets
15
+ exp = expression("4 + x^3 = 12")
16
+ exp.instance_variable_get("@variables")["x"] = 2
17
+ assert_equal 2, exp["x"]
18
+ end
19
+
20
+ def test_that_variables_can_be_written_using_square_brackets
21
+ exp = expression("4 + x^3 = 12")
22
+ exp["x"] = 2
23
+ assert_equal 2, exp.instance_variable_get("@variables")["x"]
24
+ end
25
+
26
+ def test_that_it_raises_exception_for_unexistent_variable
27
+ exp = expression("4 + x^3 = 12")
28
+ assert_raises(ArgumentError) { exp["y"] }
29
+ assert_raises(ArgumentError) { exp["y"] = 3 }
30
+ end
31
+
32
+ def test_that_it_initializes_variables_with_nils
33
+ exp = expression("x + 2^x = y")
34
+ assert_nil exp["x"]
35
+ assert_nil exp["y"]
36
+ end
37
+
38
+ def test_that_it_gives_access_to_postfix_notation
39
+ exp = expression("x + 2^x = y")
40
+ assert_equal ["x", 2, "x", :exp, :plus, "y", :eql], exp.postfix_notation
41
+ assert_equal ["x", 2, "x", :exp, :plus, "y", :eql], exp.rpn
42
+ end
43
+
44
+ def test_that_it_gives_access_to_abstract_syntax_tree
45
+ exp = expression("(2 + 3) * 4")
46
+ assert_equal [:mul, [:plus, 2, 3], 4], exp.abstract_syntax_tree
47
+ assert_equal [:mul, [:plus, 2, 3], 4], exp.ast
48
+ end
49
+
50
+ def test_that_it_gives_list_of_unbound_variables
51
+ exp = expression("x + 2^x = y")
52
+ assert_equal ["x", "y"], exp.unbound_variables
53
+ exp["x"] = 3
54
+ assert_equal ["y"], exp.unbound_variables
55
+ exp["y"] = 2
56
+ assert_equal [], exp.unbound_variables
57
+ end
58
+
59
+ def test_that_calculate_raises_unbound_variable_error_when_some_variables_missing
60
+ assert_raises(Calculus::UnboundVariableError) { expression("x + 2 * 3").calculate }
61
+ end
62
+
63
+ def test_that_calculate_raises_not_implemented_error_when_detects_equation
64
+ assert_raises(NotImplementedError) { expression("x + 2 = 7").calculate }
65
+ end
66
+
67
+ def test_that_it_calclulates_simple_expressions
68
+ assert_equal 8, expression("2 \\cdot 4").calculate
69
+ assert_equal 6, expression("2 + 2 * 2").calculate
70
+ assert_equal 4, expression("\\frac{4}{2} * 2").calculate
71
+ assert_equal 16, expression("4^2").calculate
72
+ end
73
+
74
+ def test_that_it_substitutes_variables_during_calculation
75
+ exp = expression("2 + 2 * x")
76
+ exp["x"] = 2
77
+ assert_equal 6, exp.calculate
78
+ end
79
+
80
+ def test_that_it_refresh_sha1_sub_when_variables_get_filled
81
+ exp = expression("2 \\cdot x = 4")
82
+ old_sha1 = exp.sha1
83
+ exp["x"] = 2
84
+ refute_equal old_sha1, exp.sha1
85
+ end
86
+
87
+ protected
88
+
89
+ def expression(input)
90
+ Calculus::Expression.new(input)
91
+ end
92
+
93
+ end
@@ -0,0 +1,70 @@
1
+ require 'minitest/autorun'
2
+ require 'calculus'
3
+
4
+ class TestParser < MiniTest::Unit::TestCase
5
+
6
+ def test_that_it_parses_simple_arithmetic
7
+ assert_equal [1, 2, :plus], parse("1+2")
8
+ end
9
+
10
+ def test_that_it_skips_spaces
11
+ assert_equal [1, 2, :plus], parse("1 + 2")
12
+ assert_equal [4, 2, :exp], parse(" 4 ^ 2 ")
13
+ assert_equal [4, 2, :sqrt], parse("\\sqrt [ 2 ] { 4 }")
14
+ assert_equal [5, 4, :div], parse("\\frac { 5 } { 4 }")
15
+ end
16
+
17
+ def test_that_it_properly_parses_square_root
18
+ assert_equal [2, 4, 2, :sqrt, :mul], parse("2 * \\sqrt{4}")
19
+ assert_equal [8, 3, :sqrt], parse("\\sqrt[3]{8}")
20
+ assert_equal [8, 3, 2, :plus, :sqrt], parse("\\sqrt[3+2]{8}")
21
+ end
22
+
23
+ def test_that_it_properly_parses_fractions
24
+ assert_equal [8, 3, :div], parse("\\frac{8}{3}")
25
+ assert_equal [3, 1, :plus, 3, 1, :minus, :div], parse("\\frac{3+1}{3-1}")
26
+ assert_equal [3, 1, :plus, 3, 1, :minus, 4, :mul, :div], parse("\\frac{3+1}{(3-1)*4}")
27
+ end
28
+
29
+ def test_that_it_honours_priorities
30
+ assert_equal [3, 2, 2, :mul, :plus], parse("3+2*2")
31
+ assert_equal [3, 5, 4, :exp, :mul, 2, :plus], parse("3*5^4+2")
32
+ assert_equal [3, 5, :mul, 4, :exp, 2, :plus], parse("(3*5)^4+2")
33
+ assert_equal [3, 5, :mul, 2, :sqrt, 2, :plus], parse("\\sqrt{3*5}+2")
34
+ end
35
+
36
+ def test_that_it_allows_parentesis
37
+ assert_equal [3, 2, :plus, 2, :mul], parse("(3+2)*2")
38
+ assert_equal [3, 2, :plus, 2, :mul], parse("\\left(3+2\\right)*2")
39
+ end
40
+
41
+ def test_that_it_properly_parses_floats
42
+ assert_equal [1.2], parse("1.2")
43
+ assert_equal [1.2e10], parse("1.2e10")
44
+ assert_equal [1.2e-10], parse("1.2e-10")
45
+ end
46
+
47
+ def test_that_it_allows_nesting
48
+ assert_equal [8, 2, :sqrt, 3, :div], parse("\\frac{\\sqrt{8}}{3}")
49
+ assert_equal [4, 8, 3, :div, :sqrt], parse("\\sqrt[\\frac{8}{3}]{4}")
50
+ end
51
+
52
+ def test_that_it_recognizes_equals_sign
53
+ assert_equal [2, 4, :mul, 2, :div, 16, 2, :sqrt, :eql], parse("2 \\cdot \\frac{4}{2} = \\sqrt{16}")
54
+ end
55
+
56
+ def test_that_it_recognizes_variables
57
+ assert_equal [2, "x", :mul, 16, :eql], parse("2 \\cdot x = 16")
58
+ assert_equal [2, "x_i", :mul, 16, :eql], parse("2 \\cdot x_i = 16")
59
+ assert_equal [2, "x_2", :mul, 16, :eql], parse("2 \\cdot x_2 = 16")
60
+ assert_equal [2, "x2", :mul, 16, :eql], parse("2 \\cdot x2 = 16")
61
+ assert_raises(Calculus::ParserError) { assert_equal [2, "x__2", :mul, 16, :eql], parse("2 \\cdot x__2 = 16") }
62
+ end
63
+
64
+ protected
65
+
66
+ def parse(input)
67
+ Calculus::Parser.new(input).parse
68
+ end
69
+
70
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: calculus
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Sergey Avseyev
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-12 00:00:00 +03:00
14
+ default_executable:
15
+ dependencies: []
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.
18
+ email:
19
+ - sergey.avseyev@gmail.com
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files: []
25
+
26
+ files:
27
+ - .gitignore
28
+ - .rvmrc
29
+ - Gemfile
30
+ - README.rdoc
31
+ - Rakefile
32
+ - calculus.gemspec
33
+ - lib/calculus.rb
34
+ - lib/calculus/expression.rb
35
+ - lib/calculus/latex.rb
36
+ - lib/calculus/parser.rb
37
+ - lib/calculus/version.rb
38
+ - test/expression_test.rb
39
+ - test/parser_test.rb
40
+ has_rdoc: true
41
+ homepage: http://avsej.net/calculus
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ requirements: []
62
+
63
+ rubyforge_project: calculus
64
+ rubygems_version: 1.6.2
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: A ruby parser for TeX equations
68
+ test_files:
69
+ - test/expression_test.rb
70
+ - test/parser_test.rb