dentaku 3.5.1 → 3.5.2

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: d5592654ee45adeb24167b584374fb2d69bc67d1db4d5f4ac99050130f187f8f
4
- data.tar.gz: 31d3952b08887ae934661f4c0ecc5c298b2f36f0f8365cc1503495eb044eb558
3
+ metadata.gz: 0b10f7d9e6a9d200c283dcf077fd56e8f4abe4922c02e3095ba20dbb29f6b81c
4
+ data.tar.gz: add2d3bf7c462edefb9a4c52d79595d3a161e1d78046dbb1fe90e8aa9979a13b
5
5
  SHA512:
6
- metadata.gz: d8e0d003f897e06173c91b200e62d9fed12ec3bacfe0f2ecc3f3705a1cbf914d1705948b79962f5ac6a5397138ff89c2123aa5c3753963f0046a88280b29825b
7
- data.tar.gz: 5f48d3b8fef4e56ed308e717da88937bef508e47dfca4c3e16610aff7523f11405e53e2b55d00c53b5eca8efbe7be64df0d0d12e7ddafbc61099cde08bf92a53
6
+ metadata.gz: 48c2571ea61f8bb9f8a7a4483b8d588741f2c44c4fdc2d04ecd2a5c5b75a94f414b5772a3e1dca21898f8d2ed6488e54925c6a689bfd721315c0c8e0992991d7
7
+ data.tar.gz: b469e9c4a69c6083b93cda29ea8bf5bf4ad9c91e4a6362e5880768492e21ecd476a09eb858134b8e6c78017207c6cf50479c7ebb3af661c7e20f3866f034b49a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Change Log
2
2
 
3
+ ## [v3.5.2]
4
+ - add ABS function
5
+ - add array support for AST visitors
6
+ - add support for function callbacks
7
+ - improve support for date / time values
8
+ - improve error messaging for invalid arity
9
+ - improve AVG function accuracy
10
+ - validate enum arguments at parse time
11
+ - support adding multiple functions at once to global registry
12
+ - fix bug in print visitor precedence checking
13
+ - fix handling of Math::DomainError
14
+ - fix invalid cast
15
+
3
16
  ## [v3.5.1]
4
17
  - add bitwise shift left and shift right operators
5
18
  - improve numeric conversions
@@ -231,7 +244,7 @@
231
244
  ## [v0.1.0] 2012-01-20
232
245
  - initial release
233
246
 
234
- [Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.1...HEAD
247
+ [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
235
248
  [v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
236
249
  [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
237
250
  [v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
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`
148
+ Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`
149
149
 
150
150
  Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))
151
151
 
@@ -6,6 +6,9 @@ require 'bigdecimal/util'
6
6
  module Dentaku
7
7
  module AST
8
8
  class Arithmetic < Operation
9
+ DECIMAL = /\A-?\d*\.\d+\z/.freeze
10
+ INTEGER = /\A-?\d+\z/.freeze
11
+
9
12
  def initialize(*)
10
13
  super
11
14
 
@@ -29,8 +32,14 @@ module Dentaku
29
32
  end
30
33
 
31
34
  def value(context = {})
32
- l = cast(left.value(context))
33
- r = cast(right.value(context))
35
+ calculate(left.value(context), right.value(context))
36
+ end
37
+
38
+ private
39
+
40
+ def calculate(left_value, right_value)
41
+ l = cast(left_value)
42
+ r = cast(right_value)
34
43
 
35
44
  l.public_send(operator, r)
36
45
  rescue ::TypeError => e
@@ -38,8 +47,6 @@ module Dentaku
38
47
  raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
39
48
  end
40
49
 
41
- private
42
-
43
50
  def cast(val)
44
51
  validate_value(val)
45
52
  numeric(val)
@@ -47,8 +54,8 @@ module Dentaku
47
54
 
48
55
  def numeric(val)
49
56
  case val.to_s
50
- when /\A\d*\.\d+\z/ then decimal(val)
51
- when /\A-?\d+\z/ then val.to_i
57
+ when DECIMAL then decimal(val)
58
+ when INTEGER then val.to_i
52
59
  else val
53
60
  end
54
61
  end
@@ -57,6 +64,13 @@ module Dentaku
57
64
  BigDecimal(val.to_s, Float::DIG + 1)
58
65
  end
59
66
 
67
+ def datetime?(val)
68
+ # val is a Date, Time, or DateTime
69
+ return true if val.respond_to?(:strftime)
70
+
71
+ val.to_s =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
72
+ end
73
+
60
74
  def valid_node?(node)
61
75
  node && (node.type == :numeric || node.type == :integer || node.dependencies.any?)
62
76
  end
@@ -102,10 +116,13 @@ module Dentaku
102
116
  end
103
117
 
104
118
  def value(context = {})
105
- if left.type == :datetime
106
- Dentaku::DateArithmetic.new(left.value(context)).add(right.value(context))
119
+ left_value = left.value(context)
120
+ right_value = right.value(context)
121
+
122
+ if left.type == :datetime || datetime?(left_value)
123
+ Dentaku::DateArithmetic.new(left_value).add(right_value)
107
124
  else
108
- super
125
+ calculate(left_value, right_value)
109
126
  end
110
127
  end
111
128
  end
@@ -120,10 +137,13 @@ module Dentaku
120
137
  end
121
138
 
122
139
  def value(context = {})
123
- if left.type == :datetime
124
- Dentaku::DateArithmetic.new(left.value(context)).sub(right.value(context))
140
+ left_value = left.value(context)
141
+ right_value = right.value(context)
142
+
143
+ if left.type == :datetime || datetime?(left_value)
144
+ Dentaku::DateArithmetic.new(left_value).sub(right_value)
125
145
  else
126
- super
146
+ calculate(left_value, right_value)
127
147
  end
128
148
  end
129
149
  end
@@ -28,8 +28,7 @@ module Dentaku
28
28
 
29
29
  def cast(val)
30
30
  return val unless val.is_a?(::String)
31
- return val if val.empty?
32
- return val unless val.match?(/\A-?\d*(\.\d+)?\z/)
31
+ return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
33
32
 
34
33
  v = BigDecimal(val, Float::DIG + 1)
35
34
  v = v.to_i if v.frac.zero?
@@ -8,7 +8,7 @@ module Dentaku
8
8
  nil
9
9
  end
10
10
 
11
- def register(name, type, implementation)
11
+ def register(name, type, implementation, callback = nil)
12
12
  function = Class.new(Function) do
13
13
  def self.name=(name)
14
14
  @name = name
@@ -34,6 +34,14 @@ module Dentaku
34
34
  @type
35
35
  end
36
36
 
37
+ def self.callback=(callback)
38
+ @callback = callback
39
+ end
40
+
41
+ def self.callback
42
+ @callback
43
+ end
44
+
37
45
  def self.arity
38
46
  @implementation.arity < 0 ? nil : @implementation.arity
39
47
  end
@@ -61,6 +69,7 @@ module Dentaku
61
69
  function.name = name
62
70
  function.type = type
63
71
  function.implementation = implementation
72
+ function.callback = callback
64
73
 
65
74
  self[function_name(name)] = function
66
75
  end
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:abs, :numeric, lambda { |numeric|
4
+ Dentaku::AST::Function.numeric(numeric).abs
5
+ })
@@ -9,5 +9,5 @@ Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
9
9
  ), 'AVG() requires at least one argument'
10
10
  end
11
11
 
12
- flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / flatten_args.length
12
+ flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
13
13
  })
@@ -12,9 +12,12 @@ module Dentaku
12
12
  3
13
13
  end
14
14
 
15
- def dependencies(context = {})
15
+ def initialize(*args)
16
+ super
16
17
  validate_identifier(@args[1])
18
+ end
17
19
 
20
+ def dependencies(context = {})
18
21
  collection = @args[0]
19
22
  item_identifier = @args[1].identifier
20
23
  expression = @args[2]
@@ -28,9 +31,7 @@ module Dentaku
28
31
  end
29
32
 
30
33
  def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
31
- unless arg.is_a?(Identifier)
32
- raise ArgumentError.for(:incompatible_type, value: arg, for: Identifier), message
33
- end
34
+ raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
34
35
  end
35
36
  end
36
37
  end
@@ -34,6 +34,8 @@ module Dentaku
34
34
 
35
35
  def self.call(*args)
36
36
  @implementation.call(*args)
37
+ rescue Math::DomainError => _e
38
+ raise Dentaku::MathDomainError.new(name, args)
37
39
  end
38
40
 
39
41
  def value(context = {})
data/lib/dentaku/ast.rb CHANGED
@@ -15,6 +15,7 @@ require_relative './ast/array'
15
15
  require_relative './ast/grouping'
16
16
  require_relative './ast/case'
17
17
  require_relative './ast/function_registry'
18
+ require_relative './ast/functions/abs'
18
19
  require_relative './ast/functions/all'
19
20
  require_relative './ast/functions/and'
20
21
  require_relative './ast/functions/any'
@@ -10,7 +10,7 @@ module Dentaku
10
10
  class Calculator
11
11
  include StringCasing
12
12
  attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
13
- :nested_data_support, :ast_cache
13
+ :nested_data_support, :ast_cache, :raw_date_literals
14
14
 
15
15
  def initialize(options = {})
16
16
  clear
@@ -19,22 +19,28 @@ module Dentaku
19
19
  @aliases = options.delete(:aliases) || Dentaku.aliases
20
20
  @nested_data_support = options.fetch(:nested_data_support, true)
21
21
  options.delete(:nested_data_support)
22
+ @raw_date_literals = options.fetch(:raw_date_literals, true)
23
+ options.delete(:raw_date_literals)
22
24
  @ast_cache = options
23
25
  @disable_ast_cache = false
24
26
  @function_registry = Dentaku::AST::FunctionRegistry.new
25
27
  end
26
28
 
27
- def self.add_function(name, type, body)
28
- Dentaku::AST::FunctionRegistry.default.register(name, type, body)
29
+ def self.add_function(name, type, body, callback = nil)
30
+ Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback)
29
31
  end
30
32
 
31
- def add_function(name, type, body)
32
- @function_registry.register(name, type, body)
33
+ def self.add_functions(functions)
34
+ functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
35
+ end
36
+
37
+ def add_function(name, type, body, callback = nil)
38
+ @function_registry.register(name, type, body, callback)
33
39
  self
34
40
  end
35
41
 
36
- def add_functions(fns)
37
- fns.each { |(name, type, body)| add_function(name, type, body) }
42
+ def add_functions(functions)
43
+ functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
38
44
  self
39
45
  end
40
46
 
@@ -94,9 +100,10 @@ module Dentaku
94
100
 
95
101
  @ast_cache.fetch(expression) {
96
102
  options = {
103
+ aliases: aliases,
97
104
  case_sensitive: case_sensitive,
98
105
  function_registry: @function_registry,
99
- aliases: aliases
106
+ raw_date_literals: raw_date_literals
100
107
  }
101
108
 
102
109
  tokens = tokenizer.tokenize(expression, options)
@@ -1,7 +1,11 @@
1
1
  module Dentaku
2
2
  class DateArithmetic
3
3
  def initialize(date)
4
- @base = date
4
+ if date.respond_to?(:strftime)
5
+ @base = date
6
+ else
7
+ @base = Time.parse(date).to_datetime
8
+ end
5
9
  end
6
10
 
7
11
  def add(duration)
@@ -11,6 +11,15 @@ module Dentaku
11
11
  end
12
12
  end
13
13
 
14
+ class MathDomainError < Error
15
+ attr_reader :function_name, :args
16
+
17
+ def initialize(function_name, args)
18
+ @function_name = function_name
19
+ @args = args
20
+ end
21
+ end
22
+
14
23
  class NodeError < Error
15
24
  attr_reader :child, :expect, :actual
16
25
 
@@ -45,22 +45,29 @@ module Dentaku
45
45
 
46
46
  operator.peek(output)
47
47
 
48
+ output_size = output.length
48
49
  args_size = operator.arity || count
49
50
  min_size = operator.arity || operator.min_param_count || count
50
51
  max_size = operator.arity || operator.max_param_count || count
51
52
 
52
- if output.length < min_size || args_size < min_size
53
- fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
53
+ if output_size < min_size || args_size < min_size
54
+ expect = min_size == max_size ? min_size : min_size..max_size
55
+ fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
54
56
  end
55
57
 
56
- if output.length > max_size && operations.empty? || args_size > max_size
57
- fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
58
+ if output_size > max_size && operations.empty? || args_size > max_size
59
+ expect = min_size == max_size ? min_size : min_size..max_size
60
+ fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
58
61
  end
59
62
 
60
- fail! :invalid_statement if output.size < args_size
63
+ fail! :invalid_statement if output_size < args_size
61
64
  args = Array.new(args_size) { output.pop }.reverse
62
65
 
63
66
  output.push operator.new(*args)
67
+
68
+ if operator.respond_to?(:callback) && !operator.callback.nil?
69
+ operator.callback.call(args)
70
+ end
64
71
  rescue ::ArgumentError => e
65
72
  raise Dentaku::ArgumentError, e.message
66
73
  rescue NodeError => e
@@ -320,9 +327,9 @@ module Dentaku
320
327
  when :node_invalid
321
328
  "#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
322
329
  when :too_few_operands
323
- "#{meta.fetch(:operator)} has too few operands"
330
+ "#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
324
331
  when :too_many_operands
325
- "#{meta.fetch(:operator)} has too many operands"
332
+ "#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
326
333
  when :undefined_function
327
334
  "Undefined function #{meta.fetch(:function_name)}"
328
335
  when :unprocessed_token
@@ -7,24 +7,31 @@ module Dentaku
7
7
 
8
8
  def visit_operation(node)
9
9
  if node.left
10
- visit_operand(node.left, node.class.precedence, suffix: " ")
10
+ visit_operand(node.left, node.class.precedence, suffix: " ", dir: :left)
11
11
  end
12
12
 
13
13
  @output << node.display_operator
14
14
 
15
15
  if node.right
16
- visit_operand(node.right, node.class.precedence, prefix: " ")
16
+ visit_operand(node.right, node.class.precedence, prefix: " ", dir: :right)
17
17
  end
18
18
  end
19
19
 
20
- def visit_operand(node, precedence, prefix: "", suffix: "")
20
+ def visit_operand(node, precedence, prefix: "", suffix: "", dir: :none)
21
21
  @output << prefix
22
- @output << "(" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
22
+ @output << "(" if should_output?(node, precedence, dir == :right)
23
23
  node.accept(self)
24
- @output << ")" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
24
+ @output << ")" if should_output?(node, precedence, dir == :right)
25
25
  @output << suffix
26
26
  end
27
27
 
28
+ def should_output?(node, precedence, output_on_equal)
29
+ return false unless node.is_a?(Dentaku::AST::Operation)
30
+
31
+ target_precedence = node.class.precedence
32
+ target_precedence < precedence || (output_on_equal && target_precedence == precedence)
33
+ end
34
+
28
35
  def visit_function(node)
29
36
  @output << node.name
30
37
  @output << "("
@@ -94,6 +101,10 @@ module Dentaku
94
101
  @output << "NULL"
95
102
  end
96
103
 
104
+ def visit_array(node)
105
+ @output << node.value.to_s
106
+ end
107
+
97
108
  def to_s
98
109
  @output
99
110
  end
@@ -7,6 +7,8 @@ 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
11
+
10
12
  def initialize(category, regexp, converter = nil, condition = nil)
11
13
  @category = category
12
14
  @regexp = %r{\A(#{ regexp })}i
@@ -73,7 +75,9 @@ module Dentaku
73
75
 
74
76
  def scanners(options = {})
75
77
  @case_sensitive = options.fetch(:case_sensitive, false)
76
- @scanners.values
78
+ raw_date_literals = options.fetch(:raw_date_literals, true)
79
+
80
+ @scanners.select { |k, _| raw_date_literals || k != :datetime }.values
77
81
  end
78
82
 
79
83
  def whitespace
@@ -86,7 +90,7 @@ module Dentaku
86
90
 
87
91
  # NOTE: Convert to DateTime as Array(Time) returns the parts of the time for some reason
88
92
  def datetime
89
- new(:datetime, /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?/, lambda { |raw| Time.parse(raw).to_datetime })
93
+ new(:datetime, DATE_TIME_REGEXP, lambda { |raw| Time.parse(raw).to_datetime })
90
94
  end
91
95
 
92
96
  def numeric
@@ -4,7 +4,7 @@ require 'dentaku/token_scanner'
4
4
 
5
5
  module Dentaku
6
6
  class Tokenizer
7
- attr_reader :case_sensitive, :aliases
7
+ attr_reader :aliases
8
8
 
9
9
  LPAREN = TokenMatcher.new(:grouping, :open)
10
10
  RPAREN = TokenMatcher.new(:grouping, :close)
@@ -15,10 +15,14 @@ module Dentaku
15
15
  @aliases = options.fetch(:aliases, global_aliases)
16
16
  input = strip_comments(string.to_s.dup)
17
17
  input = replace_aliases(input)
18
- @case_sensitive = options.fetch(:case_sensitive, false)
18
+
19
+ scanner_options = {
20
+ case_sensitive: options.fetch(:case_sensitive, false),
21
+ raw_date_literals: options.fetch(:raw_date_literals, true)
22
+ }
19
23
 
20
24
  until input.empty?
21
- scanned = TokenScanner.scanners(case_sensitive: case_sensitive).any? do |scanner|
25
+ scanned = TokenScanner.scanners(scanner_options).any? do |scanner|
22
26
  scanned, input = scan(input, scanner)
23
27
  scanned
24
28
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.1"
2
+ VERSION = "3.5.2"
3
3
  end
@@ -77,6 +77,10 @@ module Dentaku
77
77
  def visit_nil(node)
78
78
  process(node)
79
79
  end
80
+
81
+ def visit_array(node)
82
+ process(node)
83
+ end
80
84
  end
81
85
  end
82
86
  end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/abs'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Abs' do
6
+ it 'returns the absolute value of number' do
7
+ result = Dentaku('ABS(-4.2)')
8
+ expect(result).to eq(4.2)
9
+ end
10
+
11
+ it 'returns the correct value for positive number' do
12
+ result = Dentaku('ABS(1.3)')
13
+ expect(result).to eq(1.3)
14
+ end
15
+
16
+ it 'returns the correct value for zero' do
17
+ result = Dentaku('ABS(0)')
18
+ expect(result).to eq(0)
19
+ end
20
+
21
+ context 'checking errors' do
22
+ it 'raises an error if argument is not numeric' do
23
+ expect { Dentaku!("ABS(2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
24
+ end
25
+ end
26
+ end
data/spec/ast/all_spec.rb CHANGED
@@ -19,7 +19,7 @@ describe Dentaku::AST::All do
19
19
 
20
20
  it 'raises argument error if a string is passed as identifier' do
21
21
  expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
22
- Dentaku::ArgumentError, 'ALL() requires second argument to be an identifier'
22
+ Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
23
23
  )
24
24
  end
25
25
  end
data/spec/ast/any_spec.rb CHANGED
@@ -18,6 +18,6 @@ describe Dentaku::AST::Any do
18
18
  end
19
19
 
20
20
  it 'raises argument error if a string is passed as identifier' do
21
- expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ArgumentError)
21
+ expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
22
22
  end
23
23
  end
@@ -4,12 +4,12 @@ require 'dentaku/ast/arithmetic'
4
4
  require 'dentaku/token'
5
5
 
6
6
  describe Dentaku::AST::Arithmetic do
7
- let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
8
- let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
9
- let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
10
- let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
7
+ let(:one) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 1)) }
8
+ let(:two) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 2)) }
9
+ let(:x) { Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, 'x')) }
10
+ let(:y) { Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, 'y')) }
11
11
  let(:ctx) { {'x' => 1, 'y' => 2} }
12
- let(:date) { Dentaku::AST::DateTime.new Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16)) }
12
+ let(:date) { Dentaku::AST::DateTime.new(Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16))) }
13
13
 
14
14
  it 'performs an arithmetic operation with numeric operands' do
15
15
  expect(add(one, two)).to eq(3)
@@ -46,12 +46,20 @@ describe Dentaku::AST::Arithmetic do
46
46
  expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
47
47
  expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
48
48
 
49
- int_one = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, "1")
50
- decimal_one = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, "1.0")
49
+ int_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1"))
50
+ int_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1"))
51
+ decimal_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1.0"))
52
+ decimal_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1.0"))
51
53
 
52
- expect(add(int_one, int_one).class).to eq(Integer)
53
- expect(add(int_one, decimal_one).class).to eq(BigDecimal)
54
- expect(add(decimal_one, decimal_one).class).to eq(BigDecimal)
54
+ [int_one, int_neg_one].permutation(2).each do |(left, right)|
55
+ expect(add(left, right).class).to eq(Integer)
56
+ end
57
+
58
+ [decimal_one, decimal_neg_one].each do |left|
59
+ [int_one, int_neg_one, decimal_one, decimal_neg_one].each do |right|
60
+ expect(add(left, right).class).to eq(BigDecimal)
61
+ end
62
+ end
55
63
  end
56
64
 
57
65
  it 'performs arithmetic on arrays' do
data/spec/ast/avg_spec.rb CHANGED
@@ -3,6 +3,11 @@ require 'dentaku/ast/functions/avg'
3
3
  require 'dentaku'
4
4
 
5
5
  describe 'Dentaku::AST::Function::Avg' do
6
+ it 'returns the average of an array of Numeric values as BigDecimal' do
7
+ result = Dentaku('AVG(1, 2)')
8
+ expect(result).to eq(1.5)
9
+ end
10
+
6
11
  it 'returns the average of an array of Numeric values' do
7
12
  result = Dentaku('AVG(1, x, 1.8)', x: 2.3)
8
13
  expect(result).to eq(1.7)
@@ -19,7 +19,7 @@ describe Dentaku::AST::Filter do
19
19
 
20
20
  it 'raises argument error if a string is passed as identifier' do
21
21
  expect { calculator.evaluate!('FILTER({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
22
- Dentaku::ArgumentError, 'FILTER() requires second argument to be an identifier'
22
+ Dentaku::ParseError, 'FILTER() requires second argument to be an identifier'
23
23
  )
24
24
  end
25
25
  end
data/spec/ast/map_spec.rb CHANGED
@@ -21,7 +21,7 @@ describe Dentaku::AST::Map do
21
21
 
22
22
  it 'raises argument error if a string is passed as identifier' do
23
23
  expect { calculator.evaluate!('MAP({1, 2, 3}, "val", val + 1)') }.to raise_error(
24
- Dentaku::ArgumentError, 'MAP() requires second argument to be an identifier'
24
+ Dentaku::ParseError, 'MAP() requires second argument to be an identifier'
25
25
  )
26
26
  end
27
27
  end
@@ -21,7 +21,7 @@ describe Dentaku::AST::Pluck do
21
21
  expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
22
22
  {name: "Bob", age: 44},
23
23
  {name: "Jane", age: 27}
24
- ]) end.to raise_error(Dentaku::ArgumentError, 'PLUCK() requires second argument to be an identifier')
24
+ ]) end.to raise_error(Dentaku::ParseError, 'PLUCK() requires second argument to be an identifier')
25
25
  end
26
26
 
27
27
  it 'raises argument error if a non array of hashes is passed as collection' do
@@ -22,6 +22,7 @@ describe Dentaku::Calculator do
22
22
  expect(calculator.evaluate('(2 + 3) - 1')).to eq(4)
23
23
  expect(calculator.evaluate('(-2 + 3) - 1')).to eq(0)
24
24
  expect(calculator.evaluate('(-2 - 3) - 1')).to eq(-6)
25
+ expect(calculator.evaluate('1353+91-1-3322-22')).to eq(-1901)
25
26
  expect(calculator.evaluate('1 + -(2 ^ 2)')).to eq(-3)
26
27
  expect(calculator.evaluate('3 + -num', num: 2)).to eq(1)
27
28
  expect(calculator.evaluate('-num + 3', num: 2)).to eq(1)
@@ -329,6 +330,11 @@ describe Dentaku::Calculator do
329
330
  }.not_to raise_error
330
331
  end
331
332
 
333
+ it 'allows to compare "-" or "-."' do
334
+ expect { calculator.solve("IF('-' = '-', 0, 1)") }.not_to raise_error
335
+ expect { calculator.solve("IF('-.'= '-.', 0, 1)") }.not_to raise_error
336
+ end
337
+
332
338
  it "integrates with custom functions" do
333
339
  calculator.add_function(:custom, :integer, -> { 1 })
334
340
 
@@ -440,19 +446,42 @@ describe Dentaku::Calculator do
440
446
  expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
441
447
  end
442
448
 
443
- it 'supports date arithmetic' do
444
- expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date)
445
- expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date)
446
- expect(calculator.evaluate!('2020-01-01 - 2019-12-31')).to eq(1)
447
- expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date)
448
- expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date)
449
- expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date)
450
- expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date)
451
- expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date)
452
- expect(calculator.evaluate!('2020-01-01 + duration(30, months)').to_date).to eq(Time.local(2022, 7, 1).to_date)
453
- expect(calculator.evaluate!('2020-01-01 + duration(1, year)').to_date).to eq(Time.local(2021, 1, 1).to_date)
454
- expect(calculator.evaluate!('2020-01-01 - duration(1, year)').to_date).to eq(Time.local(2019, 1, 1).to_date)
455
- expect(calculator.evaluate!('2020-01-01 + duration(30, years)').to_date).to eq(Time.local(2050, 1, 1).to_date)
449
+ describe 'disabling date literals' do
450
+ it 'does not parse formulas with minus signs as dates' do
451
+ calculator = described_class.new(raw_date_literals: false)
452
+ expect(calculator.evaluate!('2020-01-01')).to eq(2018)
453
+ end
454
+ end
455
+
456
+ describe 'supports date arithmetic' do
457
+ it 'from hardcoded string' do
458
+ expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date)
459
+ expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date)
460
+ expect(calculator.evaluate!('2020-01-01 - 2019-12-31')).to eq(1)
461
+ expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date)
462
+ expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date)
463
+ expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date)
464
+ expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date)
465
+ expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date)
466
+ expect(calculator.evaluate!('2020-01-01 + duration(30, months)').to_date).to eq(Time.local(2022, 7, 1).to_date)
467
+ expect(calculator.evaluate!('2020-01-01 + duration(1, year)').to_date).to eq(Time.local(2021, 1, 1).to_date)
468
+ expect(calculator.evaluate!('2020-01-01 - duration(1, year)').to_date).to eq(Time.local(2019, 1, 1).to_date)
469
+ expect(calculator.evaluate!('2020-01-01 + duration(30, years)').to_date).to eq(Time.local(2050, 1, 1).to_date)
470
+ end
471
+
472
+ it 'from string variable' do
473
+ value = '2023-01-01'
474
+
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'))
477
+ end
478
+
479
+ it 'from date object' do
480
+ value = Date.parse('2023-01-01').to_date
481
+
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'))
484
+ end
456
485
  end
457
486
 
458
487
  describe 'functions' do
@@ -471,6 +500,13 @@ describe Dentaku::Calculator do
471
500
  expect(calculator.evaluate('ROUND(apples * 0.93)', apples: 10)).to eq(9)
472
501
  end
473
502
 
503
+ it 'include ABS' do
504
+ expect(calculator.evaluate('abs(-2.2)')).to eq(2.2)
505
+ expect(calculator.evaluate('abs(5)')).to eq(5)
506
+
507
+ expect(calculator.evaluate('ABS(x * -1)', x: 10)).to eq(10)
508
+ end
509
+
474
510
  it 'include NOT' do
475
511
  expect(calculator.evaluate('NOT(some_boolean)', some_boolean: true)).to be_falsey
476
512
  expect(calculator.evaluate('NOT(some_boolean)', some_boolean: false)).to be_truthy
@@ -758,9 +794,9 @@ describe Dentaku::Calculator do
758
794
  end
759
795
  end
760
796
 
761
- describe 'math functions' do
797
+ describe 'math support' do
762
798
  Math.methods(false).each do |method|
763
- it method do
799
+ it "includes `#{method}`" do
764
800
  if Math.method(method).arity == 2
765
801
  expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq(Math.send(method, 1, 2))
766
802
  expect(calculator.evaluate("#{method}(x,y) + 1", x: 1, y: '2')).to be_within(0.00001).of(Math.send(method, 1, 2) + 1)
@@ -774,11 +810,16 @@ describe Dentaku::Calculator do
774
810
  end
775
811
  end
776
812
 
777
- it 'are defined with a properly named class that represents it to support AST marshaling' do
813
+ it 'defines a properly named class to support AST marshaling' do
778
814
  expect {
779
815
  Marshal.dump(calculator.ast('SQRT(20)'))
780
816
  }.not_to raise_error
781
817
  end
818
+
819
+ it 'properly handles a Math::DomainError' do
820
+ expect(calculator.evaluate('asin(2)')).to be_nil
821
+ expect { calculator.evaluate!('asin(2)') }.to raise_error(Dentaku::MathDomainError)
822
+ end
782
823
  end
783
824
 
784
825
  describe 'disable_cache' do
@@ -5,8 +5,7 @@ require 'dentaku/calculator'
5
5
  describe Dentaku::Calculator do
6
6
  describe 'functions' do
7
7
  describe 'external functions' do
8
-
9
- let(:with_external_funcs) do
8
+ let(:custom_calculator) do
10
9
  c = described_class.new
11
10
 
12
11
  c.add_function(:now, :string, -> { Time.now.to_s })
@@ -22,30 +21,30 @@ describe Dentaku::Calculator do
22
21
  end
23
22
 
24
23
  it 'includes NOW' do
25
- now = with_external_funcs.evaluate('NOW()')
24
+ now = custom_calculator.evaluate('NOW()')
26
25
  expect(now).not_to be_nil
27
26
  expect(now).not_to be_empty
28
27
  end
29
28
 
30
29
  it 'includes POW' do
31
- expect(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
32
- expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
33
- expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
30
+ expect(custom_calculator.evaluate('POW(2,3)')).to eq(8)
31
+ expect(custom_calculator.evaluate('POW(3,2)')).to eq(9)
32
+ expect(custom_calculator.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
34
33
  end
35
34
 
36
35
  it 'includes BIGGEST' do
37
- expect(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
36
+ expect(custom_calculator.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
38
37
  end
39
38
 
40
39
  it 'includes SMALLEST' do
41
- expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
40
+ expect(custom_calculator.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
42
41
  end
43
42
 
44
43
  it 'includes OPTIONAL' do
45
- expect(with_external_funcs.evaluate('OPTIONAL(1,2)')).to eq(3)
46
- expect(with_external_funcs.evaluate('OPTIONAL(1,2,3)')).to eq(6)
47
- expect { with_external_funcs.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
48
- expect { with_external_funcs.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
44
+ expect(custom_calculator.evaluate('OPTIONAL(1,2)')).to eq(3)
45
+ expect(custom_calculator.evaluate('OPTIONAL(1,2,3)')).to eq(6)
46
+ expect { custom_calculator.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
47
+ expect { custom_calculator.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
49
48
  end
50
49
 
51
50
  it 'supports array parameters' do
@@ -62,6 +61,66 @@ describe Dentaku::Calculator do
62
61
  end
63
62
  end
64
63
 
64
+ describe 'with callbacks' do
65
+ let(:custom_calculator) do
66
+ c = described_class.new
67
+
68
+ @counts = Hash.new(0)
69
+
70
+ @initial_time = "2023-02-03"
71
+ @last_time = @initial_time
72
+
73
+ c.add_function(
74
+ :reverse,
75
+ :stringl,
76
+ ->(a) { a.reverse },
77
+ lambda do |args|
78
+ args.each do |arg|
79
+ @counts[arg.value] += 1 if arg.type == :string
80
+ end
81
+ end
82
+ )
83
+
84
+ fns = [
85
+ [:biggest_callback, :numeric, ->(*args) { args.max }, ->(args) { args.each { |arg| raise Dentaku::ArgumentError unless arg.type == :numeric } }],
86
+ [:pythagoras, :numeric, ->(l1, l2) { Math.sqrt(l1**2 + l2**2) }, ->(e) { @last_time = Time.now.to_s }],
87
+ [:callback_lambda, :string, ->() { " " }, ->() { "lambda executed" }],
88
+ [:no_lambda_function, :numeric, ->(a) { a**a }],
89
+ ]
90
+
91
+ c.add_functions(fns)
92
+ end
93
+
94
+ it 'includes BIGGEST_CALLBACK' do
95
+ expect(custom_calculator.evaluate('BIGGEST_CALLBACK(1, 2, 5, 4)')).to eq(5)
96
+ expect { custom_calculator.dependencies('BIGGEST_CALLBACK(1, 3, 6, "hi", 10)') }.to raise_error(Dentaku::ArgumentError)
97
+ end
98
+
99
+ it 'includes REVERSE' do
100
+ expect(custom_calculator.evaluate('REVERSE(\'Dentaku\')')).to eq('ukatneD')
101
+ expect { custom_calculator.evaluate('REVERSE(22)') }.to raise_error(NoMethodError)
102
+ expect(@counts["Dentaku"]).to eq(1)
103
+ end
104
+
105
+ it 'includes PYTHAGORAS' do
106
+ expect(custom_calculator.evaluate('PYTHAGORAS(8, 7)')).to eq(10.63014581273465)
107
+ expect(custom_calculator.evaluate('PYTHAGORAS(3, 4)')).to eq(5)
108
+ expect(@last_time).not_to eq(@initial_time)
109
+ end
110
+
111
+ it 'exposes the `callback` method of a function' do
112
+ expect(Dentaku::AST::Function::Callback_lambda.callback.call()).to eq("lambda executed")
113
+ end
114
+
115
+ it 'does not add a `callback` method to built-in functions' do
116
+ expect { Dentaku::AST::If.callback.call }.to raise_error(NoMethodError)
117
+ end
118
+
119
+ it 'defaults `callback` method to nil if not specified' do
120
+ expect(Dentaku::AST::Function::No_lambda_function.callback).to eq(nil)
121
+ end
122
+ end
123
+
65
124
  it 'allows registering "bang" functions' do
66
125
  calculator = described_class.new
67
126
  calculator.add_function(:hey!, :string, -> { "hey!" })
@@ -82,24 +141,36 @@ describe Dentaku::Calculator do
82
141
  end
83
142
 
84
143
  it 'does not store functions across all calculators' do
85
- calculator1 = Dentaku::Calculator.new
144
+ calculator1 = described_class.new
86
145
  calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
87
146
 
88
- calculator2 = Dentaku::Calculator.new
147
+ calculator2 = described_class.new
89
148
  calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
90
149
 
91
150
  expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
92
151
  expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
93
152
 
94
153
  expect {
95
- Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
154
+ described_class.new.evaluate!("1 + my_function(2)")
96
155
  }.to raise_error(Dentaku::ParseError)
97
156
  end
98
157
 
99
158
  describe 'Dentaku::Calculator.add_function' do
100
- it 'adds to default/global function registry' do
101
- Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
102
- expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
159
+ it 'adds a function to default/global function registry' do
160
+ described_class.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
161
+ expect(described_class.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
162
+ end
163
+ end
164
+
165
+ describe 'Dentaku::Calculator.add_functions' do
166
+ it 'adds multiple functions to default/global function registry' do
167
+ described_class.add_functions([
168
+ [:cube, :numeric, ->(x) { x**3 }],
169
+ [:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c,i| i.even? ? c.upcase : c.downcase }.join() }],
170
+ ])
171
+
172
+ expect(described_class.new.evaluate("1 + cube(3)")).to eq(28)
173
+ expect(described_class.new.evaluate("spongebob('How are you today?')")).to eq("HoW ArE YoU ToDaY?")
103
174
  end
104
175
  end
105
176
  end
@@ -8,6 +8,12 @@ describe Dentaku::PrintVisitor do
8
8
  expect(repr).to eq('5 + 4')
9
9
  end
10
10
 
11
+ it 'handles grouping correctly' do
12
+ formula = '10 - (0 - 10)'
13
+ repr = roundtrip(formula)
14
+ expect(repr).to eq(formula)
15
+ end
16
+
11
17
  it 'quotes string literals' do
12
18
  repr = roundtrip('Concat(\'a\', "B")')
13
19
  expect(repr).to eq('CONCAT("a", "B")')
@@ -10,6 +10,21 @@ class ArrayProcessor
10
10
  @expression = []
11
11
  end
12
12
 
13
+ def visit_array(node)
14
+ @expression << "{"
15
+
16
+ head, *tail = node.value
17
+
18
+ process(head) if head
19
+
20
+ tail.each do |v|
21
+ @expression << ","
22
+ process(v)
23
+ end
24
+
25
+ @expression << "}"
26
+ end
27
+
13
28
  def process(node)
14
29
  @expression << node.to_s
15
30
  end
@@ -22,10 +37,16 @@ RSpec.describe Dentaku::Visitor::Infix do
22
37
  expect(processor.expression).to eq ['5', '+', '3']
23
38
  end
24
39
 
40
+ it 'supports array nodes' do
41
+ processor = ArrayProcessor.new
42
+ processor.visit(ast('{1, 2, 3}'))
43
+ expect(processor.expression).to eq ['{', '1', ',', '2', ',', '3', '}']
44
+ end
45
+
25
46
  private
26
47
 
27
48
  def ast(expression)
28
49
  tokens = Dentaku::Tokenizer.new.tokenize(expression)
29
50
  Dentaku::Parser.new(tokens).parse
30
51
  end
31
- end
52
+ end
data/spec/visitor_spec.rb CHANGED
@@ -114,7 +114,7 @@ describe TestVisitor do
114
114
  visit_nodes('case (a % 5) when 0 then a else b end')
115
115
  visit_nodes('0xCAFE & (0xDECAF << 3) | (0xBEEF >> 5)')
116
116
  visit_nodes('2017-12-24 23:59:59')
117
- visit_nodes('ALL({1, 2, 3}, "val", val % 2 == 0)')
117
+ visit_nodes('ALL({1, 2, 3}, val, val % 2 == 0)')
118
118
  visit_nodes('ANY(vals, val, val > 1)')
119
119
  visit_nodes('COUNT({1, 2, 3})')
120
120
  visit_nodes('PLUCK(users, age)')
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.1
4
+ version: 3.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-24 00:00:00.000000000 Z
11
+ date: 2023-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -170,6 +170,7 @@ files:
170
170
  - lib/dentaku/ast/datetime.rb
171
171
  - lib/dentaku/ast/function.rb
172
172
  - lib/dentaku/ast/function_registry.rb
173
+ - lib/dentaku/ast/functions/abs.rb
173
174
  - lib/dentaku/ast/functions/all.rb
174
175
  - lib/dentaku/ast/functions/and.rb
175
176
  - lib/dentaku/ast/functions/any.rb
@@ -220,6 +221,7 @@ files:
220
221
  - lib/dentaku/tokenizer.rb
221
222
  - lib/dentaku/version.rb
222
223
  - lib/dentaku/visitor/infix.rb
224
+ - spec/ast/abs_spec.rb
223
225
  - spec/ast/addition_spec.rb
224
226
  - spec/ast/all_spec.rb
225
227
  - spec/ast/and_function_spec.rb
@@ -288,6 +290,7 @@ signing_key:
288
290
  specification_version: 4
289
291
  summary: A formula language parser and evaluator
290
292
  test_files:
293
+ - spec/ast/abs_spec.rb
291
294
  - spec/ast/addition_spec.rb
292
295
  - spec/ast/all_spec.rb
293
296
  - spec/ast/and_function_spec.rb