calculus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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