dentaku 3.5.2 → 3.5.3

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: 0b10f7d9e6a9d200c283dcf077fd56e8f4abe4922c02e3095ba20dbb29f6b81c
4
- data.tar.gz: add2d3bf7c462edefb9a4c52d79595d3a161e1d78046dbb1fe90e8aa9979a13b
3
+ metadata.gz: a51418767c413ccded1f235a56d34866005edf21cbc0d4cf5848d4cff4bc801f
4
+ data.tar.gz: 2009c09a76a5cc0b85cf04769c93f0d372cc8613f410a18401de53ca268b134d
5
5
  SHA512:
6
- metadata.gz: 48c2571ea61f8bb9f8a7a4483b8d588741f2c44c4fdc2d04ecd2a5c5b75a94f414b5772a3e1dca21898f8d2ed6488e54925c6a689bfd721315c0c8e0992991d7
7
- data.tar.gz: b469e9c4a69c6083b93cda29ea8bf5bf4ad9c91e4a6362e5880768492e21ecd476a09eb858134b8e6c78017207c6cf50479c7ebb3af661c7e20f3866f034b49a
6
+ metadata.gz: 6cfeabc676fa63016096d2f3c291d60a68ce247fe7c75ba195cdc2968c28af85de9ab9eeaaa9e88749cbf7920a120b1e6fe4d78464b98f9c825180d42aafc4f1
7
+ data.tar.gz: 42863ad11afff2dc72dfe906d1fdc1c07821f4391dcb7fc67921d862c736350be15a6671beac9c16de9e1345f17869e0de40bb131bc1fcfea70190538274bd53
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Change Log
2
2
 
3
+ ## [Unreleased]
4
+ - add support for empty array literals
5
+ - add support for quoted identifiers
6
+ - add REDUCE function
7
+ - add INTERCEPT function
8
+ - improve date/time parsing an arithmetic
9
+ - improve custom class arithmetic
10
+ - fix IF dependency
11
+
3
12
  ## [v3.5.2]
4
13
  - add ABS function
5
14
  - add array support for AST visitors
@@ -244,6 +253,7 @@
244
253
  ## [v0.1.0] 2012-01-20
245
254
  - initial release
246
255
 
256
+ [Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.2...HEAD
247
257
  [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
248
258
  [v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
249
259
  [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
data/README.md CHANGED
@@ -145,7 +145,7 @@ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
145
145
 
146
146
  Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
147
147
 
148
- Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`
148
+ Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
149
149
 
150
150
  Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))
151
151
 
@@ -62,6 +62,8 @@ module Dentaku
62
62
 
63
63
  def decimal(val)
64
64
  BigDecimal(val.to_s, Float::DIG + 1)
65
+ rescue # return as is, in case value can't be coerced to big decimal
66
+ val
65
67
  end
66
68
 
67
69
  def datetime?(val)
@@ -36,13 +36,9 @@ module Dentaku
36
36
  end
37
37
 
38
38
  def dependencies(context = {})
39
- deps = predicate.dependencies(context)
40
-
41
- if deps.empty?
42
- predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
43
- else
44
- (deps + left.dependencies(context) + right.dependencies(context)).uniq
45
- end
39
+ predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
40
+ rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError
41
+ args.flat_map { |arg| arg.dependencies(context) }.uniq
46
42
  end
47
43
  end
48
44
  end
@@ -0,0 +1,33 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
4
+ if args.length != 2
5
+ raise Dentaku::ArgumentError.for(
6
+ :wrong_number_of_arguments,
7
+ function_name: 'INTERCEPT()', exact: 2, given: args.length
8
+ ), 'INTERCEPT() requires exactly two arrays of numbers'
9
+ end
10
+
11
+ x_values, y_values = args
12
+ if !x_values.is_a?(Array) || !y_values.is_a?(Array) || x_values.length != y_values.length
13
+ raise Dentaku::ArgumentError.for(
14
+ :invalid_value,
15
+ function_name: 'INTERCEPT()'
16
+ ), 'INTERCEPT() requires arrays of equal length'
17
+ end
18
+
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) }
22
+
23
+ x_avg = x_values.sum / n
24
+ y_avg = y_values.sum / n
25
+
26
+ xy_sum = x_values.zip(y_values).map { |x, y| (x_avg - x) * (y_avg - y) }.sum
27
+ x_square_sum = x_values.map { |x| (x_avg - x)**2 }.sum
28
+
29
+ slope = xy_sum / x_square_sum
30
+ intercept = x_values.zip(y_values).map { |x, y| y - slope * x }.sum / n
31
+
32
+ BigDecimal(intercept, Float::DIG + 1)
33
+ })
@@ -0,0 +1,61 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Reduce < Function
7
+ def self.min_param_count
8
+ 4
9
+ end
10
+
11
+ def self.max_param_count
12
+ 5
13
+ end
14
+
15
+ def initialize(*args)
16
+ super
17
+
18
+ validate_identifier(@args[1], 'second')
19
+ validate_identifier(@args[2], 'third')
20
+ end
21
+
22
+ def dependencies(context = {})
23
+ collection = @args[0]
24
+ memo_identifier = @args[1].identifier
25
+ item_identifier = @args[2].identifier
26
+ expression = @args[3]
27
+
28
+ collection_deps = collection.dependencies(context)
29
+ expression_deps = expression.dependencies(context).reject do |i|
30
+ i == memo_identifier || i.start_with?("#{memo_identifier}.") ||
31
+ i == item_identifier || i.start_with?("#{item_identifier}.")
32
+ end
33
+ inital_value_deps = @args[4] ? @args[4].dependencies(context) : []
34
+
35
+ collection_deps + expression_deps + inital_value_deps
36
+ end
37
+
38
+ def value(context = {})
39
+ collection = Array(@args[0].value(context))
40
+ memo_identifier = @args[1].identifier
41
+ item_identifier = @args[2].identifier
42
+ expression = @args[3]
43
+ initial_value = @args[4] && @args[4].value(context)
44
+
45
+ collection.reduce(initial_value) do |memo, item|
46
+ expression.value(
47
+ context.merge(
48
+ FlatHash.from_hash_with_intermediates(memo_identifier => memo, item_identifier => item)
49
+ )
50
+ )
51
+ end
52
+ end
53
+
54
+ def validate_identifier(arg, position, message = "#{name}() requires #{position} argument to be an identifier")
55
+ raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ Dentaku::AST::Function.register_class(:reduce, Dentaku::AST::Reduce)
data/lib/dentaku/ast.rb CHANGED
@@ -24,12 +24,14 @@ require_relative './ast/functions/count'
24
24
  require_relative './ast/functions/duration'
25
25
  require_relative './ast/functions/filter'
26
26
  require_relative './ast/functions/if'
27
+ require_relative './ast/functions/intercept'
27
28
  require_relative './ast/functions/map'
28
29
  require_relative './ast/functions/max'
29
30
  require_relative './ast/functions/min'
30
31
  require_relative './ast/functions/not'
31
32
  require_relative './ast/functions/or'
32
33
  require_relative './ast/functions/pluck'
34
+ require_relative './ast/functions/reduce'
33
35
  require_relative './ast/functions/round'
34
36
  require_relative './ast/functions/rounddown'
35
37
  require_relative './ast/functions/roundup'
@@ -37,4 +39,4 @@ require_relative './ast/functions/ruby_math'
37
39
  require_relative './ast/functions/string_functions'
38
40
  require_relative './ast/functions/sum'
39
41
  require_relative './ast/functions/switch'
40
- require_relative './ast/functions/xor'
42
+ require_relative './ast/functions/xor'
@@ -63,8 +63,7 @@ module Dentaku
63
63
  } if expression.is_a? Array
64
64
 
65
65
  store(data) do
66
- node = expression
67
- node = ast(node) unless node.is_a?(AST::Node)
66
+ node = ast(expression)
68
67
  unbound = node.dependencies(memory)
69
68
  unless unbound.empty?
70
69
  raise UnboundVariableError.new(unbound),
@@ -96,6 +95,7 @@ module Dentaku
96
95
  end
97
96
 
98
97
  def ast(expression)
98
+ return expression if expression.is_a?(AST::Node)
99
99
  return expression.map { |e| ast(e) } if expression.is_a? Array
100
100
 
101
101
  @ast_cache.fetch(expression) {
@@ -29,7 +29,7 @@ module Dentaku
29
29
 
30
30
  def sub(duration)
31
31
  case duration
32
- when DateTime, Numeric
32
+ when Date, DateTime, Numeric
33
33
  @base - duration
34
34
  when Dentaku::AST::Duration::Value
35
35
  case duration.unit
@@ -40,6 +40,8 @@ module Dentaku
40
40
  when :day
41
41
  @base - duration.value
42
42
  end
43
+ when Dentaku::TokenScanner::DATE_TIME_REGEXP
44
+ @base - Time.parse(duration).to_datetime
43
45
  else
44
46
  raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
45
47
  "'#{duration || duration.class}' is not coercible for date arithmetic"
@@ -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)
@@ -60,10 +60,14 @@ module Dentaku
60
60
  fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
61
61
  end
62
62
 
63
- fail! :invalid_statement if output_size < args_size
64
- args = Array.new(args_size) { output.pop }.reverse
63
+ if operator == AST::Array && output.empty?
64
+ output.push(operator.new())
65
+ else
66
+ fail! :invalid_statement if output_size < args_size
67
+ args = Array.new(args_size) { output.pop }.reverse
65
68
 
66
- output.push operator.new(*args)
69
+ output.push operator.new(*args)
70
+ end
67
71
 
68
72
  if operator.respond_to?(:callback) && !operator.callback.nil?
69
73
  operator.callback.call(args)
@@ -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.3"
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)
@@ -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
@@ -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
@@ -125,6 +125,12 @@ RSpec.describe Dentaku::BulkExpressionSolver do
125
125
  .to eq(more_apples: :foo)
126
126
  end
127
127
 
128
+ it "allows passing in ast as expression" do
129
+ expressions = {more_apples: calculator.ast("1/0")}
130
+ expect(described_class.new(expressions, calculator).solve { :foo })
131
+ .to eq(more_apples: :foo)
132
+ end
133
+
128
134
  it 'stores the recipient variable on the exception when there is a div/0 error' do
129
135
  expressions = {more_apples: "1/0"}
130
136
  exception = nil
@@ -197,5 +203,16 @@ RSpec.describe Dentaku::BulkExpressionSolver do
197
203
  expect(results).to eq('key' => [3, :undefined])
198
204
  expect { solver.solve! }.to raise_error(Dentaku::UnboundVariableError)
199
205
  end
206
+
207
+ it do
208
+ calculator.store(val: nil)
209
+ expressions = {
210
+ a: 'IF(5 / 0 > 0, 100, 1000)',
211
+ b: 'IF(val = 0, 0, IF(val > 0, 0, 0))'
212
+ }
213
+ solver = described_class.new(expressions, calculator)
214
+ results = solver.solve
215
+ expect(results).to eq(a: :undefined, b: :undefined)
216
+ end
200
217
  end
201
218
  end
@@ -159,6 +159,13 @@ describe Dentaku::Calculator do
159
159
  expect(calculator.evaluate!('area', length: 5, width: 5)).to eq(25)
160
160
  end
161
161
 
162
+ it 'stores dates' do
163
+ calculator.store("d1", Date.parse("2024/01/02"))
164
+ calculator.store("d2", Date.parse("2024/01/06"))
165
+ expect(calculator.solve(diff: "d1 - d2")).to eq(diff: -4)
166
+ end
167
+
168
+
162
169
  it 'stores nested hashes' do
163
170
  calculator.store(a: {basket: {of: 'apples'}}, b: 2)
164
171
  expect(calculator.evaluate!('a.basket.of')).to eq('apples')
@@ -166,6 +173,13 @@ describe Dentaku::Calculator do
166
173
  expect(calculator.evaluate!('b')).to eq(2)
167
174
  end
168
175
 
176
+ it 'stores nested hashes with quotes' do
177
+ calculator.store(a: {basket: {of: 'apples'}}, b: 2)
178
+ expect(calculator.evaluate!('`a.basket.of`')).to eq('apples')
179
+ expect(calculator.evaluate!('`a.basket`')).to eq(of: 'apples')
180
+ expect(calculator.evaluate!('`b`')).to eq(2)
181
+ end
182
+
169
183
  it 'stores arrays' do
170
184
  calculator.store(a: [1, 2, 3])
171
185
  expect(calculator.evaluate!('a[0]')).to eq(1)
@@ -180,6 +194,10 @@ describe Dentaku::Calculator do
180
194
  end
181
195
 
182
196
  describe 'dependencies' do
197
+ it 'respects quoted identifiers in dependencies' do
198
+ expect(calculator.dependencies("`bob the builder` + `dole the digger` / 3")).to eq(['bob the builder', 'dole the digger'])
199
+ end
200
+
183
201
  it "finds dependencies in a generic statement" do
184
202
  expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
185
203
  end
@@ -188,6 +206,10 @@ describe Dentaku::Calculator do
188
206
  expect(calculator.dependencies("a + b", a: 1)).to eq(['b'])
189
207
  end
190
208
 
209
+ it "ignores dependencies passed in context for quoted identifiers" do
210
+ expect(calculator.dependencies("`a-c` + b", "a-c": 1)).to eq(['b'])
211
+ end
212
+
191
213
  it "finds dependencies in formula arguments" do
192
214
  allow(Dentaku).to receive(:cache_ast?) { true }
193
215
 
@@ -211,10 +233,11 @@ describe Dentaku::Calculator do
211
233
  describe 'solve!' do
212
234
  it "evaluates properly with variables, even if some in memory" do
213
235
  expect(with_memory.solve!(
236
+ "monthly fruit budget": "weekly_fruit_budget * 4",
214
237
  weekly_fruit_budget: "weekly_apple_budget + pear * 4",
215
238
  weekly_apple_budget: "apples * 7",
216
239
  pear: "1"
217
- )).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
240
+ )).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25, "monthly fruit budget": 100)
218
241
  end
219
242
 
220
243
  it "prefers variables over values in memory if they have no dependencies" do
@@ -412,6 +435,13 @@ describe Dentaku::Calculator do
412
435
  expect(calculator.evaluate('fo1o * 2', fo1o: 4)).to eq(8)
413
436
  end
414
437
 
438
+ it 'accepts special characters in quoted identifiers' do
439
+ expect(calculator.evaluate('`foo1 bar` * 2', "foo1 bar": 2)).to eq(4)
440
+ expect(calculator.evaluate('`foo1-bar` * 2', 'foo1-bar' => 4)).to eq(8)
441
+ expect(calculator.evaluate('`1foo (bar)` * 2', '1foo (bar)' => 2)).to eq(4)
442
+ expect(calculator.evaluate('`fo1o *bar*` * 2', 'fo1o *bar*': 4)).to eq(8)
443
+ end
444
+
415
445
  it 'compares string literals with string variables' do
416
446
  expect(calculator.evaluate('fruit = "apple"', fruit: 'apple')).to be_truthy
417
447
  expect(calculator.evaluate('fruit = "apple"', fruit: 'pear')).to be_falsey
@@ -471,16 +501,20 @@ describe Dentaku::Calculator do
471
501
 
472
502
  it 'from string variable' do
473
503
  value = '2023-01-01'
504
+ value2 = '2022-12-31'
474
505
 
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'))
506
+ expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eq(Date.parse('2023-02-01'))
507
+ expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
508
+ expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
477
509
  end
478
510
 
479
511
  it 'from date object' do
480
512
  value = Date.parse('2023-01-01').to_date
513
+ value2 = Date.parse('2022-12-31').to_date
481
514
 
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'))
515
+ expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eq(Date.parse('2023-02-01'))
516
+ expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
517
+ expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
484
518
  end
485
519
  end
486
520
 
@@ -522,6 +556,13 @@ describe Dentaku::Calculator do
522
556
  expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
523
557
  end
524
558
 
559
+ it 'calculates intercept correctly' do
560
+ x_values = [1, 2, 3, 4, 5]
561
+ y_values = [2, 3, 5, 4, 6]
562
+ result = calculator.evaluate('INTERCEPT(x_values, y_values)', x_values: x_values, y_values: y_values)
563
+ expect(result).to be_within(0.001).of(1.3)
564
+ end
565
+
525
566
  describe "any" do
526
567
  it "enumerates values and returns true if any evaluation is truthy" do
527
568
  expect(calculator.evaluate!('any(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_truthy
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
 
data/spec/visitor_spec.rb CHANGED
@@ -122,6 +122,7 @@ describe TestVisitor do
122
122
  visit_nodes('duration(1, day)')
123
123
  visit_nodes('MAP(vals, val, val + 1)')
124
124
  visit_nodes('FILTER(vals, val, val > 1)')
125
+ visit_nodes('REDUCE(vals, memo, val, memo + val)')
125
126
 
126
127
  @expected = Set.new(Dentaku::AST::constants - generic_subclasses)
127
128
  expect(@visited.sort).to eq(@expected.sort)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dentaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.2
4
+ version: 3.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-07 00:00:00.000000000 Z
11
+ date: 2024-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -180,6 +180,7 @@ files:
180
180
  - lib/dentaku/ast/functions/enum.rb
181
181
  - lib/dentaku/ast/functions/filter.rb
182
182
  - lib/dentaku/ast/functions/if.rb
183
+ - lib/dentaku/ast/functions/intercept.rb
183
184
  - lib/dentaku/ast/functions/map.rb
184
185
  - lib/dentaku/ast/functions/max.rb
185
186
  - lib/dentaku/ast/functions/min.rb
@@ -187,6 +188,7 @@ files:
187
188
  - lib/dentaku/ast/functions/not.rb
188
189
  - lib/dentaku/ast/functions/or.rb
189
190
  - lib/dentaku/ast/functions/pluck.rb
191
+ - lib/dentaku/ast/functions/reduce.rb
190
192
  - lib/dentaku/ast/functions/round.rb
191
193
  - lib/dentaku/ast/functions/rounddown.rb
192
194
  - lib/dentaku/ast/functions/roundup.rb
@@ -235,6 +237,7 @@ files:
235
237
  - spec/ast/division_spec.rb
236
238
  - spec/ast/filter_spec.rb
237
239
  - spec/ast/function_spec.rb
240
+ - spec/ast/intercept_spec.rb
238
241
  - spec/ast/map_spec.rb
239
242
  - spec/ast/max_spec.rb
240
243
  - spec/ast/min_spec.rb
@@ -244,6 +247,7 @@ files:
244
247
  - spec/ast/numeric_spec.rb
245
248
  - spec/ast/or_spec.rb
246
249
  - spec/ast/pluck_spec.rb
250
+ - spec/ast/reduce_spec.rb
247
251
  - spec/ast/round_spec.rb
248
252
  - spec/ast/rounddown_spec.rb
249
253
  - spec/ast/roundup_spec.rb
@@ -285,7 +289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
285
289
  - !ruby/object:Gem::Version
286
290
  version: '0'
287
291
  requirements: []
288
- rubygems_version: 3.3.7
292
+ rubygems_version: 3.3.9
289
293
  signing_key:
290
294
  specification_version: 4
291
295
  summary: A formula language parser and evaluator
@@ -304,6 +308,7 @@ test_files:
304
308
  - spec/ast/division_spec.rb
305
309
  - spec/ast/filter_spec.rb
306
310
  - spec/ast/function_spec.rb
311
+ - spec/ast/intercept_spec.rb
307
312
  - spec/ast/map_spec.rb
308
313
  - spec/ast/max_spec.rb
309
314
  - spec/ast/min_spec.rb
@@ -313,6 +318,7 @@ test_files:
313
318
  - spec/ast/numeric_spec.rb
314
319
  - spec/ast/or_spec.rb
315
320
  - spec/ast/pluck_spec.rb
321
+ - spec/ast/reduce_spec.rb
316
322
  - spec/ast/round_spec.rb
317
323
  - spec/ast/rounddown_spec.rb
318
324
  - spec/ast/roundup_spec.rb