dentaku 0.2.10 → 0.2.11
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +3 -0
- data/README.md +5 -1
- data/lib/dentaku/binary_operation.rb +31 -0
- data/lib/dentaku/calculator.rb +6 -2
- data/lib/dentaku/evaluator.rb +38 -125
- data/lib/dentaku/rules.rb +75 -0
- data/lib/dentaku/token_matcher.rb +21 -0
- data/lib/dentaku/token_scanner.rb +61 -0
- data/lib/dentaku/tokenizer.rb +24 -57
- data/lib/dentaku/version.rb +1 -1
- data/spec/binary_operation_spec.rb +41 -0
- data/spec/calculator_spec.rb +6 -0
- data/spec/evaluator_spec.rb +2 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/token_scanner_spec.rb +4 -0
- metadata +7 -8
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
Dentaku
|
2
2
|
=======
|
3
3
|
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/dentaku.png)](http://badge.fury.io/rb/dentaku)
|
5
|
+
[![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
|
7
|
+
|
4
8
|
http://github.com/rubysolo/dentaku
|
5
9
|
|
6
10
|
DESCRIPTION
|
@@ -72,7 +76,7 @@ SUPPORTED OPERATORS AND FUNCTIONS
|
|
72
76
|
---------------------------------
|
73
77
|
|
74
78
|
Math: `+ - * /`
|
75
|
-
Logic: `< > <= >= <> != = AND OR`
|
79
|
+
Logic: `< > <= >= <> != = AND OR NOT`
|
76
80
|
Functions: `IF ROUND`
|
77
81
|
|
78
82
|
THANKS
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dentaku
|
2
|
+
class BinaryOperation
|
3
|
+
attr_reader :left, :right
|
4
|
+
|
5
|
+
def initialize(left, right)
|
6
|
+
@left = left
|
7
|
+
@right = right
|
8
|
+
end
|
9
|
+
|
10
|
+
def pow; [:numeric, left ** right]; end
|
11
|
+
def add; [:numeric, left + right]; end
|
12
|
+
def subtract; [:numeric, left - right]; end
|
13
|
+
def multiply; [:numeric, left * right]; end
|
14
|
+
|
15
|
+
def divide
|
16
|
+
quotient, remainder = left.divmod(right)
|
17
|
+
return [:numeric, quotient] if remainder == 0
|
18
|
+
[:numeric, left.to_f / right.to_f]
|
19
|
+
end
|
20
|
+
|
21
|
+
def le; [:logical, left <= right]; end
|
22
|
+
def ge; [:logical, left >= right]; end
|
23
|
+
def lt; [:logical, left < right]; end
|
24
|
+
def gt; [:logical, left > right]; end
|
25
|
+
def ne; [:logical, left != right]; end
|
26
|
+
def eq; [:logical, left == right]; end
|
27
|
+
|
28
|
+
def and; [:logical, left && right]; end
|
29
|
+
def or; [:logical, left || right]; end
|
30
|
+
end
|
31
|
+
end
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -31,7 +31,7 @@ module Dentaku
|
|
31
31
|
@memory[key_or_hash.to_sym] = value
|
32
32
|
else
|
33
33
|
key_or_hash.each do |key, value|
|
34
|
-
@memory[key.to_sym] = value
|
34
|
+
@memory[key.to_sym] = value
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
@@ -69,7 +69,11 @@ module Dentaku
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def type_for_value(value)
|
72
|
-
value
|
72
|
+
case value
|
73
|
+
when String then :string
|
74
|
+
when TrueClass, FalseClass then :logical
|
75
|
+
else :numeric
|
76
|
+
end
|
73
77
|
end
|
74
78
|
end
|
75
79
|
end
|
data/lib/dentaku/evaluator.rb
CHANGED
@@ -1,85 +1,15 @@
|
|
1
|
-
require 'dentaku/
|
2
|
-
require 'dentaku/
|
1
|
+
require 'dentaku/rules'
|
2
|
+
require 'dentaku/binary_operation'
|
3
3
|
|
4
4
|
module Dentaku
|
5
5
|
class Evaluator
|
6
|
-
# tokens
|
7
|
-
T_NUMERIC = TokenMatcher.new(:numeric)
|
8
|
-
T_STRING = TokenMatcher.new(:string)
|
9
|
-
T_ADDSUB = TokenMatcher.new(:operator, [:add, :subtract])
|
10
|
-
T_MULDIV = TokenMatcher.new(:operator, [:multiply, :divide])
|
11
|
-
T_POW = TokenMatcher.new(:operator, :pow)
|
12
|
-
T_COMPARATOR = TokenMatcher.new(:comparator)
|
13
|
-
T_COMP_GT = TokenMatcher.new(:comparator, [:gt, :ge])
|
14
|
-
T_COMP_LT = TokenMatcher.new(:comparator, [:lt, :le])
|
15
|
-
T_OPEN = TokenMatcher.new(:grouping, :open)
|
16
|
-
T_CLOSE = TokenMatcher.new(:grouping, :close)
|
17
|
-
T_COMMA = TokenMatcher.new(:grouping, :comma)
|
18
|
-
T_NON_GROUP = TokenMatcher.new(:grouping).invert
|
19
|
-
T_LOGICAL = TokenMatcher.new(:logical)
|
20
|
-
T_COMBINATOR = TokenMatcher.new(:combinator)
|
21
|
-
T_IF = TokenMatcher.new(:function, :if)
|
22
|
-
T_ROUND = TokenMatcher.new(:function, :round)
|
23
|
-
T_ROUNDUP = TokenMatcher.new(:function, :roundup)
|
24
|
-
T_ROUNDDOWN = TokenMatcher.new(:function, :rounddown)
|
25
|
-
T_NOT = TokenMatcher.new(:function, :not)
|
26
|
-
|
27
|
-
T_NON_GROUP_STAR = TokenMatcher.new(:grouping).invert.star
|
28
|
-
|
29
|
-
# patterns
|
30
|
-
P_GROUP = [T_OPEN, T_NON_GROUP_STAR, T_CLOSE]
|
31
|
-
P_MATH_ADD = [T_NUMERIC, T_ADDSUB, T_NUMERIC]
|
32
|
-
P_MATH_MUL = [T_NUMERIC, T_MULDIV, T_NUMERIC]
|
33
|
-
P_MATH_POW = [T_NUMERIC, T_POW, T_NUMERIC]
|
34
|
-
P_RANGE_ASC = [T_NUMERIC, T_COMP_LT, T_NUMERIC, T_COMP_LT, T_NUMERIC]
|
35
|
-
P_RANGE_DESC = [T_NUMERIC, T_COMP_GT, T_NUMERIC, T_COMP_GT, T_NUMERIC]
|
36
|
-
P_NUM_COMP = [T_NUMERIC, T_COMPARATOR, T_NUMERIC]
|
37
|
-
P_STR_COMP = [T_STRING, T_COMPARATOR, T_STRING]
|
38
|
-
P_COMBINE = [T_LOGICAL, T_COMBINATOR, T_LOGICAL]
|
39
|
-
|
40
|
-
P_IF = [T_IF, T_OPEN, T_NON_GROUP, T_COMMA, T_NON_GROUP, T_COMMA, T_NON_GROUP, T_CLOSE]
|
41
|
-
P_ROUND_ONE = [T_ROUND, T_OPEN, T_NON_GROUP_STAR, T_CLOSE]
|
42
|
-
P_ROUND_TWO = [T_ROUND, T_OPEN, T_NON_GROUP_STAR, T_COMMA, T_NUMERIC, T_CLOSE]
|
43
|
-
P_ROUNDUP = [T_ROUNDUP, T_OPEN, T_NON_GROUP_STAR, T_CLOSE]
|
44
|
-
P_ROUNDDOWN = [T_ROUNDDOWN, T_OPEN, T_NON_GROUP_STAR, T_CLOSE]
|
45
|
-
P_NOT = [T_NOT, T_OPEN, T_NON_GROUP_STAR, T_CLOSE]
|
46
|
-
|
47
|
-
RULES = [
|
48
|
-
[P_IF, :if],
|
49
|
-
[P_ROUND_ONE, :round],
|
50
|
-
[P_ROUND_TWO, :round],
|
51
|
-
[P_ROUNDUP, :roundup],
|
52
|
-
[P_ROUNDDOWN, :rounddown],
|
53
|
-
[P_NOT, :not],
|
54
|
-
|
55
|
-
[P_GROUP, :evaluate_group],
|
56
|
-
[P_MATH_POW, :apply],
|
57
|
-
[P_MATH_MUL, :apply],
|
58
|
-
[P_MATH_ADD, :apply],
|
59
|
-
[P_RANGE_ASC, :expand_range],
|
60
|
-
[P_RANGE_DESC, :expand_range],
|
61
|
-
[P_NUM_COMP, :apply],
|
62
|
-
[P_STR_COMP, :apply],
|
63
|
-
[P_COMBINE, :apply]
|
64
|
-
]
|
65
|
-
|
66
6
|
def evaluate(tokens)
|
67
7
|
evaluate_token_stream(tokens).value
|
68
8
|
end
|
69
9
|
|
70
10
|
def evaluate_token_stream(tokens)
|
71
11
|
while tokens.length > 1
|
72
|
-
matched =
|
73
|
-
RULES.each do |pattern, evaluator|
|
74
|
-
pos, match = find_rule_match(pattern, tokens)
|
75
|
-
|
76
|
-
if pos
|
77
|
-
tokens = evaluate_step(tokens, pos, match.length, evaluator)
|
78
|
-
matched = true
|
79
|
-
break
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
12
|
+
matched, tokens = match_rule_pattern(tokens)
|
83
13
|
raise "no rule matched #{ tokens.map(&:category).inspect }" unless matched
|
84
14
|
end
|
85
15
|
|
@@ -88,9 +18,19 @@ module Dentaku
|
|
88
18
|
tokens.first
|
89
19
|
end
|
90
20
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
21
|
+
def match_rule_pattern(tokens)
|
22
|
+
matched = false
|
23
|
+
Rules.each do |pattern, evaluator|
|
24
|
+
pos, match = find_rule_match(pattern, tokens)
|
25
|
+
|
26
|
+
if pos
|
27
|
+
tokens = evaluate_step(tokens, pos, match.length, evaluator)
|
28
|
+
matched = true
|
29
|
+
break
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
[matched, tokens]
|
94
34
|
end
|
95
35
|
|
96
36
|
def find_rule_match(pattern, token_stream)
|
@@ -113,40 +53,19 @@ module Dentaku
|
|
113
53
|
nil
|
114
54
|
end
|
115
55
|
|
56
|
+
def evaluate_step(token_stream, start, length, evaluator)
|
57
|
+
expr = token_stream.slice!(start, length)
|
58
|
+
token_stream.insert start, *self.send(evaluator, *expr)
|
59
|
+
end
|
60
|
+
|
116
61
|
def evaluate_group(*args)
|
117
62
|
evaluate_token_stream(args[1..-2])
|
118
63
|
end
|
119
64
|
|
120
65
|
def apply(lvalue, operator, rvalue)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
case operator.value
|
125
|
-
when :pow then Token.new(:numeric, l ** r)
|
126
|
-
when :add then Token.new(:numeric, l + r)
|
127
|
-
when :subtract then Token.new(:numeric, l - r)
|
128
|
-
when :multiply then Token.new(:numeric, l * r)
|
129
|
-
when :divide then Token.new(:numeric, divide(l, r))
|
130
|
-
|
131
|
-
when :le then Token.new(:logical, l <= r)
|
132
|
-
when :ge then Token.new(:logical, l >= r)
|
133
|
-
when :lt then Token.new(:logical, l < r)
|
134
|
-
when :gt then Token.new(:logical, l > r)
|
135
|
-
when :ne then Token.new(:logical, l != r)
|
136
|
-
when :eq then Token.new(:logical, l == r)
|
137
|
-
|
138
|
-
when :and then Token.new(:logical, l && r)
|
139
|
-
when :or then Token.new(:logical, l || r)
|
140
|
-
|
141
|
-
else
|
142
|
-
raise "unknown comparator '#{ comparator }'"
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
def divide(numerator, denominator)
|
147
|
-
quotient, remainder = numerator.divmod(denominator)
|
148
|
-
return quotient if remainder == 0
|
149
|
-
numerator.to_f / denominator.to_f
|
66
|
+
operation = BinaryOperation.new(lvalue.value, rvalue.value)
|
67
|
+
raise "unknown operation #{ operator.value }" unless operation.respond_to?(operator.value)
|
68
|
+
Token.new(*operation.send(operator.value))
|
150
69
|
end
|
151
70
|
|
152
71
|
def expand_range(left, oper1, middle, oper2, right)
|
@@ -166,35 +85,29 @@ module Dentaku
|
|
166
85
|
def round(*args)
|
167
86
|
_, _, *tokens, _ = args
|
168
87
|
|
169
|
-
input_tokens = tokens.
|
170
|
-
|
171
|
-
|
88
|
+
input_tokens, places_tokens = tokens.chunk { |t| t.category == :grouping }.
|
89
|
+
reject { |flag, tokens| flag }.
|
90
|
+
map { |flag, tokens| tokens }
|
172
91
|
|
173
|
-
|
174
|
-
|
175
|
-
end
|
92
|
+
input_value = evaluate_token_stream(input_tokens).value
|
93
|
+
places = places_tokens ? evaluate_token_stream(places_tokens).value : 0
|
176
94
|
|
177
|
-
|
178
|
-
value = input_value.round(places)
|
179
|
-
rescue ArgumentError
|
180
|
-
value = (input * 10 ** places).round / (10 ** places).to_f
|
181
|
-
end
|
95
|
+
value = input_value.round(places)
|
182
96
|
|
183
97
|
Token.new(:numeric, value)
|
184
98
|
end
|
185
99
|
|
186
|
-
def
|
187
|
-
|
100
|
+
def round_int(*args)
|
101
|
+
function, _, *tokens, _ = args
|
188
102
|
|
189
103
|
value = evaluate_token_stream(tokens).value
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
104
|
+
rounded = if function.value == :roundup
|
105
|
+
value.ceil
|
106
|
+
else
|
107
|
+
value.floor
|
108
|
+
end
|
195
109
|
|
196
|
-
|
197
|
-
Token.new(:numeric, value.floor)
|
110
|
+
Token.new(:numeric, rounded)
|
198
111
|
end
|
199
112
|
|
200
113
|
def not(*args)
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'dentaku/token'
|
2
|
+
require 'dentaku/token_matcher'
|
3
|
+
|
4
|
+
module Dentaku
|
5
|
+
class Rules
|
6
|
+
def self.each
|
7
|
+
@rules ||= [
|
8
|
+
[ p(:if), :if ],
|
9
|
+
[ p(:round_one), :round ],
|
10
|
+
[ p(:round_two), :round ],
|
11
|
+
[ p(:roundup), :round_int ],
|
12
|
+
[ p(:rounddown), :round_int ],
|
13
|
+
[ p(:not), :not ],
|
14
|
+
|
15
|
+
[ p(:group), :evaluate_group ],
|
16
|
+
[ p(:math_pow), :apply ],
|
17
|
+
[ p(:math_mul), :apply ],
|
18
|
+
[ p(:math_add), :apply ],
|
19
|
+
[ p(:range_asc), :expand_range ],
|
20
|
+
[ p(:range_desc), :expand_range ],
|
21
|
+
[ p(:num_comp), :apply ],
|
22
|
+
[ p(:str_comp), :apply ],
|
23
|
+
[ p(:combine), :apply ]
|
24
|
+
]
|
25
|
+
|
26
|
+
@rules.each { |r| yield r }
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.t(name)
|
30
|
+
@matchers ||= [
|
31
|
+
:numeric, :string, :addsub, :muldiv, :pow,
|
32
|
+
:comparator, :comp_gt, :comp_lt,
|
33
|
+
:open, :close, :comma,
|
34
|
+
:non_group, :non_group_star,
|
35
|
+
:logical, :combinator,
|
36
|
+
:if, :round, :roundup, :rounddown, :not
|
37
|
+
].each_with_object({}) do |name, matchers|
|
38
|
+
matchers[name] = TokenMatcher.send(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
@matchers[name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.p(name)
|
45
|
+
@patterns ||= {
|
46
|
+
group: pattern(:open, :non_group_star, :close),
|
47
|
+
math_add: pattern(:numeric, :addsub, :numeric),
|
48
|
+
math_mul: pattern(:numeric, :muldiv, :numeric),
|
49
|
+
math_pow: pattern(:numeric, :pow, :numeric),
|
50
|
+
range_asc: pattern(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
|
51
|
+
range_desc: pattern(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
|
52
|
+
num_comp: pattern(:numeric, :comparator, :numeric),
|
53
|
+
str_comp: pattern(:string, :comparator, :string),
|
54
|
+
combine: pattern(:logical, :combinator, :logical),
|
55
|
+
|
56
|
+
if: func_pattern(:if, :non_group, :comma, :non_group, :comma, :non_group),
|
57
|
+
round_one: func_pattern(:round, :non_group_star),
|
58
|
+
round_two: func_pattern(:round, :non_group_star, :comma, :numeric),
|
59
|
+
roundup: func_pattern(:roundup, :non_group_star),
|
60
|
+
rounddown: func_pattern(:rounddown, :non_group_star),
|
61
|
+
not: func_pattern(:not, :non_group_star)
|
62
|
+
}
|
63
|
+
|
64
|
+
@patterns[name]
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.pattern(*symbols)
|
68
|
+
symbols.map { |s| t(s) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.func_pattern(func, *tokens)
|
72
|
+
pattern(func, :open, *tokens, :close)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -57,6 +57,27 @@ module Dentaku
|
|
57
57
|
def value_match(value)
|
58
58
|
@values.empty? || @values.include?(value)
|
59
59
|
end
|
60
|
+
|
61
|
+
def self.numeric; new(:numeric); end
|
62
|
+
def self.string; new(:string); end
|
63
|
+
def self.addsub; new(:operator, [:add, :subtract]); end
|
64
|
+
def self.muldiv; new(:operator, [:multiply, :divide]); end
|
65
|
+
def self.pow; new(:operator, :pow); end
|
66
|
+
def self.comparator; new(:comparator); end
|
67
|
+
def self.comp_gt; new(:comparator, [:gt, :ge]); end
|
68
|
+
def self.comp_lt; new(:comparator, [:lt, :le]); end
|
69
|
+
def self.open; new(:grouping, :open); end
|
70
|
+
def self.close; new(:grouping, :close); end
|
71
|
+
def self.comma; new(:grouping, :comma); end
|
72
|
+
def self.logical; new(:logical); end
|
73
|
+
def self.combinator; new(:combinator); end
|
74
|
+
def self.if; new(:function, :if); end
|
75
|
+
def self.round; new(:function, :round); end
|
76
|
+
def self.roundup; new(:function, :roundup); end
|
77
|
+
def self.rounddown; new(:function, :rounddown); end
|
78
|
+
def self.not; new(:function, :not); end
|
79
|
+
def self.non_group; new(:grouping).invert; end
|
80
|
+
def self.non_group_star; new(:grouping).invert.star; end
|
60
81
|
end
|
61
82
|
end
|
62
83
|
|
@@ -18,5 +18,66 @@ module Dentaku
|
|
18
18
|
|
19
19
|
false
|
20
20
|
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def scanners
|
24
|
+
@scanners ||= [
|
25
|
+
whitespace,
|
26
|
+
numeric,
|
27
|
+
double_quoted_string,
|
28
|
+
single_quoted_string,
|
29
|
+
operator,
|
30
|
+
grouping,
|
31
|
+
comparator,
|
32
|
+
combinator,
|
33
|
+
function,
|
34
|
+
identifier
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
def whitespace
|
39
|
+
new(:whitespace, '\s+')
|
40
|
+
end
|
41
|
+
|
42
|
+
def numeric
|
43
|
+
new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? raw.to_f : raw.to_i })
|
44
|
+
end
|
45
|
+
|
46
|
+
def double_quoted_string
|
47
|
+
new(:string, '"[^"]*"', lambda { |raw| raw.gsub(/^"|"$/, '') })
|
48
|
+
end
|
49
|
+
|
50
|
+
def single_quoted_string
|
51
|
+
new(:string, "'[^']*'", lambda { |raw| raw.gsub(/^'|'$/, '') })
|
52
|
+
end
|
53
|
+
|
54
|
+
def operator
|
55
|
+
names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/' }.invert
|
56
|
+
new(:operator, '\^|\+|-|\*|\/', lambda { |raw| names[raw] })
|
57
|
+
end
|
58
|
+
|
59
|
+
def grouping
|
60
|
+
names = { open: '(', close: ')', comma: ',' }.invert
|
61
|
+
new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
|
62
|
+
end
|
63
|
+
|
64
|
+
def comparator
|
65
|
+
names = { le: '<=', ge: '>=', ne: '!=', lt: '<', gt: '>', eq: '=' }.invert
|
66
|
+
alternate = { ne: '<>' }.invert
|
67
|
+
new(:comparator, '<=|>=|!=|<>|<|>|=', lambda { |raw| names[raw] || alternate[raw] })
|
68
|
+
end
|
69
|
+
|
70
|
+
def combinator
|
71
|
+
new(:combinator, '(and|or)\b', lambda {|raw| raw.strip.downcase.to_sym })
|
72
|
+
end
|
73
|
+
|
74
|
+
def function
|
75
|
+
new(:function, '(if|round(up|down)?|not)\b', lambda {|raw| raw.strip.downcase.to_sym })
|
76
|
+
end
|
77
|
+
|
78
|
+
def identifier
|
79
|
+
new(:identifier, '\w+\b', lambda {|raw| raw.strip.downcase.to_sym })
|
80
|
+
end
|
81
|
+
end
|
21
82
|
end
|
22
83
|
end
|
data/lib/dentaku/tokenizer.rb
CHANGED
@@ -4,73 +4,40 @@ require 'dentaku/token_scanner'
|
|
4
4
|
|
5
5
|
module Dentaku
|
6
6
|
class Tokenizer
|
7
|
-
SCANNERS = [
|
8
|
-
TokenScanner.new(:whitespace, '\s+'),
|
9
|
-
TokenScanner.new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? raw.to_f : raw.to_i }),
|
10
|
-
TokenScanner.new(:string, '"[^"]*"', lambda { |raw| raw.gsub(/^"|"$/, '') }),
|
11
|
-
TokenScanner.new(:string, "'[^']*'", lambda { |raw| raw.gsub(/^'|'$/, '') }),
|
12
|
-
TokenScanner.new(:operator, '\^|\+|-|\*|\/', lambda do |raw|
|
13
|
-
case raw
|
14
|
-
when '^' then :pow
|
15
|
-
when '+' then :add
|
16
|
-
when '-' then :subtract
|
17
|
-
when '*' then :multiply
|
18
|
-
when '/' then :divide
|
19
|
-
end
|
20
|
-
end),
|
21
|
-
TokenScanner.new(:grouping, '\(|\)|,', lambda do |raw|
|
22
|
-
case raw
|
23
|
-
when '(' then :open
|
24
|
-
when ')' then :close
|
25
|
-
when ',' then :comma
|
26
|
-
end
|
27
|
-
end),
|
28
|
-
TokenScanner.new(:comparator, '<=|>=|!=|<>|<|>|=', lambda do |raw|
|
29
|
-
case raw
|
30
|
-
when '<=' then :le
|
31
|
-
when '>=' then :ge
|
32
|
-
when '!=' then :ne
|
33
|
-
when '<>' then :ne
|
34
|
-
when '<' then :lt
|
35
|
-
when '>' then :gt
|
36
|
-
when '=' then :eq
|
37
|
-
end
|
38
|
-
end),
|
39
|
-
TokenScanner.new(:combinator, '(and|or)\b', lambda {|raw| raw.strip.downcase.to_sym }),
|
40
|
-
TokenScanner.new(:function, '(if|round(up|down)?|not)\b',
|
41
|
-
lambda {|raw| raw.strip.downcase.to_sym }),
|
42
|
-
TokenScanner.new(:identifier, '\w+\b', lambda {|raw| raw.strip.downcase.to_sym })
|
43
|
-
]
|
44
|
-
|
45
7
|
LPAREN = TokenMatcher.new(:grouping, :open)
|
46
8
|
RPAREN = TokenMatcher.new(:grouping, :close)
|
47
9
|
|
48
10
|
def tokenize(string)
|
49
|
-
nesting = 0
|
50
|
-
tokens = []
|
51
|
-
input
|
11
|
+
@nesting = 0
|
12
|
+
@tokens = []
|
13
|
+
input = string.dup
|
52
14
|
|
53
15
|
until input.empty?
|
54
|
-
raise "parse error at: '#{ input }'" unless
|
55
|
-
|
56
|
-
|
16
|
+
raise "parse error at: '#{ input }'" unless TokenScanner.scanners.any? do |scanner|
|
17
|
+
scanned, input = scan(input, scanner)
|
18
|
+
scanned
|
19
|
+
end
|
20
|
+
end
|
57
21
|
|
58
|
-
|
59
|
-
nesting -= 1 if RPAREN == token
|
60
|
-
raise "too many closing parentheses" if nesting < 0
|
22
|
+
raise "too many opening parentheses" if @nesting > 0
|
61
23
|
|
62
|
-
|
63
|
-
|
24
|
+
@tokens
|
25
|
+
end
|
64
26
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
27
|
+
def scan(string, scanner)
|
28
|
+
if token = scanner.scan(string)
|
29
|
+
raise "unexpected zero-width match (:#{ token.category }) at '#{ string }'" if token.length == 0
|
71
30
|
|
72
|
-
|
73
|
-
|
31
|
+
@nesting += 1 if LPAREN == token
|
32
|
+
@nesting -= 1 if RPAREN == token
|
33
|
+
raise "too many closing parentheses" if @nesting < 0
|
34
|
+
|
35
|
+
@tokens << token unless token.is?(:whitespace)
|
36
|
+
|
37
|
+
[true, string[token.length..-1]]
|
38
|
+
else
|
39
|
+
[false, string]
|
40
|
+
end
|
74
41
|
end
|
75
42
|
end
|
76
43
|
end
|
data/lib/dentaku/version.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'dentaku/binary_operation'
|
2
|
+
|
3
|
+
describe Dentaku::BinaryOperation do
|
4
|
+
let(:operation) { described_class.new(2, 3) }
|
5
|
+
let(:logical) { described_class.new(true, false) }
|
6
|
+
|
7
|
+
it 'raises a number to a power' do
|
8
|
+
operation.pow.should eq [:numeric, 8]
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'adds two numbers' do
|
12
|
+
operation.add.should eq [:numeric, 5]
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'subtracts two numbers' do
|
16
|
+
operation.subtract.should eq [:numeric, -1]
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'multiplies two numbers' do
|
20
|
+
operation.multiply.should eq [:numeric, 6]
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'divides two numbers' do
|
24
|
+
operation.divide.should eq [:numeric, (2.0/3.0)]
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'compares two numbers' do
|
28
|
+
operation.le.should eq [:logical, true]
|
29
|
+
operation.lt.should eq [:logical, true]
|
30
|
+
operation.ne.should eq [:logical, true]
|
31
|
+
|
32
|
+
operation.ge.should eq [:logical, false]
|
33
|
+
operation.gt.should eq [:logical, false]
|
34
|
+
operation.eq.should eq [:logical, false]
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'performs logical AND and OR' do
|
38
|
+
logical.and.should eq [:logical, false]
|
39
|
+
logical.or.should eq [:logical, true]
|
40
|
+
end
|
41
|
+
end
|
data/spec/calculator_spec.rb
CHANGED
@@ -68,6 +68,12 @@ describe Dentaku::Calculator do
|
|
68
68
|
calculator.evaluate('fruit = "Apple"', :fruit => 'Apple').should be_true
|
69
69
|
end
|
70
70
|
|
71
|
+
it 'should allow binding logical values' do
|
72
|
+
calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => true).should be_true
|
73
|
+
calculator.evaluate('some_boolean AND 7 < 5', :some_boolean => true).should be_false
|
74
|
+
calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => false).should be_false
|
75
|
+
end
|
76
|
+
|
71
77
|
describe 'functions' do
|
72
78
|
it 'should include IF' do
|
73
79
|
calculator.evaluate('if (foo < 8, 10, 20)', :foo => 2).should eq(10)
|
data/spec/evaluator_spec.rb
CHANGED
@@ -60,8 +60,9 @@ describe Dentaku::Evaluator do
|
|
60
60
|
end
|
61
61
|
|
62
62
|
describe 'functions' do
|
63
|
-
it 'should
|
63
|
+
it 'should be evaluated' do
|
64
64
|
evaluator.evaluate(token_stream(:round, :open, 5, :divide, 3.0, :close)).should eq 2
|
65
|
+
evaluator.evaluate(token_stream(:round, :open, 5, :divide, 3.0, :comma, 2, :close)).should eq 1.67
|
65
66
|
evaluator.evaluate(token_stream(:roundup, :open, 5, :divide, 1.2, :close)).should eq 5
|
66
67
|
evaluator.evaluate(token_stream(:rounddown, :open, 5, :divide, 1.2, :close)).should eq 4
|
67
68
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/token_scanner_spec.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.11
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-08-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -53,18 +53,22 @@ extensions: []
|
|
53
53
|
extra_rdoc_files: []
|
54
54
|
files:
|
55
55
|
- .gitignore
|
56
|
+
- .travis.yml
|
56
57
|
- Gemfile
|
57
58
|
- README.md
|
58
59
|
- Rakefile
|
59
60
|
- dentaku.gemspec
|
60
61
|
- lib/dentaku.rb
|
62
|
+
- lib/dentaku/binary_operation.rb
|
61
63
|
- lib/dentaku/calculator.rb
|
62
64
|
- lib/dentaku/evaluator.rb
|
65
|
+
- lib/dentaku/rules.rb
|
63
66
|
- lib/dentaku/token.rb
|
64
67
|
- lib/dentaku/token_matcher.rb
|
65
68
|
- lib/dentaku/token_scanner.rb
|
66
69
|
- lib/dentaku/tokenizer.rb
|
67
70
|
- lib/dentaku/version.rb
|
71
|
+
- spec/binary_operation_spec.rb
|
68
72
|
- spec/calculator_spec.rb
|
69
73
|
- spec/dentaku_spec.rb
|
70
74
|
- spec/evaluator_spec.rb
|
@@ -85,18 +89,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
89
|
- - ! '>='
|
86
90
|
- !ruby/object:Gem::Version
|
87
91
|
version: '0'
|
88
|
-
segments:
|
89
|
-
- 0
|
90
|
-
hash: -662526630847863998
|
91
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
93
|
none: false
|
93
94
|
requirements:
|
94
95
|
- - ! '>='
|
95
96
|
- !ruby/object:Gem::Version
|
96
97
|
version: '0'
|
97
|
-
segments:
|
98
|
-
- 0
|
99
|
-
hash: -662526630847863998
|
100
98
|
requirements: []
|
101
99
|
rubyforge_project: dentaku
|
102
100
|
rubygems_version: 1.8.23
|
@@ -104,6 +102,7 @@ signing_key:
|
|
104
102
|
specification_version: 3
|
105
103
|
summary: A formula language parser and evaluator
|
106
104
|
test_files:
|
105
|
+
- spec/binary_operation_spec.rb
|
107
106
|
- spec/calculator_spec.rb
|
108
107
|
- spec/dentaku_spec.rb
|
109
108
|
- spec/evaluator_spec.rb
|