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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6992ab2a13558e752be1db54e612dba4230c8e273ebb4bc11b51d655435ab463
4
- data.tar.gz: fe53f64d1015b13ec909382679e3dbc93ec822fb97d8389a3bd83870fe2dacea
3
+ metadata.gz: 5d84ad3261b937673547d328aae718b0a7389bca75b6aedaaa8b82dff2c1f9f9
4
+ data.tar.gz: 5f6e809c2a510e2d32a07ed0838ad878dd46bb9e475e73626dced73f219280c8
5
5
  SHA512:
6
- metadata.gz: cad7fc2694c626beb952b44c5014bda140ef5e7953de85e333e8482b35ac02ce87f8054e2e752f9f47a0ae2c5af7666224803d28b484c7c2defaa11aa782521f
7
- data.tar.gz: 53a267501045fce0f6f4cb26bfb7308e65a9b6cb1a1e8d4386cafffbbb865976b0c320fae2553da30b07193a9126be05068c2b7ba7c5034a341698733cf88305
6
+ metadata.gz: e939d79f7b4defcc2596b2a83fe612fbebf298cdb2b3a38b0cd7978ef8bdabeab6321e2b381ad9de83d28cd10bd807eabd4af1c8d095446ffca3c7515cbfe015
7
+ data.tar.gz: 390db232aa4a54ca779e0ddfe762cfbf9b5279c60731809ca6a5d59013ef3f3d161c298dd315daae4beeb7c5a51f4285e29b5f48d241f77d33d63e7787a2f91b
@@ -8,14 +8,12 @@ jobs:
8
8
  strategy:
9
9
  matrix:
10
10
  ruby:
11
- - '2.5'
12
- - '2.6'
13
- - '2.7'
14
11
  - '3.0'
15
12
  - '3.1'
16
13
  - '3.2'
17
14
  - '3.3'
18
15
  - '3.4'
16
+ - '4.0'
19
17
  fail-fast: false
20
18
  steps:
21
19
  - uses: actions/checkout@v4
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
- numeric(val)
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 =~ /\A-?\d*(\.\d+)?\z/ && !string.empty?
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
- v = BigDecimal(val, Float::DIG + 1)
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)
@@ -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
@@ -11,7 +11,7 @@ module Dentaku
11
11
  def register(name, type, implementation, callback = nil)
12
12
  function = Class.new(Function) do
13
13
  def self.name=(name)
14
- @name = name
14
+ @name = name.to_s
15
15
  end
16
16
 
17
17
  def self.name
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:abs, :numeric, lambda { |numeric|
4
- Dentaku::AST::Function.numeric(numeric).abs
4
+ Dentaku::NumericParser.ensure_numeric!(numeric).abs
5
5
  })
@@ -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::AST::Function.numeric(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
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::AST::Function.numeric(arg) }
21
- y_values = y_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
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
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:max, :numeric, ->(*args) {
4
- args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.max
4
+ args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.max
5
5
  })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:min, :numeric, ->(*args) {
4
- args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.min
4
+ args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.min
5
5
  })
@@ -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::AST::Function.numeric(arg) }.reduce(1, :*)
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::AST::Function.numeric(numeric).round(Dentaku::AST::Function.numeric(places || 0).to_i)
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::AST::Function.numeric(precision || 0).to_i
4
+ precision = Dentaku::NumericParser.ensure_numeric!(precision || 0).to_i
5
5
  tens = 10.0**precision
6
- result = (Dentaku::AST::Function.numeric(numeric) * tens).floor / tens
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::AST::Function.numeric(precision || 0).to_i
4
+ precision = Dentaku::NumericParser.ensure_numeric!(precision || 0).to_i
5
5
  tens = 10.0**precision
6
- result = (Dentaku::AST::Function.numeric(numeric) * tens).ceil / tens
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| Dentaku::AST::Function.numeric(a.value(context)) }
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::AST::Function.numeric(@length.value(context)).to_i
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::AST::Function.numeric(@length.value(context)).to_i
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::AST::Function.numeric(@offset.value(context)).to_i
79
+ offset = Dentaku::NumericParser.ensure_numeric!(@offset.value(context)).to_i
80
80
  negative_argument_failure('MID', 'offset') if offset < 0
81
- length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
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::AST::Function.numeric(arg) }.reduce(0, :+)
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(&:downcase)
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
- if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
127
- results[var_name] = calculator.memory[var_name.downcase]
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
- @key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [k.downcase, k] }]
13
- # ensure variables are normalized strings
14
- @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.downcase.to_s, v] }]
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: false
1
2
  module Dentaku
2
3
  class PrintVisitor
3
4
  def initialize(node)
@@ -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, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
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, '(0x[0-9a-f]+)\b', lambda { |raw| raw[2..-1].to_i(16) })
103
+ new(:numeric, NumericParser::HEXADECIMAL_PATTERN, lambda { |raw| NumericParser.parse_hexadecimal_string(raw) })
105
104
  end
106
105
 
107
106
  def double_quoted_string
@@ -83,7 +83,7 @@ module Dentaku
83
83
 
84
84
  def alias_regex
85
85
  values = @aliases.values.flatten.join('|')
86
- /(?<=\p{Punct}|[[:space:]]|\A)(#{values})(?=\()/i
86
+ /(?<=\p{Punct}|[[:space:]]|\A)(#{values})(?=\s*\()/i
87
87
  end
88
88
 
89
89
  private
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.7"
2
+ VERSION = "3.5.8"
3
3
  end
@@ -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)
@@ -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
@@ -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.7
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: 2025-12-16 00:00:00.000000000 Z
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: pry
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: :development
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.2
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