dentaku 2.0.9 → 2.0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +16 -0
- data/README.md +9 -7
- data/lib/dentaku/ast/arithmetic.rb +43 -17
- data/lib/dentaku/ast/bitwise.rb +17 -0
- data/lib/dentaku/ast/case.rb +2 -3
- 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 +33 -25
- data/lib/dentaku/ast/negation.rb +6 -2
- data/lib/dentaku/ast.rb +3 -0
- data/lib/dentaku/calculator.rb +26 -5
- data/lib/dentaku/parser.rb +27 -8
- 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/case_spec.rb +6 -2
- data/spec/ast/function_spec.rb +6 -0
- data/spec/ast/string_functions_spec.rb +9 -0
- data/spec/calculator_spec.rb +77 -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,19 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v2.0.11] 2017-05-08
|
4
|
+
- fix dependency checking for logical AST nodes
|
5
|
+
- make `CONCAT` variadic
|
6
|
+
- fix casting strings to numeric in negation operations
|
7
|
+
- add date/time support
|
8
|
+
- add `&` (bitwise and) and `|` (bitwise or) operators
|
9
|
+
- fix incompatibility with 'mathn' module
|
10
|
+
- add `CONTAINS` string function
|
11
|
+
|
12
|
+
## [v2.0.10] 2016-12-30
|
13
|
+
- fix string function initialization bug
|
14
|
+
- fix issues with CASE statements
|
15
|
+
- allow injecting AST cache
|
16
|
+
|
3
17
|
## [v2.0.9] 2016-09-19
|
4
18
|
- namespace tokenization errors
|
5
19
|
- automatically coerce arguments to string functions as strings
|
@@ -111,6 +125,8 @@
|
|
111
125
|
## [v0.1.0] 2012-01-20
|
112
126
|
- initial release
|
113
127
|
|
128
|
+
[v2.0.11]: https://github.com/rubysolo/dentaku/compare/v2.0.10...v2.0.11
|
129
|
+
[v2.0.10]: https://github.com/rubysolo/dentaku/compare/v2.0.9...v2.0.10
|
114
130
|
[v2.0.9]: https://github.com/rubysolo/dentaku/compare/v2.0.8...v2.0.9
|
115
131
|
[v2.0.8]: https://github.com/rubysolo/dentaku/compare/v2.0.7...v2.0.8
|
116
132
|
[v2.0.7]: https://github.com/rubysolo/dentaku/compare/v2.0.6...v2.0.7
|
data/README.md
CHANGED
@@ -123,17 +123,19 @@ application, AST caching will consume more memory with each new formula.
|
|
123
123
|
BUILT-IN OPERATORS AND FUNCTIONS
|
124
124
|
---------------------------------
|
125
125
|
|
126
|
-
Math: `+`, `-`, `*`, `/`,
|
126
|
+
Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`, `&`
|
127
127
|
|
128
|
-
|
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
|
@@ -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/case.rb
CHANGED
@@ -43,9 +43,8 @@ module Dentaku
|
|
43
43
|
def dependencies(context={})
|
44
44
|
# TODO: should short-circuit
|
45
45
|
@switch.dependencies(context) +
|
46
|
-
@conditions.flat_map
|
47
|
-
|
48
|
-
end
|
46
|
+
@conditions.flat_map { |condition| condition.dependencies(context) } +
|
47
|
+
@else.dependencies(context)
|
49
48
|
end
|
50
49
|
end
|
51
50
|
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)
|
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
|
@@ -4,9 +4,9 @@ module Dentaku
|
|
4
4
|
module AST
|
5
5
|
module StringFunctions
|
6
6
|
class Left < Function
|
7
|
-
def initialize(
|
8
|
-
|
9
|
-
@length =
|
7
|
+
def initialize(*args)
|
8
|
+
super
|
9
|
+
@string, @length = *@args
|
10
10
|
end
|
11
11
|
|
12
12
|
def value(context={})
|
@@ -17,9 +17,9 @@ module Dentaku
|
|
17
17
|
end
|
18
18
|
|
19
19
|
class Right < Function
|
20
|
-
def initialize(
|
21
|
-
|
22
|
-
@length =
|
20
|
+
def initialize(*args)
|
21
|
+
super
|
22
|
+
@string, @length = *@args
|
23
23
|
end
|
24
24
|
|
25
25
|
def value(context={})
|
@@ -30,10 +30,9 @@ module Dentaku
|
|
30
30
|
end
|
31
31
|
|
32
32
|
class Mid < Function
|
33
|
-
def initialize(
|
34
|
-
|
35
|
-
@offset =
|
36
|
-
@length = length
|
33
|
+
def initialize(*args)
|
34
|
+
super
|
35
|
+
@string, @offset, @length = *@args
|
37
36
|
end
|
38
37
|
|
39
38
|
def value(context={})
|
@@ -45,8 +44,9 @@ module Dentaku
|
|
45
44
|
end
|
46
45
|
|
47
46
|
class Len < Function
|
48
|
-
def initialize(
|
49
|
-
|
47
|
+
def initialize(*args)
|
48
|
+
super
|
49
|
+
@string = @args[0]
|
50
50
|
end
|
51
51
|
|
52
52
|
def value(context={})
|
@@ -56,9 +56,9 @@ module Dentaku
|
|
56
56
|
end
|
57
57
|
|
58
58
|
class Find < Function
|
59
|
-
def initialize(
|
60
|
-
|
61
|
-
@haystack =
|
59
|
+
def initialize(*args)
|
60
|
+
super
|
61
|
+
@needle, @haystack = *@args
|
62
62
|
end
|
63
63
|
|
64
64
|
def value(context={})
|
@@ -71,10 +71,9 @@ module Dentaku
|
|
71
71
|
end
|
72
72
|
|
73
73
|
class Substitute < Function
|
74
|
-
def initialize(
|
75
|
-
|
76
|
-
@search =
|
77
|
-
@replacement = replacement
|
74
|
+
def initialize(*args)
|
75
|
+
super
|
76
|
+
@original, @search, @replacement = *@args
|
78
77
|
end
|
79
78
|
|
80
79
|
def value(context={})
|
@@ -87,15 +86,23 @@ module Dentaku
|
|
87
86
|
end
|
88
87
|
|
89
88
|
class Concat < Function
|
90
|
-
def initialize(
|
91
|
-
|
92
|
-
@right = right
|
89
|
+
def initialize(*args)
|
90
|
+
super
|
93
91
|
end
|
94
92
|
|
95
93
|
def value(context={})
|
96
|
-
|
97
|
-
|
98
|
-
|
94
|
+
@args.map { |arg| arg.value(context).to_s }.join
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Contains < Function
|
99
|
+
def initialize(*args)
|
100
|
+
super
|
101
|
+
@needle, @haystack = *args
|
102
|
+
end
|
103
|
+
|
104
|
+
def value(context={})
|
105
|
+
@haystack.value(context).to_s.include? @needle.value(context).to_s
|
99
106
|
end
|
100
107
|
end
|
101
108
|
end
|
@@ -109,3 +116,4 @@ Dentaku::AST::Function.register_class(:len, Dentaku::AST::StringFunctions
|
|
109
116
|
Dentaku::AST::Function.register_class(:find, Dentaku::AST::StringFunctions::Find)
|
110
117
|
Dentaku::AST::Function.register_class(:substitute, Dentaku::AST::StringFunctions::Substitute)
|
111
118
|
Dentaku::AST::Function.register_class(:concat, Dentaku::AST::StringFunctions::Concat)
|
119
|
+
Dentaku::AST::Function.register_class(:contains, Dentaku::AST::StringFunctions::Contains)
|
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/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'
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -4,19 +4,29 @@ require 'dentaku/token'
|
|
4
4
|
require 'dentaku/dependency_resolver'
|
5
5
|
require 'dentaku/parser'
|
6
6
|
|
7
|
+
|
7
8
|
module Dentaku
|
8
9
|
class Calculator
|
9
10
|
attr_reader :result, :memory, :tokenizer
|
10
11
|
|
11
|
-
def initialize
|
12
|
+
def initialize(ast_cache={})
|
12
13
|
clear
|
13
14
|
@tokenizer = Tokenizer.new
|
14
|
-
@ast_cache =
|
15
|
+
@ast_cache = ast_cache
|
15
16
|
@disable_ast_cache = false
|
17
|
+
@function_registry = Dentaku::AST::FunctionRegistry.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.add_function(name, type, body)
|
21
|
+
Dentaku::AST::FunctionRegistry.default.register(name, type, body)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_functions(fns)
|
25
|
+
fns.each { |(name, type, body)| add_function(name, type, body) }
|
16
26
|
end
|
17
27
|
|
18
28
|
def add_function(name, type, body)
|
19
|
-
|
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
|
|
@@ -67,15 +71,24 @@ module Dentaku
|
|
67
71
|
# special handling for case nesting: strip out inner case
|
68
72
|
# statements and parse their AST segments recursively
|
69
73
|
if operations.include?(AST::Case)
|
70
|
-
|
71
|
-
|
74
|
+
open_cases = 0
|
75
|
+
case_end_index = nil
|
76
|
+
|
72
77
|
input.each_with_index do |token, index|
|
73
|
-
|
78
|
+
if token.category == :case && token.value == :open
|
79
|
+
open_cases += 1
|
80
|
+
end
|
81
|
+
|
74
82
|
if token.category == :case && token.value == :close
|
75
|
-
|
83
|
+
if open_cases > 0
|
84
|
+
open_cases -= 1
|
85
|
+
else
|
86
|
+
case_end_index = index
|
87
|
+
break
|
88
|
+
end
|
76
89
|
end
|
77
90
|
end
|
78
|
-
inner_case_inputs = input.slice!(0..
|
91
|
+
inner_case_inputs = input.slice!(0..case_end_index)
|
79
92
|
subparser = Parser.new(
|
80
93
|
inner_case_inputs,
|
81
94
|
operations: [AST::Case],
|
@@ -202,6 +215,8 @@ module Dentaku
|
|
202
215
|
pow: AST::Exponentiation,
|
203
216
|
negate: AST::Negation,
|
204
217
|
mod: AST::Modulo,
|
218
|
+
bitor: AST::BitwiseOr,
|
219
|
+
bitand: AST::BitwiseAnd,
|
205
220
|
|
206
221
|
lt: AST::LessThan,
|
207
222
|
gt: AST::GreaterThan,
|
@@ -216,7 +231,11 @@ module Dentaku
|
|
216
231
|
end
|
217
232
|
|
218
233
|
def function(token)
|
219
|
-
|
234
|
+
function_registry.get(token.value)
|
235
|
+
end
|
236
|
+
|
237
|
+
def function_registry
|
238
|
+
@function_registry ||= Dentaku::AST::FunctionRegistry.new
|
220
239
|
end
|
221
240
|
end
|
222
241
|
end
|
@@ -93,11 +93,12 @@ module Dentaku
|
|
93
93
|
@values.empty? || @values.key?(value)
|
94
94
|
end
|
95
95
|
|
96
|
+
def self.datetime; new(:datetime); end
|
96
97
|
def self.numeric; new(:numeric); end
|
97
98
|
def self.string; new(:string); end
|
98
99
|
def self.logical; new(:logical); end
|
99
100
|
def self.value
|
100
|
-
new(:numeric) | new(:string) | new(:logical)
|
101
|
+
new(:datetime) | new(:numeric) | new(:string) | new(:logical)
|
101
102
|
end
|
102
103
|
|
103
104
|
def self.addsub; new(:operator, [:add, :subtract]); end
|
@@ -12,7 +12,7 @@ module Dentaku
|
|
12
12
|
|
13
13
|
def self.matcher(symbol)
|
14
14
|
@matchers ||= [
|
15
|
-
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
15
|
+
:datetime, :numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
16
16
|
:comparator, :comp_gt, :comp_lt, :open, :close, :comma,
|
17
17
|
:non_close_plus, :non_group, :non_group_star, :arguments,
|
18
18
|
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'bigdecimal'
|
2
|
+
require 'time'
|
2
3
|
require 'dentaku/token'
|
3
4
|
|
4
5
|
module Dentaku
|
@@ -28,6 +29,7 @@ module Dentaku
|
|
28
29
|
[
|
29
30
|
:null,
|
30
31
|
:whitespace,
|
32
|
+
:datetime, # before numeric so it can pick up timestamps
|
31
33
|
:numeric,
|
32
34
|
:double_quoted_string,
|
33
35
|
:single_quoted_string,
|
@@ -73,6 +75,11 @@ module Dentaku
|
|
73
75
|
new(:null, 'null\b')
|
74
76
|
end
|
75
77
|
|
78
|
+
# NOTE: Convert to DateTime as Array(Time) returns the parts of the time for some reason
|
79
|
+
def datetime
|
80
|
+
new(:datetime, /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?/, lambda { |raw| Time.parse(raw).to_datetime })
|
81
|
+
end
|
82
|
+
|
76
83
|
def numeric
|
77
84
|
new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
|
78
85
|
end
|
@@ -97,8 +104,8 @@ module Dentaku
|
|
97
104
|
end
|
98
105
|
|
99
106
|
def operator
|
100
|
-
names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%' }.invert
|
101
|
-
new(:operator, '
|
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
|
data/spec/ast/case_spec.rb
CHANGED
@@ -67,14 +67,18 @@ describe Dentaku::AST::Case do
|
|
67
67
|
let!(:tax) do
|
68
68
|
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :tax))
|
69
69
|
end
|
70
|
+
let!(:fallback) do
|
71
|
+
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :fallback))
|
72
|
+
end
|
70
73
|
let!(:addition) { Dentaku::AST::Addition.new(two, tax) }
|
71
74
|
let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
|
72
75
|
let!(:then2) { Dentaku::AST::CaseThen.new(addition) }
|
76
|
+
let!(:else2) { Dentaku::AST::CaseElse.new(fallback) }
|
73
77
|
let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
|
74
78
|
|
75
79
|
it 'gathers dependencies from switch and conditionals' do
|
76
|
-
node = described_class.new(switch, conditional1, conditional2)
|
77
|
-
expect(node.dependencies).to eq([:fruit, :tax])
|
80
|
+
node = described_class.new(switch, conditional1, conditional2, else2)
|
81
|
+
expect(node.dependencies).to eq([:fruit, :tax, :fallback])
|
78
82
|
end
|
79
83
|
end
|
80
84
|
end
|
data/spec/ast/function_spec.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'dentaku/ast/function'
|
3
3
|
|
4
|
+
class Clazz; end
|
5
|
+
|
4
6
|
describe Dentaku::AST::Function do
|
5
7
|
it 'maintains a function registry' do
|
6
8
|
expect(described_class).to respond_to(:get)
|
@@ -18,4 +20,8 @@ describe Dentaku::AST::Function do
|
|
18
20
|
function = described_class.get("flarble").new
|
19
21
|
expect(function.value).to eq "flarble"
|
20
22
|
end
|
23
|
+
|
24
|
+
it 'does not throw an error when registering a function with a name that matches a currently defined constant' do
|
25
|
+
expect { described_class.register("clazz", :string, -> { "clazzified" }) }.not_to raise_error
|
26
|
+
end
|
21
27
|
end
|
@@ -133,3 +133,12 @@ describe Dentaku::AST::StringFunctions::Concat do
|
|
133
133
|
expect(subject.value).to eq ''
|
134
134
|
end
|
135
135
|
end
|
136
|
+
|
137
|
+
describe Dentaku::AST::StringFunctions::Contains do
|
138
|
+
it 'checks for substrings' do
|
139
|
+
subject = described_class.new(literal('app'), literal('apple'))
|
140
|
+
expect(subject.value).to be_truthy
|
141
|
+
subject = described_class.new(literal('app'), literal('orange'))
|
142
|
+
expect(subject.value).to be_falsy
|
143
|
+
end
|
144
|
+
end
|
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
|
@@ -66,6 +75,12 @@ describe Dentaku::Calculator do
|
|
66
75
|
expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
|
67
76
|
end
|
68
77
|
|
78
|
+
it "finds dependencies in formula arguments" do
|
79
|
+
allow(Dentaku).to receive(:cache_ast?) { true }
|
80
|
+
|
81
|
+
expect(calculator.dependencies("CONCAT(bob, dole)")).to eq(['bob', 'dole'])
|
82
|
+
end
|
83
|
+
|
69
84
|
it "doesn't consider variables in memory as dependencies" do
|
70
85
|
expect(with_memory.dependencies("apples + oranges")).to eq(['oranges'])
|
71
86
|
end
|
@@ -163,6 +178,14 @@ describe Dentaku::Calculator do
|
|
163
178
|
expect(calculator.evaluate('(1+1+1)/3*100')).to eq(100)
|
164
179
|
end
|
165
180
|
|
181
|
+
it 'evaluates negation' do
|
182
|
+
expect(calculator.evaluate('-negative', negative: -1)).to eq(1)
|
183
|
+
expect(calculator.evaluate('-negative', negative: '-1')).to eq(1)
|
184
|
+
expect(calculator.evaluate('-negative - 1', negative: '-1')).to eq(0)
|
185
|
+
expect(calculator.evaluate('-negative - 1', negative: '1')).to eq(-2)
|
186
|
+
expect(calculator.evaluate('-(negative) - 1', negative: '1')).to eq(-2)
|
187
|
+
end
|
188
|
+
|
166
189
|
it 'fails to evaluate unbound statements' do
|
167
190
|
unbound = 'foo * 1.5'
|
168
191
|
expect { calculator.evaluate!(unbound) }.to raise_error(Dentaku::UnboundVariableError)
|
@@ -174,6 +197,11 @@ describe Dentaku::Calculator do
|
|
174
197
|
expect(calculator.evaluate(unbound) { |e| e }).to eq unbound
|
175
198
|
end
|
176
199
|
|
200
|
+
it 'fails to evaluate incomplete statements' do
|
201
|
+
incomplete = 'true AND'
|
202
|
+
expect { calculator.evaluate!(incomplete) }.to raise_error(Dentaku::ParseError)
|
203
|
+
end
|
204
|
+
|
177
205
|
it 'evaluates unbound statements given a binding in memory' do
|
178
206
|
expect(calculator.evaluate('foo * 1.5', foo: 2)).to eq(3)
|
179
207
|
expect(calculator.bind(monkeys: 3).evaluate('monkeys < 7')).to be_truthy
|
@@ -217,6 +245,20 @@ describe Dentaku::Calculator do
|
|
217
245
|
expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
|
218
246
|
end
|
219
247
|
|
248
|
+
it 'compares Time variables' do
|
249
|
+
expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_truthy
|
250
|
+
expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_falsy
|
251
|
+
expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_falsy
|
252
|
+
expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_truthy
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'compares Time literals with Time variables' do
|
256
|
+
expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_truthy
|
257
|
+
expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_falsy
|
258
|
+
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_falsy
|
259
|
+
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
|
260
|
+
end
|
261
|
+
|
220
262
|
describe 'functions' do
|
221
263
|
it 'include IF' do
|
222
264
|
expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
|
@@ -275,16 +317,16 @@ describe Dentaku::Calculator do
|
|
275
317
|
end
|
276
318
|
end
|
277
319
|
|
278
|
-
describe '
|
279
|
-
it 'can be used
|
320
|
+
describe 'nil values' do
|
321
|
+
it 'can be used explicitly' do
|
280
322
|
expect(calculator.evaluate('IF(null, 1, 2)')).to eq(2)
|
281
323
|
end
|
282
324
|
|
283
|
-
it 'can be
|
325
|
+
it 'can be assigned to a variable' do
|
284
326
|
expect(calculator.evaluate('IF(foo, 1, 2)', foo: nil)).to eq(2)
|
285
327
|
end
|
286
328
|
|
287
|
-
it '
|
329
|
+
it 'are carried across middle terms' do
|
288
330
|
results = calculator.solve!(
|
289
331
|
choice: 'IF(bar, 1, 2)',
|
290
332
|
bar: 'foo',
|
@@ -296,7 +338,7 @@ describe Dentaku::Calculator do
|
|
296
338
|
)
|
297
339
|
end
|
298
340
|
|
299
|
-
it '
|
341
|
+
it 'raise errors when used in arithmetic operations' do
|
300
342
|
expect {
|
301
343
|
calculator.solve!(more_apples: "apples + 1", apples: nil)
|
302
344
|
}.to raise_error(Dentaku::ArgumentError)
|
@@ -379,6 +421,34 @@ describe Dentaku::Calculator do
|
|
379
421
|
fruit: 'banana')
|
380
422
|
expect(value).to eq(5)
|
381
423
|
end
|
424
|
+
|
425
|
+
it 'handles multiple nested case statements' do
|
426
|
+
formula = <<-FORMULA
|
427
|
+
CASE fruit
|
428
|
+
WHEN 'apple'
|
429
|
+
THEN
|
430
|
+
CASE quantity
|
431
|
+
WHEN 2 THEN 3
|
432
|
+
END
|
433
|
+
WHEN 'banana'
|
434
|
+
THEN
|
435
|
+
CASE quantity
|
436
|
+
WHEN 1 THEN 2
|
437
|
+
END
|
438
|
+
END
|
439
|
+
FORMULA
|
440
|
+
value = calculator.evaluate(
|
441
|
+
formula,
|
442
|
+
quantity: 1,
|
443
|
+
fruit: 'banana')
|
444
|
+
expect(value).to eq(2)
|
445
|
+
|
446
|
+
value = calculator.evaluate(
|
447
|
+
formula,
|
448
|
+
quantity: 2,
|
449
|
+
fruit: 'apple')
|
450
|
+
expect(value).to eq(3)
|
451
|
+
end
|
382
452
|
end
|
383
453
|
|
384
454
|
describe 'math functions' do
|
@@ -441,9 +511,9 @@ describe Dentaku::Calculator do
|
|
441
511
|
end
|
442
512
|
|
443
513
|
describe 'string functions' do
|
444
|
-
it 'concatenates
|
514
|
+
it 'concatenates strings' do
|
445
515
|
expect(
|
446
|
-
calculator.evaluate('CONCAT(s1, s2)', 's1' => '
|
516
|
+
calculator.evaluate('CONCAT(s1, s2, s3)', 's1' => 'ab', 's2' => 'cd', 's3' => 'ef')
|
447
517
|
).to eq 'abcdef'
|
448
518
|
end
|
449
519
|
end
|
data/spec/dentaku_spec.rb
CHANGED
@@ -19,4 +19,10 @@ describe Dentaku do
|
|
19
19
|
expect(Dentaku('40 + n', 'N' => 2)).to eql(42)
|
20
20
|
expect(Dentaku('40 + n', 'n' => 2)).to eql(42)
|
21
21
|
end
|
22
|
+
|
23
|
+
it 'raises a parse error for bad logic expressions' do
|
24
|
+
expect {
|
25
|
+
Dentaku('true AND')
|
26
|
+
}.to raise_error(Dentaku::ParseError)
|
27
|
+
end
|
22
28
|
end
|
@@ -52,5 +52,29 @@ describe Dentaku::Calculator do
|
|
52
52
|
expect(calculator.evaluate("INCLUDES(list, 2)", list: [1,2,3])).to eq(true)
|
53
53
|
end
|
54
54
|
end
|
55
|
+
|
56
|
+
it 'allows registering "bang" functions' do
|
57
|
+
calculator = described_class.new
|
58
|
+
calculator.add_function(:hey!, :string, -> { "hey!" })
|
59
|
+
expect(calculator.evaluate("hey!()")).to eq("hey!")
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'does not store functions across all calculators' do
|
63
|
+
calculator1 = Dentaku::Calculator.new
|
64
|
+
calculator1.add_function(:my_function, :numeric, ->(x) { 2*x + 1 })
|
65
|
+
|
66
|
+
calculator2 = Dentaku::Calculator.new
|
67
|
+
calculator2.add_function(:my_function, :numeric, ->(x) { 4*x + 3 })
|
68
|
+
|
69
|
+
expect(calculator1.evaluate("1 + my_function(2)")). to eq (1 + 2*2 + 1)
|
70
|
+
expect(calculator2.evaluate("1 + my_function(2)")). to eq (1 + 4*2 + 3)
|
71
|
+
|
72
|
+
expect{Dentaku::Calculator.new.evaluate("1 + my_function(2)")}.to raise_error(Dentaku::ParseError)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'self.add_function adds to default/global function registry' do
|
76
|
+
Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
|
77
|
+
expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq (10 + 3**2 + 5)
|
78
|
+
end
|
55
79
|
end
|
56
80
|
end
|
data/spec/parser_spec.rb
CHANGED
@@ -35,6 +35,15 @@ describe Dentaku::Parser do
|
|
35
35
|
expect(node.value).to eq 0.05
|
36
36
|
end
|
37
37
|
|
38
|
+
it 'calculates bitwise OR' do
|
39
|
+
two = Dentaku::Token.new(:numeric, 2)
|
40
|
+
bitor = Dentaku::Token.new(:operator, :bitor)
|
41
|
+
three = Dentaku::Token.new(:numeric, 3)
|
42
|
+
|
43
|
+
node = described_class.new([two, bitor, three]).parse
|
44
|
+
expect(node.value).to eq 3
|
45
|
+
end
|
46
|
+
|
38
47
|
it 'performs multiple operations in one stream' do
|
39
48
|
five = Dentaku::Token.new(:numeric, 5)
|
40
49
|
plus = Dentaku::Token.new(:operator, :add)
|
@@ -132,14 +141,25 @@ describe Dentaku::Parser do
|
|
132
141
|
expect(node.value(x: 3)).to eq(4)
|
133
142
|
end
|
134
143
|
|
135
|
-
|
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
|