dentaku 0.2.10 → 0.2.11
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/.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
|
+
[](http://badge.fury.io/rb/dentaku)
|
5
|
+
[](https://travis-ci.org/rubysolo/dentaku)
|
6
|
+
[](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
|