arithmetic 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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +1 -0
- data/arithmetic.gemspec +20 -0
- data/lib/arithmetic.rb +12 -0
- data/lib/arithmetic/expression.rb +15 -0
- data/lib/arithmetic/nodes.rb +42 -0
- data/lib/arithmetic/operators.rb +41 -0
- data/lib/arithmetic/parser.rb +107 -0
- data/lib/arithmetic/version.rb +3 -0
- data/spec/arithmetic_spec.rb +110 -0
- data/spec/spec_helper.rb +1 -0
- metadata +80 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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"
|
data/arithmetic.gemspec
ADDED
@@ -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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|