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 +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +11 -1
- data/README.md +9 -7
- data/lib/dentaku/ast.rb +3 -0
- data/lib/dentaku/ast/arithmetic.rb +43 -17
- data/lib/dentaku/ast/bitwise.rb +17 -0
- data/lib/dentaku/ast/combinators.rb +1 -1
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +5 -46
- data/lib/dentaku/ast/function_registry.rb +64 -0
- data/lib/dentaku/ast/functions/string_functions.rb +13 -4
- data/lib/dentaku/ast/negation.rb +6 -2
- data/lib/dentaku/calculator.rb +24 -3
- data/lib/dentaku/parser.rb +13 -3
- data/lib/dentaku/token_matcher.rb +2 -1
- data/lib/dentaku/token_matchers.rb +1 -1
- data/lib/dentaku/token_scanner.rb +11 -4
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/addition_spec.rb +27 -0
- data/spec/ast/string_functions_spec.rb +9 -0
- data/spec/calculator_spec.rb +43 -7
- data/spec/dentaku_spec.rb +6 -0
- data/spec/external_function_spec.rb +24 -0
- data/spec/parser_spec.rb +28 -8
- data/spec/token_scanner_spec.rb +1 -1
- data/spec/tokenizer_spec.rb +40 -11
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0febf923ab551e0cfce23bc38794927a3acedbe2
|
4
|
+
data.tar.gz: 3b4db1f045f21acd1e6a7c864ed9a4c272a2f765
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dad6f87cf37ba49f355b0e30b92a80dfc063eeb9444653ea09832066831dc09c19e022883b2c31c23189a1c46c842ce652fba133725fa4f887db1fb5a9becf2a
|
7
|
+
data.tar.gz: 5b01fa83b5ddd2f3123946196ff6f0be42f1bd196b38195249d24ee2c6e08c1ad077ffd0da2f2cf33dd9c664e447ffe6864ab0f021a79e30e51769189225ef53
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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
|
-
[
|
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
|
-
|
128
|
+
Also, all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
|
129
129
|
|
130
|
-
|
130
|
+
Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
|
131
131
|
|
132
|
-
|
132
|
+
Logic: `IF`, `AND`, `OR`, `NOT`
|
133
|
+
|
134
|
+
Functions: `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
|
133
135
|
|
134
|
-
|
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-
|
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
|
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'
|
@@ -7,7 +7,7 @@ module Dentaku
|
|
7
7
|
class Arithmetic < Operation
|
8
8
|
def initialize(*)
|
9
9
|
super
|
10
|
-
unless
|
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(
|
28
|
-
|
29
|
-
|
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
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
data/lib/dentaku/ast/function.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
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
|
-
|
96
|
-
|
97
|
-
|
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)
|
data/lib/dentaku/ast/negation.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
module Dentaku
|
2
2
|
module AST
|
3
|
-
class Negation <
|
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/calculator.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/dentaku/parser.rb
CHANGED
@@ -7,8 +7,9 @@ module Dentaku
|
|
7
7
|
def initialize(tokens, options={})
|
8
8
|
@input = tokens.dup
|
9
9
|
@output = []
|
10
|
-
@operations
|
11
|
-
@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
|
-
|
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, '
|
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
|
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
|
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/addition_spec.rb
CHANGED
@@ -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
|
data/spec/calculator_spec.rb
CHANGED
@@ -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 '
|
285
|
-
it 'can be used
|
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
|
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 '
|
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 '
|
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
|
514
|
+
it 'concatenates strings' do
|
479
515
|
expect(
|
480
|
-
calculator.evaluate('CONCAT(s1, s2)', 's1' => '
|
516
|
+
calculator.evaluate('CONCAT(s1, s2, s3)', 's1' => 'ab', 's2' => 'cd', 's3' => 'ef')
|
481
517
|
).to eq 'abcdef'
|
482
518
|
end
|
483
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
data/spec/token_scanner_spec.rb
CHANGED
data/spec/tokenizer_spec.rb
CHANGED
@@ -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.
|
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:
|
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
|