dentaku 2.0.10 → 2.0.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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