dentaku 3.5.4 → 3.5.5
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 +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/CHANGELOG.md +14 -6
- data/README.md +1 -4
- data/dentaku.gemspec +0 -1
- data/lib/dentaku/ast/arithmetic.rb +18 -20
- data/lib/dentaku/ast/case.rb +12 -0
- data/lib/dentaku/ast/operation.rb +5 -0
- data/lib/dentaku/ast.rb +1 -1
- data/lib/dentaku/parser.rb +206 -212
- data/lib/dentaku/print_visitor.rb +2 -2
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +1 -1
- data/spec/ast/addition_spec.rb +12 -7
- data/spec/ast/arithmetic_spec.rb +7 -0
- data/spec/ast/division_spec.rb +10 -6
- data/spec/bulk_expression_solver_spec.rb +8 -1
- data/spec/calculator_spec.rb +1 -1
- data/spec/external_function_spec.rb +1 -1
- data/spec/parser_spec.rb +11 -2
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/spec_helper.rb +1 -3
- metadata +5 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42f22227c2d7aef33e71f5c177f65ca3e7855115466d4d04851e6286bcbe1955
|
4
|
+
data.tar.gz: e779f142e5f458aed8437539bfe98ee73b4bf86a0ddb280cceec586ebcd62190
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c4305e2d7f5c8289edcc5f530dbb0d98cbc95f6db59a8ba98561506f2ec7e41ef89c6ab7271d8754f0a0dfc5ade6325833269f38595593c39eca44c8dcce7c5
|
7
|
+
data.tar.gz: 9a186d4164b031bbe27fb1490184d9c23387849267d4b1067c6bec6a39afdee4e6bf97d8bbbb364f36474fdad0e37e59f4ef63dae1454055244bc06f6791905b
|
@@ -0,0 +1,26 @@
|
|
1
|
+
name: rspec
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
pull_request:
|
5
|
+
jobs:
|
6
|
+
rspec:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
ruby:
|
11
|
+
- '2.5'
|
12
|
+
- '2.6'
|
13
|
+
- '2.7'
|
14
|
+
- '3.0'
|
15
|
+
- '3.1'
|
16
|
+
- '3.2'
|
17
|
+
- '3.3'
|
18
|
+
- '3.4'
|
19
|
+
fail-fast: false
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v4
|
22
|
+
- uses: ruby/setup-ruby@v1
|
23
|
+
with:
|
24
|
+
ruby-version: "${{ matrix.ruby }}"
|
25
|
+
bundler-cache: true
|
26
|
+
- run: bundle exec rspec --format documentation
|
data/CHANGELOG.md
CHANGED
@@ -1,13 +1,20 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [v3.5.
|
3
|
+
## [v3.5.5] 2025-08-20
|
4
|
+
- fix percentages in print visitor
|
5
|
+
- repo cleanup
|
6
|
+
- fix modulo zero
|
7
|
+
- fix array arithmetic
|
8
|
+
- refactor parser
|
9
|
+
|
10
|
+
## [v3.5.4] 2024-08-13
|
4
11
|
- add support for default value for PLUCK function
|
5
12
|
- improve error handling for MAP/ANY/ALL functions
|
6
13
|
- fix modulo / percentage operator determination
|
7
14
|
- fix string casing bug with bulk expressions
|
8
15
|
- add explicit gem dependency for BigDecimal
|
9
16
|
|
10
|
-
## [v3.5.3]
|
17
|
+
## [v3.5.3] 2024-07-04
|
11
18
|
- add support for empty array literals
|
12
19
|
- add support for quoted identifiers
|
13
20
|
- add REDUCE function
|
@@ -16,7 +23,7 @@
|
|
16
23
|
- improve custom class arithmetic
|
17
24
|
- fix IF dependency
|
18
25
|
|
19
|
-
## [v3.5.2]
|
26
|
+
## [v3.5.2] 2023-12-06
|
20
27
|
- add ABS function
|
21
28
|
- add array support for AST visitors
|
22
29
|
- add support for function callbacks
|
@@ -29,14 +36,14 @@
|
|
29
36
|
- fix handling of Math::DomainError
|
30
37
|
- fix invalid cast
|
31
38
|
|
32
|
-
## [v3.5.1]
|
39
|
+
## [v3.5.1] 2022-10-24
|
33
40
|
- add bitwise shift left and shift right operators
|
34
41
|
- improve numeric conversions
|
35
42
|
- improve parse exceptions
|
36
43
|
- improve bitwise exceptions
|
37
44
|
- include variable name in bulk expression exceptions
|
38
45
|
|
39
|
-
## [v3.5.0]
|
46
|
+
## [v3.5.0] 2022-03-17
|
40
47
|
- fix bug with function argument count
|
41
48
|
- add XOR operator
|
42
49
|
- make function args publicly accessible
|
@@ -48,7 +55,7 @@
|
|
48
55
|
- respect case sensitivity in nested case statments
|
49
56
|
- add visitor pattern
|
50
57
|
|
51
|
-
## [v3.4.2]
|
58
|
+
## [v3.4.2] 2021-07-14
|
52
59
|
- add FILTER function
|
53
60
|
- add concurrent-ruby dependency to make global calculator object thread safe
|
54
61
|
- add Ruby 3 support
|
@@ -260,6 +267,7 @@
|
|
260
267
|
## [v0.1.0] 2012-01-20
|
261
268
|
- initial release
|
262
269
|
|
270
|
+
[v3.5.5]: https://github.com/rubysolo/dentaku/compare/v3.5.4...v3.5.5
|
263
271
|
[v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
|
264
272
|
[v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
|
265
273
|
[v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
|
data/README.md
CHANGED
@@ -3,9 +3,6 @@ Dentaku
|
|
3
3
|
|
4
4
|
[](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
5
5
|
[](http://badge.fury.io/rb/dentaku)
|
6
|
-
[](https://travis-ci.org/rubysolo/dentaku)
|
7
|
-
[](https://codeclimate.com/github/rubysolo/dentaku)
|
8
|
-
[](https://codecov.io/gh/rubysolo/dentaku)
|
9
6
|
|
10
7
|
|
11
8
|
DESCRIPTION
|
@@ -147,7 +144,7 @@ Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
|
|
147
144
|
|
148
145
|
Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
|
149
146
|
|
150
|
-
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/
|
147
|
+
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/main/lib/dentaku/ast/case.rb))
|
151
148
|
|
152
149
|
String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`, `CONTAINS`
|
153
150
|
|
data/dentaku.gemspec
CHANGED
@@ -17,7 +17,6 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.add_dependency('bigdecimal')
|
18
18
|
s.add_dependency('concurrent-ruby')
|
19
19
|
|
20
|
-
s.add_development_dependency('codecov')
|
21
20
|
s.add_development_dependency('pry')
|
22
21
|
s.add_development_dependency('pry-byebug')
|
23
22
|
s.add_development_dependency('pry-stack_explorer')
|
@@ -9,20 +9,6 @@ module Dentaku
|
|
9
9
|
DECIMAL = /\A-?\d*\.\d+\z/.freeze
|
10
10
|
INTEGER = /\A-?\d+\z/.freeze
|
11
11
|
|
12
|
-
def initialize(*)
|
13
|
-
super
|
14
|
-
|
15
|
-
unless valid_left?
|
16
|
-
raise NodeError.new(:numeric, left.type, :left),
|
17
|
-
"#{self.class} requires numeric operands"
|
18
|
-
end
|
19
|
-
|
20
|
-
unless valid_right?
|
21
|
-
raise NodeError.new(:numeric, right.type, :right),
|
22
|
-
"#{self.class} requires numeric operands"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
12
|
def type
|
27
13
|
:numeric
|
28
14
|
end
|
@@ -69,8 +55,9 @@ module Dentaku
|
|
69
55
|
def datetime?(val)
|
70
56
|
# val is a Date, Time, or DateTime
|
71
57
|
return true if val.respond_to?(:strftime)
|
58
|
+
return false unless val.is_a?(::String)
|
72
59
|
|
73
|
-
val
|
60
|
+
val =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
|
74
61
|
end
|
75
62
|
|
76
63
|
def valid_node?(node)
|
@@ -193,6 +180,13 @@ module Dentaku
|
|
193
180
|
def operator
|
194
181
|
:%
|
195
182
|
end
|
183
|
+
|
184
|
+
def value(context = {})
|
185
|
+
r = decimal(cast(right.value(context)))
|
186
|
+
raise Dentaku::ZeroDivisionError if r.zero?
|
187
|
+
|
188
|
+
cast(cast(left.value(context)) % r)
|
189
|
+
end
|
196
190
|
end
|
197
191
|
|
198
192
|
class Percentage < Arithmetic
|
@@ -201,26 +195,30 @@ module Dentaku
|
|
201
195
|
end
|
202
196
|
|
203
197
|
def initialize(child)
|
204
|
-
@
|
198
|
+
@left = child
|
205
199
|
|
206
|
-
unless
|
207
|
-
raise NodeError.new(:numeric,
|
200
|
+
unless valid_left?
|
201
|
+
raise NodeError.new(:numeric, left.type, :left),
|
208
202
|
"#{self.class} requires a numeric operand"
|
209
203
|
end
|
210
204
|
end
|
211
205
|
|
212
206
|
def dependencies(context = {})
|
213
|
-
@
|
207
|
+
@left.dependencies(context)
|
214
208
|
end
|
215
209
|
|
216
210
|
def value(context = {})
|
217
|
-
cast(
|
211
|
+
cast(left.value(context)) * 0.01
|
218
212
|
end
|
219
213
|
|
220
214
|
def operator
|
221
215
|
:%
|
222
216
|
end
|
223
217
|
|
218
|
+
def operator_spacing
|
219
|
+
""
|
220
|
+
end
|
221
|
+
|
224
222
|
def self.precedence
|
225
223
|
30
|
226
224
|
end
|
data/lib/dentaku/ast/case.rb
CHANGED
@@ -7,6 +7,18 @@ require 'dentaku/exceptions'
|
|
7
7
|
|
8
8
|
module Dentaku
|
9
9
|
module AST
|
10
|
+
# Examples of using in a formula:
|
11
|
+
#
|
12
|
+
# CASE x WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE END
|
13
|
+
#
|
14
|
+
# CASE fruit
|
15
|
+
# WHEN 'apple'
|
16
|
+
# THEN 1 * quantity
|
17
|
+
# WHEN 'banana'
|
18
|
+
# THEN 2 * quantity
|
19
|
+
# ELSE
|
20
|
+
# 3 * quantity
|
21
|
+
# END
|
10
22
|
class Case < Node
|
11
23
|
attr_reader :switch, :conditions, :else
|
12
24
|
|
data/lib/dentaku/ast.rb
CHANGED
@@ -39,4 +39,4 @@ require_relative './ast/functions/ruby_math'
|
|
39
39
|
require_relative './ast/functions/string_functions'
|
40
40
|
require_relative './ast/functions/sum'
|
41
41
|
require_relative './ast/functions/switch'
|
42
|
-
require_relative './ast/functions/xor'
|
42
|
+
require_relative './ast/functions/xor'
|
data/lib/dentaku/parser.rb
CHANGED
@@ -53,17 +53,18 @@ module Dentaku
|
|
53
53
|
fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
|
54
54
|
end
|
55
55
|
|
56
|
-
if output_size > max_size && operations.empty? || args_size > max_size
|
56
|
+
if (output_size > max_size && operations.empty?) || args_size > max_size
|
57
57
|
expect = min_size == max_size ? min_size : min_size..max_size
|
58
58
|
fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
|
59
59
|
end
|
60
60
|
|
61
|
+
args = []
|
61
62
|
if operator == AST::Array && output.empty?
|
62
|
-
|
63
|
+
# special case: empty array literal '{}'
|
64
|
+
output.push(operator.new)
|
63
65
|
else
|
64
66
|
fail! :invalid_statement if output_size < args_size
|
65
67
|
args = Array.new(args_size) { output.pop }.reverse
|
66
|
-
|
67
68
|
output.push operator.new(*args)
|
68
69
|
end
|
69
70
|
|
@@ -79,251 +80,244 @@ module Dentaku
|
|
79
80
|
def parse
|
80
81
|
return AST::Nil.new if input.empty?
|
81
82
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
i = 0
|
84
|
+
while i < input.length
|
85
|
+
token = input[i]
|
86
|
+
lookahead = input[i + 1]
|
87
|
+
process_token(token, lookahead, i, input)
|
88
|
+
i += 1
|
89
|
+
end
|
86
90
|
|
87
|
-
|
88
|
-
|
91
|
+
while operations.any?
|
92
|
+
consume
|
93
|
+
end
|
89
94
|
|
90
|
-
|
91
|
-
|
95
|
+
unless output.count == 1
|
96
|
+
fail! :invalid_statement
|
97
|
+
end
|
92
98
|
|
93
|
-
|
94
|
-
|
99
|
+
output.first
|
100
|
+
end
|
95
101
|
|
96
|
-
|
97
|
-
|
102
|
+
def operation(token)
|
103
|
+
AST_OPERATIONS.fetch(token.value)
|
104
|
+
end
|
98
105
|
|
99
|
-
|
100
|
-
|
101
|
-
|
106
|
+
def function(token)
|
107
|
+
function_registry.get(token.value)
|
108
|
+
end
|
102
109
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
end
|
110
|
+
def function_registry
|
111
|
+
@function_registry ||= Dentaku::AST::FunctionRegistry.new
|
112
|
+
end
|
107
113
|
|
108
|
-
|
109
|
-
else
|
110
|
-
while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
111
|
-
consume
|
112
|
-
end
|
114
|
+
private
|
113
115
|
|
114
|
-
|
115
|
-
|
116
|
+
def process_token(token, lookahead, index, tokens)
|
117
|
+
case token.category
|
118
|
+
when :datetime then output << AST::DateTime.new(token)
|
119
|
+
when :numeric then output << AST::Numeric.new(token)
|
120
|
+
when :logical then output << AST::Logical.new(token)
|
121
|
+
when :string then output << AST::String.new(token)
|
122
|
+
when :identifier then output << AST::Identifier.new(token, case_sensitive: case_sensitive)
|
123
|
+
when :operator, :comparator, :combinator
|
124
|
+
handle_operator(token, lookahead)
|
125
|
+
when :null
|
126
|
+
output << AST::Nil.new
|
127
|
+
when :function
|
128
|
+
handle_function(token)
|
129
|
+
when :case
|
130
|
+
handle_case(token, index, tokens)
|
131
|
+
when :access
|
132
|
+
handle_access(token)
|
133
|
+
when :array
|
134
|
+
handle_array(token)
|
135
|
+
when :grouping
|
136
|
+
handle_grouping(token, lookahead, tokens)
|
137
|
+
else
|
138
|
+
fail! :not_implemented_token_category, token_category: token.category
|
139
|
+
end
|
140
|
+
end
|
116
141
|
|
117
|
-
|
118
|
-
|
142
|
+
def handle_operator(token, lookahead)
|
143
|
+
op_class = operation(token).resolve_class(lookahead)
|
144
|
+
if op_class.right_associative?
|
145
|
+
while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
146
|
+
consume
|
147
|
+
end
|
148
|
+
else
|
149
|
+
while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
150
|
+
consume
|
151
|
+
end
|
152
|
+
end
|
153
|
+
operations.push op_class
|
154
|
+
end
|
119
155
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
156
|
+
def handle_function(token)
|
157
|
+
func = function(token)
|
158
|
+
fail! :undefined_function, function_name: token.value if func.nil?
|
159
|
+
arities.push 0
|
160
|
+
operations.push func
|
161
|
+
end
|
125
162
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
case
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
if open_cases > 0
|
149
|
-
open_cases -= 1
|
150
|
-
else
|
151
|
-
case_end_index = index
|
152
|
-
break
|
153
|
-
end
|
154
|
-
end
|
163
|
+
def handle_case(token, index, tokens)
|
164
|
+
case_index = operations.index { |o| o == AST::Case } || -1
|
165
|
+
token_index = case_index + 1
|
166
|
+
|
167
|
+
case token.value
|
168
|
+
when :open
|
169
|
+
if operations.include?(AST::Case)
|
170
|
+
# nested case extraction (non-recursive outer parser)
|
171
|
+
open_cases = 0
|
172
|
+
case_end_index = nil
|
173
|
+
j = index + 1
|
174
|
+
while j < tokens.length
|
175
|
+
t = tokens[j]
|
176
|
+
if t.category == :case
|
177
|
+
if t.value == :open
|
178
|
+
open_cases += 1
|
179
|
+
elsif t.value == :close
|
180
|
+
if open_cases > 0
|
181
|
+
open_cases -= 1
|
182
|
+
else
|
183
|
+
case_end_index = j
|
184
|
+
break
|
155
185
|
end
|
156
186
|
end
|
157
|
-
inner_case_inputs = input.slice!(0..case_end_index)
|
158
|
-
subparser = Parser.new(
|
159
|
-
inner_case_inputs,
|
160
|
-
operations: [AST::Case],
|
161
|
-
arities: [0],
|
162
|
-
function_registry: @function_registry,
|
163
|
-
case_sensitive: case_sensitive
|
164
|
-
)
|
165
|
-
subparser.parse
|
166
|
-
output.concat(subparser.output)
|
167
|
-
else
|
168
|
-
operations.push AST::Case
|
169
|
-
arities.push(0)
|
170
|
-
end
|
171
|
-
when :close
|
172
|
-
if operations[token_index] == AST::CaseThen
|
173
|
-
while operations.last != AST::Case
|
174
|
-
consume
|
175
|
-
end
|
176
|
-
|
177
|
-
operations.push(AST::CaseConditional)
|
178
|
-
consume(2)
|
179
|
-
arities[-1] += 1
|
180
|
-
elsif operations[token_index] == AST::CaseElse
|
181
|
-
while operations.last != AST::Case
|
182
|
-
consume
|
183
|
-
end
|
184
|
-
|
185
|
-
arities[-1] += 1
|
186
187
|
end
|
187
|
-
|
188
|
-
unless operations.count >= 1 && operations.last == AST::Case
|
189
|
-
fail! :unprocessed_token, token_name: token.value
|
190
|
-
end
|
191
|
-
consume(arities.pop.succ)
|
192
|
-
when :when
|
193
|
-
if operations[token_index] == AST::CaseThen
|
194
|
-
while ![AST::CaseWhen, AST::Case].include?(operations.last)
|
195
|
-
consume
|
196
|
-
end
|
197
|
-
operations.push(AST::CaseConditional)
|
198
|
-
consume(2)
|
199
|
-
arities[-1] += 1
|
200
|
-
elsif operations.last == AST::Case
|
201
|
-
operations.push(AST::CaseSwitchVariable)
|
202
|
-
consume
|
203
|
-
end
|
204
|
-
|
205
|
-
operations.push(AST::CaseWhen)
|
206
|
-
when :then
|
207
|
-
if operations[token_index] == AST::CaseWhen
|
208
|
-
while ![AST::CaseThen, AST::Case].include?(operations.last)
|
209
|
-
consume
|
210
|
-
end
|
211
|
-
end
|
212
|
-
operations.push(AST::CaseThen)
|
213
|
-
when :else
|
214
|
-
if operations[token_index] == AST::CaseThen
|
215
|
-
while operations.last != AST::Case
|
216
|
-
consume
|
217
|
-
end
|
218
|
-
|
219
|
-
operations.push(AST::CaseConditional)
|
220
|
-
consume(2)
|
221
|
-
arities[-1] += 1
|
222
|
-
end
|
223
|
-
|
224
|
-
operations.push(AST::CaseElse)
|
225
|
-
else
|
226
|
-
fail! :unknown_case_token, token_name: token.value
|
188
|
+
j += 1
|
227
189
|
end
|
190
|
+
inner_case_inputs = tokens.slice!(index + 1, case_end_index - index) || []
|
191
|
+
subparser = Parser.new(
|
192
|
+
inner_case_inputs,
|
193
|
+
operations: [AST::Case],
|
194
|
+
arities: [0],
|
195
|
+
function_registry: @function_registry,
|
196
|
+
case_sensitive: case_sensitive
|
197
|
+
)
|
198
|
+
subparser.parse
|
199
|
+
output.concat(subparser.output)
|
200
|
+
else
|
201
|
+
operations.push AST::Case
|
202
|
+
arities.push(0)
|
203
|
+
end
|
228
204
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
operations.push AST::Access
|
233
|
-
when :rbracket
|
234
|
-
while operations.any? && operations.last != AST::Access
|
235
|
-
consume
|
236
|
-
end
|
237
|
-
|
238
|
-
unless operations.last == AST::Access
|
239
|
-
fail! :unbalanced_bracket, token: token
|
240
|
-
end
|
205
|
+
when :close
|
206
|
+
if operations[token_index] == AST::CaseThen
|
207
|
+
while operations.last != AST::Case
|
241
208
|
consume
|
242
209
|
end
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
when :array_end
|
250
|
-
while operations.any? && operations.last != AST::Array
|
251
|
-
consume
|
252
|
-
end
|
253
|
-
|
254
|
-
unless operations.last == AST::Array
|
255
|
-
fail! :unbalanced_bracket, token: token
|
256
|
-
end
|
257
|
-
|
258
|
-
consume(arities.pop.succ)
|
210
|
+
operations.push(AST::CaseConditional)
|
211
|
+
consume(2)
|
212
|
+
arities[-1] += 1
|
213
|
+
elsif operations[token_index] == AST::CaseElse
|
214
|
+
while operations.last != AST::Case
|
215
|
+
consume
|
259
216
|
end
|
217
|
+
arities[-1] += 1
|
218
|
+
end
|
219
|
+
fail! :unprocessed_token, token_name: token.value unless operations.count >= 1 && operations.last == AST::Case
|
220
|
+
consume(arities.pop.succ)
|
260
221
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
consume
|
275
|
-
end
|
276
|
-
|
277
|
-
lparen = operations.pop
|
278
|
-
unless lparen == AST::Grouping
|
279
|
-
fail! :unbalanced_parenthesis, token
|
280
|
-
end
|
281
|
-
|
282
|
-
if operations.last && operations.last < AST::Function
|
283
|
-
consume(arities.pop.succ)
|
284
|
-
end
|
285
|
-
|
286
|
-
when :comma
|
287
|
-
fail! :invalid_statement if arities.empty?
|
288
|
-
arities[-1] += 1
|
289
|
-
while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
|
290
|
-
consume
|
291
|
-
end
|
222
|
+
when :when
|
223
|
+
if operations[token_index] == AST::CaseThen
|
224
|
+
while ![AST::CaseWhen, AST::Case].include?(operations.last)
|
225
|
+
consume
|
226
|
+
end
|
227
|
+
operations.push(AST::CaseConditional)
|
228
|
+
consume(2)
|
229
|
+
arities[-1] += 1
|
230
|
+
elsif operations.last == AST::Case
|
231
|
+
operations.push(AST::CaseSwitchVariable)
|
232
|
+
consume
|
233
|
+
end
|
234
|
+
operations.push(AST::CaseWhen)
|
292
235
|
|
293
|
-
|
294
|
-
|
236
|
+
when :then
|
237
|
+
if operations[token_index] == AST::CaseWhen
|
238
|
+
while ![AST::CaseThen, AST::Case].include?(operations.last)
|
239
|
+
consume
|
295
240
|
end
|
241
|
+
end
|
242
|
+
operations.push(AST::CaseThen)
|
296
243
|
|
297
|
-
|
298
|
-
|
244
|
+
when :else
|
245
|
+
if operations[token_index] == AST::CaseThen
|
246
|
+
while operations.last != AST::Case
|
247
|
+
consume
|
248
|
+
end
|
249
|
+
operations.push(AST::CaseConditional)
|
250
|
+
consume(2)
|
251
|
+
arities[-1] += 1
|
299
252
|
end
|
253
|
+
operations.push(AST::CaseElse)
|
254
|
+
else
|
255
|
+
fail! :unknown_case_token, token_name: token.value
|
300
256
|
end
|
257
|
+
end
|
301
258
|
|
302
|
-
|
259
|
+
def handle_access(token)
|
260
|
+
case token.value
|
261
|
+
when :lbracket
|
262
|
+
operations.push AST::Access
|
263
|
+
|
264
|
+
when :rbracket
|
265
|
+
while operations.any? && operations.last != AST::Access
|
266
|
+
consume
|
267
|
+
end
|
268
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
|
303
269
|
consume
|
304
270
|
end
|
271
|
+
end
|
305
272
|
|
306
|
-
|
307
|
-
|
308
|
-
|
273
|
+
def handle_array(token)
|
274
|
+
case token.value
|
275
|
+
when :array_start
|
276
|
+
operations.push AST::Array
|
277
|
+
arities.push 0
|
309
278
|
|
310
|
-
|
279
|
+
when :array_end
|
280
|
+
while operations.any? && operations.last != AST::Array
|
281
|
+
consume
|
282
|
+
end
|
283
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
|
284
|
+
consume(arities.pop.succ)
|
285
|
+
end
|
311
286
|
end
|
312
287
|
|
313
|
-
def
|
314
|
-
|
315
|
-
|
288
|
+
def handle_grouping(token, lookahead, tokens)
|
289
|
+
case token.value
|
290
|
+
when :open
|
291
|
+
if lookahead && lookahead.value == :close
|
292
|
+
# empty grouping (e.g. function with zero arguments) — we trigger consume later
|
293
|
+
tokens.delete_at(tokens.index(lookahead)) # remove the close to mimic previous shift behavior
|
294
|
+
arities.pop
|
295
|
+
consume(0)
|
296
|
+
else
|
297
|
+
operations.push AST::Grouping
|
298
|
+
end
|
316
299
|
|
317
|
-
|
318
|
-
|
319
|
-
|
300
|
+
when :close
|
301
|
+
while operations.any? && operations.last != AST::Grouping
|
302
|
+
consume
|
303
|
+
end
|
304
|
+
lparen = operations.pop
|
305
|
+
fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
|
306
|
+
if operations.last && operations.last < AST::Function
|
307
|
+
consume(arities.pop.succ)
|
308
|
+
end
|
320
309
|
|
321
|
-
|
322
|
-
|
310
|
+
when :comma
|
311
|
+
fail! :invalid_statement if arities.empty?
|
312
|
+
arities[-1] += 1
|
313
|
+
while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
|
314
|
+
consume
|
315
|
+
end
|
316
|
+
else
|
317
|
+
fail! :unknown_grouping_token, token_name: token.value
|
318
|
+
end
|
323
319
|
end
|
324
320
|
|
325
|
-
private
|
326
|
-
|
327
321
|
def fail!(reason, **meta)
|
328
322
|
message =
|
329
323
|
case reason
|
@@ -7,13 +7,13 @@ module Dentaku
|
|
7
7
|
|
8
8
|
def visit_operation(node)
|
9
9
|
if node.left
|
10
|
-
visit_operand(node.left, node.class.precedence, suffix:
|
10
|
+
visit_operand(node.left, node.class.precedence, suffix: node.operator_spacing, dir: :left)
|
11
11
|
end
|
12
12
|
|
13
13
|
@output << node.display_operator
|
14
14
|
|
15
15
|
if node.right
|
16
|
-
visit_operand(node.right, node.class.precedence, prefix:
|
16
|
+
visit_operand(node.right, node.class.precedence, prefix: node.operator_spacing, dir: :right)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/addition_spec.rb
CHANGED
@@ -20,22 +20,21 @@ describe Dentaku::AST::Addition do
|
|
20
20
|
expect(node.value).to eq(11)
|
21
21
|
end
|
22
22
|
|
23
|
-
it 'requires
|
23
|
+
it 'requires operands that respond to +' do
|
24
24
|
expect {
|
25
|
-
described_class.new(five, t)
|
26
|
-
}.to raise_error(Dentaku::
|
25
|
+
described_class.new(five, t).value
|
26
|
+
}.to raise_error(Dentaku::ArgumentError, /requires operands that respond to +/)
|
27
27
|
|
28
28
|
expression = Dentaku::AST::Multiplication.new(five, five)
|
29
29
|
group = Dentaku::AST::Grouping.new(expression)
|
30
30
|
|
31
31
|
expect {
|
32
|
-
described_class.new(group, five)
|
32
|
+
described_class.new(group, five).value
|
33
33
|
}.not_to raise_error
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'allows operands that respond to addition' do
|
37
37
|
# Sample struct that has a custom definition for addition
|
38
|
-
|
39
38
|
Addable = Struct.new(:value) do
|
40
39
|
def +(other)
|
41
40
|
case other
|
@@ -51,12 +50,18 @@ describe Dentaku::AST::Addition do
|
|
51
50
|
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(6))
|
52
51
|
|
53
52
|
expect {
|
54
|
-
described_class.new(operand_five, operand_six)
|
53
|
+
described_class.new(operand_five, operand_six).value
|
55
54
|
}.not_to raise_error
|
56
55
|
|
57
56
|
expect {
|
58
|
-
described_class.new(operand_five, six)
|
57
|
+
described_class.new(operand_five, six).value
|
59
58
|
}.not_to raise_error
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'does not try to parse nested string as date' do
|
62
|
+
a = ['2017-01-01', '2017-01-02']
|
63
|
+
b = ['2017-01-01']
|
60
64
|
|
65
|
+
expect(Dentaku('a + b', a: a, b: b)).to eq(['2017-01-01', '2017-01-02', '2017-01-01'])
|
61
66
|
end
|
62
67
|
end
|
data/spec/ast/arithmetic_spec.rb
CHANGED
@@ -111,6 +111,13 @@ describe Dentaku::AST::Arithmetic do
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
+
it 'does not try to parse nested string as date' do
|
115
|
+
a = ['2017-01-01', '2017-01-02']
|
116
|
+
b = ['2017-01-01']
|
117
|
+
|
118
|
+
expect(Dentaku('a - b', a: a, b: b)).to eq(['2017-01-02'])
|
119
|
+
end
|
120
|
+
|
114
121
|
it 'raises ArgumentError if given individually valid but incompatible arguments' do
|
115
122
|
expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
|
116
123
|
expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)
|
data/spec/ast/division_spec.rb
CHANGED
@@ -20,16 +20,16 @@ describe Dentaku::AST::Division do
|
|
20
20
|
expect(node.value.round(4)).to eq(0.8333)
|
21
21
|
end
|
22
22
|
|
23
|
-
it 'requires
|
23
|
+
it 'requires operands that respond to /' do
|
24
24
|
expect {
|
25
|
-
described_class.new(five, t)
|
26
|
-
}.to raise_error(Dentaku::
|
25
|
+
described_class.new(five, t).value
|
26
|
+
}.to raise_error(Dentaku::ArgumentError, /requires operands that respond to \//)
|
27
27
|
|
28
28
|
expression = Dentaku::AST::Multiplication.new(five, five)
|
29
29
|
group = Dentaku::AST::Grouping.new(expression)
|
30
30
|
|
31
31
|
expect {
|
32
|
-
described_class.new(group, five)
|
32
|
+
described_class.new(group, five).value
|
33
33
|
}.not_to raise_error
|
34
34
|
end
|
35
35
|
|
@@ -44,17 +44,21 @@ describe Dentaku::AST::Division do
|
|
44
44
|
value + other
|
45
45
|
end
|
46
46
|
end
|
47
|
+
|
48
|
+
def zero?
|
49
|
+
value.zero?
|
50
|
+
end
|
47
51
|
end
|
48
52
|
|
49
53
|
operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(5))
|
50
54
|
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(6))
|
51
55
|
|
52
56
|
expect {
|
53
|
-
described_class.new(operand_five, operand_six)
|
57
|
+
described_class.new(operand_five, operand_six).value
|
54
58
|
}.not_to raise_error
|
55
59
|
|
56
60
|
expect {
|
57
|
-
described_class.new(operand_five, six)
|
61
|
+
described_class.new(operand_five, six).value
|
58
62
|
}.not_to raise_error
|
59
63
|
end
|
60
64
|
end
|
@@ -33,13 +33,20 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
33
33
|
}.to raise_error(Dentaku::UnboundVariableError)
|
34
34
|
end
|
35
35
|
|
36
|
-
it "lets you know if the result is a div/0 error" do
|
36
|
+
it "lets you know if the result is a div/0 error when dividing" do
|
37
37
|
expressions = {more_apples: "1/0"}
|
38
38
|
expect {
|
39
39
|
described_class.new(expressions, calculator).solve!
|
40
40
|
}.to raise_error(Dentaku::ZeroDivisionError)
|
41
41
|
end
|
42
42
|
|
43
|
+
it "lets you know if the result is a div/0 error when taking modulo" do
|
44
|
+
expressions = {more_apples: "1%0"}
|
45
|
+
expect {
|
46
|
+
described_class.new(expressions, calculator).solve!
|
47
|
+
}.to raise_error(Dentaku::ZeroDivisionError)
|
48
|
+
end
|
49
|
+
|
43
50
|
it "does not require keys to be parseable" do
|
44
51
|
expressions = { "the value of x, incremented" => "x + 1" }
|
45
52
|
solver = described_class.new(expressions, calculator.store("x" => 3))
|
data/spec/calculator_spec.rb
CHANGED
@@ -531,7 +531,7 @@ describe Dentaku::Calculator do
|
|
531
531
|
expect(calculator.evaluate!('value + duration(1, month)', { value: value })).to eq(Time.local(2023, 8, 13, 10, 42, 11))
|
532
532
|
expect(calculator.evaluate!('value - duration(1, day)', { value: value })).to eq(Time.local(2023, 7, 12, 10, 42, 11))
|
533
533
|
expect(calculator.evaluate!('value - duration(1, year)', { value: value })).to eq(Time.local(2022, 7, 13, 10, 42, 11))
|
534
|
-
expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(
|
534
|
+
expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(value2 - value)
|
535
535
|
expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
|
536
536
|
end
|
537
537
|
end
|
@@ -166,7 +166,7 @@ describe Dentaku::Calculator do
|
|
166
166
|
it 'adds multiple functions to default/global function registry' do
|
167
167
|
described_class.add_functions([
|
168
168
|
[:cube, :numeric, ->(x) { x**3 }],
|
169
|
-
[:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c,i| i.even? ? c.upcase : c.downcase }.join() }],
|
169
|
+
[:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c, i| i.even? ? c.upcase : c.downcase }.join() }],
|
170
170
|
])
|
171
171
|
|
172
172
|
expect(described_class.new.evaluate("1 + cube(3)")).to eq(28)
|
data/spec/parser_spec.rb
CHANGED
@@ -27,6 +27,9 @@ describe Dentaku::Parser do
|
|
27
27
|
it 'calculates bitwise OR' do
|
28
28
|
node = parse('2|3')
|
29
29
|
expect(node.value).to eq(3)
|
30
|
+
|
31
|
+
node = parse('(5 | 2) + 1')
|
32
|
+
expect(node.value).to eq(8)
|
30
33
|
end
|
31
34
|
|
32
35
|
it 'performs multiple operations in one stream' do
|
@@ -77,11 +80,17 @@ describe Dentaku::Parser do
|
|
77
80
|
end
|
78
81
|
|
79
82
|
it 'evaluates arrays' do
|
83
|
+
node = parse('{}')
|
84
|
+
expect(node.value).to eq([])
|
85
|
+
|
80
86
|
node = parse('{1, 2, 3}')
|
81
87
|
expect(node.value).to eq([1, 2, 3])
|
82
88
|
|
83
|
-
node = parse('{}')
|
84
|
-
expect(node.value).to eq([])
|
89
|
+
node = parse('{1, 2, 3} + {4,5,6}')
|
90
|
+
expect(node.value).to eq([1, 2, 3, 4, 5, 6])
|
91
|
+
|
92
|
+
node = parse('{1, 2, 3} - {2,3}')
|
93
|
+
expect(node.value).to eq([1])
|
85
94
|
end
|
86
95
|
|
87
96
|
context 'invalid expression' do
|
data/spec/print_visitor_spec.rb
CHANGED
@@ -59,6 +59,11 @@ describe Dentaku::PrintVisitor do
|
|
59
59
|
expect(repr).to eq('2017-12-24 23:59:59')
|
60
60
|
end
|
61
61
|
|
62
|
+
it 'handles a percentage in a formula' do
|
63
|
+
repr = roundtrip('((3*4%) * 0.001)')
|
64
|
+
expect(repr).to eq('3 * 4% * 0.001')
|
65
|
+
end
|
66
|
+
|
62
67
|
private
|
63
68
|
|
64
69
|
def roundtrip(string)
|
data/spec/spec_helper.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
require 'pry'
|
2
2
|
require 'simplecov'
|
3
|
-
require 'codecov'
|
4
3
|
|
5
4
|
SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
|
6
5
|
SimpleCov::Formatter::HTMLFormatter,
|
7
|
-
SimpleCov::Formatter::Codecov,
|
8
6
|
])
|
9
7
|
|
10
8
|
SimpleCov.minimum_coverage 90
|
11
|
-
SimpleCov.minimum_coverage_by_file 80
|
9
|
+
# SimpleCov.minimum_coverage_by_file 80
|
12
10
|
|
13
11
|
SimpleCov.start do
|
14
12
|
add_filter "spec/"
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.5.
|
4
|
+
version: 3.5.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-08-21 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: bigdecimal
|
@@ -38,20 +37,6 @@ dependencies:
|
|
38
37
|
- - ">="
|
39
38
|
- !ruby/object:Gem::Version
|
40
39
|
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: codecov
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
40
|
- !ruby/object:Gem::Dependency
|
56
41
|
name: pry
|
57
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -157,6 +142,8 @@ executables: []
|
|
157
142
|
extensions: []
|
158
143
|
extra_rdoc_files: []
|
159
144
|
files:
|
145
|
+
- ".github/workflows/rspec.yml"
|
146
|
+
- ".github/workflows/rubocop.yml"
|
160
147
|
- ".gitignore"
|
161
148
|
- ".pryrc"
|
162
149
|
- ".rubocop.yml"
|
@@ -289,7 +276,6 @@ homepage: http://github.com/rubysolo/dentaku
|
|
289
276
|
licenses:
|
290
277
|
- MIT
|
291
278
|
metadata: {}
|
292
|
-
post_install_message:
|
293
279
|
rdoc_options: []
|
294
280
|
require_paths:
|
295
281
|
- lib
|
@@ -304,8 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
304
290
|
- !ruby/object:Gem::Version
|
305
291
|
version: '0'
|
306
292
|
requirements: []
|
307
|
-
rubygems_version: 3.
|
308
|
-
signing_key:
|
293
|
+
rubygems_version: 3.6.2
|
309
294
|
specification_version: 4
|
310
295
|
summary: A formula language parser and evaluator
|
311
296
|
test_files:
|