dentaku 3.5.7 → 4.0.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +2 -6
- data/.github/workflows/rubocop.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +91 -0
- data/Gemfile +14 -1
- data/README.md +87 -0
- data/dentaku.gemspec +14 -14
- data/lib/dentaku/ast/access.rb +18 -1
- data/lib/dentaku/ast/arithmetic.rb +5 -17
- data/lib/dentaku/ast/array.rb +6 -0
- data/lib/dentaku/ast/bitwise.rb +6 -4
- data/lib/dentaku/ast/case/case_conditional.rb +6 -0
- data/lib/dentaku/ast/case/case_else.rb +6 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
- data/lib/dentaku/ast/case/case_then.rb +6 -0
- data/lib/dentaku/ast/case/case_when.rb +7 -0
- data/lib/dentaku/ast/case.rb +30 -3
- data/lib/dentaku/ast/combinators.rb +49 -4
- data/lib/dentaku/ast/comparators.rb +3 -7
- data/lib/dentaku/ast/function.rb +12 -13
- data/lib/dentaku/ast/function_registry.rb +11 -2
- data/lib/dentaku/ast/functions/abs.rb +1 -1
- data/lib/dentaku/ast/functions/and.rb +4 -4
- data/lib/dentaku/ast/functions/avg.rb +3 -3
- data/lib/dentaku/ast/functions/duration.rb +1 -1
- data/lib/dentaku/ast/functions/if.rb +9 -1
- data/lib/dentaku/ast/functions/intercept.rb +4 -4
- data/lib/dentaku/ast/functions/max.rb +1 -1
- data/lib/dentaku/ast/functions/min.rb +1 -1
- data/lib/dentaku/ast/functions/mul.rb +3 -3
- data/lib/dentaku/ast/functions/or.rb +4 -4
- data/lib/dentaku/ast/functions/pluck.rb +1 -1
- data/lib/dentaku/ast/functions/round.rb +1 -1
- data/lib/dentaku/ast/functions/rounddown.rb +2 -2
- data/lib/dentaku/ast/functions/roundup.rb +2 -2
- data/lib/dentaku/ast/functions/ruby_math.rb +5 -2
- data/lib/dentaku/ast/functions/string_functions.rb +5 -5
- data/lib/dentaku/ast/functions/sum.rb +3 -3
- data/lib/dentaku/ast/functions/xor.rb +4 -4
- data/lib/dentaku/ast/grouping.rb +6 -0
- data/lib/dentaku/ast/negation.rb +5 -0
- data/lib/dentaku/ast/node.rb +25 -0
- data/lib/dentaku/ast/operation.rb +7 -0
- data/lib/dentaku/bulk_expression_solver.rb +22 -16
- data/lib/dentaku/calculator.rb +45 -17
- data/lib/dentaku/date_arithmetic.rb +2 -2
- data/lib/dentaku/dependency_resolver.rb +12 -5
- data/lib/dentaku/exceptions.rb +105 -13
- data/lib/dentaku/numeric_parser.rb +68 -0
- data/lib/dentaku/parser.rb +24 -48
- data/lib/dentaku/print_visitor.rb +1 -0
- data/lib/dentaku/token_matcher.rb +31 -37
- data/lib/dentaku/token_matchers.rb +3 -3
- data/lib/dentaku/token_scanner.rb +3 -4
- data/lib/dentaku/tokenizer.rb +2 -16
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/case_spec.rb +7 -0
- data/spec/ast/function_spec.rb +11 -25
- data/spec/bulk_expression_solver_spec.rb +48 -3
- data/spec/calculator_spec.rb +77 -0
- data/spec/dependency_resolver_spec.rb +14 -0
- data/spec/exceptions_spec.rb +87 -0
- data/spec/gemspec_spec.rb +10 -0
- data/spec/identifiers_spec.rb +66 -0
- data/spec/numeric_parser_spec.rb +66 -0
- data/spec/parser_spec.rb +35 -0
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/tokenizer_spec.rb +6 -0
- data/spec/volatile_functions_spec.rb +121 -0
- metadata +27 -150
- data/.travis.yml +0 -10
data/lib/dentaku/calculator.rb
CHANGED
|
@@ -9,38 +9,46 @@ require 'dentaku/token'
|
|
|
9
9
|
module Dentaku
|
|
10
10
|
class Calculator
|
|
11
11
|
include StringCasing
|
|
12
|
-
attr_reader :result, :memory, :tokenizer, :case_sensitive,
|
|
12
|
+
attr_reader :result, :memory, :tokenizer, :case_sensitive,
|
|
13
13
|
:nested_data_support, :ast_cache, :raw_date_literals
|
|
14
14
|
|
|
15
|
-
def initialize(
|
|
15
|
+
def initialize(case_sensitive: false, aliases: nil, nested_data_support: true,
|
|
16
|
+
raw_date_literals: true, ast_cache: {},
|
|
17
|
+
cache_ast: nil, cache_dependency_order: nil)
|
|
16
18
|
clear
|
|
17
19
|
@tokenizer = Tokenizer.new
|
|
18
|
-
@case_sensitive =
|
|
19
|
-
@aliases =
|
|
20
|
-
@nested_data_support =
|
|
21
|
-
|
|
22
|
-
@
|
|
23
|
-
|
|
24
|
-
@
|
|
20
|
+
@case_sensitive = case_sensitive
|
|
21
|
+
@aliases = aliases
|
|
22
|
+
@nested_data_support = nested_data_support
|
|
23
|
+
@raw_date_literals = raw_date_literals
|
|
24
|
+
@ast_cache = ast_cache
|
|
25
|
+
@cache_ast = cache_ast
|
|
26
|
+
@cache_dependency_order = cache_dependency_order
|
|
25
27
|
@disable_ast_cache = false
|
|
26
28
|
@function_registry = Dentaku::AST::FunctionRegistry.new
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
# explicitly configured aliases win; otherwise the module-level default is
|
|
32
|
+
# resolved lazily so it can be set after this calculator was created
|
|
33
|
+
def aliases
|
|
34
|
+
@aliases || Dentaku.aliases
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.add_function(name, type, body, callback = nil, volatile: false)
|
|
38
|
+
Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback, volatile: volatile)
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def self.add_functions(functions)
|
|
34
|
-
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
42
|
+
functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) }
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
def add_function(name, type, body, callback = nil)
|
|
38
|
-
@function_registry.register(name, type, body, callback)
|
|
45
|
+
def add_function(name, type, body, callback = nil, volatile: false)
|
|
46
|
+
@function_registry.register(name, type, body, callback, volatile: volatile)
|
|
39
47
|
self
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
def add_functions(functions)
|
|
43
|
-
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
51
|
+
functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) }
|
|
44
52
|
self
|
|
45
53
|
end
|
|
46
54
|
|
|
@@ -56,7 +64,7 @@ module Dentaku
|
|
|
56
64
|
return evaluate_array(expression, context, &block) if expression.is_a?(Array)
|
|
57
65
|
|
|
58
66
|
evaluate!(expression, context)
|
|
59
|
-
rescue Dentaku::Error
|
|
67
|
+
rescue Dentaku::Error => ex
|
|
60
68
|
block.call(expression, ex) if block_given?
|
|
61
69
|
end
|
|
62
70
|
|
|
@@ -106,6 +114,20 @@ module Dentaku
|
|
|
106
114
|
end
|
|
107
115
|
end
|
|
108
116
|
|
|
117
|
+
# every identifier the expression could reference, regardless of
|
|
118
|
+
# branching: purely syntactic, ignores stored memory, and never
|
|
119
|
+
# evaluates guards or functions
|
|
120
|
+
def identifiers(expression)
|
|
121
|
+
case expression
|
|
122
|
+
when Dentaku::AST::Node
|
|
123
|
+
expression.dependencies(AST::Node::STATIC_CONTEXT).uniq
|
|
124
|
+
when Array
|
|
125
|
+
expression.flat_map { |e| identifiers(e) }.uniq
|
|
126
|
+
else
|
|
127
|
+
ast(expression).dependencies(AST::Node::STATIC_CONTEXT).uniq
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
109
131
|
def ast(expression)
|
|
110
132
|
return expression if expression.is_a?(AST::Node)
|
|
111
133
|
return expression.map { |e| ast(e) } if expression.is_a? Array
|
|
@@ -186,7 +208,13 @@ module Dentaku
|
|
|
186
208
|
end
|
|
187
209
|
|
|
188
210
|
def cache_ast?
|
|
189
|
-
|
|
211
|
+
return false if @disable_ast_cache
|
|
212
|
+
|
|
213
|
+
@cache_ast.nil? ? Dentaku.cache_ast? : @cache_ast
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def cache_dependency_order?
|
|
217
|
+
@cache_dependency_order.nil? ? Dentaku.cache_dependency_order? : @cache_dependency_order
|
|
190
218
|
end
|
|
191
219
|
end
|
|
192
220
|
end
|
|
@@ -20,7 +20,7 @@ module Dentaku
|
|
|
20
20
|
change_datetime(@base, duration.unit, duration.value)
|
|
21
21
|
end
|
|
22
22
|
else
|
|
23
|
-
raise Dentaku::ArgumentError.for(:incompatible_type,
|
|
23
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: duration, expected: Numeric),
|
|
24
24
|
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -39,7 +39,7 @@ module Dentaku
|
|
|
39
39
|
when Dentaku::TokenScanner::DATE_TIME_REGEXP
|
|
40
40
|
@base - Time.parse(duration).to_datetime
|
|
41
41
|
else
|
|
42
|
-
raise Dentaku::ArgumentError.for(:incompatible_type,
|
|
42
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: duration, expected: Numeric),
|
|
43
43
|
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -5,13 +5,14 @@ module Dentaku
|
|
|
5
5
|
include TSort
|
|
6
6
|
|
|
7
7
|
def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
|
|
8
|
-
self.new(vars_to_dependencies_hash).sort
|
|
8
|
+
self.new(vars_to_dependencies_hash, case_sensitive).sort
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def initialize(vars_to_dependencies_hash)
|
|
12
|
-
@
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
def initialize(vars_to_dependencies_hash, case_sensitive = false)
|
|
12
|
+
@case_sensitive = case_sensitive
|
|
13
|
+
@key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [normalized_name(k), k] }]
|
|
14
|
+
# normalize variables and their dependencies alike so child lookups match
|
|
15
|
+
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [normalized_name(k), v.map { |d| normalized_name(d) }] }]
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def sort
|
|
@@ -25,5 +26,11 @@ module Dentaku
|
|
|
25
26
|
def tsort_each_child(node, &block)
|
|
26
27
|
@vars_to_deps.fetch(node.to_s, []).each(&block)
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def normalized_name(variable_name)
|
|
33
|
+
@case_sensitive ? variable_name.to_s : variable_name.to_s.downcase
|
|
34
|
+
end
|
|
28
35
|
end
|
|
29
36
|
end
|
data/lib/dentaku/exceptions.rb
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
module Dentaku
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
# Shared by all Dentaku exceptions so that `rescue Dentaku::Error` catches
|
|
3
|
+
# everything the gem raises, including exceptions that subclass Ruby
|
|
4
|
+
# built-ins (Dentaku::ArgumentError, Dentaku::ZeroDivisionError).
|
|
5
|
+
module Error
|
|
6
|
+
attr_accessor :assigned_to
|
|
4
7
|
end
|
|
5
8
|
|
|
6
|
-
class
|
|
9
|
+
class BaseError < StandardError
|
|
10
|
+
include Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class UnboundVariableError < BaseError
|
|
7
14
|
attr_reader :unbound_variables
|
|
8
15
|
|
|
9
16
|
def initialize(unbound_variables)
|
|
@@ -11,7 +18,7 @@ module Dentaku
|
|
|
11
18
|
end
|
|
12
19
|
end
|
|
13
20
|
|
|
14
|
-
class MathDomainError <
|
|
21
|
+
class MathDomainError < BaseError
|
|
15
22
|
attr_reader :function_name, :args
|
|
16
23
|
|
|
17
24
|
def initialize(function_name, args)
|
|
@@ -20,22 +27,23 @@ module Dentaku
|
|
|
20
27
|
end
|
|
21
28
|
end
|
|
22
29
|
|
|
23
|
-
class NodeError <
|
|
24
|
-
attr_reader :
|
|
30
|
+
class NodeError < BaseError
|
|
31
|
+
attr_reader :operand, :expected, :actual
|
|
25
32
|
|
|
26
|
-
def initialize(
|
|
27
|
-
@
|
|
33
|
+
def initialize(expected, actual, operand)
|
|
34
|
+
@expected = Array(expected)
|
|
28
35
|
@actual = actual
|
|
29
|
-
@
|
|
36
|
+
@operand = operand
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
|
|
33
|
-
class ParseError <
|
|
40
|
+
class ParseError < BaseError
|
|
34
41
|
attr_reader :reason, :meta
|
|
35
42
|
|
|
36
43
|
def initialize(reason, **meta)
|
|
37
44
|
@reason = reason
|
|
38
45
|
@meta = meta
|
|
46
|
+
super(default_message)
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
private_class_method :new
|
|
@@ -54,14 +62,49 @@ module Dentaku
|
|
|
54
62
|
|
|
55
63
|
new(reason, **meta)
|
|
56
64
|
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def default_message
|
|
69
|
+
case reason
|
|
70
|
+
when :node_invalid
|
|
71
|
+
if meta.key?(:operation)
|
|
72
|
+
expected = Array(meta[:expected]).map { |e| e == :incompatible ? :compatible : e }
|
|
73
|
+
"#{meta[:operation]} requires #{expected.join(', ')} operands, but got #{meta[:actual]}"
|
|
74
|
+
else
|
|
75
|
+
"Node is invalid"
|
|
76
|
+
end
|
|
77
|
+
when :too_few_operands
|
|
78
|
+
"#{meta[:operation]} has too few operands (given #{meta[:actual]}, expected #{meta[:expected]})"
|
|
79
|
+
when :too_many_operands
|
|
80
|
+
"#{meta[:operation]} has too many operands (given #{meta[:actual]}, expected #{meta[:expected]})"
|
|
81
|
+
when :undefined_function
|
|
82
|
+
"Undefined function #{meta[:function_name]}"
|
|
83
|
+
when :unprocessed_token
|
|
84
|
+
"Unprocessed token #{meta[:token_name]}"
|
|
85
|
+
when :unknown_case_token
|
|
86
|
+
"Unknown case token #{meta[:token_name]}"
|
|
87
|
+
when :unbalanced_bracket
|
|
88
|
+
"Unbalanced bracket"
|
|
89
|
+
when :unbalanced_parenthesis
|
|
90
|
+
"Unbalanced parenthesis"
|
|
91
|
+
when :unknown_grouping_token
|
|
92
|
+
"Unknown grouping token #{meta[:token_name]}"
|
|
93
|
+
when :not_implemented_token_category
|
|
94
|
+
"Not implemented for tokens of category #{meta[:token_category]}"
|
|
95
|
+
when :invalid_statement
|
|
96
|
+
"Invalid statement"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
57
99
|
end
|
|
58
100
|
|
|
59
|
-
class TokenizerError <
|
|
101
|
+
class TokenizerError < BaseError
|
|
60
102
|
attr_reader :reason, :meta
|
|
61
103
|
|
|
62
104
|
def initialize(reason, **meta)
|
|
63
105
|
@reason = reason
|
|
64
106
|
@meta = meta
|
|
107
|
+
super(default_message)
|
|
65
108
|
end
|
|
66
109
|
|
|
67
110
|
private_class_method :new
|
|
@@ -80,15 +123,32 @@ module Dentaku
|
|
|
80
123
|
|
|
81
124
|
new(reason, **meta)
|
|
82
125
|
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def default_message
|
|
130
|
+
case reason
|
|
131
|
+
when :parse_error
|
|
132
|
+
"parse error at: '#{meta[:at]}'"
|
|
133
|
+
when :too_many_opening_parentheses
|
|
134
|
+
"too many opening parentheses"
|
|
135
|
+
when :too_many_closing_parentheses
|
|
136
|
+
"too many closing parentheses"
|
|
137
|
+
when :unexpected_zero_width_match
|
|
138
|
+
"unexpected zero-width match (:#{meta[:token_category]}) at '#{meta[:at]}'"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
83
141
|
end
|
|
84
142
|
|
|
85
143
|
class ArgumentError < ::ArgumentError
|
|
144
|
+
include Error
|
|
145
|
+
|
|
86
146
|
attr_reader :reason, :meta
|
|
87
|
-
attr_accessor :recipient_variable
|
|
88
147
|
|
|
89
148
|
def initialize(reason, **meta)
|
|
90
149
|
@reason = reason
|
|
91
150
|
@meta = meta
|
|
151
|
+
super(default_message)
|
|
92
152
|
end
|
|
93
153
|
|
|
94
154
|
private_class_method :new
|
|
@@ -108,9 +168,41 @@ module Dentaku
|
|
|
108
168
|
|
|
109
169
|
new(reason, **meta)
|
|
110
170
|
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def default_message
|
|
175
|
+
case reason
|
|
176
|
+
when :incompatible_type
|
|
177
|
+
if meta.key?(:function_name)
|
|
178
|
+
"#{meta[:function_name]}() requires #{meta[:expected]} arguments, but got #{meta[:actual].class}"
|
|
179
|
+
else
|
|
180
|
+
"#{meta[:actual].class} is not compatible with #{meta[:expected]}"
|
|
181
|
+
end
|
|
182
|
+
when :invalid_operator
|
|
183
|
+
"#{meta[:operation]} requires operands that respond to #{meta[:operator]}"
|
|
184
|
+
when :invalid_value
|
|
185
|
+
if meta.key?(:expected)
|
|
186
|
+
"'#{meta[:actual]}' is not a valid #{meta[:expected]}"
|
|
187
|
+
elsif meta.key?(:actual)
|
|
188
|
+
"'#{meta[:actual]}' is not a valid value"
|
|
189
|
+
else
|
|
190
|
+
"Invalid value"
|
|
191
|
+
end
|
|
192
|
+
when :too_few_arguments
|
|
193
|
+
"#{meta[:function_name]}() has too few arguments (given #{meta[:actual]}, expected #{expected_count})"
|
|
194
|
+
when :wrong_number_of_arguments
|
|
195
|
+
"#{meta[:function_name]}() has the wrong number of arguments (given #{meta[:actual]}, expected #{expected_count})"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def expected_count
|
|
200
|
+
expected = meta[:expected]
|
|
201
|
+
expected.is_a?(Range) && expected.end.nil? ? "at least #{expected.begin}" : expected
|
|
202
|
+
end
|
|
111
203
|
end
|
|
112
204
|
|
|
113
205
|
class ZeroDivisionError < ::ZeroDivisionError
|
|
114
|
-
|
|
206
|
+
include Error
|
|
115
207
|
end
|
|
116
208
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
module NumericParser
|
|
3
|
+
NUMERIC_PATTERN = '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b'.freeze
|
|
4
|
+
# e.g: [2, 1.2, .5, 1e10, 1.5E-10]
|
|
5
|
+
HEXADECIMAL_PATTERN = '(0x[0-9a-f]+)\b'.freeze
|
|
6
|
+
# e.g: [0x1A3F]
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def match(string)
|
|
10
|
+
regex = %r{\A(-?(#{ NUMERIC_PATTERN }|#{ HEXADECIMAL_PATTERN }))\z}i
|
|
11
|
+
string.match(regex)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def match_numeric(string)
|
|
15
|
+
regex = %r{\A(#{ NUMERIC_PATTERN })\z}i
|
|
16
|
+
string.match(regex)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def match_hexadecimal(string)
|
|
20
|
+
regex = %r{\A(#{ HEXADECIMAL_PATTERN })\z}i
|
|
21
|
+
string.match(regex)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_numeric_string(raw)
|
|
25
|
+
raw =~ /(\.|e|E)/ ? BigDecimal(raw, Float::DIG + 1) : raw.to_i
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_hexadecimal_string(raw)
|
|
29
|
+
raw[2..-1].to_i(16)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse_string(raw)
|
|
33
|
+
return nil unless !!match(raw)
|
|
34
|
+
|
|
35
|
+
is_negative = raw.start_with?('-')
|
|
36
|
+
number_part = is_negative ? raw[1..-1] : raw
|
|
37
|
+
|
|
38
|
+
value = if number_part =~ /\A0x/i
|
|
39
|
+
parse_hexadecimal_string(number_part)
|
|
40
|
+
else
|
|
41
|
+
parse_numeric_string(number_part)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
is_negative ? -value : value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Numeric] where possible it returns an Integer otherwise a BigDecimal.
|
|
48
|
+
# An Exception will be raised if a value is passed that cannot be cast to a Number.
|
|
49
|
+
def ensure_numeric!(value)
|
|
50
|
+
return value if value.is_a?(::Numeric)
|
|
51
|
+
|
|
52
|
+
if value.is_a?(::String)
|
|
53
|
+
number = parse_string(value)
|
|
54
|
+
return number if number
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: value, expected: Numeric),
|
|
58
|
+
"'#{value || value.class}' is not coercible to numeric"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ensure_numeric(value)
|
|
62
|
+
ensure_numeric!(value)
|
|
63
|
+
rescue Dentaku::ArgumentError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/dentaku/parser.rb
CHANGED
|
@@ -41,41 +41,42 @@ module Dentaku
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def consume(count = 2)
|
|
44
|
-
|
|
45
|
-
fail! :invalid_statement if
|
|
44
|
+
operation = operations.pop
|
|
45
|
+
fail! :invalid_statement if operation.nil?
|
|
46
46
|
|
|
47
47
|
output_size = output.length
|
|
48
|
-
args_size =
|
|
49
|
-
min_size =
|
|
50
|
-
max_size =
|
|
48
|
+
args_size = operation.arity || count
|
|
49
|
+
min_size = operation.arity || operation.min_param_count || count
|
|
50
|
+
max_size = operation.arity || operation.max_param_count || count
|
|
51
51
|
|
|
52
52
|
if output_size < min_size || args_size < min_size
|
|
53
|
-
|
|
54
|
-
fail! :too_few_operands,
|
|
53
|
+
expected = min_size == max_size ? min_size : min_size..max_size
|
|
54
|
+
fail! :too_few_operands, operation: operation, expected: expected, actual: output_size
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
if (output_size > max_size && operations.empty?) || args_size > max_size
|
|
58
|
-
|
|
59
|
-
fail! :too_many_operands,
|
|
58
|
+
expected = min_size == max_size ? min_size : min_size..max_size
|
|
59
|
+
fail! :too_many_operands, operation: operation, expected: expected, actual: output_size
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
args = []
|
|
63
|
-
if
|
|
63
|
+
if operation == AST::Array && output.empty?
|
|
64
64
|
# special case: empty array literal '{}'
|
|
65
|
-
output.push(
|
|
65
|
+
output.push(operation.new)
|
|
66
66
|
else
|
|
67
67
|
fail! :invalid_statement if output_size < args_size
|
|
68
68
|
args = Array.new(args_size) { output.pop }.reverse
|
|
69
|
-
output.push
|
|
69
|
+
output.push operation.new(*args)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
if
|
|
73
|
-
|
|
72
|
+
if operation.respond_to?(:callback) && !operation.callback.nil?
|
|
73
|
+
operation.callback.call(args)
|
|
74
74
|
end
|
|
75
75
|
rescue ::ArgumentError => e
|
|
76
|
-
raise Dentaku::ArgumentError
|
|
76
|
+
raise if e.is_a?(Dentaku::ArgumentError)
|
|
77
|
+
raise Dentaku::ArgumentError.for(:invalid_value), e.message
|
|
77
78
|
rescue NodeError => e
|
|
78
|
-
|
|
79
|
+
raise ParseError.for(:node_invalid, operation: operation, operand: e.operand, expected: e.expected, actual: e.actual), e.message
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def parse
|
|
@@ -190,8 +191,11 @@ module Dentaku
|
|
|
190
191
|
operations.push(AST::CaseConditional)
|
|
191
192
|
consume(2)
|
|
192
193
|
arities[-1] += 1
|
|
193
|
-
elsif operations.
|
|
194
|
-
|
|
194
|
+
elsif operations[token_index].nil? ||
|
|
195
|
+
(operations[token_index] != AST::CaseWhen && operations[token_index] < AST::Operation)
|
|
196
|
+
# First WHEN: fold any operators pending from an unparenthesized
|
|
197
|
+
# switch expression, then finalize the switch variable.
|
|
198
|
+
consume_until(AST::Case)
|
|
195
199
|
operations.push(AST::CaseSwitchVariable)
|
|
196
200
|
consume
|
|
197
201
|
end
|
|
@@ -268,7 +272,7 @@ module Dentaku
|
|
|
268
272
|
when :close
|
|
269
273
|
consume while operations.any? && operations.last != AST::Grouping
|
|
270
274
|
lparen = operations.pop
|
|
271
|
-
fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
|
|
275
|
+
fail! :unbalanced_parenthesis, token: token unless lparen == AST::Grouping
|
|
272
276
|
if operations.last && operations.last < AST::Function
|
|
273
277
|
consume(arities.pop.succ)
|
|
274
278
|
end
|
|
@@ -284,35 +288,7 @@ module Dentaku
|
|
|
284
288
|
end
|
|
285
289
|
|
|
286
290
|
def fail!(reason, **meta)
|
|
287
|
-
|
|
288
|
-
case reason
|
|
289
|
-
when :node_invalid
|
|
290
|
-
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
|
291
|
-
when :too_few_operands
|
|
292
|
-
"#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
293
|
-
when :too_many_operands
|
|
294
|
-
"#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
295
|
-
when :undefined_function
|
|
296
|
-
"Undefined function #{meta.fetch(:function_name)}"
|
|
297
|
-
when :unprocessed_token
|
|
298
|
-
"Unprocessed token #{meta.fetch(:token_name)}"
|
|
299
|
-
when :unknown_case_token
|
|
300
|
-
"Unknown case token #{meta.fetch(:token_name)}"
|
|
301
|
-
when :unbalanced_bracket
|
|
302
|
-
"Unbalanced bracket"
|
|
303
|
-
when :unbalanced_parenthesis
|
|
304
|
-
"Unbalanced parenthesis"
|
|
305
|
-
when :unknown_grouping_token
|
|
306
|
-
"Unknown grouping token #{meta.fetch(:token_name)}"
|
|
307
|
-
when :not_implemented_token_category
|
|
308
|
-
"Not implemented for tokens of category #{meta.fetch(:token_category)}"
|
|
309
|
-
when :invalid_statement
|
|
310
|
-
"Invalid statement"
|
|
311
|
-
else
|
|
312
|
-
raise ::ArgumentError, "Unhandled #{reason}"
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
raise ParseError.for(reason, **meta), message
|
|
291
|
+
raise ParseError.for(reason, **meta)
|
|
316
292
|
end
|
|
317
293
|
end
|
|
318
294
|
end
|
|
@@ -93,46 +93,40 @@ module Dentaku
|
|
|
93
93
|
@values.empty? || @values.key?(value)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
def self.datetime
|
|
97
|
-
def self.numeric
|
|
98
|
-
def self.string
|
|
99
|
-
def self.logical
|
|
96
|
+
def self.datetime = new(:datetime)
|
|
97
|
+
def self.numeric = new(:numeric)
|
|
98
|
+
def self.string = new(:string)
|
|
99
|
+
def self.logical = new(:logical)
|
|
100
100
|
def self.value
|
|
101
101
|
new(:datetime) | new(:numeric) | new(:string) | new(:logical)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def self.addsub
|
|
105
|
-
def self.subtract
|
|
106
|
-
def self.anchored_minus
|
|
107
|
-
def self.muldiv
|
|
108
|
-
def self.pow
|
|
109
|
-
def self.mod
|
|
110
|
-
def self.combinator
|
|
111
|
-
|
|
112
|
-
def self.comparator
|
|
113
|
-
def self.comp_gt
|
|
114
|
-
def self.comp_lt
|
|
115
|
-
|
|
116
|
-
def self.open
|
|
117
|
-
def self.close
|
|
118
|
-
def self.comma
|
|
119
|
-
def self.non_group
|
|
120
|
-
def self.non_group_star
|
|
121
|
-
def self.non_close_plus
|
|
122
|
-
def self.arguments
|
|
123
|
-
|
|
124
|
-
def self.
|
|
125
|
-
|
|
126
|
-
def self.
|
|
127
|
-
def self.
|
|
128
|
-
def self.
|
|
129
|
-
|
|
130
|
-
def self.
|
|
131
|
-
new(:function, name)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def self.respond_to_missing?(name, include_priv)
|
|
135
|
-
true
|
|
136
|
-
end
|
|
104
|
+
def self.addsub = new(:operator, [:add, :subtract])
|
|
105
|
+
def self.subtract = new(:operator, :subtract)
|
|
106
|
+
def self.anchored_minus = new(:operator, :subtract).caret
|
|
107
|
+
def self.muldiv = new(:operator, [:multiply, :divide])
|
|
108
|
+
def self.pow = new(:operator, :pow)
|
|
109
|
+
def self.mod = new(:operator, :mod)
|
|
110
|
+
def self.combinator = new(:combinator)
|
|
111
|
+
|
|
112
|
+
def self.comparator = new(:comparator)
|
|
113
|
+
def self.comp_gt = new(:comparator, [:gt, :ge])
|
|
114
|
+
def self.comp_lt = new(:comparator, [:lt, :le])
|
|
115
|
+
|
|
116
|
+
def self.open = new(:grouping, :open)
|
|
117
|
+
def self.close = new(:grouping, :close)
|
|
118
|
+
def self.comma = new(:grouping, :comma)
|
|
119
|
+
def self.non_group = new(:grouping).invert
|
|
120
|
+
def self.non_group_star = new(:grouping).invert.star
|
|
121
|
+
def self.non_close_plus = new(:grouping, :close).invert.plus
|
|
122
|
+
def self.arguments = (value | comma).plus
|
|
123
|
+
|
|
124
|
+
def self.function(name) = new(:function, name)
|
|
125
|
+
|
|
126
|
+
def self.if = function(:if)
|
|
127
|
+
def self.round = function(:round)
|
|
128
|
+
def self.roundup = function(:roundup)
|
|
129
|
+
def self.rounddown = function(:rounddown)
|
|
130
|
+
def self.not = function(:not)
|
|
137
131
|
end
|
|
138
132
|
end
|
|
@@ -6,7 +6,7 @@ module Dentaku
|
|
|
6
6
|
|
|
7
7
|
def self.function_token_matchers(function_name, *symbols)
|
|
8
8
|
token_matchers(:open, *symbols, :close).unshift(
|
|
9
|
-
TokenMatcher.
|
|
9
|
+
TokenMatcher.function(function_name)
|
|
10
10
|
)
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -16,9 +16,9 @@ module Dentaku
|
|
|
16
16
|
:comparator, :comp_gt, :comp_lt, :open, :close, :comma,
|
|
17
17
|
:non_close_plus, :non_group, :non_group_star, :arguments,
|
|
18
18
|
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
|
19
|
-
:anchored_minus
|
|
19
|
+
:anchored_minus
|
|
20
20
|
].each_with_object({}) do |name, matchers|
|
|
21
|
-
matchers[name] = TokenMatcher.
|
|
21
|
+
matchers[name] = TokenMatcher.public_send(name)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
@matchers.fetch(symbol) do
|
|
@@ -2,6 +2,7 @@ require 'bigdecimal'
|
|
|
2
2
|
require 'time'
|
|
3
3
|
require 'dentaku/string_casing'
|
|
4
4
|
require 'dentaku/token'
|
|
5
|
+
require 'dentaku/numeric_parser'
|
|
5
6
|
|
|
6
7
|
module Dentaku
|
|
7
8
|
class TokenScanner
|
|
@@ -95,13 +96,11 @@ module Dentaku
|
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
def numeric
|
|
98
|
-
new(:numeric,
|
|
99
|
-
raw =~ /(\.|e|E)/ ? BigDecimal(raw) : raw.to_i
|
|
100
|
-
})
|
|
99
|
+
new(:numeric, NumericParser::NUMERIC_PATTERN, lambda { |raw| NumericParser.parse_numeric_string(raw) })
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
def hexadecimal
|
|
104
|
-
new(:numeric,
|
|
103
|
+
new(:numeric, NumericParser::HEXADECIMAL_PATTERN, lambda { |raw| NumericParser.parse_hexadecimal_string(raw) })
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
def double_quoted_string
|