arithmetic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in arithmetic.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,24 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ arithmetic (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.1.3)
10
+ rspec (2.11.0)
11
+ rspec-core (~> 2.11.0)
12
+ rspec-expectations (~> 2.11.0)
13
+ rspec-mocks (~> 2.11.0)
14
+ rspec-core (2.11.1)
15
+ rspec-expectations (2.11.3)
16
+ diff-lcs (~> 1.1.3)
17
+ rspec-mocks (2.11.3)
18
+
19
+ PLATFORMS
20
+ ruby
21
+
22
+ DEPENDENCIES
23
+ arithmetic!
24
+ rspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Sean Kirby
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ arithmetic
2
+ ==========
3
+
4
+ Simple arithmetic evaluator for Ruby
5
+
6
+ Based on http://rosettacode.org/wiki/Arithmetic_evaluation#Ruby but adds:
7
+
8
+ * tests
9
+ * support for nicer errors on invalid expressions
10
+ * optional spaces between operators and operands
11
+ * uses BigDecimal instead of floats
12
+
13
+ Shortcomings:
14
+
15
+ * only supports `+ - * /` operators
16
+ * no localization
17
+
18
+ Usage
19
+ =====
20
+
21
+ expression = Arithmetic.parse("-2 * (1+3.5)")
22
+ expression.eval # => -18
23
+ expression.to_s # => "-2 * (1 + 3.5)"
24
+
25
+ Arithmetic.parse("2 + wtf?") # => raises Arithmetic::InvalidExpression with the
26
+ # original expression as the message
27
+
28
+ I18N support
29
+ ============
30
+
31
+ Please ensure that decimal separators are decimals ('`.`'). Use a gem like Delocalize to perform this conversion.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'arithmetic/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "arithmetic"
8
+ gem.version = Arithmetic::VERSION
9
+ gem.authors = ["Sean Kirby", "Justin Fitzsimmons"]
10
+ gem.email = ["seank@nulogy.com", "justin@fitzsimmons.ca"]
11
+ gem.description = %q{Simple arithmetic calculator for Ruby}
12
+ gem.summary = %q{Simple arithmetic calculator for Ruby}
13
+ gem.homepage = "https://github.com/nulogy/arithmetic"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_development_dependency('rspec')
20
+ end
data/lib/arithmetic.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'bigdecimal'
2
+ require 'arithmetic/expression'
3
+ require 'arithmetic/parser'
4
+ require 'arithmetic/nodes'
5
+ require 'arithmetic/operators'
6
+
7
+ module Arithmetic
8
+ # make lazy?
9
+ def self.parse(expression)
10
+ Expression.new(Parser.new(expression).parse)
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Arithmetic
2
+ class Expression
3
+ def initialize(expression)
4
+ @parsed_expression = expression
5
+ end
6
+
7
+ def eval
8
+ @result ||= @parsed_expression.eval
9
+ end
10
+
11
+ def to_s
12
+ @string ||= @parsed_expression.to_s
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ module Arithmetic
2
+ class OperandNode
3
+ attr_accessor :operand
4
+
5
+ def initialize(operand)
6
+ @operand = operand
7
+ end
8
+
9
+ def to_s(na=nil)
10
+ @operand
11
+ end
12
+
13
+ def eval
14
+ BigDecimal.new(@operand)
15
+ end
16
+ end
17
+
18
+ class OperatorNode
19
+ attr_accessor :operator, :operands
20
+
21
+ def initialize(operator, operands)
22
+ @operator = operator
23
+ @operands = operands
24
+ end
25
+
26
+ def to_s(top=true)
27
+ strs = @operands.map {|o| o.to_s(false) }
28
+
29
+ if @operator.arity == 1
30
+ "#{@operator}#{strs.first}"
31
+ else
32
+ result = [strs.first, @operator, *strs[1..-1]].join(" ")
33
+ result = "(" + result + ")" unless top
34
+ result
35
+ end
36
+ end
37
+
38
+ def eval
39
+ @operator.eval(*@operands.map(&:eval))
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ module Arithmetic
2
+ class Operator
3
+ attr_reader :string, :priority
4
+
5
+ def initialize(string, priority, function)
6
+ @string = string
7
+ @priority = priority
8
+ @function = function
9
+ end
10
+
11
+ def eval(*args)
12
+ @function.call(*args)
13
+ end
14
+
15
+ def to_s
16
+ @string
17
+ end
18
+
19
+ def arity
20
+ @function.arity
21
+ end
22
+ end
23
+
24
+ module Operators
25
+ extend self
26
+
27
+ UNARY_MINUS = Operator.new("-", 2, lambda {|x| -x})
28
+ MINUS = Operator.new("-", 0, lambda {|x, y| x - y})
29
+
30
+ @operators = {
31
+ "+" => Operator.new("+", 0, lambda {|x, y| x + y}),
32
+ "-" => MINUS,
33
+ "*" => Operator.new("*", 1, lambda {|x, y| x * y}),
34
+ "/" => Operator.new("/", 1, lambda {|x, y| x / y})
35
+ }
36
+
37
+ def get(token)
38
+ @operators[token]
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,107 @@
1
+ module Arithmetic
2
+ class Parser
3
+ def initialize(exp)
4
+ @expression = exp.to_s
5
+ @node_stack = []
6
+ end
7
+
8
+ def parse
9
+ tokens = Tokenizer.new.tokenize(@expression)
10
+ op_stack = []
11
+
12
+ tokens.each do |token|
13
+ if token.is_a? Operator
14
+ # clear stack of higher priority operators
15
+ while (!op_stack.empty? &&
16
+ op_stack.last != "(" &&
17
+ op_stack.last.priority >= token.priority)
18
+ push_operator(op_stack.pop)
19
+ end
20
+
21
+ op_stack.push(token)
22
+ elsif token == "("
23
+ op_stack.push(token)
24
+ elsif token == ")"
25
+ while op_stack.last != "("
26
+ push_operator(op_stack.pop)
27
+ end
28
+
29
+ # throw away the '('
30
+ op_stack.pop
31
+ else
32
+ push_operand(token)
33
+ end
34
+ end
35
+
36
+ until op_stack.empty?
37
+ push_operator(op_stack.pop)
38
+ end
39
+
40
+ parsed_expression = @node_stack.pop
41
+ raise InvalidExpression.new(@expression) unless @node_stack.empty?
42
+ parsed_expression
43
+ end
44
+
45
+ private
46
+
47
+ def push_operand(operand)
48
+ raise InvalidExpression.new(@expression) unless is_a_number?(operand)
49
+ @node_stack.push(OperandNode.new(operand))
50
+ end
51
+
52
+ def push_operator(operator)
53
+ raise InvalidExpression.new(@expression) unless operator.is_a?(Operator)
54
+
55
+ operands = []
56
+ operator.arity.times do
57
+ operands.unshift(@node_stack.pop)
58
+ end
59
+ raise InvalidExpression.new(@expression) if operands.any?(&:nil?)
60
+
61
+ @node_stack.push(OperatorNode.new(operator, operands))
62
+ end
63
+
64
+ def is_a_number?(str)
65
+ !!str.match(/^[\d\.]+$/)
66
+ end
67
+ end
68
+
69
+ class Tokenizer
70
+ def tokenize(exp)
71
+ tokens = exp
72
+ .gsub('*', ' * ')
73
+ .gsub('/', ' / ')
74
+ .gsub('+', ' + ')
75
+ .gsub('-', ' - ')
76
+ .gsub('(', ' ( ')
77
+ .gsub(')', ' ) ')
78
+ .split(' ')
79
+ tokens = parse_operators(tokens)
80
+ replace_unary_minus(tokens)
81
+ end
82
+
83
+ private
84
+
85
+ def parse_operators(tokens)
86
+ tokens.map do |token|
87
+ Operators.get(token) || token
88
+ end
89
+ end
90
+
91
+ def replace_unary_minus(tokens)
92
+ new_tokens = []
93
+ tokens.each_with_index do |current_token, i|
94
+ previous_token = tokens[i-1]
95
+ if current_token == Operators::MINUS && (i == 0 || previous_token.is_a?(Operator))
96
+ new_tokens << Operators::UNARY_MINUS
97
+ else
98
+ new_tokens << current_token
99
+ end
100
+ end
101
+ new_tokens
102
+ end
103
+ end
104
+
105
+ class InvalidExpression < Exception
106
+ end
107
+ end
@@ -0,0 +1,3 @@
1
+ module Arithmetic
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ describe Arithmetic do
4
+ it "evaluates simple expressions" do
5
+ test_eval("2").should == 2.0
6
+ end
7
+
8
+ it "evaluates addition" do
9
+ test_eval("2 + 2").should == 4.0
10
+ end
11
+
12
+ it "evaluates subtraction" do
13
+ test_eval("2.1 - 2").should == 0.1
14
+ end
15
+
16
+ it "handles negative numbers" do
17
+ test_eval("3--2").should == 5
18
+ end
19
+
20
+ it "handles negative numbers with parens" do
21
+ test_eval("-(3+2)").should == -5
22
+ end
23
+
24
+ it "handles leading minus signs" do
25
+ test_eval("-3+2").should == -1
26
+ end
27
+
28
+ it "has unary minus take precedence over multiplication" do
29
+ test_eval("-3 * -2").should == 6
30
+ end
31
+
32
+ it "evaluates division" do
33
+ test_eval("10.5 / 5").should == 2.1
34
+ end
35
+
36
+ it "evaluates multiplication" do
37
+ test_eval("2 * 3.1").should == 6.2
38
+ end
39
+
40
+ it "evaluates parens" do
41
+ test_eval("2 * (2.1 + 1)").should == 6.2
42
+ end
43
+
44
+ it "evaluates regardless of whitespace" do
45
+ test_eval("2*(1+\t1)").should == 4
46
+ end
47
+
48
+ it "evaluates order of operations" do
49
+ expect( test_eval("2 * 2.1 + 1 / 2") ).to eq 4.7
50
+ end
51
+
52
+ it "evaluates multiple levels of parens" do
53
+ test_eval("2*(1/(1+3))").should == 0.5
54
+ end
55
+
56
+ it "formats the expression" do
57
+ test_to_s(" -1+\n 2* \t3").should == '-1 + (2 * 3)'
58
+ end
59
+
60
+ it "handles ridiculous precision" do
61
+ test_eval("1.111111111111111111111111111111111111111111 + 2").should == BigDecimal.new('3.111111111111111111111111111111111111111111')
62
+ end
63
+
64
+ it "handles simple numbers" do
65
+ test_eval(2).should == 2
66
+ end
67
+
68
+ context "invalid expressions" do
69
+ it "handles missing operand" do
70
+ exp_should_error "1 *"
71
+ exp_should_error "1 * + 1"
72
+ end
73
+
74
+ it "handles missing operator" do
75
+ exp_should_error "1 2 * 3"
76
+ end
77
+
78
+ it "handles invalid characters" do
79
+ exp_should_error "1 * hi_there!"
80
+ end
81
+
82
+ it "handles invalid operators" do
83
+ exp_should_error "1 & 2"
84
+ end
85
+
86
+ it "handles unmatched leading paren" do
87
+ exp_should_error "(1 + 2"
88
+ end
89
+
90
+ it "handles unmatched trailing paren" do
91
+ exp_should_error "1 + 2)"
92
+ end
93
+ end
94
+
95
+ def exp_should_error(exp)
96
+ expect {test_init exp}.to raise_error Arithmetic::InvalidExpression
97
+ end
98
+
99
+ def test_eval(exp)
100
+ test_init(exp).eval
101
+ end
102
+
103
+ def test_to_s(exp)
104
+ test_init(exp).to_s
105
+ end
106
+
107
+ def test_init(exp)
108
+ Arithmetic::parse(exp)
109
+ end
110
+ end
@@ -0,0 +1 @@
1
+ require 'arithmetic'
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arithmetic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sean Kirby
9
+ - Justin Fitzsimmons
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-10-26 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ description: Simple arithmetic calculator for Ruby
32
+ email:
33
+ - seank@nulogy.com
34
+ - justin@fitzsimmons.ca
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - .gitignore
40
+ - Gemfile
41
+ - Gemfile.lock
42
+ - LICENSE.txt
43
+ - README.md
44
+ - Rakefile
45
+ - arithmetic.gemspec
46
+ - lib/arithmetic.rb
47
+ - lib/arithmetic/expression.rb
48
+ - lib/arithmetic/nodes.rb
49
+ - lib/arithmetic/operators.rb
50
+ - lib/arithmetic/parser.rb
51
+ - lib/arithmetic/version.rb
52
+ - spec/arithmetic_spec.rb
53
+ - spec/spec_helper.rb
54
+ homepage: https://github.com/nulogy/arithmetic
55
+ licenses: []
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 1.8.24
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: Simple arithmetic calculator for Ruby
78
+ test_files:
79
+ - spec/arithmetic_spec.rb
80
+ - spec/spec_helper.rb