dentaku 2.0.9 → 2.0.11

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
  SHA1:
3
- metadata.gz: cc6a362189b735fe68596e1b94e28787d7290ac8
4
- data.tar.gz: cec7635943905eaa4cb8cd888649f5c34a0c1871
3
+ metadata.gz: 0febf923ab551e0cfce23bc38794927a3acedbe2
4
+ data.tar.gz: 3b4db1f045f21acd1e6a7c864ed9a4c272a2f765
5
5
  SHA512:
6
- metadata.gz: d82af702ad0a2b009680e7a3292388174915408f7d8e67c5b5cb0d545bd24544edd40a66a0611268bbb5614920c2de386784f8c7132c420802a1577d30b67a43
7
- data.tar.gz: 417cd814e7f46973c312568e8dcd9dd4bbe9fe1dfa27abcb8ac178c58e78850c477d99f9a76abd285abd8141d1e647dceb52b40edcb54681743d9af95cde9684
6
+ metadata.gz: dad6f87cf37ba49f355b0e30b92a80dfc063eeb9444653ea09832066831dc09c19e022883b2c31c23189a1c46c842ce652fba133725fa4f887db1fb5a9becf2a
7
+ data.tar.gz: 5b01fa83b5ddd2f3123946196ff6f0be42f1bd196b38195249d24ee2c6e08c1ad077ffd0da2f2cf33dd9c664e447ffe6864ab0f021a79e30e51769189225ef53
data/.travis.yml CHANGED
@@ -10,4 +10,4 @@ rvm:
10
10
  - 2.2.2
11
11
  - 2.2.3
12
12
  - 2.3.0
13
- - rbx-2
13
+ - 2.4.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Change Log
2
2
 
3
+ ## [v2.0.11] 2017-05-08
4
+ - fix dependency checking for logical AST nodes
5
+ - make `CONCAT` variadic
6
+ - fix casting strings to numeric in negation operations
7
+ - add date/time support
8
+ - add `&` (bitwise and) and `|` (bitwise or) operators
9
+ - fix incompatibility with 'mathn' module
10
+ - add `CONTAINS` string function
11
+
12
+ ## [v2.0.10] 2016-12-30
13
+ - fix string function initialization bug
14
+ - fix issues with CASE statements
15
+ - allow injecting AST cache
16
+
3
17
  ## [v2.0.9] 2016-09-19
4
18
  - namespace tokenization errors
5
19
  - automatically coerce arguments to string functions as strings
@@ -111,6 +125,8 @@
111
125
  ## [v0.1.0] 2012-01-20
112
126
  - initial release
113
127
 
128
+ [v2.0.11]: https://github.com/rubysolo/dentaku/compare/v2.0.10...v2.0.11
129
+ [v2.0.10]: https://github.com/rubysolo/dentaku/compare/v2.0.9...v2.0.10
114
130
  [v2.0.9]: https://github.com/rubysolo/dentaku/compare/v2.0.8...v2.0.9
115
131
  [v2.0.8]: https://github.com/rubysolo/dentaku/compare/v2.0.7...v2.0.8
116
132
  [v2.0.7]: https://github.com/rubysolo/dentaku/compare/v2.0.6...v2.0.7
data/README.md CHANGED
@@ -123,17 +123,19 @@ application, AST caching will consume more memory with each new formula.
123
123
  BUILT-IN OPERATORS AND FUNCTIONS
124
124
  ---------------------------------
125
125
 
126
- Math: `+`, `-`, `*`, `/`, `%`
126
+ Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`, `&`
127
127
 
128
- Logic: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`, `AND`, `OR`
128
+ Also, all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
129
129
 
130
- Functions: `IF`, `NOT`, `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
130
+ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
131
131
 
132
- Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
132
+ Logic: `IF`, `AND`, `OR`, `NOT`
133
+
134
+ Functions: `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
133
135
 
134
- Math: all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
136
+ Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
135
137
 
136
- String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`
138
+ String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`, `CONTAINS`
137
139
 
138
140
  RESOLVING DEPENDENCIES
139
141
  ----------------------
@@ -277,7 +279,7 @@ LICENSE
277
279
 
278
280
  (The MIT License)
279
281
 
280
- Copyright © 2012-2016 Solomon White
282
+ Copyright © 2012-2017 Solomon White
281
283
 
282
284
  Permission is hereby granted, free of charge, to any person obtaining a copy of
283
285
  this software and associated documentation files (the ‘Software’), to deal in
@@ -7,7 +7,7 @@ module Dentaku
7
7
  class Arithmetic < Operation
8
8
  def initialize(*)
9
9
  super
10
- unless valid_node?(left) && valid_node?(right)
10
+ unless valid_left? && valid_right?
11
11
  fail ParseError, "#{ self.class } requires numeric operands"
12
12
  end
13
13
  end
@@ -16,6 +16,10 @@ module Dentaku
16
16
  :numeric
17
17
  end
18
18
 
19
+ def operator
20
+ raise "Not implemented"
21
+ end
22
+
19
23
  def value(context={})
20
24
  l = cast(left.value(context))
21
25
  r = cast(right.value(context))
@@ -24,21 +28,44 @@ module Dentaku
24
28
 
25
29
  private
26
30
 
27
- def cast(value, prefer_integer=true)
28
- validate_numeric(value)
29
- v = BigDecimal.new(value, Float::DIG+1)
31
+ def cast(val, prefer_integer=true)
32
+ validate_operation(val)
33
+ validate_format(val) if val.is_a?(::String)
34
+ numeric(val, prefer_integer)
35
+ end
36
+
37
+ def numeric(val, prefer_integer)
38
+ v = BigDecimal.new(val, Float::DIG+1)
30
39
  v = v.to_i if prefer_integer && v.frac.zero?
31
40
  v
41
+ rescue ::TypeError
42
+ # If we got a TypeError BigDecimal or to_i failed;
43
+ # let value through so ruby things like Time - integer work
44
+ val
32
45
  end
33
46
 
34
47
  def valid_node?(node)
35
48
  node && (node.dependencies.any? || node.type == :numeric)
36
49
  end
37
50
 
38
- def validate_numeric(value)
39
- Float(value)
40
- rescue ::ArgumentError, ::TypeError
41
- fail Dentaku::ArgumentError, "#{ self.class } requires numeric operands"
51
+ def valid_left?
52
+ valid_node?(left)
53
+ end
54
+
55
+ def valid_right?
56
+ valid_node?(right)
57
+ end
58
+
59
+ def validate_operation(val)
60
+ unless val.respond_to?(operator)
61
+ fail Dentaku::ArgumentError, "#{ self.class } requires operands that respond to #{ operator }"
62
+ end
63
+ end
64
+
65
+ def validate_format(string)
66
+ unless string =~ /\A-?\d+(\.\d+)?\z/
67
+ fail Dentaku::ArgumentError, "String input '#{ string }' is not coercible to numeric"
68
+ end
42
69
  end
43
70
  end
44
71
 
@@ -73,6 +100,10 @@ module Dentaku
73
100
  end
74
101
 
75
102
  class Division < Arithmetic
103
+ def operator
104
+ :/
105
+ end
106
+
76
107
  def value(context={})
77
108
  r = cast(right.value(context), false)
78
109
  raise Dentaku::ZeroDivisionError if r.zero?
@@ -86,15 +117,6 @@ module Dentaku
86
117
  end
87
118
 
88
119
  class Modulo < Arithmetic
89
- def initialize(left, right)
90
- @left = left
91
- @right = right
92
-
93
- unless (valid_node?(left) || left.nil?) && valid_node?(right)
94
- fail ParseError, "#{ self.class } requires numeric operands"
95
- end
96
- end
97
-
98
120
  def percent?
99
121
  left.nil?
100
122
  end
@@ -114,6 +136,10 @@ module Dentaku
114
136
  def self.precedence
115
137
  20
116
138
  end
139
+
140
+ def valid_left?
141
+ valid_node?(left) || left.nil?
142
+ end
117
143
  end
118
144
 
119
145
  class Exponentiation < Arithmetic
@@ -0,0 +1,17 @@
1
+ require_relative './operation'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class BitwiseOr < Operation
6
+ def value(context={})
7
+ left.value(context) | right.value(context)
8
+ end
9
+ end
10
+
11
+ class BitwiseAnd < Operation
12
+ def value(context={})
13
+ left.value(context) & right.value(context)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -43,9 +43,8 @@ module Dentaku
43
43
  def dependencies(context={})
44
44
  # TODO: should short-circuit
45
45
  @switch.dependencies(context) +
46
- @conditions.flat_map do |condition|
47
- condition.dependencies(context)
48
- end
46
+ @conditions.flat_map { |condition| condition.dependencies(context) } +
47
+ @else.dependencies(context)
49
48
  end
50
49
  end
51
50
  end
@@ -17,7 +17,7 @@ module Dentaku
17
17
  private
18
18
 
19
19
  def valid_node?(node)
20
- node.dependencies.any? || node.type == :logical
20
+ node && (node.dependencies.any? || node.type == :logical)
21
21
  end
22
22
  end
23
23
 
@@ -0,0 +1,8 @@
1
+ require_relative "./literal"
2
+
3
+ module Dentaku
4
+ module AST
5
+ class DateTime < Literal
6
+ end
7
+ end
8
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative 'node'
2
+ require_relative 'function_registry'
2
3
 
3
4
  module Dentaku
4
5
  module AST
@@ -12,61 +13,19 @@ module Dentaku
12
13
  end
13
14
 
14
15
  def self.get(name)
15
- registry.fetch(function_name(name)) {
16
- fail ParseError, "Undefined function #{ name }"
17
- }
16
+ registry.get(name)
18
17
  end
19
18
 
20
19
  def self.register(name, type, implementation)
21
- function = Class.new(self) do
22
- def self.implementation=(impl)
23
- @implementation = impl
24
- end
25
-
26
- def self.implementation
27
- @implementation
28
- end
29
-
30
- def self.type=(type)
31
- @type = type
32
- end
33
-
34
- def self.type
35
- @type
36
- end
37
-
38
- def value(context={})
39
- args = @args.map { |a| a.value(context) }
40
- self.class.implementation.call(*args)
41
- end
42
-
43
- def type
44
- self.class.type
45
- end
46
- end
47
-
48
- function_class = name.to_s.capitalize
49
- Dentaku::AST.send(:remove_const, function_class) if Dentaku::AST.const_defined?(function_class)
50
- Dentaku::AST.const_set(function_class, function)
51
-
52
- function.implementation = implementation
53
- function.type = type
54
-
55
- registry[function_name(name)] = function
20
+ registry.register(name, type, implementation)
56
21
  end
57
22
 
58
23
  def self.register_class(name, function_class)
59
- registry[function_name(name)] = function_class
60
- end
61
-
62
- private
63
-
64
- def self.function_name(name)
65
- name.to_s.downcase
24
+ registry.register_class(name, function_class)
66
25
  end
67
26
 
68
27
  def self.registry
69
- @registry ||= {}
28
+ @registry ||= FunctionRegistry.new
70
29
  end
71
30
  end
72
31
  end
@@ -0,0 +1,64 @@
1
+ module Dentaku
2
+ module AST
3
+ class FunctionRegistry < Hash
4
+ def get(name)
5
+ name = function_name(name)
6
+ return self[name] if has_key?(name)
7
+ return default[name] if default.has_key?(name)
8
+ fail ParseError, "Undefined function #{ name }"
9
+ end
10
+
11
+ def register(name, type, implementation)
12
+ function = Class.new(Function) do
13
+ def self.implementation=(impl)
14
+ @implementation = impl
15
+ end
16
+
17
+ def self.implementation
18
+ @implementation
19
+ end
20
+
21
+ def self.type=(type)
22
+ @type = type
23
+ end
24
+
25
+ def self.type
26
+ @type
27
+ end
28
+
29
+ def value(context={})
30
+ args = @args.map { |a| a.value(context) }
31
+ self.class.implementation.call(*args)
32
+ end
33
+
34
+ def type
35
+ self.class.type
36
+ end
37
+ end
38
+
39
+ function.implementation = implementation
40
+ function.type = type
41
+
42
+ self[function_name(name)] = function
43
+ end
44
+
45
+ def register_class(name, function_class)
46
+ self[function_name(name)] = function_class
47
+ end
48
+
49
+ def default
50
+ self.class.default
51
+ end
52
+
53
+ def self.default
54
+ Dentaku::AST::Function.registry
55
+ end
56
+
57
+ private
58
+
59
+ def function_name(name)
60
+ name.to_s.downcase
61
+ end
62
+ end
63
+ end
64
+ end
@@ -4,9 +4,9 @@ module Dentaku
4
4
  module AST
5
5
  module StringFunctions
6
6
  class Left < Function
7
- def initialize(string, length)
8
- @string = string
9
- @length = length
7
+ def initialize(*args)
8
+ super
9
+ @string, @length = *@args
10
10
  end
11
11
 
12
12
  def value(context={})
@@ -17,9 +17,9 @@ module Dentaku
17
17
  end
18
18
 
19
19
  class Right < Function
20
- def initialize(string, length)
21
- @string = string
22
- @length = length
20
+ def initialize(*args)
21
+ super
22
+ @string, @length = *@args
23
23
  end
24
24
 
25
25
  def value(context={})
@@ -30,10 +30,9 @@ module Dentaku
30
30
  end
31
31
 
32
32
  class Mid < Function
33
- def initialize(string, offset, length)
34
- @string = string
35
- @offset = offset
36
- @length = length
33
+ def initialize(*args)
34
+ super
35
+ @string, @offset, @length = *@args
37
36
  end
38
37
 
39
38
  def value(context={})
@@ -45,8 +44,9 @@ module Dentaku
45
44
  end
46
45
 
47
46
  class Len < Function
48
- def initialize(string)
49
- @string = string
47
+ def initialize(*args)
48
+ super
49
+ @string = @args[0]
50
50
  end
51
51
 
52
52
  def value(context={})
@@ -56,9 +56,9 @@ module Dentaku
56
56
  end
57
57
 
58
58
  class Find < Function
59
- def initialize(needle, haystack)
60
- @needle = needle
61
- @haystack = haystack
59
+ def initialize(*args)
60
+ super
61
+ @needle, @haystack = *@args
62
62
  end
63
63
 
64
64
  def value(context={})
@@ -71,10 +71,9 @@ module Dentaku
71
71
  end
72
72
 
73
73
  class Substitute < Function
74
- def initialize(original, search, replacement)
75
- @original = original
76
- @search = search
77
- @replacement = replacement
74
+ def initialize(*args)
75
+ super
76
+ @original, @search, @replacement = *@args
78
77
  end
79
78
 
80
79
  def value(context={})
@@ -87,15 +86,23 @@ module Dentaku
87
86
  end
88
87
 
89
88
  class Concat < Function
90
- def initialize(left, right)
91
- @left = left
92
- @right = right
89
+ def initialize(*args)
90
+ super
93
91
  end
94
92
 
95
93
  def value(context={})
96
- left = @left.value(context).to_s
97
- right = @right.value(context).to_s
98
- left + right
94
+ @args.map { |arg| arg.value(context).to_s }.join
95
+ end
96
+ end
97
+
98
+ class Contains < Function
99
+ def initialize(*args)
100
+ super
101
+ @needle, @haystack = *args
102
+ end
103
+
104
+ def value(context={})
105
+ @haystack.value(context).to_s.include? @needle.value(context).to_s
99
106
  end
100
107
  end
101
108
  end
@@ -109,3 +116,4 @@ Dentaku::AST::Function.register_class(:len, Dentaku::AST::StringFunctions
109
116
  Dentaku::AST::Function.register_class(:find, Dentaku::AST::StringFunctions::Find)
110
117
  Dentaku::AST::Function.register_class(:substitute, Dentaku::AST::StringFunctions::Substitute)
111
118
  Dentaku::AST::Function.register_class(:concat, Dentaku::AST::StringFunctions::Concat)
119
+ Dentaku::AST::Function.register_class(:contains, Dentaku::AST::StringFunctions::Contains)
@@ -1,13 +1,17 @@
1
1
  module Dentaku
2
2
  module AST
3
- class Negation < Operation
3
+ class Negation < Arithmetic
4
4
  def initialize(node)
5
5
  @node = node
6
6
  fail ParseError, "Negation requires numeric operand" unless valid_node?(node)
7
7
  end
8
8
 
9
+ def operator
10
+ :*
11
+ end
12
+
9
13
  def value(context={})
10
- @node.value(context) * -1
14
+ cast(@node.value(context)) * -1
11
15
  end
12
16
 
13
17
  def type
data/lib/dentaku/ast.rb CHANGED
@@ -1,15 +1,18 @@
1
1
  require_relative './ast/node'
2
2
  require_relative './ast/nil'
3
+ require_relative './ast/datetime'
3
4
  require_relative './ast/numeric'
4
5
  require_relative './ast/logical'
5
6
  require_relative './ast/string'
6
7
  require_relative './ast/identifier'
7
8
  require_relative './ast/arithmetic'
9
+ require_relative './ast/bitwise'
8
10
  require_relative './ast/negation'
9
11
  require_relative './ast/comparators'
10
12
  require_relative './ast/combinators'
11
13
  require_relative './ast/grouping'
12
14
  require_relative './ast/case'
15
+ require_relative './ast/function_registry'
13
16
  require_relative './ast/functions/if'
14
17
  require_relative './ast/functions/max'
15
18
  require_relative './ast/functions/min'
@@ -4,19 +4,29 @@ require 'dentaku/token'
4
4
  require 'dentaku/dependency_resolver'
5
5
  require 'dentaku/parser'
6
6
 
7
+
7
8
  module Dentaku
8
9
  class Calculator
9
10
  attr_reader :result, :memory, :tokenizer
10
11
 
11
- def initialize
12
+ def initialize(ast_cache={})
12
13
  clear
13
14
  @tokenizer = Tokenizer.new
14
- @ast_cache = {}
15
+ @ast_cache = ast_cache
15
16
  @disable_ast_cache = false
17
+ @function_registry = Dentaku::AST::FunctionRegistry.new
18
+ end
19
+
20
+ def self.add_function(name, type, body)
21
+ Dentaku::AST::FunctionRegistry.default.register(name, type, body)
22
+ end
23
+
24
+ def add_functions(fns)
25
+ fns.each { |(name, type, body)| add_function(name, type, body) }
16
26
  end
17
27
 
18
28
  def add_function(name, type, body)
19
- Dentaku::AST::Function.register(name, type, body)
29
+ @function_registry.register(name, type, body)
20
30
  self
21
31
  end
22
32
 
@@ -60,7 +70,7 @@ module Dentaku
60
70
 
61
71
  def ast(expression)
62
72
  @ast_cache.fetch(expression) {
63
- Parser.new(tokenizer.tokenize(expression)).parse.tap do |node|
73
+ Parser.new(tokenizer.tokenize(expression), function_registry: @function_registry).parse.tap do |node|
64
74
  @ast_cache[expression] = node if cache_ast?
65
75
  end
66
76
  }
@@ -83,7 +93,7 @@ module Dentaku
83
93
  restore = Hash[memory]
84
94
 
85
95
  if value.nil?
86
- key_or_hash.each do |key, val|
96
+ _flat_hash(key_or_hash).each do |key, val|
87
97
  memory[key.to_s.downcase] = val
88
98
  end
89
99
  else
@@ -120,5 +130,16 @@ module Dentaku
120
130
  def cache_ast?
121
131
  Dentaku.cache_ast? && !@disable_ast_cache
122
132
  end
133
+
134
+ private
135
+
136
+ def _flat_hash(hash, k = [])
137
+ if hash.is_a?(Hash)
138
+ hash.inject({}) { |h, v| h.merge! _flat_hash(v[-1], k + [v[0]]) }
139
+ else
140
+ return { k.join('.') => hash } if k.is_a?(Array)
141
+ { k => hash }
142
+ end
143
+ end
123
144
  end
124
145
  end
@@ -7,8 +7,9 @@ module Dentaku
7
7
  def initialize(tokens, options={})
8
8
  @input = tokens.dup
9
9
  @output = []
10
- @operations = options.fetch(:operations, [])
11
- @arities = options.fetch(:arities, [])
10
+ @operations = options.fetch(:operations, [])
11
+ @arities = options.fetch(:arities, [])
12
+ @function_registry = options.fetch(:function_registry, nil)
12
13
  end
13
14
 
14
15
  def get_args(count)
@@ -25,6 +26,9 @@ module Dentaku
25
26
 
26
27
  while token = input.shift
27
28
  case token.category
29
+ when :datetime
30
+ output.push AST::DateTime.new(token)
31
+
28
32
  when :numeric
29
33
  output.push AST::Numeric.new(token)
30
34
 
@@ -67,15 +71,24 @@ module Dentaku
67
71
  # special handling for case nesting: strip out inner case
68
72
  # statements and parse their AST segments recursively
69
73
  if operations.include?(AST::Case)
70
- last_case_close_index = nil
71
- first_nested_case_close_index = nil
74
+ open_cases = 0
75
+ case_end_index = nil
76
+
72
77
  input.each_with_index do |token, index|
73
- first_nested_case_close_index = last_case_close_index
78
+ if token.category == :case && token.value == :open
79
+ open_cases += 1
80
+ end
81
+
74
82
  if token.category == :case && token.value == :close
75
- last_case_close_index = index
83
+ if open_cases > 0
84
+ open_cases -= 1
85
+ else
86
+ case_end_index = index
87
+ break
88
+ end
76
89
  end
77
90
  end
78
- inner_case_inputs = input.slice!(0..first_nested_case_close_index)
91
+ inner_case_inputs = input.slice!(0..case_end_index)
79
92
  subparser = Parser.new(
80
93
  inner_case_inputs,
81
94
  operations: [AST::Case],
@@ -202,6 +215,8 @@ module Dentaku
202
215
  pow: AST::Exponentiation,
203
216
  negate: AST::Negation,
204
217
  mod: AST::Modulo,
218
+ bitor: AST::BitwiseOr,
219
+ bitand: AST::BitwiseAnd,
205
220
 
206
221
  lt: AST::LessThan,
207
222
  gt: AST::GreaterThan,
@@ -216,7 +231,11 @@ module Dentaku
216
231
  end
217
232
 
218
233
  def function(token)
219
- Dentaku::AST::Function.get(token.value)
234
+ function_registry.get(token.value)
235
+ end
236
+
237
+ def function_registry
238
+ @function_registry ||= Dentaku::AST::FunctionRegistry.new
220
239
  end
221
240
  end
222
241
  end
@@ -93,11 +93,12 @@ module Dentaku
93
93
  @values.empty? || @values.key?(value)
94
94
  end
95
95
 
96
+ def self.datetime; new(:datetime); end
96
97
  def self.numeric; new(:numeric); end
97
98
  def self.string; new(:string); end
98
99
  def self.logical; new(:logical); end
99
100
  def self.value
100
- new(:numeric) | new(:string) | new(:logical)
101
+ new(:datetime) | new(:numeric) | new(:string) | new(:logical)
101
102
  end
102
103
 
103
104
  def self.addsub; new(:operator, [:add, :subtract]); end
@@ -12,7 +12,7 @@ module Dentaku
12
12
 
13
13
  def self.matcher(symbol)
14
14
  @matchers ||= [
15
- :numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
15
+ :datetime, :numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
16
16
  :comparator, :comp_gt, :comp_lt, :open, :close, :comma,
17
17
  :non_close_plus, :non_group, :non_group_star, :arguments,
18
18
  :logical, :combinator, :if, :round, :roundup, :rounddown, :not,
@@ -1,4 +1,5 @@
1
1
  require 'bigdecimal'
2
+ require 'time'
2
3
  require 'dentaku/token'
3
4
 
4
5
  module Dentaku
@@ -28,6 +29,7 @@ module Dentaku
28
29
  [
29
30
  :null,
30
31
  :whitespace,
32
+ :datetime, # before numeric so it can pick up timestamps
31
33
  :numeric,
32
34
  :double_quoted_string,
33
35
  :single_quoted_string,
@@ -73,6 +75,11 @@ module Dentaku
73
75
  new(:null, 'null\b')
74
76
  end
75
77
 
78
+ # NOTE: Convert to DateTime as Array(Time) returns the parts of the time for some reason
79
+ def datetime
80
+ 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 })
81
+ end
82
+
76
83
  def numeric
77
84
  new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
78
85
  end
@@ -97,8 +104,8 @@ module Dentaku
97
104
  end
98
105
 
99
106
  def operator
100
- names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%' }.invert
101
- new(:operator, '\^|\+|-|\*|\/|%', lambda { |raw| names[raw] })
107
+ names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&' }.invert
108
+ new(:operator, '\^|\+|-|\*|\/|%|\||&', lambda { |raw| names[raw] })
102
109
  end
103
110
 
104
111
  def grouping
@@ -126,14 +133,14 @@ module Dentaku
126
133
  end
127
134
 
128
135
  def function
129
- new(:function, '\w+\s*\(', lambda do |raw|
136
+ new(:function, '\w+!?\s*\(', lambda do |raw|
130
137
  function_name = raw.gsub('(', '')
131
138
  [Token.new(:function, function_name.strip.downcase.to_sym, function_name), Token.new(:grouping, :open, '(')]
132
139
  end)
133
140
  end
134
141
 
135
142
  def identifier
136
- new(:identifier, '\w+\b', lambda { |raw| raw.strip.downcase })
143
+ new(:identifier, '[\w\.]+\b', lambda { |raw| raw.strip.downcase })
137
144
  end
138
145
  end
139
146
 
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "2.0.9"
2
+ VERSION = "2.0.11"
3
3
  end
@@ -26,4 +26,31 @@ describe Dentaku::AST::Addition do
26
26
  described_class.new(group, five)
27
27
  }.not_to raise_error
28
28
  end
29
+
30
+ it 'allows operands that respond to addition' do
31
+ # Sample struct that has a custom definition for addition
32
+
33
+ Operand = Struct.new(:value) do
34
+ def +(other)
35
+ case other
36
+ when Operand
37
+ value + other.value
38
+ when Numeric
39
+ value + other
40
+ end
41
+ end
42
+ end
43
+
44
+ operand_five = Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, Operand.new(5))
45
+ operand_six = Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, Operand.new(6))
46
+
47
+ expect {
48
+ described_class.new(operand_five, operand_six)
49
+ }.not_to raise_error
50
+
51
+ expect {
52
+ described_class.new(operand_five, six)
53
+ }.not_to raise_error
54
+
55
+ end
29
56
  end
@@ -67,14 +67,18 @@ describe Dentaku::AST::Case do
67
67
  let!(:tax) do
68
68
  Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :tax))
69
69
  end
70
+ let!(:fallback) do
71
+ Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :fallback))
72
+ end
70
73
  let!(:addition) { Dentaku::AST::Addition.new(two, tax) }
71
74
  let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
72
75
  let!(:then2) { Dentaku::AST::CaseThen.new(addition) }
76
+ let!(:else2) { Dentaku::AST::CaseElse.new(fallback) }
73
77
  let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
74
78
 
75
79
  it 'gathers dependencies from switch and conditionals' do
76
- node = described_class.new(switch, conditional1, conditional2)
77
- expect(node.dependencies).to eq([:fruit, :tax])
80
+ node = described_class.new(switch, conditional1, conditional2, else2)
81
+ expect(node.dependencies).to eq([:fruit, :tax, :fallback])
78
82
  end
79
83
  end
80
84
  end
@@ -1,6 +1,8 @@
1
1
  require 'spec_helper'
2
2
  require 'dentaku/ast/function'
3
3
 
4
+ class Clazz; end
5
+
4
6
  describe Dentaku::AST::Function do
5
7
  it 'maintains a function registry' do
6
8
  expect(described_class).to respond_to(:get)
@@ -18,4 +20,8 @@ describe Dentaku::AST::Function do
18
20
  function = described_class.get("flarble").new
19
21
  expect(function.value).to eq "flarble"
20
22
  end
23
+
24
+ it 'does not throw an error when registering a function with a name that matches a currently defined constant' do
25
+ expect { described_class.register("clazz", :string, -> { "clazzified" }) }.not_to raise_error
26
+ end
21
27
  end
@@ -133,3 +133,12 @@ describe Dentaku::AST::StringFunctions::Concat do
133
133
  expect(subject.value).to eq ''
134
134
  end
135
135
  end
136
+
137
+ describe Dentaku::AST::StringFunctions::Contains do
138
+ it 'checks for substrings' do
139
+ subject = described_class.new(literal('app'), literal('apple'))
140
+ expect(subject.value).to be_truthy
141
+ subject = described_class.new(literal('app'), literal('orange'))
142
+ expect(subject.value).to be_falsy
143
+ end
144
+ end
@@ -32,6 +32,9 @@ describe Dentaku::Calculator do
32
32
  expect(calculator.evaluate('0.253/0.253')).to eq(1)
33
33
  expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
34
34
  expect(calculator.evaluate('10 + x', x: 'abc')).to be_nil
35
+ expect(calculator.evaluate('t + 1*24*60*60', t: Time.local(2017, 1, 1))).to eq(Time.local(2017, 1, 2))
36
+ expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
37
+ expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
35
38
  end
36
39
 
37
40
  describe 'memory' do
@@ -59,6 +62,12 @@ describe Dentaku::Calculator do
59
62
  calculator.store_formula('area', 'length * width')
60
63
  expect(calculator.evaluate!('area', length: 5, width: 5)).to eq 25
61
64
  end
65
+
66
+ it 'stores nested hashes' do
67
+ calculator.store({a: {basket: {of: 'apples'}}, b: 2})
68
+ expect(calculator.evaluate!('a.basket.of')).to eq 'apples'
69
+ expect(calculator.evaluate!('b')).to eq 2
70
+ end
62
71
  end
63
72
 
64
73
  describe 'dependencies' do
@@ -66,6 +75,12 @@ describe Dentaku::Calculator do
66
75
  expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
67
76
  end
68
77
 
78
+ it "finds dependencies in formula arguments" do
79
+ allow(Dentaku).to receive(:cache_ast?) { true }
80
+
81
+ expect(calculator.dependencies("CONCAT(bob, dole)")).to eq(['bob', 'dole'])
82
+ end
83
+
69
84
  it "doesn't consider variables in memory as dependencies" do
70
85
  expect(with_memory.dependencies("apples + oranges")).to eq(['oranges'])
71
86
  end
@@ -163,6 +178,14 @@ describe Dentaku::Calculator do
163
178
  expect(calculator.evaluate('(1+1+1)/3*100')).to eq(100)
164
179
  end
165
180
 
181
+ it 'evaluates negation' do
182
+ expect(calculator.evaluate('-negative', negative: -1)).to eq(1)
183
+ expect(calculator.evaluate('-negative', negative: '-1')).to eq(1)
184
+ expect(calculator.evaluate('-negative - 1', negative: '-1')).to eq(0)
185
+ expect(calculator.evaluate('-negative - 1', negative: '1')).to eq(-2)
186
+ expect(calculator.evaluate('-(negative) - 1', negative: '1')).to eq(-2)
187
+ end
188
+
166
189
  it 'fails to evaluate unbound statements' do
167
190
  unbound = 'foo * 1.5'
168
191
  expect { calculator.evaluate!(unbound) }.to raise_error(Dentaku::UnboundVariableError)
@@ -174,6 +197,11 @@ describe Dentaku::Calculator do
174
197
  expect(calculator.evaluate(unbound) { |e| e }).to eq unbound
175
198
  end
176
199
 
200
+ it 'fails to evaluate incomplete statements' do
201
+ incomplete = 'true AND'
202
+ expect { calculator.evaluate!(incomplete) }.to raise_error(Dentaku::ParseError)
203
+ end
204
+
177
205
  it 'evaluates unbound statements given a binding in memory' do
178
206
  expect(calculator.evaluate('foo * 1.5', foo: 2)).to eq(3)
179
207
  expect(calculator.bind(monkeys: 3).evaluate('monkeys < 7')).to be_truthy
@@ -217,6 +245,20 @@ describe Dentaku::Calculator do
217
245
  expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
218
246
  end
219
247
 
248
+ it 'compares Time variables' do
249
+ expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_truthy
250
+ expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_falsy
251
+ expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_falsy
252
+ expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_truthy
253
+ end
254
+
255
+ it 'compares Time literals with Time variables' do
256
+ expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_truthy
257
+ expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_falsy
258
+ expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_falsy
259
+ expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
260
+ end
261
+
220
262
  describe 'functions' do
221
263
  it 'include IF' do
222
264
  expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
@@ -275,16 +317,16 @@ describe Dentaku::Calculator do
275
317
  end
276
318
  end
277
319
 
278
- describe 'explicit NULL' do
279
- it 'can be used in IF statements' do
320
+ describe 'nil values' do
321
+ it 'can be used explicitly' do
280
322
  expect(calculator.evaluate('IF(null, 1, 2)')).to eq(2)
281
323
  end
282
324
 
283
- it 'can be used in IF statements when passed in' do
325
+ it 'can be assigned to a variable' do
284
326
  expect(calculator.evaluate('IF(foo, 1, 2)', foo: nil)).to eq(2)
285
327
  end
286
328
 
287
- it 'nil values are carried across middle terms' do
329
+ it 'are carried across middle terms' do
288
330
  results = calculator.solve!(
289
331
  choice: 'IF(bar, 1, 2)',
290
332
  bar: 'foo',
@@ -296,7 +338,7 @@ describe Dentaku::Calculator do
296
338
  )
297
339
  end
298
340
 
299
- it 'raises errors when used in arithmetic operation' do
341
+ it 'raise errors when used in arithmetic operations' do
300
342
  expect {
301
343
  calculator.solve!(more_apples: "apples + 1", apples: nil)
302
344
  }.to raise_error(Dentaku::ArgumentError)
@@ -379,6 +421,34 @@ describe Dentaku::Calculator do
379
421
  fruit: 'banana')
380
422
  expect(value).to eq(5)
381
423
  end
424
+
425
+ it 'handles multiple nested case statements' do
426
+ formula = <<-FORMULA
427
+ CASE fruit
428
+ WHEN 'apple'
429
+ THEN
430
+ CASE quantity
431
+ WHEN 2 THEN 3
432
+ END
433
+ WHEN 'banana'
434
+ THEN
435
+ CASE quantity
436
+ WHEN 1 THEN 2
437
+ END
438
+ END
439
+ FORMULA
440
+ value = calculator.evaluate(
441
+ formula,
442
+ quantity: 1,
443
+ fruit: 'banana')
444
+ expect(value).to eq(2)
445
+
446
+ value = calculator.evaluate(
447
+ formula,
448
+ quantity: 2,
449
+ fruit: 'apple')
450
+ expect(value).to eq(3)
451
+ end
382
452
  end
383
453
 
384
454
  describe 'math functions' do
@@ -441,9 +511,9 @@ describe Dentaku::Calculator do
441
511
  end
442
512
 
443
513
  describe 'string functions' do
444
- it 'concatenates two strings' do
514
+ it 'concatenates strings' do
445
515
  expect(
446
- calculator.evaluate('CONCAT(s1, s2)', 's1' => 'abc', 's2' => 'def')
516
+ calculator.evaluate('CONCAT(s1, s2, s3)', 's1' => 'ab', 's2' => 'cd', 's3' => 'ef')
447
517
  ).to eq 'abcdef'
448
518
  end
449
519
  end
data/spec/dentaku_spec.rb CHANGED
@@ -19,4 +19,10 @@ describe Dentaku do
19
19
  expect(Dentaku('40 + n', 'N' => 2)).to eql(42)
20
20
  expect(Dentaku('40 + n', 'n' => 2)).to eql(42)
21
21
  end
22
+
23
+ it 'raises a parse error for bad logic expressions' do
24
+ expect {
25
+ Dentaku('true AND')
26
+ }.to raise_error(Dentaku::ParseError)
27
+ end
22
28
  end
@@ -52,5 +52,29 @@ describe Dentaku::Calculator do
52
52
  expect(calculator.evaluate("INCLUDES(list, 2)", list: [1,2,3])).to eq(true)
53
53
  end
54
54
  end
55
+
56
+ it 'allows registering "bang" functions' do
57
+ calculator = described_class.new
58
+ calculator.add_function(:hey!, :string, -> { "hey!" })
59
+ expect(calculator.evaluate("hey!()")).to eq("hey!")
60
+ end
61
+
62
+ it 'does not store functions across all calculators' do
63
+ calculator1 = Dentaku::Calculator.new
64
+ calculator1.add_function(:my_function, :numeric, ->(x) { 2*x + 1 })
65
+
66
+ calculator2 = Dentaku::Calculator.new
67
+ calculator2.add_function(:my_function, :numeric, ->(x) { 4*x + 3 })
68
+
69
+ expect(calculator1.evaluate("1 + my_function(2)")). to eq (1 + 2*2 + 1)
70
+ expect(calculator2.evaluate("1 + my_function(2)")). to eq (1 + 4*2 + 3)
71
+
72
+ expect{Dentaku::Calculator.new.evaluate("1 + my_function(2)")}.to raise_error(Dentaku::ParseError)
73
+ end
74
+
75
+ it 'self.add_function adds to default/global function registry' do
76
+ Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
77
+ expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq (10 + 3**2 + 5)
78
+ end
55
79
  end
56
80
  end
data/spec/parser_spec.rb CHANGED
@@ -35,6 +35,15 @@ describe Dentaku::Parser do
35
35
  expect(node.value).to eq 0.05
36
36
  end
37
37
 
38
+ it 'calculates bitwise OR' do
39
+ two = Dentaku::Token.new(:numeric, 2)
40
+ bitor = Dentaku::Token.new(:operator, :bitor)
41
+ three = Dentaku::Token.new(:numeric, 3)
42
+
43
+ node = described_class.new([two, bitor, three]).parse
44
+ expect(node.value).to eq 3
45
+ end
46
+
38
47
  it 'performs multiple operations in one stream' do
39
48
  five = Dentaku::Token.new(:numeric, 5)
40
49
  plus = Dentaku::Token.new(:operator, :add)
@@ -132,14 +141,25 @@ describe Dentaku::Parser do
132
141
  expect(node.value(x: 3)).to eq(4)
133
142
  end
134
143
 
135
- it 'raises an error on parse failure' do
136
- five = Dentaku::Token.new(:numeric, 5)
137
- times = Dentaku::Token.new(:operator, :multiply)
138
- minus = Dentaku::Token.new(:operator, :subtract)
139
-
140
- expect {
141
- described_class.new([five, times, minus]).parse
142
- }.to raise_error(Dentaku::ParseError)
144
+ context 'invalid expression' do
145
+ it 'raises a parse error for bad math' do
146
+ five = Dentaku::Token.new(:numeric, 5)
147
+ times = Dentaku::Token.new(:operator, :multiply)
148
+ minus = Dentaku::Token.new(:operator, :subtract)
149
+
150
+ expect {
151
+ described_class.new([five, times, minus]).parse
152
+ }.to raise_error(Dentaku::ParseError)
153
+ end
154
+
155
+ it 'raises a parse error for bad logic' do
156
+ this = Dentaku::Token.new(:logical, true)
157
+ also = Dentaku::Token.new(:combinator, :and)
158
+
159
+ expect {
160
+ described_class.new([this, also]).parse
161
+ }.to raise_error(Dentaku::ParseError)
162
+ end
143
163
  end
144
164
 
145
165
  it "evaluates explicit 'NULL' as a Nil" do
@@ -28,7 +28,7 @@ describe Dentaku::TokenScanner do
28
28
  end
29
29
 
30
30
  it 'returns a list of all configured scanners' do
31
- expect(described_class.scanners.length).to eq 14
31
+ expect(described_class.scanners.length).to eq 15
32
32
  end
33
33
 
34
34
  it 'allows customizing available scanners' do
@@ -45,37 +45,43 @@ describe Dentaku::Tokenizer do
45
45
  expect(tokens.map(&:value)).to eq(['number', :eq, 5])
46
46
  end
47
47
 
48
- it 'tokenizes comparison with =' do
49
- tokens = tokenizer.tokenize('number = 5')
50
- expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
51
- expect(tokens.map(&:value)).to eq(['number', :eq, 5])
52
- end
53
-
54
48
  it 'tokenizes comparison with alternate ==' do
55
49
  tokens = tokenizer.tokenize('number == 5')
56
50
  expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
57
51
  expect(tokens.map(&:value)).to eq(['number', :eq, 5])
58
52
  end
59
53
 
54
+ it 'tokenizes bitwise OR' do
55
+ tokens = tokenizer.tokenize('2 | 3')
56
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
57
+ expect(tokens.map(&:value)).to eq([2, :bitor, 3])
58
+ end
59
+
60
+ it 'tokenizes bitwise AND' do
61
+ tokens = tokenizer.tokenize('2 & 3')
62
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
63
+ expect(tokens.map(&:value)).to eq([2, :bitand, 3])
64
+ end
65
+
60
66
  it 'ignores whitespace' do
61
67
  tokens = tokenizer.tokenize('1 / 1 ')
62
68
  expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
63
69
  expect(tokens.map(&:value)).to eq([1, :divide, 1])
64
70
  end
65
71
 
66
- it 'tokenizes power operations' do
72
+ it 'tokenizes power operations in simple expressions' do
67
73
  tokens = tokenizer.tokenize('10 ^ 2')
68
74
  expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
69
75
  expect(tokens.map(&:value)).to eq([10, :pow, 2])
70
76
  end
71
77
 
72
- it 'tokenizes power operations' do
78
+ it 'tokenizes power operations in complex expressions' do
73
79
  tokens = tokenizer.tokenize('0 * 10 ^ -5')
74
80
  expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric, :operator, :operator, :numeric])
75
81
  expect(tokens.map(&:value)).to eq([0, :multiply, 10, :pow, :negate, 5])
76
82
  end
77
83
 
78
- it 'handles floating point' do
84
+ it 'handles floating point operands' do
79
85
  tokens = tokenizer.tokenize('1.5 * 3.7')
80
86
  expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
81
87
  expect(tokens.map(&:value)).to eq([1.5, :multiply, 3.7])
@@ -111,13 +117,13 @@ describe Dentaku::Tokenizer do
111
117
  expect(tokens.map(&:value)).to eq([2, :subtract, 3])
112
118
  end
113
119
 
114
- it 'recognizes unary minus operator' do
120
+ it 'recognizes unary minus operator applied to left operand' do
115
121
  tokens = tokenizer.tokenize('-2 + 3')
116
122
  expect(tokens.map(&:category)).to eq([:operator, :numeric, :operator, :numeric])
117
123
  expect(tokens.map(&:value)).to eq([:negate, 2, :add, 3])
118
124
  end
119
125
 
120
- it 'recognizes unary minus operator' do
126
+ it 'recognizes unary minus operator applied to right operand' do
121
127
  tokens = tokenizer.tokenize('2 - -3')
122
128
  expect(tokens.map(&:category)).to eq([:numeric, :operator, :operator, :numeric])
123
129
  expect(tokens.map(&:value)).to eq([2, :subtract, :negate, 3])
@@ -165,6 +171,22 @@ describe Dentaku::Tokenizer do
165
171
  expect(tokens.map(&:value)).to eq(['true_lies', :and, 'falsehoods'])
166
172
  end
167
173
 
174
+ it 'tokenizes Time literals' do
175
+ 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')
176
+ expect(tokens.length).to eq(8)
177
+ expect(tokens.map(&:category)).to eq([:datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime])
178
+ expect(tokens.map(&:value)).to eq([
179
+ Time.local(2017, 1, 1).to_datetime,
180
+ Time.local(2017, 1, 2).to_datetime,
181
+ Time.local(2017, 1, 3).to_datetime,
182
+ Time.local(2017, 1, 4, 12, 23, 42).to_datetime,
183
+ Time.local(2017, 1, 5, 1, 2, 3).to_datetime,
184
+ Time.local(2017, 1, 6, 1, 2, 30).to_datetime,
185
+ Time.utc(2017, 1, 7, 12, 34, 56).to_datetime,
186
+ Time.new(2017, 1, 8, 1, 2, 3, "+08:00").to_datetime
187
+ ])
188
+ end
189
+
168
190
  describe 'functions' do
169
191
  it 'include IF' do
170
192
  tokens = tokenizer.tokenize('if(x < 10, y, z)')
@@ -208,5 +230,12 @@ describe Dentaku::Tokenizer do
208
230
  expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :comparator, :numeric, :grouping])
209
231
  expect(tokens.map(&:value)).to eq([:not, :open, 8, :lt, 5, :close])
210
232
  end
233
+
234
+ it 'can end with a bang' do
235
+ tokens = tokenizer.tokenize('exp!(5 * 3)')
236
+ expect(tokens.length).to eq(6)
237
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :operator, :numeric, :grouping])
238
+ expect(tokens.map(&:value)).to eq([:exp!, :open, 5, :multiply, 3, :close])
239
+ end
211
240
  end
212
241
  end
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: 2.0.9
4
+ version: 2.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-19 00:00:00.000000000 Z
11
+ date: 2017-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -70,6 +70,7 @@ files:
70
70
  - lib/dentaku.rb
71
71
  - lib/dentaku/ast.rb
72
72
  - lib/dentaku/ast/arithmetic.rb
73
+ - lib/dentaku/ast/bitwise.rb
73
74
  - lib/dentaku/ast/case.rb
74
75
  - lib/dentaku/ast/case/case_conditional.rb
75
76
  - lib/dentaku/ast/case/case_else.rb
@@ -78,7 +79,9 @@ files:
78
79
  - lib/dentaku/ast/case/case_when.rb
79
80
  - lib/dentaku/ast/combinators.rb
80
81
  - lib/dentaku/ast/comparators.rb
82
+ - lib/dentaku/ast/datetime.rb
81
83
  - lib/dentaku/ast/function.rb
84
+ - lib/dentaku/ast/function_registry.rb
82
85
  - lib/dentaku/ast/functions/if.rb
83
86
  - lib/dentaku/ast/functions/max.rb
84
87
  - lib/dentaku/ast/functions/min.rb