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