dentaku 3.5.2 → 3.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/README.md +1 -1
  4. data/dentaku.gemspec +1 -0
  5. data/lib/dentaku/ast/access.rb +0 -3
  6. data/lib/dentaku/ast/arithmetic.rb +25 -35
  7. data/lib/dentaku/ast/array.rb +1 -4
  8. data/lib/dentaku/ast/functions/all.rb +1 -5
  9. data/lib/dentaku/ast/functions/any.rb +1 -5
  10. data/lib/dentaku/ast/functions/enum.rb +13 -0
  11. data/lib/dentaku/ast/functions/if.rb +3 -7
  12. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  13. data/lib/dentaku/ast/functions/map.rb +1 -5
  14. data/lib/dentaku/ast/functions/pluck.rb +6 -2
  15. data/lib/dentaku/ast/functions/reduce.rb +61 -0
  16. data/lib/dentaku/ast/node.rb +2 -1
  17. data/lib/dentaku/ast.rb +3 -1
  18. data/lib/dentaku/bulk_expression_solver.rb +37 -7
  19. data/lib/dentaku/calculator.rb +23 -7
  20. data/lib/dentaku/date_arithmetic.rb +26 -15
  21. data/lib/dentaku/dependency_resolver.rb +9 -4
  22. data/lib/dentaku/exceptions.rb +8 -3
  23. data/lib/dentaku/parser.rb +8 -5
  24. data/lib/dentaku/token.rb +12 -0
  25. data/lib/dentaku/token_scanner.rb +7 -2
  26. data/lib/dentaku/version.rb +1 -1
  27. data/spec/ast/addition_spec.rb +4 -4
  28. data/spec/ast/all_spec.rb +13 -0
  29. data/spec/ast/any_spec.rb +13 -0
  30. data/spec/ast/arithmetic_spec.rb +43 -2
  31. data/spec/ast/division_spec.rb +25 -0
  32. data/spec/ast/intercept_spec.rb +30 -0
  33. data/spec/ast/map_spec.rb +13 -0
  34. data/spec/ast/pluck_spec.rb +17 -0
  35. data/spec/ast/reduce_spec.rb +22 -0
  36. data/spec/bulk_expression_solver_spec.rb +33 -0
  37. data/spec/calculator_spec.rb +66 -7
  38. data/spec/dependency_resolver_spec.rb +18 -0
  39. data/spec/parser_spec.rb +3 -0
  40. data/spec/tokenizer_spec.rb +6 -4
  41. data/spec/visitor_spec.rb +2 -1
  42. metadata +25 -3
@@ -67,7 +67,9 @@ module Dentaku
67
67
  private_class_method :new
68
68
 
69
69
  VALID_REASONS = %i[
70
- parse_error too_many_opening_parentheses too_many_closing_parentheses
70
+ parse_error
71
+ too_many_closing_parentheses
72
+ too_many_opening_parentheses
71
73
  unexpected_zero_width_match
72
74
  ].freeze
73
75
 
@@ -92,8 +94,11 @@ module Dentaku
92
94
  private_class_method :new
93
95
 
94
96
  VALID_REASONS = %i[
95
- invalid_operator invalid_value too_few_arguments
96
- too_much_arguments incompatible_type
97
+ incompatible_type
98
+ invalid_operator
99
+ invalid_value
100
+ too_few_arguments
101
+ wrong_number_of_arguments
97
102
  ].freeze
98
103
 
99
104
  def self.for(reason, **meta)
@@ -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
@@ -60,10 +58,14 @@ module Dentaku
60
58
  fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
61
59
  end
62
60
 
63
- fail! :invalid_statement if output_size < args_size
64
- args = Array.new(args_size) { output.pop }.reverse
61
+ if operator == AST::Array && output.empty?
62
+ output.push(operator.new())
63
+ else
64
+ fail! :invalid_statement if output_size < args_size
65
+ args = Array.new(args_size) { output.pop }.reverse
65
66
 
66
- output.push operator.new(*args)
67
+ output.push operator.new(*args)
68
+ end
67
69
 
68
70
  if operator.respond_to?(:callback) && !operator.callback.nil?
69
71
  operator.callback.call(args)
@@ -96,6 +98,7 @@ module Dentaku
96
98
 
97
99
  when :operator, :comparator, :combinator
98
100
  op_class = operation(token)
101
+ op_class = op_class.resolve_class(input.first)
99
102
 
100
103
  if op_class.right_associative?
101
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
@@ -7,7 +7,7 @@ module Dentaku
7
7
  class TokenScanner
8
8
  extend StringCasing
9
9
 
10
- DATE_TIME_REGEXP = /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?(?!\d)/.freeze
10
+ DATE_TIME_REGEXP = /\d{2}\d{2}?-\d{1,2}-\d{1,2}([ |T]\d{1,2}:\d{1,2}:\d{1,2}(\.\d*)?)? ?(Z|((\+|\-)\d{2}\:?\d{2}))?(?!\d)/.freeze
11
11
 
12
12
  def initialize(category, regexp, converter = nil, condition = nil)
13
13
  @category = category
@@ -51,7 +51,8 @@ module Dentaku
51
51
  :comparator,
52
52
  :boolean,
53
53
  :function,
54
- :identifier
54
+ :identifier,
55
+ :quoted_identifier
55
56
  ]
56
57
  end
57
58
 
@@ -180,6 +181,10 @@ module Dentaku
180
181
  def identifier
181
182
  new(:identifier, '[[[:word:]]\.]+\b', lambda { |raw| standardize_case(raw.strip) })
182
183
  end
184
+
185
+ def quoted_identifier
186
+ new(:identifier, '`[^`]*`', lambda { |raw| raw.gsub(/^`|`$/, '') })
187
+ end
183
188
  end
184
189
 
185
190
  register_default_scanners
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.2"
2
+ VERSION = "3.5.4"
3
3
  end
@@ -36,10 +36,10 @@ describe Dentaku::AST::Addition do
36
36
  it 'allows operands that respond to addition' do
37
37
  # Sample struct that has a custom definition for addition
38
38
 
39
- Operand = Struct.new(:value) do
39
+ Addable = Struct.new(:value) do
40
40
  def +(other)
41
41
  case other
42
- when Operand
42
+ when Addable
43
43
  value + other.value
44
44
  when Numeric
45
45
  value + other
@@ -47,8 +47,8 @@ describe Dentaku::AST::Addition do
47
47
  end
48
48
  end
49
49
 
50
- operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(5))
51
- operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(6))
50
+ operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(5))
51
+ operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(6))
52
52
 
53
53
  expect {
54
54
  described_class.new(operand_five, operand_six)
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
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'dentaku/ast/arithmetic'
3
-
4
- require 'dentaku/token'
3
+ require 'dentaku'
5
4
 
6
5
  describe Dentaku::AST::Arithmetic do
7
6
  let(:one) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 1)) }
@@ -64,10 +63,52 @@ describe Dentaku::AST::Arithmetic do
64
63
 
65
64
  it 'performs arithmetic on arrays' do
66
65
  expect(add(x, y, 'x' => [1], 'y' => [2])).to eq([1, 2])
66
+ expect(sub(x, y, 'x' => [1], 'y' => [2])).to eq([1])
67
67
  end
68
68
 
69
69
  it 'performs date arithmetic' do
70
70
  expect(add(date, one)).to eq(DateTime.new(2020, 4, 17))
71
+ expect(sub(date, one)).to eq(DateTime.new(2020, 4, 15))
72
+ end
73
+
74
+ it 'performs arithmetic on object which implements arithmetic' do
75
+ CanHazMath = Struct.new(:value) do
76
+ extend Forwardable
77
+
78
+ def_delegators :value, :zero?
79
+
80
+ def coerce(other)
81
+ case other
82
+ when Numeric
83
+ [other, value]
84
+ else
85
+ super
86
+ end
87
+ end
88
+
89
+ [:+, :-, :/, :*].each do |operand|
90
+ define_method(operand) do |other|
91
+ case other
92
+ when CanHazMath
93
+ value.public_send(operand, other.value)
94
+ when Numeric
95
+ value.public_send(operand, other)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ op_one = CanHazMath.new(1)
102
+ op_two = CanHazMath.new(2)
103
+
104
+ [op_two, two].each do |left|
105
+ [op_one, one].each do |right|
106
+ expect(add(x, y, 'x' => left, 'y' => right)).to eq(3)
107
+ expect(sub(x, y, 'x' => left, 'y' => right)).to eq(1)
108
+ expect(mul(x, y, 'x' => left, 'y' => right)).to eq(2)
109
+ expect(div(x, y, 'x' => left, 'y' => right)).to eq(2)
110
+ end
111
+ end
71
112
  end
72
113
 
73
114
  it 'raises ArgumentError if given individually valid but incompatible arguments' do
@@ -32,4 +32,29 @@ describe Dentaku::AST::Division do
32
32
  described_class.new(group, five)
33
33
  }.not_to raise_error
34
34
  end
35
+
36
+ it 'allows operands that respond to division' do
37
+ # Sample struct that has a custom definition for division
38
+ Divisible = Struct.new(:value) do
39
+ def /(other)
40
+ case other
41
+ when Divisible
42
+ value + other.value
43
+ when Numeric
44
+ value + other
45
+ end
46
+ end
47
+ end
48
+
49
+ operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(5))
50
+ operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(6))
51
+
52
+ expect {
53
+ described_class.new(operand_five, operand_six)
54
+ }.not_to raise_error
55
+
56
+ expect {
57
+ described_class.new(operand_five, six)
58
+ }.not_to raise_error
59
+ end
35
60
  end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/intercept'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Intercept' do
6
+ it 'returns the correct intercept for given x and y arrays' do
7
+ x_values = [6, 13, 15, 10, 11, 10]
8
+ y_values = [-1, 8, 8, 13, 3, 15]
9
+ result = Dentaku('INTERCEPT(ys, xs)', xs: x_values, ys: y_values)
10
+ expect(result).to be_within(0.001).of(9.437)
11
+ end
12
+
13
+ context 'checking errors' do
14
+ it 'raises an error if arguments are not arrays' do
15
+ expect { Dentaku!("INTERCEPT(1, 2)") }.to raise_error(Dentaku::ArgumentError)
16
+ end
17
+
18
+ it 'raises an error if the arrays are not of equal length' do
19
+ x_values = [1, 2, 3]
20
+ y_values = [2, 3, 5, 4]
21
+ expect { Dentaku!("INTERCEPT(y, x)", x: x_values, y: y_values) }.to raise_error(Dentaku::ArgumentError)
22
+ end
23
+
24
+ it 'raises an error if any of the arrays is empty' do
25
+ x_values = []
26
+ y_values = [2, 3, 5, 4]
27
+ expect { Dentaku!("INTERCEPT(y, x)", x: x_values, y: y_values) }.to raise_error(Dentaku::ArgumentError)
28
+ end
29
+ end
30
+ 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([])
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/reduce'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::Reduce do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+
8
+ it 'performs REDUCE operation with initial value' do
9
+ result = Dentaku('REDUCE(vals, memo, val, CONCAT(memo, val), "hello")', vals: ["wo", "rl", "d"])
10
+ expect(result).to eq("helloworld")
11
+ end
12
+
13
+ it 'performs REDUCE operation without initial value' do
14
+ result = Dentaku('REDUCE(vals, memo, val, CONCAT(memo, val))', vals: ["wo", "rl", "d"])
15
+ expect(result).to eq("world")
16
+ end
17
+
18
+ it 'raises argument error if a string is passed as identifier' do
19
+ expect { calculator.evaluate!('REDUCE({1, 2, 3}, memo, "val", memo + val)') }.to raise_error(Dentaku::ParseError)
20
+ expect { calculator.evaluate!('REDUCE({1, 2, 3}, "memo", val, memo + val)') }.to raise_error(Dentaku::ParseError)
21
+ end
22
+ end
@@ -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)
@@ -125,6 +141,12 @@ RSpec.describe Dentaku::BulkExpressionSolver do
125
141
  .to eq(more_apples: :foo)
126
142
  end
127
143
 
144
+ it "allows passing in ast as expression" do
145
+ expressions = {more_apples: calculator.ast("1/0")}
146
+ expect(described_class.new(expressions, calculator).solve { :foo })
147
+ .to eq(more_apples: :foo)
148
+ end
149
+
128
150
  it 'stores the recipient variable on the exception when there is a div/0 error' do
129
151
  expressions = {more_apples: "1/0"}
130
152
  exception = nil
@@ -197,5 +219,16 @@ RSpec.describe Dentaku::BulkExpressionSolver do
197
219
  expect(results).to eq('key' => [3, :undefined])
198
220
  expect { solver.solve! }.to raise_error(Dentaku::UnboundVariableError)
199
221
  end
222
+
223
+ it do
224
+ calculator.store(val: nil)
225
+ expressions = {
226
+ a: 'IF(5 / 0 > 0, 100, 1000)',
227
+ b: 'IF(val = 0, 0, IF(val > 0, 0, 0))'
228
+ }
229
+ solver = described_class.new(expressions, calculator)
230
+ results = solver.solve
231
+ expect(results).to eq(a: :undefined, b: :undefined)
232
+ end
200
233
  end
201
234
  end
@@ -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
@@ -159,6 +167,12 @@ describe Dentaku::Calculator do
159
167
  expect(calculator.evaluate!('area', length: 5, width: 5)).to eq(25)
160
168
  end
161
169
 
170
+ it 'stores dates' do
171
+ calculator.store("d1", Date.parse("2024/01/02"))
172
+ calculator.store("d2", Date.parse("2024/01/06"))
173
+ expect(calculator.solve(diff: "d1 - d2")).to eq(diff: -4)
174
+ end
175
+
162
176
  it 'stores nested hashes' do
163
177
  calculator.store(a: {basket: {of: 'apples'}}, b: 2)
164
178
  expect(calculator.evaluate!('a.basket.of')).to eq('apples')
@@ -166,6 +180,13 @@ describe Dentaku::Calculator do
166
180
  expect(calculator.evaluate!('b')).to eq(2)
167
181
  end
168
182
 
183
+ it 'stores nested hashes with quotes' do
184
+ calculator.store(a: {basket: {of: 'apples'}}, b: 2)
185
+ expect(calculator.evaluate!('`a.basket.of`')).to eq('apples')
186
+ expect(calculator.evaluate!('`a.basket`')).to eq(of: 'apples')
187
+ expect(calculator.evaluate!('`b`')).to eq(2)
188
+ end
189
+
169
190
  it 'stores arrays' do
170
191
  calculator.store(a: [1, 2, 3])
171
192
  expect(calculator.evaluate!('a[0]')).to eq(1)
@@ -180,6 +201,10 @@ describe Dentaku::Calculator do
180
201
  end
181
202
 
182
203
  describe 'dependencies' do
204
+ it 'respects quoted identifiers in dependencies' do
205
+ expect(calculator.dependencies("`bob the builder` + `dole the digger` / 3")).to eq(['bob the builder', 'dole the digger'])
206
+ end
207
+
183
208
  it "finds dependencies in a generic statement" do
184
209
  expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
185
210
  end
@@ -188,6 +213,10 @@ describe Dentaku::Calculator do
188
213
  expect(calculator.dependencies("a + b", a: 1)).to eq(['b'])
189
214
  end
190
215
 
216
+ it "ignores dependencies passed in context for quoted identifiers" do
217
+ expect(calculator.dependencies("`a-c` + b", "a-c": 1)).to eq(['b'])
218
+ end
219
+
191
220
  it "finds dependencies in formula arguments" do
192
221
  allow(Dentaku).to receive(:cache_ast?) { true }
193
222
 
@@ -211,10 +240,11 @@ describe Dentaku::Calculator do
211
240
  describe 'solve!' do
212
241
  it "evaluates properly with variables, even if some in memory" do
213
242
  expect(with_memory.solve!(
243
+ "monthly fruit budget": "weekly_fruit_budget * 4",
214
244
  weekly_fruit_budget: "weekly_apple_budget + pear * 4",
215
245
  weekly_apple_budget: "apples * 7",
216
246
  pear: "1"
217
- )).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
247
+ )).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25, "monthly fruit budget": 100)
218
248
  end
219
249
 
220
250
  it "prefers variables over values in memory if they have no dependencies" do
@@ -412,6 +442,13 @@ describe Dentaku::Calculator do
412
442
  expect(calculator.evaluate('fo1o * 2', fo1o: 4)).to eq(8)
413
443
  end
414
444
 
445
+ it 'accepts special characters in quoted identifiers' do
446
+ expect(calculator.evaluate('`foo1 bar` * 2', "foo1 bar": 2)).to eq(4)
447
+ expect(calculator.evaluate('`foo1-bar` * 2', 'foo1-bar' => 4)).to eq(8)
448
+ expect(calculator.evaluate('`1foo (bar)` * 2', '1foo (bar)' => 2)).to eq(4)
449
+ expect(calculator.evaluate('`fo1o *bar*` * 2', 'fo1o *bar*': 4)).to eq(8)
450
+ end
451
+
415
452
  it 'compares string literals with string variables' do
416
453
  expect(calculator.evaluate('fruit = "apple"', fruit: 'apple')).to be_truthy
417
454
  expect(calculator.evaluate('fruit = "apple"', fruit: 'pear')).to be_falsey
@@ -471,16 +508,31 @@ describe Dentaku::Calculator do
471
508
 
472
509
  it 'from string variable' do
473
510
  value = '2023-01-01'
511
+ value2 = '2022-12-31'
474
512
 
475
- expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eql(Date.parse('2023-02-01'))
476
- expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eql(Date.parse('2022-12-01'))
513
+ expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eq(Date.parse('2023-02-01'))
514
+ expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
515
+ expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
477
516
  end
478
517
 
479
518
  it 'from date object' do
480
519
  value = Date.parse('2023-01-01').to_date
520
+ value2 = Date.parse('2022-12-31').to_date
481
521
 
482
- expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eql(Date.parse('2023-02-01'))
483
- expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eql(Date.parse('2022-12-01'))
522
+ expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eq(Date.parse('2023-02-01'))
523
+ expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
524
+ expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
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))
484
536
  end
485
537
  end
486
538
 
@@ -522,6 +574,13 @@ describe Dentaku::Calculator do
522
574
  expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
523
575
  end
524
576
 
577
+ it 'calculates intercept correctly' do
578
+ x_values = [1, 2, 3, 4, 5]
579
+ y_values = [2, 3, 5, 4, 6]
580
+ result = calculator.evaluate('INTERCEPT(x_values, y_values)', x_values: x_values, y_values: y_values)
581
+ expect(result).to be_within(0.001).of(1.3)
582
+ end
583
+
525
584
  describe "any" do
526
585
  it "enumerates values and returns true if any evaluation is truthy" do
527
586
  expect(calculator.evaluate!('any(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_truthy
@@ -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/parser_spec.rb CHANGED
@@ -79,6 +79,9 @@ describe Dentaku::Parser do
79
79
  it 'evaluates arrays' do
80
80
  node = parse('{1, 2, 3}')
81
81
  expect(node.value).to eq([1, 2, 3])
82
+
83
+ node = parse('{}')
84
+ expect(node.value).to eq([])
82
85
  end
83
86
 
84
87
  context 'invalid expression' do
@@ -1,3 +1,4 @@
1
+ require 'dentaku/exceptions'
1
2
  require 'dentaku/tokenizer'
2
3
 
3
4
  describe Dentaku::Tokenizer do
@@ -234,9 +235,9 @@ describe Dentaku::Tokenizer do
234
235
  end
235
236
 
236
237
  it 'tokenizes Time literals' do
237
- tokens = tokenizer.tokenize('2017-01-01 2017-01-2 2017-1-03 2017-01-04 12:23:42 2017-1-5 1:2:3 2017-1-06 1:02:30 2017-01-07 12:34:56 Z 2017-01-08 1:2:3 +0800')
238
- expect(tokens.length).to eq(8)
239
- expect(tokens.map(&:category)).to eq([:datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime])
238
+ tokens = tokenizer.tokenize('2017-01-01 2017-01-2 2017-1-03 2017-01-04 12:23:42 2017-1-5 1:2:3 2017-1-06 1:02:30 2017-01-07 12:34:56 Z 2017-01-08 1:2:3 +0800 2017-01-08T01:02:03.456Z')
239
+ expect(tokens.length).to eq(9)
240
+ expect(tokens.map(&:category)).to eq([:datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime])
240
241
  expect(tokens.map(&:value)).to eq([
241
242
  Time.local(2017, 1, 1).to_datetime,
242
243
  Time.local(2017, 1, 2).to_datetime,
@@ -245,7 +246,8 @@ describe Dentaku::Tokenizer do
245
246
  Time.local(2017, 1, 5, 1, 2, 3).to_datetime,
246
247
  Time.local(2017, 1, 6, 1, 2, 30).to_datetime,
247
248
  Time.utc(2017, 1, 7, 12, 34, 56).to_datetime,
248
- Time.new(2017, 1, 8, 1, 2, 3, "+08:00").to_datetime
249
+ Time.new(2017, 1, 8, 1, 2, 3, "+08:00").to_datetime,
250
+ Time.utc(2017, 1, 8, 1, 2, 3, 456000).to_datetime
249
251
  ])
250
252
  end
251
253