dentaku 0.2.2 → 0.2.3

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/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