dentaku 2.0.9 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|