dentaku 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -44,15 +44,23 @@ to ensure proper evaluation:
44
44
  calculator.evaluate('(5 + 3) * 2')
45
45
  => 16
46
46
 
47
- A number of functions are also supported. Okay, the number is currently one,
48
- but more will be added soon. The current function is `if`, which works like
49
- Excel's `if`:
47
+ A number of functions are also supported. Okay, the number is currently two,
48
+ but more will be added soon. The current functions are `round` and `if`, and
49
+ they work like their counterparts in Excel:
50
50
 
51
51
  calculator.evaluate('if (pears < 10, 10, 20)', :pears => 5)
52
52
  => 10
53
53
  calculator.evaluate('if (pears < 10, 10, 20)', :pears => 15)
54
54
  => 20
55
55
 
56
+ `Round` can be called with or without the number of decimal places:
57
+
58
+ calculator.evaluate('round(8.2)')
59
+ => 8
60
+ calculator.evaluate('round(8.2759, 2)')
61
+ => 8.28
62
+
63
+
56
64
  If you're too lazy to be building calculator objects, there's a shortcut just
57
65
  for you:
58
66
 
@@ -65,7 +73,7 @@ SUPPORTED OPERATORS AND FUNCTIONS
65
73
 
66
74
  Math: `+ - * /`
67
75
  Logic: `< > <= >= <> != = AND OR`
68
- Functions: `IF`
76
+ Functions: `IF ROUND`
69
77
 
70
78
  THANKS
71
79
  ------
data/dentaku.gemspec CHANGED
@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.rubyforge_project = "dentaku"
17
17
 
18
+ s.add_development_dependency('rake')
18
19
  s.add_development_dependency('rspec')
19
20
 
20
21
  s.files = `git ls-files`.split("\n")
@@ -18,13 +18,15 @@ module Dentaku
18
18
  T_IF = TokenMatcher.new(:function, :if)
19
19
  T_ROUND = TokenMatcher.new(:function, :round)
20
20
 
21
+ T_NON_GROUP_STAR = TokenMatcher.new(:grouping).invert.star
22
+
21
23
  # patterns
22
- P_GROUP = [T_OPEN, T_NON_GROUP, T_CLOSE]
23
- P_MATH_ADD = [T_NUMERIC, T_ADDSUB, T_NUMERIC]
24
- P_MATH_MUL = [T_NUMERIC, T_MULDIV, T_NUMERIC]
25
- P_NUM_COMP = [T_NUMERIC, T_COMPARATOR, T_NUMERIC]
26
- P_STR_COMP = [T_STRING, T_COMPARATOR, T_STRING]
27
- P_COMBINE = [T_LOGICAL, T_COMBINATOR, T_LOGICAL]
24
+ P_GROUP = [T_OPEN, T_NON_GROUP_STAR, T_CLOSE]
25
+ P_MATH_ADD = [T_NUMERIC, T_ADDSUB, T_NUMERIC]
26
+ P_MATH_MUL = [T_NUMERIC, T_MULDIV, T_NUMERIC]
27
+ P_NUM_COMP = [T_NUMERIC, T_COMPARATOR, T_NUMERIC]
28
+ P_STR_COMP = [T_STRING, T_COMPARATOR, T_STRING]
29
+ P_COMBINE = [T_LOGICAL, T_COMBINATOR, T_LOGICAL]
28
30
 
29
31
  P_IF = [T_IF, T_OPEN, T_NON_GROUP, T_COMMA, T_NON_GROUP, T_COMMA, T_NON_GROUP, T_CLOSE]
30
32
  P_ROUND_ONE = [T_ROUND, T_OPEN, T_NUMERIC, T_CLOSE]
@@ -51,8 +53,10 @@ module Dentaku
51
53
  while tokens.length > 1
52
54
  matched = false
53
55
  RULES.each do |pattern, evaluator|
54
- if pos = find_rule_match(pattern, tokens)
55
- tokens = evaluate_step(tokens, pos, pattern.length, evaluator)
56
+ pos, match = find_rule_match(pattern, tokens)
57
+
58
+ if pos
59
+ tokens = evaluate_step(tokens, pos, match.length, evaluator)
56
60
  matched = true
57
61
  break
58
62
  end
@@ -73,11 +77,21 @@ module Dentaku
73
77
 
74
78
  def find_rule_match(pattern, token_stream)
75
79
  position = 0
76
- while position <= token_stream.length - pattern.length
77
- substream = token_stream.slice(position, pattern.length)
78
- return position if pattern == substream
80
+
81
+ while position <= token_stream.length
82
+ matches = []
83
+ matched = true
84
+
85
+ pattern.each do |matcher|
86
+ match = matcher.match(token_stream, position + matches.length)
87
+ matched &&= match.matched?
88
+ matches += match
89
+ end
90
+
91
+ return position, matches if matched
79
92
  position += 1
80
93
  end
94
+
81
95
  nil
82
96
  end
83
97
 
@@ -6,6 +6,9 @@ module Dentaku
6
6
  @categories = [categories].compact.flatten
7
7
  @values = [values].compact.flatten
8
8
  @invert = false
9
+
10
+ @min = 1
11
+ @max = 1
9
12
  end
10
13
 
11
14
  def invert
@@ -14,9 +17,37 @@ module Dentaku
14
17
  end
15
18
 
16
19
  def ==(token)
20
+ return false if token.nil?
17
21
  (category_match(token.category) && value_match(token.value)) ^ @invert
18
22
  end
19
23
 
24
+ def match(token_stream, offset=0)
25
+ matched_tokens = []
26
+
27
+ while self == token_stream[matched_tokens.length + offset] && matched_tokens.length < @max
28
+ matched_tokens << token_stream[matched_tokens.length + offset]
29
+ end
30
+
31
+ if (@min..@max).include? matched_tokens.length
32
+ def matched_tokens.matched?() true end
33
+ else
34
+ def matched_tokens.matched?() false end
35
+ end
36
+
37
+ matched_tokens
38
+ end
39
+
40
+ def star
41
+ @min = 0
42
+ @max = 1.0/0
43
+ self
44
+ end
45
+
46
+ def plus
47
+ @max = 1.0/0
48
+ self
49
+ end
50
+
20
51
  private
21
52
 
22
53
  def category_match(category)
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.3"
3
3
  end
@@ -28,6 +28,7 @@ describe Dentaku::Calculator do
28
28
 
29
29
  it 'should evaluate a statement with no variables' do
30
30
  calculator.evaluate('5+3').should eq(8)
31
+ calculator.evaluate('(1+1+1)/3*100').should eq(100)
31
32
  end
32
33
 
33
34
  it 'should fail to evaluate unbound statements' do
@@ -1,3 +1,4 @@
1
+ require 'spec_helper'
1
2
  require 'dentaku/evaluator'
2
3
 
3
4
  describe Dentaku::Evaluator do
@@ -5,9 +6,10 @@ describe Dentaku::Evaluator do
5
6
 
6
7
  describe 'rule scanning' do
7
8
  it 'should find a matching rule' do
8
- rule = [Dentaku::Token.new(:numeric, nil)]
9
+ rule = [Dentaku::TokenMatcher.new(:numeric, nil)]
9
10
  stream = [Dentaku::Token.new(:numeric, 1), Dentaku::Token.new(:operator, :add), Dentaku::Token.new(:numeric, 1)]
10
- evaluator.find_rule_match(rule, stream).should eq(0)
11
+ position, match = evaluator.find_rule_match(rule, stream)
12
+ position.should eq(0)
11
13
  end
12
14
  end
13
15
 
@@ -26,72 +28,42 @@ describe Dentaku::Evaluator do
26
28
  end
27
29
 
28
30
  it 'should evaluate one apply step' do
29
- stream = ts(1, :add, 1, :add, 1)
30
- expected = ts(2, :add, 1)
31
+ stream = token_stream(1, :add, 1, :add, 1)
32
+ expected = token_stream(2, :add, 1)
31
33
 
32
34
  evaluator.evaluate_step(stream, 0, 3, :apply).should eq(expected)
33
35
  end
34
36
 
35
37
  it 'should evaluate one grouping step' do
36
- stream = ts(:open, 1, :add, 1, :close, :multiply, 5)
37
- expected = ts(2, :multiply, 5)
38
+ stream = token_stream(:open, 1, :add, 1, :close, :multiply, 5)
39
+ expected = token_stream(2, :multiply, 5)
38
40
 
39
41
  evaluator.evaluate_step(stream, 0, 5, :evaluate_group).should eq(expected)
40
42
  end
41
43
 
42
44
  describe 'maths' do
43
45
  it 'should perform addition' do
44
- evaluator.evaluate(ts(1, :add, 1)).should eq(2)
46
+ evaluator.evaluate(token_stream(1, :add, 1)).should eq(2)
45
47
  end
46
48
 
47
49
  it 'should respect order of precedence' do
48
- evaluator.evaluate(ts(1, :add, 1, :multiply, 5)).should eq(6)
50
+ evaluator.evaluate(token_stream(1, :add, 1, :multiply, 5)).should eq(6)
49
51
  end
50
52
 
51
53
  it 'should respect explicit grouping' do
52
- evaluator.evaluate(ts(:open, 1, :add, 1, :close, :multiply, 5)).should eq(10)
54
+ evaluator.evaluate(token_stream(:open, 1, :add, 1, :close, :multiply, 5)).should eq(10)
53
55
  end
54
56
  end
55
57
 
56
58
  describe 'logic' do
57
59
  it 'should evaluate conditional' do
58
- evaluator.evaluate(ts(5, :gt, 1)).should be_true
60
+ evaluator.evaluate(token_stream(5, :gt, 1)).should be_true
59
61
  end
60
62
 
61
63
  it 'should evaluate combined conditionals' do
62
- evaluator.evaluate(ts(5, :gt, 1, :or, :false)).should be_true
63
- evaluator.evaluate(ts(5, :gt, 1, :and, :false)).should be_false
64
+ evaluator.evaluate(token_stream(5, :gt, 1, :or, :false)).should be_true
65
+ evaluator.evaluate(token_stream(5, :gt, 1, :and, :false)).should be_false
64
66
  end
65
67
  end
66
68
  end
67
-
68
- private
69
-
70
- def ts(*args)
71
- args.map do |arg|
72
- category = (arg.is_a? Fixnum) ? :numeric : category_for(arg)
73
- arg = (arg == :true) if category == :logical
74
- Dentaku::Token.new(category, arg)
75
- end
76
- end
77
-
78
- def category_for(value)
79
- case value
80
- when Numeric
81
- :numeric
82
- when :add, :subtract, :multiply, :divide
83
- :operator
84
- when :open, :close
85
- :grouping
86
- when :le, :ge, :ne, :ne, :lt, :gt, :eq
87
- :comparator
88
- when :and, :or
89
- :combinator
90
- when :true, :false
91
- :logical
92
- else
93
- :identifier
94
- end
95
- end
96
69
  end
97
-
@@ -0,0 +1,29 @@
1
+ # automatically create a token stream from bare values
2
+ def token_stream(*args)
3
+ args.map do |value|
4
+ type = type_for(value)
5
+ value = (value == :true) if type == :logical
6
+ Dentaku::Token.new(type, value)
7
+ end
8
+ end
9
+
10
+ # make a (hopefully intelligent) guess about type
11
+ def type_for(value)
12
+ case value
13
+ when Numeric
14
+ :numeric
15
+ when :add, :subtract, :multiply, :divide
16
+ :operator
17
+ when :open, :close
18
+ :grouping
19
+ when :le, :ge, :ne, :ne, :lt, :gt, :eq
20
+ :comparator
21
+ when :and, :or
22
+ :combinator
23
+ when :true, :false
24
+ :logical
25
+ else
26
+ :identifier
27
+ end
28
+ end
29
+
@@ -1,3 +1,4 @@
1
+ require 'spec_helper'
1
2
  require 'dentaku/token_matcher'
2
3
 
3
4
  describe Dentaku::TokenMatcher do
@@ -51,5 +52,54 @@ describe Dentaku::TokenMatcher do
51
52
  matcher.should == mul
52
53
  matcher.should == cmp
53
54
  end
55
+
56
+ describe 'stream matching' do
57
+ let(:stream) { token_stream(5, 11, 9, 24, :hello, 8) }
58
+
59
+ describe :standard do
60
+ let(:standard) { described_class.new(:numeric) }
61
+
62
+ it 'should match zero or more occurrences in a token stream' do
63
+ substream = standard.match(stream)
64
+ substream.should be_matched
65
+ substream.length.should eq 1
66
+ substream.map(&:value).should eq [5]
67
+
68
+ substream = standard.match(stream, 4)
69
+ substream.should be_empty
70
+ substream.should_not be_matched
71
+ end
72
+ end
73
+
74
+ describe :star do
75
+ let(:star) { described_class.new(:numeric).star }
76
+
77
+ it 'should match zero or more occurrences in a token stream' do
78
+ substream = star.match(stream)
79
+ substream.should be_matched
80
+ substream.length.should eq 4
81
+ substream.map(&:value).should eq [5, 11, 9, 24]
82
+
83
+ substream = star.match(stream, 4)
84
+ substream.should be_empty
85
+ substream.should be_matched
86
+ end
87
+ end
88
+
89
+ describe :plus do
90
+ let(:plus) { described_class.new(:numeric).plus }
91
+
92
+ it 'should match one or more occurrences in a token stream' do
93
+ substream = plus.match(stream)
94
+ substream.should be_matched
95
+ substream.length.should eq 4
96
+ substream.map(&:value).should eq [5, 11, 9, 24]
97
+
98
+ substream = plus.match(stream, 4)
99
+ substream.should be_empty
100
+ substream.should_not be_matched
101
+ end
102
+ end
103
+ end
54
104
  end
55
105
 
metadata CHANGED
@@ -1,46 +1,47 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: dentaku
3
- version: !ruby/object:Gem::Version
4
- hash: 19
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.3
5
5
  prerelease:
6
- segments:
7
- - 0
8
- - 2
9
- - 2
10
- version: 0.2.2
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Solomon White
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2012-02-22 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
21
- name: rspec
12
+ date: 2012-02-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70181931083120 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
22
23
  prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
24
+ version_requirements: *70181931083120
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70181931082440 !ruby/object:Gem::Requirement
24
28
  none: false
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- hash: 3
29
- segments:
30
- - 0
31
- version: "0"
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
32
33
  type: :development
33
- version_requirements: *id001
34
- description: " Dentaku is a parser and evaluator for mathematical formulas\n"
35
- email:
34
+ prerelease: false
35
+ version_requirements: *70181931082440
36
+ description: ! ' Dentaku is a parser and evaluator for mathematical formulas
37
+
38
+ '
39
+ email:
36
40
  - rubysolo@gmail.com
37
41
  executables: []
38
-
39
42
  extensions: []
40
-
41
43
  extra_rdoc_files: []
42
-
43
- files:
44
+ files:
44
45
  - .gitignore
45
46
  - Gemfile
46
47
  - README.md
@@ -57,47 +58,46 @@ files:
57
58
  - spec/calculator_spec.rb
58
59
  - spec/dentaku_spec.rb
59
60
  - spec/evaluator_spec.rb
61
+ - spec/spec_helper.rb
60
62
  - spec/token_matcher_spec.rb
61
63
  - spec/token_scanner_spec.rb
62
64
  - spec/token_spec.rb
63
65
  - spec/tokenizer_spec.rb
64
66
  homepage: http://github.com/rubysolo/dentaku
65
67
  licenses: []
66
-
67
68
  post_install_message:
68
69
  rdoc_options: []
69
-
70
- require_paths:
70
+ require_paths:
71
71
  - lib
72
- required_ruby_version: !ruby/object:Gem::Requirement
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
73
  none: false
74
- requirements:
75
- - - ">="
76
- - !ruby/object:Gem::Version
77
- hash: 3
78
- segments:
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ segments:
79
79
  - 0
80
- version: "0"
81
- required_rubygems_version: !ruby/object:Gem::Requirement
80
+ hash: -2425877477882422646
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  none: false
83
- requirements:
84
- - - ">="
85
- - !ruby/object:Gem::Version
86
- hash: 3
87
- segments:
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ segments:
88
88
  - 0
89
- version: "0"
89
+ hash: -2425877477882422646
90
90
  requirements: []
91
-
92
91
  rubyforge_project: dentaku
93
92
  rubygems_version: 1.8.15
94
93
  signing_key:
95
94
  specification_version: 3
96
95
  summary: A formula language parser and evaluator
97
- test_files:
96
+ test_files:
98
97
  - spec/calculator_spec.rb
99
98
  - spec/dentaku_spec.rb
100
99
  - spec/evaluator_spec.rb
100
+ - spec/spec_helper.rb
101
101
  - spec/token_matcher_spec.rb
102
102
  - spec/token_scanner_spec.rb
103
103
  - spec/token_spec.rb