dentaku 3.5.7 → 3.5.8
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 +1 -3
- data/CHANGELOG.md +18 -0
- data/Gemfile +13 -0
- data/dentaku.gemspec +1 -8
- data/lib/dentaku/ast/arithmetic.rb +2 -13
- data/lib/dentaku/ast/comparators.rb +1 -4
- data/lib/dentaku/ast/function.rb +0 -14
- data/lib/dentaku/ast/function_registry.rb +1 -1
- data/lib/dentaku/ast/functions/abs.rb +1 -1
- data/lib/dentaku/ast/functions/avg.rb +1 -1
- data/lib/dentaku/ast/functions/intercept.rb +2 -2
- 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 +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 +4 -4
- data/lib/dentaku/ast/functions/sum.rb +1 -1
- data/lib/dentaku/bulk_expression_solver.rb +13 -6
- data/lib/dentaku/dependency_resolver.rb +12 -5
- data/lib/dentaku/numeric_parser.rb +68 -0
- data/lib/dentaku/print_visitor.rb +1 -0
- data/lib/dentaku/token_scanner.rb +3 -4
- data/lib/dentaku/tokenizer.rb +1 -1
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/function_spec.rb +11 -25
- data/spec/bulk_expression_solver_spec.rb +35 -0
- data/spec/calculator_spec.rb +10 -0
- data/spec/dependency_resolver_spec.rb +14 -0
- data/spec/numeric_parser_spec.rb +66 -0
- data/spec/tokenizer_spec.rb +6 -0
- metadata +8 -89
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d84ad3261b937673547d328aae718b0a7389bca75b6aedaaa8b82dff2c1f9f9
|
|
4
|
+
data.tar.gz: 5f6e809c2a510e2d32a07ed0838ad878dd46bb9e475e73626dced73f219280c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e939d79f7b4defcc2596b2a83fe612fbebf298cdb2b3a38b0cd7978ef8bdabeab6321e2b381ad9de83d28cd10bd807eabd4af1c8d095446ffca3c7515cbfe015
|
|
7
|
+
data.tar.gz: 390db232aa4a54ca779e0ddfe762cfbf9b5279c60731809ca6a5d59013ef3f3d161c298dd315daae4beeb7c5a51f4285e29b5f48d241f77d33d63e7787a2f91b
|
data/.github/workflows/rspec.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## [UNRELEASED]
|
|
4
|
+
|
|
5
|
+
## [v3.5.8] 2026-08-01
|
|
6
|
+
- unify numeric matching and parsing
|
|
7
|
+
- fix frozen-string-literal warning
|
|
8
|
+
- fix alias function calls with whitespace before the opening parenthesis
|
|
9
|
+
(#335, thanks @DirkDoes)
|
|
10
|
+
- declare `tsort` as an explicit dependency for Ruby 4.1 (#334, thanks @david942j)
|
|
11
|
+
- fix dependency resolution order for mixed-case variables in case-sensitive
|
|
12
|
+
bulk expressions (#353, thanks @connortorrell)
|
|
13
|
+
- stop exposing downcased aliases of constants to case-sensitive bulk expressions,
|
|
14
|
+
and fix the memory fallback missing mixed-case values
|
|
15
|
+
- return a String from `name` for Ruby Math functions and functions registered
|
|
16
|
+
under a Symbol, per the `Module#name` contract (#348, thanks @toy)
|
|
17
|
+
- test against Ruby 3.0 through 4.0 (2.5 - 2.7 are no longer covered by CI)
|
|
18
|
+
|
|
3
19
|
## [v3.5.7] 2025-12-16
|
|
4
20
|
- fix misclassification of unary minus as subtraction
|
|
5
21
|
- fix parsing empty function call
|
|
@@ -277,6 +293,8 @@
|
|
|
277
293
|
## [v0.1.0] 2012-01-20
|
|
278
294
|
- initial release
|
|
279
295
|
|
|
296
|
+
[UNRELEASED]: https://github.com/rubysolo/dentaku/compare/v3.5.7...HEAD
|
|
297
|
+
[v3.5.7]: https://github.com/rubysolo/dentaku/compare/v3.5.6...v3.5.7
|
|
280
298
|
[v3.5.6]: https://github.com/rubysolo/dentaku/compare/v3.5.5...v3.5.6
|
|
281
299
|
[v3.5.5]: https://github.com/rubysolo/dentaku/compare/v3.5.4...v3.5.5
|
|
282
300
|
[v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
|
data/Gemfile
CHANGED
|
@@ -2,3 +2,16 @@ source "http://rubygems.org"
|
|
|
2
2
|
|
|
3
3
|
# Specify your gem's dependencies in dentaku.gemspec
|
|
4
4
|
gemspec
|
|
5
|
+
|
|
6
|
+
group :development, :test do
|
|
7
|
+
gem 'pry'
|
|
8
|
+
gem 'pry-byebug'
|
|
9
|
+
gem 'pry-stack_explorer'
|
|
10
|
+
gem 'rake'
|
|
11
|
+
gem 'rspec'
|
|
12
|
+
gem 'rubocop'
|
|
13
|
+
# Stay on the 0.x line: simplecov 1.0 (2026-07) removed SimpleCov.running,
|
|
14
|
+
# which the RubyMine/IntelliJ coverage runner still relies on. Lift this
|
|
15
|
+
# once the IDE integration supports 1.0.
|
|
16
|
+
gem 'simplecov', '< 1.0'
|
|
17
|
+
end
|
data/dentaku.gemspec
CHANGED
|
@@ -16,14 +16,7 @@ Gem::Specification.new do |s|
|
|
|
16
16
|
|
|
17
17
|
s.add_dependency('bigdecimal')
|
|
18
18
|
s.add_dependency('concurrent-ruby')
|
|
19
|
-
|
|
20
|
-
s.add_development_dependency('pry')
|
|
21
|
-
s.add_development_dependency('pry-byebug')
|
|
22
|
-
s.add_development_dependency('pry-stack_explorer')
|
|
23
|
-
s.add_development_dependency('rake')
|
|
24
|
-
s.add_development_dependency('rspec')
|
|
25
|
-
s.add_development_dependency('rubocop')
|
|
26
|
-
s.add_development_dependency('simplecov')
|
|
19
|
+
s.add_dependency('tsort')
|
|
27
20
|
|
|
28
21
|
s.files = `git ls-files`.split("\n")
|
|
29
22
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
@@ -6,9 +6,6 @@ require 'bigdecimal/util'
|
|
|
6
6
|
module Dentaku
|
|
7
7
|
module AST
|
|
8
8
|
class Arithmetic < Operation
|
|
9
|
-
DECIMAL = /\A-?\d*\.\d+\z/.freeze
|
|
10
|
-
INTEGER = /\A-?\d+\z/.freeze
|
|
11
|
-
|
|
12
9
|
def initialize(*)
|
|
13
10
|
super
|
|
14
11
|
|
|
@@ -49,15 +46,7 @@ module Dentaku
|
|
|
49
46
|
|
|
50
47
|
def cast(val)
|
|
51
48
|
validate_value(val)
|
|
52
|
-
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def numeric(val)
|
|
56
|
-
case val.to_s
|
|
57
|
-
when DECIMAL then decimal(val)
|
|
58
|
-
when INTEGER then val.to_i
|
|
59
|
-
else val
|
|
60
|
-
end
|
|
49
|
+
Dentaku::NumericParser.ensure_numeric(val) || val
|
|
61
50
|
end
|
|
62
51
|
|
|
63
52
|
def decimal(val)
|
|
@@ -114,7 +103,7 @@ module Dentaku
|
|
|
114
103
|
end
|
|
115
104
|
|
|
116
105
|
def validate_format(string)
|
|
117
|
-
unless string
|
|
106
|
+
unless Dentaku::NumericParser.match(string)
|
|
118
107
|
raise Dentaku::ArgumentError.for(:invalid_value, value: string, for: BigDecimal),
|
|
119
108
|
"String input '#{string}' is not coercible to numeric"
|
|
120
109
|
end
|
|
@@ -28,11 +28,8 @@ module Dentaku
|
|
|
28
28
|
|
|
29
29
|
def cast(val)
|
|
30
30
|
return val unless val.is_a?(::String)
|
|
31
|
-
return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
v = v.to_i if v.frac.zero?
|
|
35
|
-
v
|
|
32
|
+
Dentaku::NumericParser.parse_string(val) || val
|
|
36
33
|
end
|
|
37
34
|
|
|
38
35
|
def validate_value(value)
|
data/lib/dentaku/ast/function.rb
CHANGED
|
@@ -37,20 +37,6 @@ module Dentaku
|
|
|
37
37
|
def self.registry
|
|
38
38
|
@registry ||= FunctionRegistry.new
|
|
39
39
|
end
|
|
40
|
-
|
|
41
|
-
# @return [Numeric] where possible it returns an Integer otherwise a BigDecimal.
|
|
42
|
-
# An Exception will be raised if a value is passed that cannot be cast to a Number.
|
|
43
|
-
def self.numeric(value)
|
|
44
|
-
return value if value.is_a?(::Numeric)
|
|
45
|
-
|
|
46
|
-
if value.is_a?(::String)
|
|
47
|
-
number = value[/\A-?\d*\.?\d+\z/]
|
|
48
|
-
return number.include?('.') ? BigDecimal(number, DIG) : number.to_i if number
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
raise Dentaku::ArgumentError.for(:incompatible_type, value: value, for: Numeric),
|
|
52
|
-
"'#{value || value.class}' is not coercible to numeric"
|
|
53
|
-
end
|
|
54
40
|
end
|
|
55
41
|
end
|
|
56
42
|
end
|
|
@@ -9,5 +9,5 @@ Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
|
|
|
9
9
|
), 'AVG() requires at least one argument'
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
flatten_args.map { |arg| Dentaku::
|
|
12
|
+
flatten_args.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
|
|
13
13
|
})
|
|
@@ -17,8 +17,8 @@ Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
n = x_values.length.to_f
|
|
20
|
-
x_values = x_values.map { |arg| Dentaku::
|
|
21
|
-
y_values = y_values.map { |arg| Dentaku::
|
|
20
|
+
x_values = x_values.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }
|
|
21
|
+
y_values = y_values.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }
|
|
22
22
|
|
|
23
23
|
x_avg = x_values.sum / n
|
|
24
24
|
y_avg = y_values.sum / n
|
|
@@ -8,5 +8,5 @@ Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
|
|
|
8
8
|
), 'MUL() requires at least one argument'
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
args.flatten.map { |arg| Dentaku::
|
|
11
|
+
args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(1, :*)
|
|
12
12
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:round, :numeric, lambda { |numeric, places = 0|
|
|
4
|
-
Dentaku::
|
|
4
|
+
Dentaku::NumericParser.ensure_numeric!(numeric).round(Dentaku::NumericParser.ensure_numeric!(places || 0).to_i)
|
|
5
5
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:rounddown, :numeric, lambda { |numeric, precision = 0|
|
|
4
|
-
precision = Dentaku::
|
|
4
|
+
precision = Dentaku::NumericParser.ensure_numeric!(precision || 0).to_i
|
|
5
5
|
tens = 10.0**precision
|
|
6
|
-
result = (Dentaku::
|
|
6
|
+
result = (Dentaku::NumericParser.ensure_numeric!(numeric) * tens).floor / tens
|
|
7
7
|
precision <= 0 ? result.to_i : result
|
|
8
8
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:roundup, :numeric, lambda { |numeric, precision = 0|
|
|
4
|
-
precision = Dentaku::
|
|
4
|
+
precision = Dentaku::NumericParser.ensure_numeric!(precision || 0).to_i
|
|
5
5
|
tens = 10.0**precision
|
|
6
|
-
result = (Dentaku::
|
|
6
|
+
result = (Dentaku::NumericParser.ensure_numeric!(numeric) * tens).ceil / tens
|
|
7
7
|
precision <= 0 ? result.to_i : result
|
|
8
8
|
})
|
|
@@ -16,8 +16,11 @@ module Dentaku
|
|
|
16
16
|
@implementation = Math.method(method)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Ruby documents Module#name as a String (or nil for anonymous
|
|
20
|
+
# classes), so expose one even though the function is registered
|
|
21
|
+
# under a Symbol
|
|
19
22
|
def self.name
|
|
20
|
-
@name
|
|
23
|
+
@name.to_s
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def self.arity
|
|
@@ -39,7 +42,7 @@ module Dentaku
|
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def value(context = {})
|
|
42
|
-
args = @args.flatten.map { |a|
|
|
45
|
+
args = @args.flatten.map { |a|Dentaku::NumericParser.ensure_numeric!(a.value(context)) }
|
|
43
46
|
self.class.call(*args)
|
|
44
47
|
end
|
|
45
48
|
|
|
@@ -32,7 +32,7 @@ module Dentaku
|
|
|
32
32
|
|
|
33
33
|
def value(context = {})
|
|
34
34
|
string = @string.value(context).to_s
|
|
35
|
-
length = Dentaku::
|
|
35
|
+
length = Dentaku::NumericParser.ensure_numeric!(@length.value(context)).to_i
|
|
36
36
|
negative_argument_failure('LEFT') if length < 0
|
|
37
37
|
string[0, length]
|
|
38
38
|
end
|
|
@@ -54,7 +54,7 @@ module Dentaku
|
|
|
54
54
|
|
|
55
55
|
def value(context = {})
|
|
56
56
|
string = @string.value(context).to_s
|
|
57
|
-
length = Dentaku::
|
|
57
|
+
length = Dentaku::NumericParser.ensure_numeric!(@length.value(context)).to_i
|
|
58
58
|
negative_argument_failure('RIGHT') if length < 0
|
|
59
59
|
string[length * -1, length] || string
|
|
60
60
|
end
|
|
@@ -76,9 +76,9 @@ module Dentaku
|
|
|
76
76
|
|
|
77
77
|
def value(context = {})
|
|
78
78
|
string = @string.value(context).to_s
|
|
79
|
-
offset = Dentaku::
|
|
79
|
+
offset = Dentaku::NumericParser.ensure_numeric!(@offset.value(context)).to_i
|
|
80
80
|
negative_argument_failure('MID', 'offset') if offset < 0
|
|
81
|
-
length = Dentaku::
|
|
81
|
+
length = Dentaku::NumericParser.ensure_numeric!(@length.value(context)).to_i
|
|
82
82
|
negative_argument_failure('MID') if length < 0
|
|
83
83
|
string[offset - 1, length].to_s
|
|
84
84
|
end
|
|
@@ -8,5 +8,5 @@ Dentaku::AST::Function.register(:sum, :numeric, ->(*args) {
|
|
|
8
8
|
), 'SUM() requires at least one argument'
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
args.flatten.map { |arg| Dentaku::
|
|
11
|
+
args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(0, :+)
|
|
12
12
|
})
|
|
@@ -85,7 +85,7 @@ module Dentaku
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def load_results(&block)
|
|
88
|
-
facts, _formulas = expressions.transform_keys(
|
|
88
|
+
facts, _formulas = expressions.transform_keys { |k| normalized_name(k) }
|
|
89
89
|
.transform_values { |v| calculator.ast(v) }
|
|
90
90
|
.partition { |_, v| calculator.dependencies(v, nil).empty? }
|
|
91
91
|
|
|
@@ -101,7 +101,7 @@ module Dentaku
|
|
|
101
101
|
next if expressions[var_name].nil?
|
|
102
102
|
|
|
103
103
|
with_rescues(var_name, results, block) do
|
|
104
|
-
results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
|
|
104
|
+
results[var_name] = evaluated_facts[normalized_name(var_name)] || evaluator.evaluate(
|
|
105
105
|
expressions[var_name],
|
|
106
106
|
context.merge(results),
|
|
107
107
|
&expression_with_exception_handler(var_name, &block)
|
|
@@ -123,11 +123,18 @@ module Dentaku
|
|
|
123
123
|
ex.recipient_variable = var_name
|
|
124
124
|
results[var_name] = block.call(ex)
|
|
125
125
|
ensure
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
stored_name = normalized_name(var_name)
|
|
127
|
+
if results[var_name] == :undefined && calculator.memory.has_key?(stored_name)
|
|
128
|
+
results[var_name] = calculator.memory[stored_name]
|
|
128
129
|
end
|
|
129
130
|
end
|
|
130
131
|
|
|
132
|
+
# names are normalized the same way the calculator stores them, so that
|
|
133
|
+
# facts and memory lookups line up with the identifiers the parser emits
|
|
134
|
+
def normalized_name(variable_name)
|
|
135
|
+
calculator.standardize_case(variable_name.to_s)
|
|
136
|
+
end
|
|
137
|
+
|
|
131
138
|
def expressions
|
|
132
139
|
@expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
|
|
133
140
|
end
|
|
@@ -147,9 +154,9 @@ module Dentaku
|
|
|
147
154
|
end
|
|
148
155
|
|
|
149
156
|
def variables_in_resolve_order
|
|
150
|
-
cache_key = expressions.keys.map(&:to_s).sort.join("|")
|
|
157
|
+
cache_key = "#{!!calculator.case_sensitive}|#{expressions.keys.map(&:to_s).sort.join("|")}"
|
|
151
158
|
@ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
|
|
152
|
-
DependencyResolver.find_resolve_order(dependencies).tap do |d|
|
|
159
|
+
DependencyResolver.find_resolve_order(dependencies, calculator.case_sensitive).tap do |d|
|
|
153
160
|
self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
|
|
154
161
|
end
|
|
155
162
|
}
|
|
@@ -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
|
|
@@ -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, value: value, for: 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
|
|
@@ -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
|
data/lib/dentaku/tokenizer.rb
CHANGED
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/function_spec.rb
CHANGED
|
@@ -37,33 +37,19 @@ describe Dentaku::AST::Function do
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
it 'casts a String to an Integer if possible' do
|
|
41
|
-
expect(described_class.numeric('3')).to eq(3)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
it 'casts a String to a BigDecimal if possible and if Integer would loose information' do
|
|
45
|
-
expect(described_class.numeric('3.2')).to eq(3.2)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it 'casts a String to a BigDecimal with a negative number' do
|
|
49
|
-
expect(described_class.numeric('-3.2')).to eq(-3.2)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
it 'casts a String to a BigDecimal without a leading zero' do
|
|
53
|
-
expect(described_class.numeric('-.2')).to eq(-0.2)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it 'raises an error if the value could not be cast to a Numeric' do
|
|
57
|
-
expect { described_class.numeric('flarble') }.to raise_error Dentaku::ArgumentError
|
|
58
|
-
expect { described_class.numeric('-') }.to raise_error Dentaku::ArgumentError
|
|
59
|
-
expect { described_class.numeric('') }.to raise_error Dentaku::ArgumentError
|
|
60
|
-
expect { described_class.numeric(nil) }.to raise_error Dentaku::ArgumentError
|
|
61
|
-
expect { described_class.numeric('7.') }.to raise_error Dentaku::ArgumentError
|
|
62
|
-
expect { described_class.numeric(true) }.to raise_error Dentaku::ArgumentError
|
|
63
|
-
end
|
|
64
|
-
|
|
65
40
|
it "allows read access to arguments" do
|
|
66
41
|
fn = described_class.new(1, 2, 3)
|
|
67
42
|
expect(fn.args).to eq([1, 2, 3])
|
|
68
43
|
end
|
|
44
|
+
|
|
45
|
+
describe "#name" do
|
|
46
|
+
it "is a String for functions registered with a Symbol" do
|
|
47
|
+
fn = described_class.register(:symbol_named, :numeric, ->(x) { x })
|
|
48
|
+
expect(fn.name).to eq("symbol_named")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "is a String for functions imported from Ruby's Math module" do
|
|
52
|
+
expect(described_class.get("sqrt").name).to eq("sqrt")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
69
55
|
end
|
|
@@ -130,6 +130,41 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
|
130
130
|
)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
it 'resolves mixed-case keys declared out of order when case-sensitive' do
|
|
134
|
+
calculator = Dentaku::Calculator.new(case_sensitive: true)
|
|
135
|
+
expressions = {
|
|
136
|
+
'TopLevel' => 'MidLevel * 2',
|
|
137
|
+
'MidLevel' => 'BottomLevel * 2',
|
|
138
|
+
'BottomLevel' => '2'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
result = described_class.new(expressions, calculator).solve
|
|
142
|
+
|
|
143
|
+
expect(result).to eq(
|
|
144
|
+
'TopLevel' => 8,
|
|
145
|
+
'MidLevel' => 4,
|
|
146
|
+
'BottomLevel' => 2
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'does not expose a downcased alias of a constant when case-sensitive' do
|
|
151
|
+
calculator = Dentaku::Calculator.new(case_sensitive: true)
|
|
152
|
+
expressions = { 'BottomLevel' => '2', 'TopLevel' => 'bottomlevel * 2' }
|
|
153
|
+
|
|
154
|
+
expect {
|
|
155
|
+
described_class.new(expressions, calculator).solve!
|
|
156
|
+
}.to raise_error(Dentaku::UnboundVariableError, /bottomlevel/)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'falls back to mixed-case memory values when case-sensitive' do
|
|
160
|
+
calculator = Dentaku::Calculator.new(case_sensitive: true)
|
|
161
|
+
calculator.store('MyVar' => 7)
|
|
162
|
+
|
|
163
|
+
result = described_class.new({ 'MyVar' => 'Missing + 1' }, calculator).solve
|
|
164
|
+
|
|
165
|
+
expect(result).to eq('MyVar' => 7)
|
|
166
|
+
end
|
|
167
|
+
|
|
133
168
|
it "returns :undefined when variables are unbound" do
|
|
134
169
|
expressions = {more_apples: "apples + 1"}
|
|
135
170
|
expect(described_class.new(expressions, calculator).solve)
|
data/spec/calculator_spec.rb
CHANGED
|
@@ -961,10 +961,20 @@ describe Dentaku::Calculator do
|
|
|
961
961
|
end
|
|
962
962
|
end
|
|
963
963
|
|
|
964
|
+
describe 'functions with whitespace before parentheses' do
|
|
965
|
+
it 'accepts regular functions' do
|
|
966
|
+
expect(calculator.evaluate('round (5.1)')).to eq(5)
|
|
967
|
+
end
|
|
968
|
+
end
|
|
969
|
+
|
|
964
970
|
describe 'aliases' do
|
|
965
971
|
it 'accepts aliases as instance option' do
|
|
966
972
|
expect(with_aliases.evaluate('rrround(5.1)')).to eq(5)
|
|
967
973
|
end
|
|
974
|
+
|
|
975
|
+
it 'accepts aliases with whitespace before parentheses' do
|
|
976
|
+
expect(with_aliases.evaluate('rrround (5.1)')).to eq(5)
|
|
977
|
+
end
|
|
968
978
|
end
|
|
969
979
|
|
|
970
980
|
describe 'nested_data' do
|
|
@@ -15,4 +15,18 @@ describe Dentaku::DependencyResolver do
|
|
|
15
15
|
["THIRD", "SeCoNd", "FIRST"]
|
|
16
16
|
)
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
it 'sorts mixed-case expressions in dependency order when case-sensitive' do
|
|
20
|
+
dependencies = {"TopLevel" => ["MidLevel"], "MidLevel" => ["BottomLevel"], "BottomLevel" => []}
|
|
21
|
+
expect(described_class.find_resolve_order(dependencies, true)).to eq(
|
|
22
|
+
["BottomLevel", "MidLevel", "TopLevel"]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'treats variables that differ only in case as distinct when case-sensitive' do
|
|
27
|
+
dependencies = {"foo" => ["FOO"], "FOO" => []}
|
|
28
|
+
expect(described_class.find_resolve_order(dependencies, true)).to eq(
|
|
29
|
+
["FOO", "foo"]
|
|
30
|
+
)
|
|
31
|
+
end
|
|
18
32
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'dentaku/numeric_parser'
|
|
2
|
+
|
|
3
|
+
describe Dentaku::NumericParser do
|
|
4
|
+
it 'casts a String to an Integer if possible' do
|
|
5
|
+
result = Dentaku::NumericParser.ensure_numeric!('3')
|
|
6
|
+
expect(result).to eq(3)
|
|
7
|
+
expect(result).to be_a(Integer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'casts a String to a BigDecimal if possible and if Integer would loose information' do
|
|
11
|
+
result = Dentaku::NumericParser.ensure_numeric!('3.2')
|
|
12
|
+
expect(result).to eq(3.2)
|
|
13
|
+
expect(result).to be_a(BigDecimal)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'casts a String to a BigDecimal with a negative number' do
|
|
17
|
+
result = Dentaku::NumericParser.ensure_numeric!('-3.2')
|
|
18
|
+
expect(result).to eq(-3.2)
|
|
19
|
+
expect(result).to be_a(BigDecimal)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'casts a String to a BigDecimal without a leading zero' do
|
|
23
|
+
result = Dentaku::NumericParser.ensure_numeric!('-.2')
|
|
24
|
+
expect(result).to eq(-0.2)
|
|
25
|
+
expect(result).to be_a(BigDecimal)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'casts a String with Sientific notiation to a BigDecimal' do
|
|
29
|
+
result = Dentaku::NumericParser.ensure_numeric!('12E1')
|
|
30
|
+
expect(result).to eq(12e1)
|
|
31
|
+
expect(result).to be_a(BigDecimal)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'casts a String with negative Sientific notiation to a BigDecimal' do
|
|
35
|
+
result = Dentaku::NumericParser.ensure_numeric!('12e-2')
|
|
36
|
+
expect(result).to eq(0.12)
|
|
37
|
+
expect(result).to be_a(BigDecimal)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'casts a String with hexadecimal to an Integer' do
|
|
41
|
+
result1 = Dentaku::NumericParser.ensure_numeric!('0xFFD')
|
|
42
|
+
result2 = Dentaku::NumericParser.ensure_numeric!('0x00000001')
|
|
43
|
+
expect(result1).to eq(4093)
|
|
44
|
+
expect(result2).to eq(1)
|
|
45
|
+
expect(result1).to be_a(Integer)
|
|
46
|
+
expect(result2).to be_a(Integer)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'casts a String with a negative hexadecimal to an Integer' do
|
|
50
|
+
result = Dentaku::NumericParser.ensure_numeric!('-0xFF')
|
|
51
|
+
expect(result).to eq(-0xFF)
|
|
52
|
+
expect(result).to be_a(Integer)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'raises an error if the value could not be cast to a Numeric' do
|
|
56
|
+
expect { Dentaku::NumericParser.ensure_numeric!('flarble') }.to raise_error Dentaku::ArgumentError
|
|
57
|
+
expect { Dentaku::NumericParser.ensure_numeric!('-') }.to raise_error Dentaku::ArgumentError
|
|
58
|
+
expect { Dentaku::NumericParser.ensure_numeric!('') }.to raise_error Dentaku::ArgumentError
|
|
59
|
+
expect { Dentaku::NumericParser.ensure_numeric!(nil) }.to raise_error Dentaku::ArgumentError
|
|
60
|
+
expect { Dentaku::NumericParser.ensure_numeric!('7.') }.to raise_error Dentaku::ArgumentError
|
|
61
|
+
expect { Dentaku::NumericParser.ensure_numeric!(true) }.to raise_error Dentaku::ArgumentError
|
|
62
|
+
expect { Dentaku::NumericParser.ensure_numeric!("1 - 2") }.to raise_error Dentaku::ArgumentError
|
|
63
|
+
expect { Dentaku::NumericParser.ensure_numeric!([1]) }.to raise_error Dentaku::ArgumentError
|
|
64
|
+
expect { Dentaku::NumericParser.ensure_numeric!("1f") }.to raise_error Dentaku::ArgumentError
|
|
65
|
+
end
|
|
66
|
+
end
|
data/spec/tokenizer_spec.rb
CHANGED
|
@@ -360,6 +360,12 @@ describe Dentaku::Tokenizer do
|
|
|
360
360
|
expect(tokenizer.replace_aliases(input)).to eq('min(4,6,2)')
|
|
361
361
|
end
|
|
362
362
|
|
|
363
|
+
it 'replaces aliases with whitespace before parentheses' do
|
|
364
|
+
input = 'rrrrround! (8.2) + minimo (4,6,2)'
|
|
365
|
+
tokenizer.tokenize(input, aliases: aliases)
|
|
366
|
+
expect(tokenizer.replace_aliases(input)).to eq('round (8.2) + min (4,6,2)')
|
|
367
|
+
end
|
|
368
|
+
|
|
363
369
|
it 'replace only whole aliases without word parts' do
|
|
364
370
|
input = 'maximo(2,minimoooo())' # `minimoooo` doesn't match `minimo`
|
|
365
371
|
tokenizer.tokenize(input, aliases: aliases)
|
metadata
CHANGED
|
@@ -1,13 +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.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Solomon White
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|
|
@@ -38,97 +38,13 @@ dependencies:
|
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
41
|
+
name: tsort
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
46
|
version: '0'
|
|
47
|
-
type: :
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: pry-byebug
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0'
|
|
61
|
-
type: :development
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0'
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: pry-stack_explorer
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - ">="
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0'
|
|
75
|
-
type: :development
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - ">="
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '0'
|
|
82
|
-
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: rake
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - ">="
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '0'
|
|
89
|
-
type: :development
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - ">="
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '0'
|
|
96
|
-
- !ruby/object:Gem::Dependency
|
|
97
|
-
name: rspec
|
|
98
|
-
requirement: !ruby/object:Gem::Requirement
|
|
99
|
-
requirements:
|
|
100
|
-
- - ">="
|
|
101
|
-
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0'
|
|
103
|
-
type: :development
|
|
104
|
-
prerelease: false
|
|
105
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
-
requirements:
|
|
107
|
-
- - ">="
|
|
108
|
-
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0'
|
|
110
|
-
- !ruby/object:Gem::Dependency
|
|
111
|
-
name: rubocop
|
|
112
|
-
requirement: !ruby/object:Gem::Requirement
|
|
113
|
-
requirements:
|
|
114
|
-
- - ">="
|
|
115
|
-
- !ruby/object:Gem::Version
|
|
116
|
-
version: '0'
|
|
117
|
-
type: :development
|
|
118
|
-
prerelease: false
|
|
119
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
-
requirements:
|
|
121
|
-
- - ">="
|
|
122
|
-
- !ruby/object:Gem::Version
|
|
123
|
-
version: '0'
|
|
124
|
-
- !ruby/object:Gem::Dependency
|
|
125
|
-
name: simplecov
|
|
126
|
-
requirement: !ruby/object:Gem::Requirement
|
|
127
|
-
requirements:
|
|
128
|
-
- - ">="
|
|
129
|
-
- !ruby/object:Gem::Version
|
|
130
|
-
version: '0'
|
|
131
|
-
type: :development
|
|
47
|
+
type: :runtime
|
|
132
48
|
prerelease: false
|
|
133
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
50
|
requirements:
|
|
@@ -214,6 +130,7 @@ files:
|
|
|
214
130
|
- lib/dentaku/dependency_resolver.rb
|
|
215
131
|
- lib/dentaku/exceptions.rb
|
|
216
132
|
- lib/dentaku/flat_hash.rb
|
|
133
|
+
- lib/dentaku/numeric_parser.rb
|
|
217
134
|
- lib/dentaku/parser.rb
|
|
218
135
|
- lib/dentaku/print_visitor.rb
|
|
219
136
|
- lib/dentaku/string_casing.rb
|
|
@@ -263,6 +180,7 @@ files:
|
|
|
263
180
|
- spec/dependency_resolver_spec.rb
|
|
264
181
|
- spec/exceptions_spec.rb
|
|
265
182
|
- spec/external_function_spec.rb
|
|
183
|
+
- spec/numeric_parser_spec.rb
|
|
266
184
|
- spec/parser_spec.rb
|
|
267
185
|
- spec/print_visitor_spec.rb
|
|
268
186
|
- spec/spec_helper.rb
|
|
@@ -290,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
290
208
|
- !ruby/object:Gem::Version
|
|
291
209
|
version: '0'
|
|
292
210
|
requirements: []
|
|
293
|
-
rubygems_version: 3.6.
|
|
211
|
+
rubygems_version: 3.6.9
|
|
294
212
|
specification_version: 4
|
|
295
213
|
summary: A formula language parser and evaluator
|
|
296
214
|
test_files:
|
|
@@ -333,6 +251,7 @@ test_files:
|
|
|
333
251
|
- spec/dependency_resolver_spec.rb
|
|
334
252
|
- spec/exceptions_spec.rb
|
|
335
253
|
- spec/external_function_spec.rb
|
|
254
|
+
- spec/numeric_parser_spec.rb
|
|
336
255
|
- spec/parser_spec.rb
|
|
337
256
|
- spec/print_visitor_spec.rb
|
|
338
257
|
- spec/spec_helper.rb
|