dentaku 2.0.10 → 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: c2b19bb69372e3700ffb09ca14b5f3fbcb630b06
4
- data.tar.gz: 4a67960044c94875d8a0ab2487e9464c820af5ca
3
+ metadata.gz: 0febf923ab551e0cfce23bc38794927a3acedbe2
4
+ data.tar.gz: 3b4db1f045f21acd1e6a7c864ed9a4c272a2f765
5
5
  SHA512:
6
- metadata.gz: 017f2d92d2b8110151d51ea2c93abe8964a82a662b40ce1f38a8133b75dea64b51ba774f64d06ffb24353024046eeb59457d239527560a57eeb0671b6dff8cc1
7
- data.tar.gz: cd39bb1acee73e4b05d65940310749601bf758c8797c2cca5ed4e45f6850b59a0637b132a8c4eafd0b60ac52a8714ad5b6933ca38fe3dcc0e60ef9588d2565c0
6
+ metadata.gz: dad6f87cf37ba49f355b0e30b92a80dfc063eeb9444653ea09832066831dc09c19e022883b2c31c23189a1c46c842ce652fba133725fa4f887db1fb5a9becf2a
7
+ data.tar.gz: 5b01fa83b5ddd2f3123946196ff6f0be42f1bd196b38195249d24ee2c6e08c1ad077ffd0da2f2cf33dd9c664e447ffe6864ab0f021a79e30e51769189225ef53
@@ -10,3 +10,4 @@ rvm:
10
10
  - 2.2.2
11
11
  - 2.2.3
12
12
  - 2.3.0
13
+ - 2.4.0
@@ -1,5 +1,14 @@
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
+
3
12
  ## [v2.0.10] 2016-12-30
4
13
  - fix string function initialization bug
5
14
  - fix issues with CASE statements
@@ -116,7 +125,8 @@
116
125
  ## [v0.1.0] 2012-01-20
117
126
  - initial release
118
127
 
119
- [HEAD]: https://github.com/rubysolo/dentaku/compare/v2.0.9...HEAD
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
120
130
  [v2.0.9]: https://github.com/rubysolo/dentaku/compare/v2.0.8...v2.0.9
121
131
  [v2.0.8]: https://github.com/rubysolo/dentaku/compare/v2.0.7...v2.0.8
122
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
@@ -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'
@@ -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
@@ -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, false)
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
@@ -88,13 +88,21 @@ module Dentaku
88
88
  class Concat < Function
89
89
  def initialize(*args)
90
90
  super
91
- @left, @right = *@args
92
91
  end
93
92
 
94
93
  def value(context={})
95
- left = @left.value(context).to_s
96
- right = @right.value(context).to_s
97
- 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
98
106
  end
99
107
  end
100
108
  end
@@ -108,3 +116,4 @@ Dentaku::AST::Function.register_class(:len, Dentaku::AST::StringFunctions
108
116
  Dentaku::AST::Function.register_class(:find, Dentaku::AST::StringFunctions::Find)
109
117
  Dentaku::AST::Function.register_class(:substitute, Dentaku::AST::StringFunctions::Substitute)
110
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
@@ -4,6 +4,7 @@ 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
@@ -13,10 +14,19 @@ module Dentaku
13
14
  @tokenizer = Tokenizer.new
14
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
 
@@ -211,6 +215,8 @@ module Dentaku
211
215
  pow: AST::Exponentiation,
212
216
  negate: AST::Negation,
213
217
  mod: AST::Modulo,
218
+ bitor: AST::BitwiseOr,
219
+ bitand: AST::BitwiseAnd,
214
220
 
215
221
  lt: AST::LessThan,
216
222
  gt: AST::GreaterThan,
@@ -225,7 +231,11 @@ module Dentaku
225
231
  end
226
232
 
227
233
  def function(token)
228
- 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
229
239
  end
230
240
  end
231
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.10"
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
@@ -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
@@ -169,6 +178,14 @@ describe Dentaku::Calculator do
169
178
  expect(calculator.evaluate('(1+1+1)/3*100')).to eq(100)
170
179
  end
171
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
+
172
189
  it 'fails to evaluate unbound statements' do
173
190
  unbound = 'foo * 1.5'
174
191
  expect { calculator.evaluate!(unbound) }.to raise_error(Dentaku::UnboundVariableError)
@@ -180,6 +197,11 @@ describe Dentaku::Calculator do
180
197
  expect(calculator.evaluate(unbound) { |e| e }).to eq unbound
181
198
  end
182
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
+
183
205
  it 'evaluates unbound statements given a binding in memory' do
184
206
  expect(calculator.evaluate('foo * 1.5', foo: 2)).to eq(3)
185
207
  expect(calculator.bind(monkeys: 3).evaluate('monkeys < 7')).to be_truthy
@@ -223,6 +245,20 @@ describe Dentaku::Calculator do
223
245
  expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
224
246
  end
225
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
+
226
262
  describe 'functions' do
227
263
  it 'include IF' do
228
264
  expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
@@ -281,16 +317,16 @@ describe Dentaku::Calculator do
281
317
  end
282
318
  end
283
319
 
284
- describe 'explicit NULL' do
285
- it 'can be used in IF statements' do
320
+ describe 'nil values' do
321
+ it 'can be used explicitly' do
286
322
  expect(calculator.evaluate('IF(null, 1, 2)')).to eq(2)
287
323
  end
288
324
 
289
- it 'can be used in IF statements when passed in' do
325
+ it 'can be assigned to a variable' do
290
326
  expect(calculator.evaluate('IF(foo, 1, 2)', foo: nil)).to eq(2)
291
327
  end
292
328
 
293
- it 'nil values are carried across middle terms' do
329
+ it 'are carried across middle terms' do
294
330
  results = calculator.solve!(
295
331
  choice: 'IF(bar, 1, 2)',
296
332
  bar: 'foo',
@@ -302,7 +338,7 @@ describe Dentaku::Calculator do
302
338
  )
303
339
  end
304
340
 
305
- it 'raises errors when used in arithmetic operation' do
341
+ it 'raise errors when used in arithmetic operations' do
306
342
  expect {
307
343
  calculator.solve!(more_apples: "apples + 1", apples: nil)
308
344
  }.to raise_error(Dentaku::ArgumentError)
@@ -475,9 +511,9 @@ describe Dentaku::Calculator do
475
511
  end
476
512
 
477
513
  describe 'string functions' do
478
- it 'concatenates two strings' do
514
+ it 'concatenates strings' do
479
515
  expect(
480
- calculator.evaluate('CONCAT(s1, s2)', 's1' => 'abc', 's2' => 'def')
516
+ calculator.evaluate('CONCAT(s1, s2, s3)', 's1' => 'ab', 's2' => 'cd', 's3' => 'ef')
481
517
  ).to eq 'abcdef'
482
518
  end
483
519
  end
@@ -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
@@ -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.10
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-12-30 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