dentaku_zevo 3.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.pryrc +2 -0
- data/.rubocop.yml +114 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +281 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +342 -0
- data/Rakefile +31 -0
- data/dentaku.gemspec +32 -0
- data/lib/dentaku/ast/access.rb +47 -0
- data/lib/dentaku/ast/arithmetic.rb +241 -0
- data/lib/dentaku/ast/array.rb +41 -0
- data/lib/dentaku/ast/bitwise.rb +42 -0
- data/lib/dentaku/ast/case/case_conditional.rb +38 -0
- data/lib/dentaku/ast/case/case_else.rb +35 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
- data/lib/dentaku/ast/case/case_then.rb +35 -0
- data/lib/dentaku/ast/case/case_when.rb +39 -0
- data/lib/dentaku/ast/case.rb +81 -0
- data/lib/dentaku/ast/combinators.rb +50 -0
- data/lib/dentaku/ast/comparators.rb +89 -0
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +56 -0
- data/lib/dentaku/ast/function_registry.rb +98 -0
- data/lib/dentaku/ast/functions/all.rb +23 -0
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/any.rb +23 -0
- data/lib/dentaku/ast/functions/avg.rb +13 -0
- data/lib/dentaku/ast/functions/count.rb +26 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +37 -0
- data/lib/dentaku/ast/functions/filter.rb +23 -0
- data/lib/dentaku/ast/functions/if.rb +51 -0
- data/lib/dentaku/ast/functions/map.rb +23 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/mul.rb +12 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/pluck.rb +30 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +8 -0
- data/lib/dentaku/ast/functions/roundup.rb +8 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +55 -0
- data/lib/dentaku/ast/functions/string_functions.rb +212 -0
- data/lib/dentaku/ast/functions/sum.rb +12 -0
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +23 -0
- data/lib/dentaku/ast/identifier.rb +52 -0
- data/lib/dentaku/ast/literal.rb +30 -0
- data/lib/dentaku/ast/logical.rb +8 -0
- data/lib/dentaku/ast/negation.rb +54 -0
- data/lib/dentaku/ast/nil.rb +13 -0
- data/lib/dentaku/ast/node.rb +28 -0
- data/lib/dentaku/ast/numeric.rb +8 -0
- data/lib/dentaku/ast/operation.rb +39 -0
- data/lib/dentaku/ast/string.rb +15 -0
- data/lib/dentaku/ast.rb +39 -0
- data/lib/dentaku/bulk_expression_solver.rb +128 -0
- data/lib/dentaku/calculator.rb +169 -0
- data/lib/dentaku/date_arithmetic.rb +45 -0
- data/lib/dentaku/dependency_resolver.rb +24 -0
- data/lib/dentaku/exceptions.rb +102 -0
- data/lib/dentaku/flat_hash.rb +38 -0
- data/lib/dentaku/parser.rb +349 -0
- data/lib/dentaku/print_visitor.rb +101 -0
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +36 -0
- data/lib/dentaku/token_matcher.rb +138 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +183 -0
- data/lib/dentaku/tokenizer.rb +110 -0
- data/lib/dentaku/version.rb +3 -0
- data/lib/dentaku/visitor/infix.rb +82 -0
- data/lib/dentaku.rb +69 -0
- data/spec/ast/addition_spec.rb +62 -0
- data/spec/ast/all_spec.rb +25 -0
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +32 -0
- data/spec/ast/any_spec.rb +23 -0
- data/spec/ast/arithmetic_spec.rb +91 -0
- data/spec/ast/avg_spec.rb +37 -0
- data/spec/ast/case_spec.rb +84 -0
- data/spec/ast/comparator_spec.rb +87 -0
- data/spec/ast/count_spec.rb +40 -0
- data/spec/ast/division_spec.rb +35 -0
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +69 -0
- data/spec/ast/map_spec.rb +27 -0
- data/spec/ast/max_spec.rb +33 -0
- data/spec/ast/min_spec.rb +33 -0
- data/spec/ast/mul_spec.rb +43 -0
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +43 -0
- data/spec/ast/numeric_spec.rb +16 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/pluck_spec.rb +32 -0
- data/spec/ast/round_spec.rb +35 -0
- data/spec/ast/rounddown_spec.rb +35 -0
- data/spec/ast/roundup_spec.rb +35 -0
- data/spec/ast/string_functions_spec.rb +217 -0
- data/spec/ast/sum_spec.rb +43 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/benchmark.rb +70 -0
- data/spec/bulk_expression_solver_spec.rb +201 -0
- data/spec/calculator_spec.rb +898 -0
- data/spec/dentaku_spec.rb +52 -0
- data/spec/exceptions_spec.rb +9 -0
- data/spec/external_function_spec.rb +106 -0
- data/spec/parser_spec.rb +166 -0
- data/spec/print_visitor_spec.rb +66 -0
- data/spec/spec_helper.rb +71 -0
- data/spec/token_matcher_spec.rb +134 -0
- data/spec/token_scanner_spec.rb +49 -0
- data/spec/token_spec.rb +16 -0
- data/spec/tokenizer_spec.rb +359 -0
- data/spec/visitor/infix_spec.rb +31 -0
- data/spec/visitor_spec.rb +138 -0
- metadata +335 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'dentaku/token'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
class TokenMatcher
|
5
|
+
attr_reader :children, :categories, :values
|
6
|
+
|
7
|
+
def initialize(categories = nil, values = nil, children = [])
|
8
|
+
# store categories and values as hash to optimize key lookup, h/t @jan-mangs
|
9
|
+
@categories = [categories].compact.flatten.each_with_object({}) { |c, h| h[c] = 1 }
|
10
|
+
@values = [values].compact.flatten.each_with_object({}) { |v, h| h[v] = 1 }
|
11
|
+
@children = children.compact
|
12
|
+
@invert = false
|
13
|
+
|
14
|
+
@min = 1
|
15
|
+
@max = 1
|
16
|
+
@range = (@min..@max)
|
17
|
+
end
|
18
|
+
|
19
|
+
def |(other_matcher)
|
20
|
+
self.class.new(:nomatch, :nomatch, leaf_matchers + other_matcher.leaf_matchers)
|
21
|
+
end
|
22
|
+
|
23
|
+
def invert
|
24
|
+
@invert = ! @invert
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(token)
|
29
|
+
leaf_matcher? ? matches_token?(token) : any_child_matches_token?(token)
|
30
|
+
end
|
31
|
+
|
32
|
+
def match(token_stream, offset = 0)
|
33
|
+
matched_tokens = []
|
34
|
+
matched = false
|
35
|
+
|
36
|
+
while self == token_stream[matched_tokens.length + offset] && matched_tokens.length < @max
|
37
|
+
matched_tokens << token_stream[matched_tokens.length + offset]
|
38
|
+
end
|
39
|
+
|
40
|
+
if @range.cover?(matched_tokens.length)
|
41
|
+
matched = true
|
42
|
+
end
|
43
|
+
|
44
|
+
[matched, matched_tokens]
|
45
|
+
end
|
46
|
+
|
47
|
+
def caret
|
48
|
+
@caret = true
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def caret?
|
53
|
+
@caret
|
54
|
+
end
|
55
|
+
|
56
|
+
def star
|
57
|
+
@min = 0
|
58
|
+
@max = Float::INFINITY
|
59
|
+
@range = (@min..@max)
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def plus
|
64
|
+
@max = Float::INFINITY
|
65
|
+
@range = (@min..@max)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def leaf_matcher?
|
70
|
+
children.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
def leaf_matchers
|
74
|
+
leaf_matcher? ? [self] : children
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def any_child_matches_token?(token)
|
80
|
+
children.any? { |child| child == token }
|
81
|
+
end
|
82
|
+
|
83
|
+
def matches_token?(token)
|
84
|
+
return false if token.nil?
|
85
|
+
(category_match(token.category) && value_match(token.value)) ^ @invert
|
86
|
+
end
|
87
|
+
|
88
|
+
def category_match(category)
|
89
|
+
@categories.empty? || @categories.key?(category)
|
90
|
+
end
|
91
|
+
|
92
|
+
def value_match(value)
|
93
|
+
@values.empty? || @values.key?(value)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.datetime; new(:datetime); end
|
97
|
+
def self.numeric; new(:numeric); end
|
98
|
+
def self.string; new(:string); end
|
99
|
+
def self.logical; new(:logical); end
|
100
|
+
def self.value
|
101
|
+
new(:datetime) | new(:numeric) | new(:string) | new(:logical)
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.addsub; new(:operator, [:add, :subtract]); end
|
105
|
+
def self.subtract; new(:operator, :subtract); end
|
106
|
+
def self.anchored_minus; new(:operator, :subtract).caret; end
|
107
|
+
def self.muldiv; new(:operator, [:multiply, :divide]); end
|
108
|
+
def self.pow; new(:operator, :pow); end
|
109
|
+
def self.mod; new(:operator, :mod); end
|
110
|
+
def self.combinator; new(:combinator); end
|
111
|
+
|
112
|
+
def self.comparator; new(:comparator); end
|
113
|
+
def self.comp_gt; new(:comparator, [:gt, :ge]); end
|
114
|
+
def self.comp_lt; new(:comparator, [:lt, :le]); end
|
115
|
+
|
116
|
+
def self.open; new(:grouping, :open); end
|
117
|
+
def self.close; new(:grouping, :close); end
|
118
|
+
def self.comma; new(:grouping, :comma); end
|
119
|
+
def self.non_group; new(:grouping).invert; end
|
120
|
+
def self.non_group_star; new(:grouping).invert.star; end
|
121
|
+
def self.non_close_plus; new(:grouping, :close).invert.plus; end
|
122
|
+
def self.arguments; (value | comma).plus; end
|
123
|
+
|
124
|
+
def self.if; new(:function, :if); end
|
125
|
+
def self.round; new(:function, :round); end
|
126
|
+
def self.roundup; new(:function, :roundup); end
|
127
|
+
def self.rounddown; new(:function, :rounddown); end
|
128
|
+
def self.not; new(:function, :not); end
|
129
|
+
|
130
|
+
def self.method_missing(name, *args, &block)
|
131
|
+
new(:function, name)
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.respond_to_missing?(name, include_priv)
|
135
|
+
true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module TokenMatchers
|
3
|
+
def self.token_matchers(*symbols)
|
4
|
+
symbols.map { |s| matcher(s) }
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.function_token_matchers(function_name, *symbols)
|
8
|
+
token_matchers(:open, *symbols, :close).unshift(
|
9
|
+
TokenMatcher.send(function_name)
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.matcher(symbol)
|
14
|
+
@matchers ||= [
|
15
|
+
:datetime, :numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
16
|
+
:comparator, :comp_gt, :comp_lt, :open, :close, :comma,
|
17
|
+
:non_close_plus, :non_group, :non_group_star, :arguments,
|
18
|
+
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
19
|
+
:anchored_minus, :math_neg_pow, :math_neg_mul
|
20
|
+
].each_with_object({}) do |name, matchers|
|
21
|
+
matchers[name] = TokenMatcher.send(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
@matchers.fetch(symbol) do
|
25
|
+
raise "Unknown token symbol #{ symbol }"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'time'
|
3
|
+
require 'dentaku/string_casing'
|
4
|
+
require 'dentaku/token'
|
5
|
+
|
6
|
+
module Dentaku
|
7
|
+
class TokenScanner
|
8
|
+
extend StringCasing
|
9
|
+
|
10
|
+
def initialize(category, regexp, converter = nil, condition = nil)
|
11
|
+
@category = category
|
12
|
+
@regexp = %r{\A(#{ regexp })}i
|
13
|
+
@converter = converter
|
14
|
+
@condition = condition || ->(*) { true }
|
15
|
+
end
|
16
|
+
|
17
|
+
def scan(string, last_token = nil)
|
18
|
+
if (m = @regexp.match(string)) && @condition.call(last_token)
|
19
|
+
value = raw = m.to_s
|
20
|
+
value = @converter.call(raw) if @converter
|
21
|
+
|
22
|
+
return Array(value).map do |v|
|
23
|
+
Token === v ? v : Token.new(@category, v, raw)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_reader :case_sensitive
|
32
|
+
|
33
|
+
def available_scanners
|
34
|
+
[
|
35
|
+
:null,
|
36
|
+
:whitespace,
|
37
|
+
:datetime, # before numeric so it can pick up timestamps
|
38
|
+
:numeric,
|
39
|
+
:hexadecimal,
|
40
|
+
:double_quoted_string,
|
41
|
+
:single_quoted_string,
|
42
|
+
:negate,
|
43
|
+
:combinator,
|
44
|
+
:operator,
|
45
|
+
:grouping,
|
46
|
+
:array,
|
47
|
+
:access,
|
48
|
+
:case_statement,
|
49
|
+
:comparator,
|
50
|
+
:boolean,
|
51
|
+
:function,
|
52
|
+
:identifier
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
def register_default_scanners
|
57
|
+
register_scanners(available_scanners)
|
58
|
+
end
|
59
|
+
|
60
|
+
def register_scanners(scanner_ids)
|
61
|
+
@scanners = scanner_ids.each_with_object({}) do |id, scanners|
|
62
|
+
scanners[id] = self.send(id)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def register_scanner(id, scanner)
|
67
|
+
@scanners[id] = scanner
|
68
|
+
end
|
69
|
+
|
70
|
+
def scanners=(scanner_ids)
|
71
|
+
@scanners.select! { |k, v| scanner_ids.include?(k) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def scanners(options = {})
|
75
|
+
@case_sensitive = options.fetch(:case_sensitive, false)
|
76
|
+
@scanners.values
|
77
|
+
end
|
78
|
+
|
79
|
+
def whitespace
|
80
|
+
new(:whitespace, '\s+')
|
81
|
+
end
|
82
|
+
|
83
|
+
def null
|
84
|
+
new(:null, 'null\b')
|
85
|
+
end
|
86
|
+
|
87
|
+
# NOTE: Convert to DateTime as Array(Time) returns the parts of the time for some reason
|
88
|
+
def datetime
|
89
|
+
new(:datetime, /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?/, lambda { |raw| Time.parse(raw).to_datetime })
|
90
|
+
end
|
91
|
+
|
92
|
+
def numeric
|
93
|
+
new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
|
94
|
+
raw =~ /(\.|e|E)/ ? BigDecimal(raw) : raw.to_i
|
95
|
+
})
|
96
|
+
end
|
97
|
+
|
98
|
+
def hexadecimal
|
99
|
+
new(:numeric, '(0x[0-9a-f]+)\b', lambda { |raw| raw[2..-1].to_i(16) })
|
100
|
+
end
|
101
|
+
|
102
|
+
def double_quoted_string
|
103
|
+
new(:string, '"[^"]*"', lambda { |raw| raw.gsub(/^"|"$/, '') })
|
104
|
+
end
|
105
|
+
|
106
|
+
def single_quoted_string
|
107
|
+
new(:string, "'[^']*'", lambda { |raw| raw.gsub(/^'|'$/, '') })
|
108
|
+
end
|
109
|
+
|
110
|
+
def negate
|
111
|
+
new(:operator, '-', lambda { |raw| :negate }, lambda { |last_token|
|
112
|
+
last_token.nil? ||
|
113
|
+
last_token.is?(:operator) ||
|
114
|
+
last_token.is?(:comparator) ||
|
115
|
+
last_token.is?(:combinator) ||
|
116
|
+
last_token.value == :open ||
|
117
|
+
last_token.value == :comma
|
118
|
+
})
|
119
|
+
end
|
120
|
+
|
121
|
+
def operator
|
122
|
+
names = {
|
123
|
+
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&', bitshiftleft: '<<', bitshiftright: '>>'
|
124
|
+
}.invert
|
125
|
+
new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
|
126
|
+
end
|
127
|
+
|
128
|
+
def grouping
|
129
|
+
names = { open: '(', close: ')', comma: ',' }.invert
|
130
|
+
new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
|
131
|
+
end
|
132
|
+
|
133
|
+
def array
|
134
|
+
names = { array_start: '{', array_end: '}', }.invert
|
135
|
+
new(:array, '\{|\}|,', lambda { |raw| names[raw] })
|
136
|
+
end
|
137
|
+
|
138
|
+
def access
|
139
|
+
names = { lbracket: '[', rbracket: ']' }.invert
|
140
|
+
new(:access, '\[|\]', lambda { |raw| names[raw] })
|
141
|
+
end
|
142
|
+
|
143
|
+
def case_statement
|
144
|
+
names = { open: 'case', close: 'end', then: 'then', when: 'when', else: 'else' }.invert
|
145
|
+
new(:case, '(case|end|then|when|else)\b', lambda { |raw| names[raw.downcase] })
|
146
|
+
end
|
147
|
+
|
148
|
+
def comparator
|
149
|
+
names = { le: '<=', ge: '>=', ne: '!=', lt: '<', gt: '>', eq: '=' }.invert
|
150
|
+
alternate = { ne: '<>', eq: '==' }.invert
|
151
|
+
new(:comparator, '<=|>=|!=|<>|<|>|==|=', lambda { |raw| names[raw] || alternate[raw] })
|
152
|
+
end
|
153
|
+
|
154
|
+
def combinator
|
155
|
+
names = { and: '&&', or: '||' }.invert
|
156
|
+
new(:combinator, '(and|or|&&|\|\|)\s', lambda { |raw|
|
157
|
+
norm = raw.strip.downcase
|
158
|
+
names.fetch(norm) { norm.to_sym }
|
159
|
+
})
|
160
|
+
end
|
161
|
+
|
162
|
+
def boolean
|
163
|
+
new(:logical, '(true|false)\b', lambda { |raw| raw.strip.downcase == 'true' })
|
164
|
+
end
|
165
|
+
|
166
|
+
def function
|
167
|
+
new(:function, '\w+!?\s*\(', lambda do |raw|
|
168
|
+
function_name = raw.gsub('(', '')
|
169
|
+
[
|
170
|
+
Token.new(:function, function_name.strip.downcase.to_sym, function_name),
|
171
|
+
Token.new(:grouping, :open, '(')
|
172
|
+
]
|
173
|
+
end)
|
174
|
+
end
|
175
|
+
|
176
|
+
def identifier
|
177
|
+
new(:identifier, '[[[:word:]]\.]+\b', lambda { |raw| standardize_case(raw.strip) })
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
register_default_scanners
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'dentaku/token'
|
2
|
+
require 'dentaku/token_matcher'
|
3
|
+
require 'dentaku/token_scanner'
|
4
|
+
|
5
|
+
module Dentaku
|
6
|
+
class Tokenizer
|
7
|
+
attr_reader :case_sensitive, :aliases
|
8
|
+
|
9
|
+
LPAREN = TokenMatcher.new(:grouping, :open)
|
10
|
+
RPAREN = TokenMatcher.new(:grouping, :close)
|
11
|
+
|
12
|
+
def tokenize(string, options = {})
|
13
|
+
@nesting = 0
|
14
|
+
@tokens = []
|
15
|
+
@aliases = options.fetch(:aliases, global_aliases)
|
16
|
+
input = strip_comments(string.to_s.dup)
|
17
|
+
input = replace_aliases(input)
|
18
|
+
@case_sensitive = options.fetch(:case_sensitive, false)
|
19
|
+
|
20
|
+
until input.empty?
|
21
|
+
scanned = TokenScanner.scanners(case_sensitive: case_sensitive).any? do |scanner|
|
22
|
+
scanned, input = scan(input, scanner)
|
23
|
+
scanned
|
24
|
+
end
|
25
|
+
|
26
|
+
unless scanned
|
27
|
+
fail! :parse_error, at: input
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
fail! :too_many_opening_parentheses if @nesting > 0
|
32
|
+
|
33
|
+
@tokens
|
34
|
+
end
|
35
|
+
|
36
|
+
def last_token
|
37
|
+
@tokens.last
|
38
|
+
end
|
39
|
+
|
40
|
+
def scan(string, scanner)
|
41
|
+
if tokens = scanner.scan(string, last_token)
|
42
|
+
tokens.each do |token|
|
43
|
+
if token.empty?
|
44
|
+
fail! :unexpected_zero_width_match,
|
45
|
+
token_category: token.category, at: string
|
46
|
+
end
|
47
|
+
|
48
|
+
@nesting += 1 if LPAREN == token
|
49
|
+
@nesting -= 1 if RPAREN == token
|
50
|
+
fail! :too_many_closing_parentheses if @nesting < 0
|
51
|
+
|
52
|
+
@tokens << token unless token.is?(:whitespace)
|
53
|
+
end
|
54
|
+
|
55
|
+
match_length = tokens.map(&:length).reduce(:+)
|
56
|
+
[true, string[match_length..-1]]
|
57
|
+
else
|
58
|
+
[false, string]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def strip_comments(input)
|
63
|
+
input.gsub(/\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//, '')
|
64
|
+
end
|
65
|
+
|
66
|
+
def replace_aliases(string)
|
67
|
+
return string unless @aliases.any?
|
68
|
+
|
69
|
+
string.gsub!(alias_regex) do |match|
|
70
|
+
match_regex = /^#{Regexp.escape(match)}$/i
|
71
|
+
|
72
|
+
@aliases.detect do |(_key, aliases)|
|
73
|
+
!aliases.grep(match_regex).empty?
|
74
|
+
end.first
|
75
|
+
end
|
76
|
+
|
77
|
+
string
|
78
|
+
end
|
79
|
+
|
80
|
+
def alias_regex
|
81
|
+
values = @aliases.values.flatten.join('|')
|
82
|
+
/(?<=\p{Punct}|[[:space:]]|\A)(#{values})(?=\()/i
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def global_aliases
|
88
|
+
return {} unless Dentaku.respond_to?(:aliases)
|
89
|
+
Dentaku.aliases
|
90
|
+
end
|
91
|
+
|
92
|
+
def fail!(reason, **meta)
|
93
|
+
message =
|
94
|
+
case reason
|
95
|
+
when :parse_error
|
96
|
+
"parse error at: '#{meta.fetch(:at)}'"
|
97
|
+
when :too_many_opening_parentheses
|
98
|
+
"too many opening parentheses"
|
99
|
+
when :too_many_closing_parentheses
|
100
|
+
"too many closing parentheses"
|
101
|
+
when :unexpected_zero_width_match
|
102
|
+
"unexpected zero-width match (:#{meta.fetch(:category)}) at '#{meta.fetch(:at)}'"
|
103
|
+
else
|
104
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
105
|
+
end
|
106
|
+
|
107
|
+
raise TokenizerError.for(reason, **meta), message
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# infix visitor
|
2
|
+
#
|
3
|
+
# use this visitor in a processor to get infix visiting order
|
4
|
+
#
|
5
|
+
# visitor node deps
|
6
|
+
# accept -> visit left ->
|
7
|
+
# process
|
8
|
+
# visit right ->
|
9
|
+
module Dentaku
|
10
|
+
module Visitor
|
11
|
+
module Infix
|
12
|
+
def visit(ast)
|
13
|
+
ast.accept(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def process(_ast)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit_function(node)
|
21
|
+
node.args.each do |arg|
|
22
|
+
visit(arg)
|
23
|
+
end
|
24
|
+
process(node)
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_identifier(node)
|
28
|
+
process(node)
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit_operation(node)
|
32
|
+
visit(node.left) if node.left
|
33
|
+
process(node)
|
34
|
+
visit(node.right) if node.right
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_operand(node)
|
38
|
+
process(node)
|
39
|
+
end
|
40
|
+
|
41
|
+
def visit_case(node)
|
42
|
+
process(node)
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_switch(node)
|
46
|
+
process(node)
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_case_conditional(node)
|
50
|
+
process(node)
|
51
|
+
end
|
52
|
+
|
53
|
+
def visit_when(node)
|
54
|
+
process(node)
|
55
|
+
end
|
56
|
+
|
57
|
+
def visit_then(node)
|
58
|
+
process(node)
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_else(node)
|
62
|
+
process(node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_negation(node)
|
66
|
+
process(node)
|
67
|
+
end
|
68
|
+
|
69
|
+
def visit_access(node)
|
70
|
+
process(node)
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_literal(node)
|
74
|
+
process(node)
|
75
|
+
end
|
76
|
+
|
77
|
+
def visit_nil(node)
|
78
|
+
process(node)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/dentaku.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require "bigdecimal"
|
2
|
+
require "concurrent"
|
3
|
+
require "dentaku/calculator"
|
4
|
+
require "dentaku/version"
|
5
|
+
|
6
|
+
module Dentaku
|
7
|
+
@enable_ast_caching = false
|
8
|
+
@enable_dependency_order_caching = false
|
9
|
+
@enable_identifier_caching = false
|
10
|
+
@aliases = {}
|
11
|
+
|
12
|
+
def self.evaluate(expression, data = {}, &block)
|
13
|
+
calculator.value.evaluate(expression, data, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.evaluate!(expression, data = {}, &block)
|
17
|
+
calculator.value.evaluate!(expression, data, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.enable_caching!
|
21
|
+
enable_ast_cache!
|
22
|
+
enable_dependency_order_cache!
|
23
|
+
enable_identifier_cache!
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.enable_ast_cache!
|
27
|
+
@enable_ast_caching = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.cache_ast?
|
31
|
+
@enable_ast_caching
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.enable_dependency_order_cache!
|
35
|
+
@enable_dependency_order_caching = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.cache_dependency_order?
|
39
|
+
@enable_dependency_order_caching
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.enable_identifier_cache!
|
43
|
+
@enable_identifier_caching = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.cache_identifier?
|
47
|
+
@enable_identifier_caching
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.aliases
|
51
|
+
@aliases
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.aliases=(hash)
|
55
|
+
@aliases = hash
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.calculator
|
59
|
+
@calculator ||= Concurrent::ThreadLocalVar.new { Dentaku::Calculator.new }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def Dentaku(expression, data = {})
|
64
|
+
Dentaku.evaluate(expression, data)
|
65
|
+
end
|
66
|
+
|
67
|
+
def Dentaku!(expression, data = {})
|
68
|
+
Dentaku.evaluate!(expression, data)
|
69
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/arithmetic'
|
3
|
+
|
4
|
+
require 'dentaku/token'
|
5
|
+
|
6
|
+
describe Dentaku::AST::Addition do
|
7
|
+
let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
|
8
|
+
let(:six) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 6) }
|
9
|
+
|
10
|
+
let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
|
11
|
+
|
12
|
+
it 'allows access to its sub-trees' do
|
13
|
+
node = described_class.new(five, six)
|
14
|
+
expect(node.left).to eq(five)
|
15
|
+
expect(node.right).to eq(six)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'performs addition' do
|
19
|
+
node = described_class.new(five, six)
|
20
|
+
expect(node.value).to eq(11)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'requires numeric operands' do
|
24
|
+
expect {
|
25
|
+
described_class.new(five, t)
|
26
|
+
}.to raise_error(Dentaku::NodeError, /requires numeric operands/)
|
27
|
+
|
28
|
+
expression = Dentaku::AST::Multiplication.new(five, five)
|
29
|
+
group = Dentaku::AST::Grouping.new(expression)
|
30
|
+
|
31
|
+
expect {
|
32
|
+
described_class.new(group, five)
|
33
|
+
}.not_to raise_error
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'allows operands that respond to addition' do
|
37
|
+
# Sample struct that has a custom definition for addition
|
38
|
+
|
39
|
+
Operand = Struct.new(:value) do
|
40
|
+
def +(other)
|
41
|
+
case other
|
42
|
+
when Operand
|
43
|
+
value + other.value
|
44
|
+
when Numeric
|
45
|
+
value + other
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(5))
|
51
|
+
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(6))
|
52
|
+
|
53
|
+
expect {
|
54
|
+
described_class.new(operand_five, operand_six)
|
55
|
+
}.not_to raise_error
|
56
|
+
|
57
|
+
expect {
|
58
|
+
described_class.new(operand_five, six)
|
59
|
+
}.not_to raise_error
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/all'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::All do
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
it 'performs ALL operation' do
|
8
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
|
9
|
+
expect(result).to eq(false)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'works with a single value if needed for some reason' do
|
13
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: 1)
|
14
|
+
expect(result).to eq(false)
|
15
|
+
|
16
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: 2)
|
17
|
+
expect(result).to eq(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'raises argument error if a string is passed as identifier' do
|
21
|
+
expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
22
|
+
Dentaku::ArgumentError, 'ALL() requires second argument to be an identifier'
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|