dentaku 3.5.1 → 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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