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 +12 -4
- data/dentaku.gemspec +1 -0
- data/lib/dentaku/evaluator.rb +25 -11
- data/lib/dentaku/token_matcher.rb +31 -0
- data/lib/dentaku/version.rb +1 -1
- data/spec/calculator_spec.rb +1 -0
- data/spec/evaluator_spec.rb +14 -42
- data/spec/spec_helper.rb +29 -0
- data/spec/token_matcher_spec.rb +50 -0
- metadata +48 -48
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
|
48
|
-
but more will be added soon. The current
|
49
|
-
Excel
|
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
data/lib/dentaku/evaluator.rb
CHANGED
@@ -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,
|
23
|
-
P_MATH_ADD = [T_NUMERIC, T_ADDSUB,
|
24
|
-
P_MATH_MUL = [T_NUMERIC, T_MULDIV,
|
25
|
-
P_NUM_COMP = [T_NUMERIC, T_COMPARATOR,
|
26
|
-
P_STR_COMP = [T_STRING, T_COMPARATOR,
|
27
|
-
P_COMBINE = [T_LOGICAL, T_COMBINATOR,
|
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
|
-
|
55
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
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)
|
data/lib/dentaku/version.rb
CHANGED
data/spec/calculator_spec.rb
CHANGED
data/spec/evaluator_spec.rb
CHANGED
@@ -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::
|
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)
|
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 =
|
30
|
-
expected =
|
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 =
|
37
|
-
expected =
|
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(
|
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(
|
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(
|
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(
|
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(
|
63
|
-
evaluator.evaluate(
|
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
|
-
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|
data/spec/token_matcher_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
29
|
-
segments:
|
30
|
-
- 0
|
31
|
-
version: "0"
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
32
33
|
type: :development
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
78
|
-
segments:
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
segments:
|
79
79
|
- 0
|
80
|
-
|
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
|
-
|
87
|
-
segments:
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
segments:
|
88
88
|
- 0
|
89
|
-
|
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
|