dentaku 3.5.3 → 3.5.4

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: a51418767c413ccded1f235a56d34866005edf21cbc0d4cf5848d4cff4bc801f
4
- data.tar.gz: 2009c09a76a5cc0b85cf04769c93f0d372cc8613f410a18401de53ca268b134d
3
+ metadata.gz: ef9e8cb8d852db9a8e4c81c28815c6a7e7008c6e9bebcc8a11222768756fd7a8
4
+ data.tar.gz: f7d1c005ef1ba8bcfc77fcac8b885378a0e3e36fdba8f97a2c602b6b0ba6f34c
5
5
  SHA512:
6
- metadata.gz: 6cfeabc676fa63016096d2f3c291d60a68ce247fe7c75ba195cdc2968c28af85de9ab9eeaaa9e88749cbf7920a120b1e6fe4d78464b98f9c825180d42aafc4f1
7
- data.tar.gz: 42863ad11afff2dc72dfe906d1fdc1c07821f4391dcb7fc67921d862c736350be15a6671beac9c16de9e1345f17869e0de40bb131bc1fcfea70190538274bd53
6
+ metadata.gz: 6d616fd597440c13e8432ac6f9d5480592cdcbb7e9aec30c1f2c21e94e0a4bcf3ffced31cf486dc7390bd8668bb5e377745a7eebf467edfab1cd5fa0ae4f70e9
7
+ data.tar.gz: fca27bf921a54469f065f634a44d4a6481c6420ccbcaa39400ffe4a5cdd172ff6ae209a04aee96208a64979bbff814635719732b557df33f7d406f87123b1c3a
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Change Log
2
2
 
3
- ## [Unreleased]
3
+ ## [v3.5.4]
4
+ - add support for default value for PLUCK function
5
+ - improve error handling for MAP/ANY/ALL functions
6
+ - fix modulo / percentage operator determination
7
+ - fix string casing bug with bulk expressions
8
+ - add explicit gem dependency for BigDecimal
9
+
10
+ ## [v3.5.3]
4
11
  - add support for empty array literals
5
12
  - add support for quoted identifiers
6
13
  - add REDUCE function
@@ -253,7 +260,8 @@
253
260
  ## [v0.1.0] 2012-01-20
254
261
  - initial release
255
262
 
256
- [Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.2...HEAD
263
+ [v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
264
+ [v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
257
265
  [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
258
266
  [v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
259
267
  [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
data/dentaku.gemspec CHANGED
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
14
14
  Dentaku is a parser and evaluator for mathematical formulas
15
15
  DESC
16
16
 
17
+ s.add_dependency('bigdecimal')
17
18
  s.add_dependency('concurrent-ruby')
18
19
 
19
20
  s.add_development_dependency('codecov')
@@ -17,9 +17,6 @@ module Dentaku
17
17
  arity
18
18
  end
19
19
 
20
- def self.peek(*)
21
- end
22
-
23
20
  def initialize(data_structure, index)
24
21
  @structure = data_structure
25
22
  @index = index
@@ -179,50 +179,42 @@ module Dentaku
179
179
 
180
180
  class Modulo < Arithmetic
181
181
  def self.arity
182
- @arity
182
+ 2
183
183
  end
184
184
 
185
- def self.peek(input)
186
- @arity = 1
187
- @arity = 2 if input.length > 1
185
+ def self.precedence
186
+ 20
188
187
  end
189
188
 
190
- def initialize(left, right = nil)
191
- if right
192
- @left = left
193
- @right = right
194
- else
195
- @right = left
196
- end
189
+ def self.resolve_class(next_token)
190
+ next_token.nil? || next_token.operator? || next_token.close? ? Percentage : self
191
+ end
192
+
193
+ def operator
194
+ :%
195
+ end
196
+ end
197
+
198
+ class Percentage < Arithmetic
199
+ def self.arity
200
+ 1
201
+ end
202
+
203
+ def initialize(child)
204
+ @right = child
197
205
 
198
- unless valid_left?
199
- raise NodeError.new(%i[numeric nil], left.type, :left),
200
- "#{self.class} requires numeric operands or nil"
201
- end
202
206
  unless valid_right?
203
207
  raise NodeError.new(:numeric, right.type, :right),
204
- "#{self.class} requires numeric operands"
208
+ "#{self.class} requires a numeric operand"
205
209
  end
206
210
  end
207
211
 
208
212
  def dependencies(context = {})
209
- if percent?
210
- @right.dependencies(context)
211
- else
212
- super
213
- end
214
- end
215
-
216
- def percent?
217
- left.nil?
213
+ @right.dependencies(context)
218
214
  end
219
215
 
220
216
  def value(context = {})
221
- if percent?
222
- cast(right.value(context)) * 0.01
223
- else
224
- super
225
- end
217
+ cast(right.value(context)) * 0.01
226
218
  end
227
219
 
228
220
  def operator
@@ -230,11 +222,7 @@ module Dentaku
230
222
  end
231
223
 
232
224
  def self.precedence
233
- 20
234
- end
235
-
236
- def valid_left?
237
- valid_node?(left) || left.nil?
225
+ 30
238
226
  end
239
227
  end
240
228
 
@@ -14,9 +14,6 @@ module Dentaku
14
14
  Float::INFINITY
15
15
  end
16
16
 
17
- def self.peek(*)
18
- end
19
-
20
17
  def initialize(*elements)
21
18
  @elements = *elements
22
19
  end
@@ -32,7 +29,7 @@ module Dentaku
32
29
  def type
33
30
  nil
34
31
  end
35
-
32
+
36
33
  def accept(visitor)
37
34
  visitor.visit_array(self)
38
35
  end
@@ -9,11 +9,7 @@ module Dentaku
9
9
  expression = @args[2]
10
10
 
11
11
  collection.all? do |item_value|
12
- expression.value(
13
- context.merge(
14
- FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
- )
16
- )
12
+ mapped_value(expression, context, item_identifier => item_value)
17
13
  end
18
14
  end
19
15
  end
@@ -9,11 +9,7 @@ module Dentaku
9
9
  expression = @args[2]
10
10
 
11
11
  collection.any? do |item_value|
12
- expression.value(
13
- context.merge(
14
- FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
- )
16
- )
12
+ mapped_value(expression, context, item_identifier => item_value)
17
13
  end
18
14
  end
19
15
  end
@@ -33,6 +33,19 @@ module Dentaku
33
33
  def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
34
34
  raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
35
35
  end
36
+
37
+ private
38
+
39
+ def mapped_value(expression, context, item_context)
40
+ expression.value(
41
+ context.merge(
42
+ FlatHash.from_hash_with_intermediates(item_context)
43
+ )
44
+ )
45
+ rescue => e
46
+ raise e if context["__evaluation_mode"] == :strict
47
+ nil
48
+ end
36
49
  end
37
50
  end
38
51
  end
@@ -9,11 +9,7 @@ module Dentaku
9
9
  expression = @args[2]
10
10
 
11
11
  collection.map do |item_value|
12
- expression.value(
13
- context.merge(
14
- FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
- )
16
- )
12
+ mapped_value(expression, context, item_identifier => item_value)
17
13
  end
18
14
  end
19
15
  end
@@ -9,19 +9,23 @@ module Dentaku
9
9
  end
10
10
 
11
11
  def self.max_param_count
12
- 2
12
+ 3
13
13
  end
14
14
 
15
15
  def value(context = {})
16
16
  collection = Array(@args[0].value(context))
17
+
17
18
  unless collection.all? { |elem| elem.is_a?(Hash) }
18
19
  raise ArgumentError.for(:incompatible_type, value: collection),
19
20
  'PLUCK() requires first argument to be an array of hashes'
20
21
  end
21
22
 
22
23
  pluck_path = @args[1].identifier
24
+ default = @args[2]
23
25
 
24
- collection.map { |h| h.transform_keys(&:to_s)[pluck_path] }
26
+ collection.map { |h|
27
+ h.transform_keys(&:to_s).fetch(pluck_path, default&.value(context))
28
+ }
25
29
  end
26
30
  end
27
31
  end
@@ -9,7 +9,8 @@ module Dentaku
9
9
  nil
10
10
  end
11
11
 
12
- def self.peek(*)
12
+ def self.resolve_class(*)
13
+ self
13
14
  end
14
15
 
15
16
  def dependencies(context = {})
@@ -6,16 +6,41 @@ require 'dentaku/tokenizer'
6
6
 
7
7
  module Dentaku
8
8
  class BulkExpressionSolver
9
+ class StrictEvaluator
10
+ def initialize(calculator)
11
+ @calculator = calculator
12
+ end
13
+
14
+ def evaluate(*args)
15
+ @calculator.evaluate!(*args)
16
+ end
17
+ end
18
+
19
+ class PermissiveEvaluator
20
+ def initialize(calculator, block)
21
+ @calculator = calculator
22
+ @block = block || ->(*) { :undefined }
23
+ end
24
+
25
+ def evaluate(*args)
26
+ @calculator.evaluate(*args) { |expr, ex|
27
+ @block.call(ex)
28
+ }
29
+ end
30
+ end
31
+
9
32
  def initialize(expressions, calculator)
10
33
  @expression_hash = FlatHash.from_hash(expressions)
11
34
  @calculator = calculator
12
35
  end
13
36
 
14
37
  def solve!
38
+ @evaluator = StrictEvaluator.new(calculator)
15
39
  solve(&raise_exception_handler)
16
40
  end
17
41
 
18
42
  def solve(&block)
43
+ @evaluator ||= PermissiveEvaluator.new(calculator, block)
19
44
  error_handler = block || return_undefined_handler
20
45
  results = load_results(&error_handler)
21
46
 
@@ -42,7 +67,7 @@ module Dentaku
42
67
  @dep_cache ||= {}
43
68
  end
44
69
 
45
- attr_reader :expression_hash, :calculator
70
+ attr_reader :expression_hash, :calculator, :evaluator
46
71
 
47
72
  def return_undefined_handler
48
73
  ->(*) { :undefined }
@@ -52,8 +77,11 @@ module Dentaku
52
77
  ->(ex) { raise ex }
53
78
  end
54
79
 
55
- def expression_with_exception_handler(&block)
56
- ->(_expr, ex) { block.call(ex) }
80
+ def expression_with_exception_handler(var_name, &block)
81
+ ->(_expr, ex) {
82
+ ex.recipient_variable = var_name
83
+ block.call(ex)
84
+ }
57
85
  end
58
86
 
59
87
  def load_results(&block)
@@ -73,11 +101,14 @@ module Dentaku
73
101
  next if expressions[var_name].nil?
74
102
 
75
103
  with_rescues(var_name, results, block) do
76
- results[var_name] = evaluated_facts[var_name] || calculator.evaluate!(
104
+ results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
77
105
  expressions[var_name],
78
106
  context.merge(results),
79
- &expression_with_exception_handler(&block)
80
- )
107
+ &expression_with_exception_handler(var_name, &block)
108
+ ).tap { |res|
109
+ res.recipient_variable = var_name if res.respond_to?(:recipient_variable=)
110
+ res
111
+ }
81
112
  end
82
113
  end
83
114
 
@@ -88,7 +119,6 @@ module Dentaku
88
119
 
89
120
  def with_rescues(var_name, results, block)
90
121
  yield
91
-
92
122
  rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
93
123
  ex.recipient_variable = var_name
94
124
  results[var_name] = block.call(ex)
@@ -52,27 +52,39 @@ module Dentaku
52
52
  end
53
53
 
54
54
  def evaluate(expression, data = {}, &block)
55
- evaluate!(expression, data)
55
+ context = evaluation_context(data, :permissive)
56
+ return evaluate_array(expression, context, &block) if expression.is_a?(Array)
57
+
58
+ evaluate!(expression, context)
56
59
  rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
57
60
  block.call(expression, ex) if block_given?
58
61
  end
59
62
 
63
+ private def evaluate_array(expression, data = {}, &block)
64
+ expression.map { |e| evaluate(e, data, &block) }
65
+ end
66
+
60
67
  def evaluate!(expression, data = {}, &block)
61
- return expression.map { |e|
62
- evaluate(e, data, &block)
63
- } if expression.is_a? Array
68
+ context = evaluation_context(data, :strict)
69
+ return evaluate_array!(expression, context, &block) if expression.is_a? Array
64
70
 
65
- store(data) do
71
+ store(context) do
66
72
  node = ast(expression)
67
73
  unbound = node.dependencies(memory)
74
+
68
75
  unless unbound.empty?
69
76
  raise UnboundVariableError.new(unbound),
70
77
  "no value provided for variables: #{unbound.uniq.join(', ')}"
71
78
  end
79
+
72
80
  node.value(memory)
73
81
  end
74
82
  end
75
83
 
84
+ private def evaluate_array!(expression, data = {}, &block)
85
+ expression.map { |e| evaluate!(e, data, &block) }
86
+ end
87
+
76
88
  def solve!(expression_hash)
77
89
  BulkExpressionSolver.new(expression_hash, self).solve!
78
90
  end
@@ -130,6 +142,10 @@ module Dentaku
130
142
  end
131
143
  end
132
144
 
145
+ def evaluation_context(data, evaluation_mode)
146
+ data.key?(:__evaluation_mode) ? data : data.merge(__evaluation_mode: evaluation_mode)
147
+ end
148
+
133
149
  def store(key_or_hash, value = nil)
134
150
  restore = Hash[memory]
135
151
 
@@ -13,13 +13,11 @@ module Dentaku
13
13
  when Numeric
14
14
  @base + duration
15
15
  when Dentaku::AST::Duration::Value
16
- case duration.unit
17
- when :year
18
- Time.local(@base.year + duration.value, @base.month, @base.day).to_datetime
19
- when :month
20
- @base >> duration.value
21
- when :day
22
- @base + duration.value
16
+ case @base
17
+ when Time
18
+ change_datetime(@base.to_datetime, duration.unit, duration.value).to_time
19
+ else
20
+ change_datetime(@base, duration.unit, duration.value)
23
21
  end
24
22
  else
25
23
  raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
@@ -29,16 +27,14 @@ module Dentaku
29
27
 
30
28
  def sub(duration)
31
29
  case duration
32
- when Date, DateTime, Numeric
30
+ when Date, DateTime, Numeric, Time
33
31
  @base - duration
34
32
  when Dentaku::AST::Duration::Value
35
- case duration.unit
36
- when :year
37
- Time.local(@base.year - duration.value, @base.month, @base.day).to_datetime
38
- when :month
39
- @base << duration.value
40
- when :day
41
- @base - duration.value
33
+ case @base
34
+ when Time
35
+ change_datetime(@base.to_datetime, duration.unit, -duration.value).to_time
36
+ else
37
+ change_datetime(@base, duration.unit, -duration.value)
42
38
  end
43
39
  when Dentaku::TokenScanner::DATE_TIME_REGEXP
44
40
  @base - Time.parse(duration).to_datetime
@@ -47,5 +43,18 @@ module Dentaku
47
43
  "'#{duration || duration.class}' is not coercible for date arithmetic"
48
44
  end
49
45
  end
46
+
47
+ private
48
+
49
+ def change_datetime(base, unit, value)
50
+ case unit
51
+ when :year
52
+ base >> (value * 12)
53
+ when :month
54
+ base >> value
55
+ when :day
56
+ base + value
57
+ end
58
+ end
50
59
  end
51
60
  end
@@ -4,13 +4,18 @@ module Dentaku
4
4
  class DependencyResolver
5
5
  include TSort
6
6
 
7
- def self.find_resolve_order(vars_to_dependencies_hash)
8
- self.new(vars_to_dependencies_hash).tsort
7
+ def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
8
+ self.new(vars_to_dependencies_hash).sort
9
9
  end
10
10
 
11
11
  def initialize(vars_to_dependencies_hash)
12
- # ensure variables are strings
13
- @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v] }]
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] }]
15
+ end
16
+
17
+ def sort
18
+ tsort.map { |k| @key_mapping.fetch(k, k) }
14
19
  end
15
20
 
16
21
  def tsort_each_node(&block)
@@ -43,8 +43,6 @@ module Dentaku
43
43
  operator = operations.pop
44
44
  fail! :invalid_statement if operator.nil?
45
45
 
46
- operator.peek(output)
47
-
48
46
  output_size = output.length
49
47
  args_size = operator.arity || count
50
48
  min_size = operator.arity || operator.min_param_count || count
@@ -100,6 +98,7 @@ module Dentaku
100
98
 
101
99
  when :operator, :comparator, :combinator
102
100
  op_class = operation(token)
101
+ op_class = op_class.resolve_class(input.first)
103
102
 
104
103
  if op_class.right_associative?
105
104
  while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
data/lib/dentaku/token.rb CHANGED
@@ -20,10 +20,22 @@ module Dentaku
20
20
  length.zero?
21
21
  end
22
22
 
23
+ def operator?
24
+ is?(:operator)
25
+ end
26
+
23
27
  def grouping?
24
28
  is?(:grouping)
25
29
  end
26
30
 
31
+ def open?
32
+ grouping? && value == :open
33
+ end
34
+
35
+ def close?
36
+ grouping? && value == :close
37
+ end
38
+
27
39
  def is?(c)
28
40
  category == c
29
41
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.3"
2
+ VERSION = "3.5.4"
3
3
  end
data/spec/ast/all_spec.rb CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::All do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'performs ALL operation' do
8
9
  result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
9
10
  expect(result).to eq(false)
@@ -22,4 +23,16 @@ describe Dentaku::AST::All do
22
23
  Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
23
24
  )
24
25
  end
26
+
27
+ it 'treats missing keys in hashes as NULL in permissive mode' do
28
+ expect(
29
+ calculator.evaluate('ALL(items, item, item.value)', items: [{value: 1}, {}])
30
+ ).to be_falsy
31
+ end
32
+
33
+ it 'raises an error if accessing a missing key in a hash in strict mode' do
34
+ expect {
35
+ calculator.evaluate!('ALL(items, item, item.value)', items: [{value: 1}, {}])
36
+ }.to raise_error(Dentaku::UnboundVariableError)
37
+ end
25
38
  end
data/spec/ast/any_spec.rb CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Any do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'performs ANY operation' do
8
9
  result = Dentaku('ANY(vals, val, val > 1)', vals: [1, 2, 3])
9
10
  expect(result).to eq(true)
@@ -20,4 +21,16 @@ describe Dentaku::AST::Any do
20
21
  it 'raises argument error if a string is passed as identifier' do
21
22
  expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
22
23
  end
24
+
25
+ it 'treats missing keys in hashes as NULL in permissive mode' do
26
+ expect(
27
+ calculator.evaluate('ANY(items, item, item.value)', items: [{value: 1}, {}])
28
+ ).to be_truthy
29
+ end
30
+
31
+ it 'raises an error if accessing a missing key in a hash in strict mode' do
32
+ expect {
33
+ calculator.evaluate!('ANY(items, item, item.value)', items: [{}, {value: 1}])
34
+ }.to raise_error(Dentaku::UnboundVariableError)
35
+ end
23
36
  end
data/spec/ast/map_spec.rb CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Map do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'operates on each value in an array' do
8
9
  result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
9
10
  expect(result).to eq(9)
@@ -24,4 +25,16 @@ describe Dentaku::AST::Map do
24
25
  Dentaku::ParseError, 'MAP() requires second argument to be an identifier'
25
26
  )
26
27
  end
28
+
29
+ it 'treats missing keys in hashes as NULL in permissive mode' do
30
+ expect(
31
+ calculator.evaluate('MAP(items, item, item.value)', items: [{value: 1}, {}])
32
+ ).to eq([1, nil])
33
+ end
34
+
35
+ it 'raises an error if accessing a missing key in a hash in strict mode' do
36
+ expect {
37
+ calculator.evaluate!('MAP(items, item, item.value)', items: [{value: 1}, {}])
38
+ }.to raise_error(Dentaku::UnboundVariableError)
39
+ end
27
40
  end
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Pluck do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'operates on each value in an array' do
8
9
  result = Dentaku('PLUCK(users, age)', users: [
9
10
  {name: "Bob", age: 44},
@@ -12,6 +13,22 @@ describe Dentaku::AST::Pluck do
12
13
  expect(result).to eq([44, 27])
13
14
  end
14
15
 
16
+ it 'allows specifying a default for missing values' do
17
+ result = Dentaku!('PLUCK(users, age, -1)', users: [
18
+ {name: "Bob"},
19
+ {name: "Jane", age: 27}
20
+ ])
21
+ expect(result).to eq([-1, 27])
22
+ end
23
+
24
+ it 'returns nil if pluck key is missing from a hash' do
25
+ result = Dentaku!('PLUCK(users, age)', users: [
26
+ {name: "Bob"},
27
+ {name: "Jane", age: 27}
28
+ ])
29
+ expect(result).to eq([nil, 27])
30
+ end
31
+
15
32
  it 'works with an empty array' do
16
33
  result = Dentaku('PLUCK(users, age)', users: [])
17
34
  expect(result).to eq([])
@@ -107,6 +107,22 @@ RSpec.describe Dentaku::BulkExpressionSolver do
107
107
  end
108
108
 
109
109
  describe "#solve" do
110
+ it 'resolves capitalized keys when they are declared out of order' do
111
+ expressions = {
112
+ FIRST: "SECOND * 2",
113
+ SECOND: "THIRD * 2",
114
+ THIRD: 2,
115
+ }
116
+
117
+ result = described_class.new(expressions, calculator).solve
118
+
119
+ expect(result).to eq(
120
+ FIRST: 8,
121
+ SECOND: 4,
122
+ THIRD: 2
123
+ )
124
+ end
125
+
110
126
  it "returns :undefined when variables are unbound" do
111
127
  expressions = {more_apples: "apples + 1"}
112
128
  expect(described_class.new(expressions, calculator).solve)
@@ -30,7 +30,6 @@ describe Dentaku::Calculator do
30
30
  expect(calculator.evaluate('0 * 10 ^ -5')).to eq(0)
31
31
  expect(calculator.evaluate('3 + 0 * -3')).to eq(3)
32
32
  expect(calculator.evaluate('3 + 0 / -3')).to eq(3)
33
- expect(calculator.evaluate('15 % 8')).to eq(7)
34
33
  expect(calculator.evaluate('(((695759/735000)^(1/(1981-1991)))-1)*1000').round(4)).to eq(5.5018)
35
34
  expect(calculator.evaluate('0.253/0.253')).to eq(1)
36
35
  expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
@@ -40,11 +39,20 @@ describe Dentaku::Calculator do
40
39
  expect(calculator.evaluate('t + 1*24*60*60', t: Time.local(2017, 1, 1))).to eq(Time.local(2017, 1, 2))
41
40
  expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
42
41
  expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
43
- expect(calculator.evaluate("5%")).to eq (0.05)
44
42
  expect(calculator.evaluate('1 << 3')).to eq (8)
45
43
  expect(calculator.evaluate('0xFF >> 6')).to eq (3)
46
44
  end
47
45
 
46
+ it "differentiates between percentage and modulo operators" do
47
+ expect(calculator.evaluate('15 % 8')).to eq(7)
48
+ expect(calculator.evaluate('15 % (4 * 2)')).to eq(7)
49
+ expect(calculator.evaluate("5%")).to eq (0.05)
50
+ expect(calculator.evaluate("400/60%").round(2)).to eq (666.67)
51
+ expect(calculator.evaluate("(400/60%)*1").round(2)).to eq (666.67)
52
+ expect(calculator.evaluate("60% * 1").round(2)).to eq (0.60)
53
+ expect(calculator.evaluate("50% + 50%")).to eq (1.0)
54
+ end
55
+
48
56
  describe 'evaluate' do
49
57
  it 'returns nil when formula has error' do
50
58
  expect(calculator.evaluate('1 + + 1')).to be_nil
@@ -165,7 +173,6 @@ describe Dentaku::Calculator do
165
173
  expect(calculator.solve(diff: "d1 - d2")).to eq(diff: -4)
166
174
  end
167
175
 
168
-
169
176
  it 'stores nested hashes' do
170
177
  calculator.store(a: {basket: {of: 'apples'}}, b: 2)
171
178
  expect(calculator.evaluate!('a.basket.of')).to eq('apples')
@@ -516,6 +523,17 @@ describe Dentaku::Calculator do
516
523
  expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
517
524
  expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
518
525
  end
526
+
527
+ it 'from time object' do
528
+ value = Time.local(2023, 7, 13, 10, 42, 11)
529
+ value2 = Time.local(2023, 12, 1, 9, 42, 10)
530
+
531
+ expect(calculator.evaluate!('value + duration(1, month)', { value: value })).to eq(Time.local(2023, 8, 13, 10, 42, 11))
532
+ expect(calculator.evaluate!('value - duration(1, day)', { value: value })).to eq(Time.local(2023, 7, 12, 10, 42, 11))
533
+ expect(calculator.evaluate!('value - duration(1, year)', { value: value })).to eq(Time.local(2022, 7, 13, 10, 42, 11))
534
+ expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(12_182_399.0)
535
+ expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
536
+ end
519
537
  end
520
538
 
521
539
  describe 'functions' do
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/dependency_resolver'
3
+
4
+ describe Dentaku::DependencyResolver do
5
+ it 'sorts expressions in dependency order' do
6
+ dependencies = {"first" => ["second"], "second" => ["third"], "third" => []}
7
+ expect(described_class.find_resolve_order(dependencies)).to eq(
8
+ ["third", "second", "first"]
9
+ )
10
+ end
11
+
12
+ it 'handles case differences' do
13
+ dependencies = {"FIRST" => ["second"], "SeCoNd" => ["third"], "THIRD" => []}
14
+ expect(described_class.find_resolve_order(dependencies)).to eq(
15
+ ["THIRD", "SeCoNd", "FIRST"]
16
+ )
17
+ end
18
+ end
data/spec/visitor_spec.rb CHANGED
@@ -108,7 +108,7 @@ describe TestVisitor do
108
108
  it 'visits all concrete AST node types' do
109
109
  @visited = Set.new
110
110
 
111
- visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples')
111
+ visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples * 5%')
112
112
  visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
113
113
  visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
114
114
  visit_nodes('case (a % 5) when 0 then a else b end')
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dentaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.3
4
+ version: 3.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-04 00:00:00.000000000 Z
11
+ date: 2024-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bigdecimal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: concurrent-ruby
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -259,6 +273,7 @@ files:
259
273
  - spec/bulk_expression_solver_spec.rb
260
274
  - spec/calculator_spec.rb
261
275
  - spec/dentaku_spec.rb
276
+ - spec/dependency_resolver_spec.rb
262
277
  - spec/exceptions_spec.rb
263
278
  - spec/external_function_spec.rb
264
279
  - spec/parser_spec.rb
@@ -330,6 +345,7 @@ test_files:
330
345
  - spec/bulk_expression_solver_spec.rb
331
346
  - spec/calculator_spec.rb
332
347
  - spec/dentaku_spec.rb
348
+ - spec/dependency_resolver_spec.rb
333
349
  - spec/exceptions_spec.rb
334
350
  - spec/external_function_spec.rb
335
351
  - spec/parser_spec.rb