dentaku 3.5.3 → 3.5.4

Sign up to get free protection for your applications and to get access to all the features.
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