dentaku 3.5.4 → 3.5.6
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 +35 -11
- data/lib/dentaku/ast/case.rb +12 -0
- data/lib/dentaku/ast/comparators.rb +1 -1
- data/lib/dentaku/ast/operation.rb +5 -0
- data/lib/dentaku/ast.rb +1 -1
- data/lib/dentaku/parser.rb +185 -226
- 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 +10 -5
- data/spec/ast/arithmetic_spec.rb +7 -0
- data/spec/ast/division_spec.rb +8 -4
- data/spec/bulk_expression_solver_spec.rb +8 -1
- data/spec/calculator_spec.rb +8 -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: fc1bbfde891d6c98eda08b9176f9ed791744b2043176f07d3aac997fae34a3e9
|
|
4
|
+
data.tar.gz: b8658f3d74364672f1a4538928e40d7b4d7aff7489273b573dbd1970303a60b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc53fba03a05ddf47875ffd08aa54266b3f6894a91d292f18236aba4db4c4c270bfeb4f039f84f10d4ac7ce2ca0f6a86580d65c1f5a0169b74bd7c2dfacf9363
|
|
7
|
+
data.tar.gz: 88a65a6e103b40fbf3ef0dfc89173ced19e3df7f568514f4a12b451f7d4a91c69b1e25af89b5f9f1c9cadf34b71c200fc4a14ea11ef49aeeb2f2d4be6e36da2e
|
|
@@ -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')
|
|
@@ -13,13 +13,13 @@ module Dentaku
|
|
|
13
13
|
super
|
|
14
14
|
|
|
15
15
|
unless valid_left?
|
|
16
|
-
raise NodeError.new(:
|
|
17
|
-
"#{self.class} requires numeric
|
|
16
|
+
raise NodeError.new(:incompatible, left.type, :left),
|
|
17
|
+
"#{self.class} requires operands that are numeric or compatible types, not #{left.type}"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
unless valid_right?
|
|
21
|
-
raise NodeError.new(:
|
|
22
|
-
"#{self.class} requires numeric
|
|
21
|
+
raise NodeError.new(:incompatible, right.type, :right),
|
|
22
|
+
"#{self.class} requires operands that are numeric or compatible types, not #{right.type}"
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -69,12 +69,25 @@ module Dentaku
|
|
|
69
69
|
def datetime?(val)
|
|
70
70
|
# val is a Date, Time, or DateTime
|
|
71
71
|
return true if val.respond_to?(:strftime)
|
|
72
|
+
return false unless val.is_a?(::String)
|
|
72
73
|
|
|
73
|
-
val
|
|
74
|
+
val =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
def valid_node?(node)
|
|
77
|
-
|
|
78
|
+
return false unless node
|
|
79
|
+
|
|
80
|
+
# Allow nodes with dependencies (identifiers that will be resolved later)
|
|
81
|
+
return true if node.dependencies.any?
|
|
82
|
+
|
|
83
|
+
# Allow compatible types
|
|
84
|
+
return true if [:numeric, :integer, :array].include?(node.type)
|
|
85
|
+
|
|
86
|
+
# Allow nodes without a type (operations, groupings)
|
|
87
|
+
return true if node.type.nil?
|
|
88
|
+
|
|
89
|
+
# Reject incompatible types
|
|
90
|
+
false
|
|
78
91
|
end
|
|
79
92
|
|
|
80
93
|
def valid_left?
|
|
@@ -193,6 +206,13 @@ module Dentaku
|
|
|
193
206
|
def operator
|
|
194
207
|
:%
|
|
195
208
|
end
|
|
209
|
+
|
|
210
|
+
def value(context = {})
|
|
211
|
+
r = decimal(cast(right.value(context)))
|
|
212
|
+
raise Dentaku::ZeroDivisionError if r.zero?
|
|
213
|
+
|
|
214
|
+
cast(cast(left.value(context)) % r)
|
|
215
|
+
end
|
|
196
216
|
end
|
|
197
217
|
|
|
198
218
|
class Percentage < Arithmetic
|
|
@@ -201,26 +221,30 @@ module Dentaku
|
|
|
201
221
|
end
|
|
202
222
|
|
|
203
223
|
def initialize(child)
|
|
204
|
-
@
|
|
224
|
+
@left = child
|
|
205
225
|
|
|
206
|
-
unless
|
|
207
|
-
raise NodeError.new(:numeric,
|
|
226
|
+
unless valid_left?
|
|
227
|
+
raise NodeError.new(:numeric, left.type, :left),
|
|
208
228
|
"#{self.class} requires a numeric operand"
|
|
209
229
|
end
|
|
210
230
|
end
|
|
211
231
|
|
|
212
232
|
def dependencies(context = {})
|
|
213
|
-
@
|
|
233
|
+
@left.dependencies(context)
|
|
214
234
|
end
|
|
215
235
|
|
|
216
236
|
def value(context = {})
|
|
217
|
-
cast(
|
|
237
|
+
cast(left.value(context)) * 0.01
|
|
218
238
|
end
|
|
219
239
|
|
|
220
240
|
def operator
|
|
221
241
|
:%
|
|
222
242
|
end
|
|
223
243
|
|
|
244
|
+
def operator_spacing
|
|
245
|
+
""
|
|
246
|
+
end
|
|
247
|
+
|
|
224
248
|
def self.precedence
|
|
225
249
|
30
|
|
226
250
|
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
|
|
|
@@ -20,7 +20,7 @@ module Dentaku
|
|
|
20
20
|
r = validate_value(cast(right.value(context)))
|
|
21
21
|
|
|
22
22
|
l.public_send(operator, r)
|
|
23
|
-
rescue ::ArgumentError => e
|
|
23
|
+
rescue ::ArgumentError, ::TypeError => e
|
|
24
24
|
raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
|
|
25
25
|
end
|
|
26
26
|
|
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
|
@@ -37,6 +37,7 @@ module Dentaku
|
|
|
37
37
|
@arities = options.fetch(:arities, [])
|
|
38
38
|
@function_registry = options.fetch(:function_registry, nil)
|
|
39
39
|
@case_sensitive = options.fetch(:case_sensitive, false)
|
|
40
|
+
@skip_indices = []
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def consume(count = 2)
|
|
@@ -53,17 +54,18 @@ module Dentaku
|
|
|
53
54
|
fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
if output_size > max_size && operations.empty? || args_size > max_size
|
|
57
|
+
if (output_size > max_size && operations.empty?) || args_size > max_size
|
|
57
58
|
expect = min_size == max_size ? min_size : min_size..max_size
|
|
58
59
|
fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
args = []
|
|
61
63
|
if operator == AST::Array && output.empty?
|
|
62
|
-
|
|
64
|
+
# special case: empty array literal '{}'
|
|
65
|
+
output.push(operator.new)
|
|
63
66
|
else
|
|
64
67
|
fail! :invalid_statement if output_size < args_size
|
|
65
68
|
args = Array.new(args_size) { output.pop }.reverse
|
|
66
|
-
|
|
67
69
|
output.push operator.new(*args)
|
|
68
70
|
end
|
|
69
71
|
|
|
@@ -79,233 +81,21 @@ module Dentaku
|
|
|
79
81
|
def parse
|
|
80
82
|
return AST::Nil.new if input.empty?
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
when :numeric
|
|
88
|
-
output.push AST::Numeric.new(token)
|
|
89
|
-
|
|
90
|
-
when :logical
|
|
91
|
-
output.push AST::Logical.new(token)
|
|
92
|
-
|
|
93
|
-
when :string
|
|
94
|
-
output.push AST::String.new(token)
|
|
95
|
-
|
|
96
|
-
when :identifier
|
|
97
|
-
output.push AST::Identifier.new(token, case_sensitive: case_sensitive)
|
|
98
|
-
|
|
99
|
-
when :operator, :comparator, :combinator
|
|
100
|
-
op_class = operation(token)
|
|
101
|
-
op_class = op_class.resolve_class(input.first)
|
|
102
|
-
|
|
103
|
-
if op_class.right_associative?
|
|
104
|
-
while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
|
105
|
-
consume
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
operations.push op_class
|
|
109
|
-
else
|
|
110
|
-
while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
|
111
|
-
consume
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
operations.push op_class
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
when :null
|
|
118
|
-
output.push AST::Nil.new
|
|
119
|
-
|
|
120
|
-
when :function
|
|
121
|
-
func = function(token)
|
|
122
|
-
if func.nil?
|
|
123
|
-
fail! :undefined_function, function_name: token.value
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
arities.push 0
|
|
127
|
-
operations.push func
|
|
128
|
-
|
|
129
|
-
when :case
|
|
130
|
-
case_index = operations.index { |o| o == AST::Case } || -1
|
|
131
|
-
token_index = case_index + 1
|
|
132
|
-
|
|
133
|
-
case token.value
|
|
134
|
-
when :open
|
|
135
|
-
# special handling for case nesting: strip out inner case
|
|
136
|
-
# statements and parse their AST segments recursively
|
|
137
|
-
if operations.include?(AST::Case)
|
|
138
|
-
open_cases = 0
|
|
139
|
-
case_end_index = nil
|
|
140
|
-
|
|
141
|
-
input.each_with_index do |input_token, index|
|
|
142
|
-
if input_token.category == :case
|
|
143
|
-
if input_token.value == :open
|
|
144
|
-
open_cases += 1
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
if input_token.value == :close
|
|
148
|
-
if open_cases > 0
|
|
149
|
-
open_cases -= 1
|
|
150
|
-
else
|
|
151
|
-
case_end_index = index
|
|
152
|
-
break
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
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
|
-
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
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
when :access
|
|
230
|
-
case token.value
|
|
231
|
-
when :lbracket
|
|
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
|
|
241
|
-
consume
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
when :array
|
|
245
|
-
case token.value
|
|
246
|
-
when :array_start
|
|
247
|
-
operations.push AST::Array
|
|
248
|
-
arities.push 0
|
|
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)
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
when :grouping
|
|
262
|
-
case token.value
|
|
263
|
-
when :open
|
|
264
|
-
if input.first && input.first.value == :close
|
|
265
|
-
input.shift
|
|
266
|
-
arities.pop
|
|
267
|
-
consume(0)
|
|
268
|
-
else
|
|
269
|
-
operations.push AST::Grouping
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
when :close
|
|
273
|
-
while operations.any? && operations.last != AST::Grouping
|
|
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
|
|
292
|
-
|
|
293
|
-
else
|
|
294
|
-
fail! :unknown_grouping_token, token_name: token.value
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
else
|
|
298
|
-
fail! :not_implemented_token_category, token_category: token.category
|
|
84
|
+
i = 0
|
|
85
|
+
while i < input.length
|
|
86
|
+
if @skip_indices.include?(i)
|
|
87
|
+
i += 1
|
|
88
|
+
next
|
|
299
89
|
end
|
|
90
|
+
token = input[i]
|
|
91
|
+
lookahead = input[i + 1]
|
|
92
|
+
process_token(token, lookahead, i, input)
|
|
93
|
+
i += 1
|
|
300
94
|
end
|
|
301
95
|
|
|
302
|
-
while operations.any?
|
|
303
|
-
consume
|
|
304
|
-
end
|
|
96
|
+
consume while operations.any?
|
|
305
97
|
|
|
306
|
-
unless output.count == 1
|
|
307
|
-
fail! :invalid_statement
|
|
308
|
-
end
|
|
98
|
+
fail! :invalid_statement unless output.count == 1
|
|
309
99
|
|
|
310
100
|
output.first
|
|
311
101
|
end
|
|
@@ -324,6 +114,175 @@ module Dentaku
|
|
|
324
114
|
|
|
325
115
|
private
|
|
326
116
|
|
|
117
|
+
def process_token(token, lookahead, index, tokens)
|
|
118
|
+
case token.category
|
|
119
|
+
when :datetime then output << AST::DateTime.new(token)
|
|
120
|
+
when :numeric then output << AST::Numeric.new(token)
|
|
121
|
+
when :logical then output << AST::Logical.new(token)
|
|
122
|
+
when :string then output << AST::String.new(token)
|
|
123
|
+
when :identifier then output << AST::Identifier.new(token, case_sensitive: case_sensitive)
|
|
124
|
+
when :operator, :comparator, :combinator
|
|
125
|
+
handle_operator(token, lookahead)
|
|
126
|
+
when :null
|
|
127
|
+
output << AST::Nil.new
|
|
128
|
+
when :function
|
|
129
|
+
handle_function(token)
|
|
130
|
+
when :case
|
|
131
|
+
handle_case(token)
|
|
132
|
+
when :access
|
|
133
|
+
handle_access(token)
|
|
134
|
+
when :array
|
|
135
|
+
handle_array(token)
|
|
136
|
+
when :grouping
|
|
137
|
+
handle_grouping(token, lookahead, tokens)
|
|
138
|
+
else
|
|
139
|
+
fail! :not_implemented_token_category, token_category: token.category
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def handle_operator(token, lookahead)
|
|
144
|
+
op_class = operation(token).resolve_class(lookahead)
|
|
145
|
+
if op_class.right_associative?
|
|
146
|
+
consume while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
|
147
|
+
else
|
|
148
|
+
consume while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
|
149
|
+
end
|
|
150
|
+
operations.push op_class
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def handle_function(token)
|
|
154
|
+
func = function(token)
|
|
155
|
+
fail! :undefined_function, function_name: token.value if func.nil?
|
|
156
|
+
arities.push 0
|
|
157
|
+
operations.push func
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def handle_case(token)
|
|
161
|
+
# We always operate on the innermost (most recent) CASE on the stack.
|
|
162
|
+
case_index = operations.rindex(AST::Case) || -1
|
|
163
|
+
token_index = case_index + 1
|
|
164
|
+
|
|
165
|
+
case token.value
|
|
166
|
+
when :open
|
|
167
|
+
# Start a new CASE context.
|
|
168
|
+
operations.push AST::Case
|
|
169
|
+
arities.push(0)
|
|
170
|
+
|
|
171
|
+
when :close
|
|
172
|
+
# Finalize any trailing THEN/ELSE expression still on the stack.
|
|
173
|
+
if operations[token_index] == AST::CaseThen
|
|
174
|
+
consume_until(AST::Case)
|
|
175
|
+
operations.push(AST::CaseConditional)
|
|
176
|
+
consume(2)
|
|
177
|
+
arities[-1] += 1
|
|
178
|
+
elsif operations[token_index] == AST::CaseElse
|
|
179
|
+
consume_until(AST::Case)
|
|
180
|
+
arities[-1] += 1
|
|
181
|
+
end
|
|
182
|
+
fail! :unprocessed_token, token_name: token.value unless operations.last == AST::Case
|
|
183
|
+
consume(arities.pop.succ)
|
|
184
|
+
|
|
185
|
+
when :when
|
|
186
|
+
if operations[token_index] == AST::CaseThen
|
|
187
|
+
# Close out previous WHEN/THEN pair.
|
|
188
|
+
consume_until([AST::CaseWhen, AST::Case])
|
|
189
|
+
operations.push(AST::CaseConditional)
|
|
190
|
+
consume(2)
|
|
191
|
+
arities[-1] += 1
|
|
192
|
+
elsif operations.last == AST::Case
|
|
193
|
+
# First WHEN: finalize switch variable expression.
|
|
194
|
+
operations.push(AST::CaseSwitchVariable)
|
|
195
|
+
consume
|
|
196
|
+
end
|
|
197
|
+
operations.push(AST::CaseWhen)
|
|
198
|
+
|
|
199
|
+
when :then
|
|
200
|
+
if operations[token_index] == AST::CaseWhen
|
|
201
|
+
consume_until([AST::CaseThen, AST::Case])
|
|
202
|
+
end
|
|
203
|
+
operations.push(AST::CaseThen)
|
|
204
|
+
|
|
205
|
+
when :else
|
|
206
|
+
if operations[token_index] == AST::CaseThen
|
|
207
|
+
consume_until(AST::Case)
|
|
208
|
+
operations.push(AST::CaseConditional)
|
|
209
|
+
consume(2)
|
|
210
|
+
arities[-1] += 1
|
|
211
|
+
end
|
|
212
|
+
operations.push(AST::CaseElse)
|
|
213
|
+
|
|
214
|
+
else
|
|
215
|
+
fail! :unknown_case_token, token_name: token.value
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def consume_until(target)
|
|
220
|
+
matcher =
|
|
221
|
+
case target
|
|
222
|
+
when Array then ->(op) { target.include?(op) }
|
|
223
|
+
else ->(op) { op == target }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
consume while operations.any? && !matcher.call(operations.last)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def handle_access(token)
|
|
230
|
+
case token.value
|
|
231
|
+
when :lbracket
|
|
232
|
+
operations.push AST::Access
|
|
233
|
+
|
|
234
|
+
when :rbracket
|
|
235
|
+
consume while operations.any? && operations.last != AST::Access
|
|
236
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
|
|
237
|
+
consume
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def handle_array(token)
|
|
242
|
+
case token.value
|
|
243
|
+
when :array_start
|
|
244
|
+
operations.push AST::Array
|
|
245
|
+
arities.push 0
|
|
246
|
+
|
|
247
|
+
when :array_end
|
|
248
|
+
consume while operations.any? && operations.last != AST::Array
|
|
249
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
|
|
250
|
+
consume(arities.pop.succ)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def handle_grouping(token, lookahead, tokens)
|
|
255
|
+
case token.value
|
|
256
|
+
when :open
|
|
257
|
+
if lookahead && lookahead.value == :close
|
|
258
|
+
# empty grouping (e.g. function with zero arguments) — we trigger consume later
|
|
259
|
+
# skip to the end
|
|
260
|
+
lookahead_index = tokens.index(lookahead)
|
|
261
|
+
@skip_indices << lookahead_index if lookahead_index
|
|
262
|
+
arities.pop
|
|
263
|
+
consume(0)
|
|
264
|
+
else
|
|
265
|
+
operations.push AST::Grouping
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
when :close
|
|
269
|
+
consume while operations.any? && operations.last != AST::Grouping
|
|
270
|
+
lparen = operations.pop
|
|
271
|
+
fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
|
|
272
|
+
if operations.last && operations.last < AST::Function
|
|
273
|
+
consume(arities.pop.succ)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
when :comma
|
|
277
|
+
fail! :invalid_statement if arities.empty?
|
|
278
|
+
arities[-1] += 1
|
|
279
|
+
consume while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
|
|
280
|
+
|
|
281
|
+
else
|
|
282
|
+
fail! :unknown_grouping_token, token_name: token.value
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
327
286
|
def fail!(reason, **meta)
|
|
328
287
|
message =
|
|
329
288
|
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,10 +20,10 @@ 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
25
|
described_class.new(five, t)
|
|
26
|
-
}.to raise_error(Dentaku::NodeError, /requires
|
|
26
|
+
}.to raise_error(Dentaku::NodeError, /requires operands/)
|
|
27
27
|
|
|
28
28
|
expression = Dentaku::AST::Multiplication.new(five, five)
|
|
29
29
|
group = Dentaku::AST::Grouping.new(expression)
|
|
@@ -35,7 +35,6 @@ describe Dentaku::AST::Addition do
|
|
|
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,10 +20,10 @@ 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
25
|
described_class.new(five, t)
|
|
26
|
-
}.to raise_error(Dentaku::NodeError, /requires
|
|
26
|
+
}.to raise_error(Dentaku::NodeError, /requires operands/)
|
|
27
27
|
|
|
28
28
|
expression = Dentaku::AST::Multiplication.new(five, five)
|
|
29
29
|
group = Dentaku::AST::Grouping.new(expression)
|
|
@@ -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
|
@@ -130,6 +130,7 @@ describe Dentaku::Calculator do
|
|
|
130
130
|
expect { calculator.evaluate!('"foo" & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
|
131
131
|
expect { calculator.evaluate!('1.0 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
|
132
132
|
expect { calculator.evaluate!('1 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
|
133
|
+
expect { calculator.evaluate!('data < 1', data: { a: 5 }) }.to raise_error(Dentaku::ArgumentError)
|
|
133
134
|
end
|
|
134
135
|
|
|
135
136
|
it 'raises argument error if a function is called with incorrect arity' do
|
|
@@ -235,6 +236,12 @@ describe Dentaku::Calculator do
|
|
|
235
236
|
expect(calculator.dependencies('MAP(vals, val, val + step)')).to eq(['vals', 'step'])
|
|
236
237
|
expect(calculator.dependencies('ALL(people, person, person.age < adult)')).to eq(['people', 'adult'])
|
|
237
238
|
end
|
|
239
|
+
|
|
240
|
+
it "raises an error when trying to find dependencies with invalid syntax" do
|
|
241
|
+
expect { calculator.dependencies('bob + / 3') }.to raise_error(Dentaku::ParseError)
|
|
242
|
+
expect { calculator.dependencies('123 * TRUE') }.to raise_error(Dentaku::ParseError)
|
|
243
|
+
expect { calculator.dependencies('4 + "asdf"') }.to raise_error(Dentaku::ParseError)
|
|
244
|
+
end
|
|
238
245
|
end
|
|
239
246
|
|
|
240
247
|
describe 'solve!' do
|
|
@@ -531,7 +538,7 @@ describe Dentaku::Calculator do
|
|
|
531
538
|
expect(calculator.evaluate!('value + duration(1, month)', { value: value })).to eq(Time.local(2023, 8, 13, 10, 42, 11))
|
|
532
539
|
expect(calculator.evaluate!('value - duration(1, day)', { value: value })).to eq(Time.local(2023, 7, 12, 10, 42, 11))
|
|
533
540
|
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(
|
|
541
|
+
expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(value2 - value)
|
|
535
542
|
expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
|
|
536
543
|
end
|
|
537
544
|
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.6
|
|
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-10-20 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:
|