dentaku 3.5.0 → 3.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +1 -1
- data/lib/dentaku/ast/arithmetic.rb +18 -17
- data/lib/dentaku/ast/bitwise.rb +23 -6
- data/lib/dentaku/ast/comparators.rb +12 -2
- data/lib/dentaku/bulk_expression_solver.rb +1 -5
- data/lib/dentaku/exceptions.rb +2 -2
- data/lib/dentaku/parser.rb +5 -0
- data/lib/dentaku/token_scanner.rb +2 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +7 -0
- data/spec/ast/comparator_spec.rb +8 -0
- data/spec/ast/or_spec.rb +1 -1
- data/spec/bulk_expression_solver_spec.rb +9 -0
- data/spec/calculator_spec.rb +13 -0
- data/spec/tokenizer_spec.rb +12 -0
- data/spec/visitor_spec.rb +2 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5592654ee45adeb24167b584374fb2d69bc67d1db4d5f4ac99050130f187f8f
|
4
|
+
data.tar.gz: 31d3952b08887ae934661f4c0ecc5c298b2f36f0f8365cc1503495eb044eb558
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8e0d003f897e06173c91b200e62d9fed12ec3bacfe0f2ecc3f3705a1cbf914d1705948b79962f5ac6a5397138ff89c2123aa5c3753963f0046a88280b29825b
|
7
|
+
data.tar.gz: 5f48d3b8fef4e56ed308e717da88937bef508e47dfca4c3e16610aff7523f11405e53e2b55d00c53b5eca8efbe7be64df0d0d12e7ddafbc61099cde08bf92a53
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v3.5.1]
|
4
|
+
- add bitwise shift left and shift right operators
|
5
|
+
- improve numeric conversions
|
6
|
+
- improve parse exceptions
|
7
|
+
- improve bitwise exceptions
|
8
|
+
- include variable name in bulk expression exceptions
|
9
|
+
|
3
10
|
## [v3.5.0]
|
4
11
|
- fix bug with function argument count
|
5
12
|
- add XOR operator
|
@@ -224,6 +231,8 @@
|
|
224
231
|
## [v0.1.0] 2012-01-20
|
225
232
|
- initial release
|
226
233
|
|
234
|
+
[Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.1...HEAD
|
235
|
+
[v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
|
227
236
|
[v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
|
228
237
|
[v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
|
229
238
|
[v3.4.1]: https://github.com/rubysolo/dentaku/compare/v3.4.0...v3.4.1
|
data/README.md
CHANGED
@@ -137,7 +137,7 @@ application, AST caching will consume more memory with each new formula.
|
|
137
137
|
BUILT-IN OPERATORS AND FUNCTIONS
|
138
138
|
---------------------------------
|
139
139
|
|
140
|
-
Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`,
|
140
|
+
Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`, `&`, `<<`, `>>`
|
141
141
|
|
142
142
|
Also, all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
|
143
143
|
|
@@ -31,29 +31,30 @@ module Dentaku
|
|
31
31
|
def value(context = {})
|
32
32
|
l = cast(left.value(context))
|
33
33
|
r = cast(right.value(context))
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
34
|
+
|
35
|
+
l.public_send(operator, r)
|
36
|
+
rescue ::TypeError => e
|
37
|
+
# Right cannot be converted to a suitable type for left. e.g. [] + 1
|
38
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
|
40
39
|
end
|
41
40
|
|
42
41
|
private
|
43
42
|
|
44
|
-
def cast(val
|
43
|
+
def cast(val)
|
45
44
|
validate_value(val)
|
46
|
-
numeric(val
|
45
|
+
numeric(val)
|
46
|
+
end
|
47
|
+
|
48
|
+
def numeric(val)
|
49
|
+
case val.to_s
|
50
|
+
when /\A\d*\.\d+\z/ then decimal(val)
|
51
|
+
when /\A-?\d+\z/ then val.to_i
|
52
|
+
else val
|
53
|
+
end
|
47
54
|
end
|
48
55
|
|
49
|
-
def
|
50
|
-
|
51
|
-
v = v.to_i if prefer_integer && v.frac.zero?
|
52
|
-
v
|
53
|
-
rescue ::TypeError
|
54
|
-
# If we got a TypeError BigDecimal or to_i failed;
|
55
|
-
# let value through so ruby things like Time - integer work
|
56
|
-
val
|
56
|
+
def decimal(val)
|
57
|
+
BigDecimal(val.to_s, Float::DIG + 1)
|
57
58
|
end
|
58
59
|
|
59
60
|
def valid_node?(node)
|
@@ -143,7 +144,7 @@ module Dentaku
|
|
143
144
|
end
|
144
145
|
|
145
146
|
def value(context = {})
|
146
|
-
r = cast(right.value(context)
|
147
|
+
r = decimal(cast(right.value(context)))
|
147
148
|
raise Dentaku::ZeroDivisionError if r.zero?
|
148
149
|
|
149
150
|
cast(cast(left.value(context)) / r)
|
data/lib/dentaku/ast/bitwise.rb
CHANGED
@@ -2,23 +2,40 @@ require_relative './operation'
|
|
2
2
|
|
3
3
|
module Dentaku
|
4
4
|
module AST
|
5
|
-
class
|
5
|
+
class Bitwise < Operation
|
6
6
|
def value(context = {})
|
7
|
-
|
7
|
+
left_value = left.value(context)
|
8
|
+
right_value = right.value(context)
|
9
|
+
|
10
|
+
left_value.public_send(operator, right_value)
|
11
|
+
rescue NoMethodError => e
|
12
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, value: left_value, for: left_value.class)
|
13
|
+
rescue TypeError => e
|
14
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, value: right_value, for: right_value.class)
|
8
15
|
end
|
16
|
+
end
|
9
17
|
|
18
|
+
class BitwiseOr < Bitwise
|
10
19
|
def operator
|
11
20
|
:|
|
12
21
|
end
|
13
22
|
end
|
14
23
|
|
15
|
-
class BitwiseAnd <
|
16
|
-
def
|
17
|
-
|
24
|
+
class BitwiseAnd < Bitwise
|
25
|
+
def operator
|
26
|
+
:&
|
18
27
|
end
|
28
|
+
end
|
19
29
|
|
30
|
+
class BitwiseShiftLeft < Bitwise
|
20
31
|
def operator
|
21
|
-
|
32
|
+
:<<
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class BitwiseShiftRight < Bitwise
|
37
|
+
def operator
|
38
|
+
:>>
|
22
39
|
end
|
23
40
|
end
|
24
41
|
end
|
@@ -16,8 +16,8 @@ module Dentaku
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def value(context = {})
|
19
|
-
l = validate_value(left.value(context))
|
20
|
-
r = validate_value(right.value(context))
|
19
|
+
l = validate_value(cast(left.value(context)))
|
20
|
+
r = validate_value(cast(right.value(context)))
|
21
21
|
|
22
22
|
l.public_send(operator, r)
|
23
23
|
rescue ::ArgumentError => e
|
@@ -26,6 +26,16 @@ module Dentaku
|
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
+
def cast(val)
|
30
|
+
return val unless val.is_a?(::String)
|
31
|
+
return val if val.empty?
|
32
|
+
return val unless val.match?(/\A-?\d*(\.\d+)?\z/)
|
33
|
+
|
34
|
+
v = BigDecimal(val, Float::DIG + 1)
|
35
|
+
v = v.to_i if v.frac.zero?
|
36
|
+
v
|
37
|
+
end
|
38
|
+
|
29
39
|
def validate_value(value)
|
30
40
|
unless value.respond_to?(operator)
|
31
41
|
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator),
|
@@ -89,13 +89,9 @@ module Dentaku
|
|
89
89
|
def with_rescues(var_name, results, block)
|
90
90
|
yield
|
91
91
|
|
92
|
-
rescue UnboundVariableError,
|
92
|
+
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
|
93
93
|
ex.recipient_variable = var_name
|
94
94
|
results[var_name] = block.call(ex)
|
95
|
-
|
96
|
-
rescue Dentaku::ArgumentError => ex
|
97
|
-
results[var_name] = block.call(ex)
|
98
|
-
|
99
95
|
ensure
|
100
96
|
if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
|
101
97
|
results[var_name] = calculator.memory[var_name.downcase]
|
data/lib/dentaku/exceptions.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
module Dentaku
|
2
2
|
class Error < StandardError
|
3
|
+
attr_accessor :recipient_variable
|
3
4
|
end
|
4
5
|
|
5
6
|
class UnboundVariableError < Error
|
6
|
-
attr_accessor :recipient_variable
|
7
|
-
|
8
7
|
attr_reader :unbound_variables
|
9
8
|
|
10
9
|
def initialize(unbound_variables)
|
@@ -74,6 +73,7 @@ module Dentaku
|
|
74
73
|
|
75
74
|
class ArgumentError < ::ArgumentError
|
76
75
|
attr_reader :reason, :meta
|
76
|
+
attr_accessor :recipient_variable
|
77
77
|
|
78
78
|
def initialize(reason, **meta)
|
79
79
|
@reason = reason
|
data/lib/dentaku/parser.rb
CHANGED
@@ -10,8 +10,11 @@ module Dentaku
|
|
10
10
|
pow: AST::Exponentiation,
|
11
11
|
negate: AST::Negation,
|
12
12
|
mod: AST::Modulo,
|
13
|
+
|
13
14
|
bitor: AST::BitwiseOr,
|
14
15
|
bitand: AST::BitwiseAnd,
|
16
|
+
bitshiftleft: AST::BitwiseShiftLeft,
|
17
|
+
bitshiftright: AST::BitwiseShiftRight,
|
15
18
|
|
16
19
|
lt: AST::LessThan,
|
17
20
|
gt: AST::GreaterThan,
|
@@ -38,6 +41,8 @@ module Dentaku
|
|
38
41
|
|
39
42
|
def consume(count = 2)
|
40
43
|
operator = operations.pop
|
44
|
+
fail! :invalid_statement if operator.nil?
|
45
|
+
|
41
46
|
operator.peek(output)
|
42
47
|
|
43
48
|
args_size = operator.arity || count
|
@@ -120,9 +120,9 @@ module Dentaku
|
|
120
120
|
|
121
121
|
def operator
|
122
122
|
names = {
|
123
|
-
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&'
|
123
|
+
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&', bitshiftleft: '<<', bitshiftright: '>>'
|
124
124
|
}.invert
|
125
|
-
new(:operator, '
|
125
|
+
new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
|
126
126
|
end
|
127
127
|
|
128
128
|
def grouping
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/arithmetic_spec.rb
CHANGED
@@ -45,6 +45,13 @@ describe Dentaku::AST::Arithmetic do
|
|
45
45
|
expect(add(x, one, 'x' => '.1')).to eq(1.1)
|
46
46
|
expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
|
47
47
|
expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
|
48
|
+
|
49
|
+
int_one = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, "1")
|
50
|
+
decimal_one = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, "1.0")
|
51
|
+
|
52
|
+
expect(add(int_one, int_one).class).to eq(Integer)
|
53
|
+
expect(add(int_one, decimal_one).class).to eq(BigDecimal)
|
54
|
+
expect(add(decimal_one, decimal_one).class).to eq(BigDecimal)
|
48
55
|
end
|
49
56
|
|
50
57
|
it 'performs arithmetic on arrays' do
|
data/spec/ast/comparator_spec.rb
CHANGED
@@ -5,7 +5,9 @@ require 'dentaku/token'
|
|
5
5
|
|
6
6
|
describe Dentaku::AST::Comparator do
|
7
7
|
let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
|
8
|
+
let(:one_str) { Dentaku::AST::String.new Dentaku::Token.new(:string, '1') }
|
8
9
|
let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
|
10
|
+
let(:two_str) { Dentaku::AST::String.new Dentaku::Token.new(:string, '2') }
|
9
11
|
let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
|
10
12
|
let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
|
11
13
|
let(:nilly) do
|
@@ -21,6 +23,12 @@ describe Dentaku::AST::Comparator do
|
|
21
23
|
expect(equal(x, y).value(ctx)).to be_falsey
|
22
24
|
end
|
23
25
|
|
26
|
+
it 'performs conversion from string to numeric operands' do
|
27
|
+
expect(less_than(one, two_str).value(ctx)).to be_truthy
|
28
|
+
expect(less_than(one_str, two_str).value(ctx)).to be_truthy
|
29
|
+
expect(less_than(one_str, two).value(ctx)).to be_truthy
|
30
|
+
end
|
31
|
+
|
24
32
|
it 'raises a dentaku argument error when incorrect arguments are passed in' do
|
25
33
|
expect { less_than(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
26
34
|
expect { less_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
data/spec/ast/or_spec.rb
CHANGED
@@ -143,6 +143,15 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
143
143
|
expect(exception.recipient_variable).to eq('more_apples')
|
144
144
|
end
|
145
145
|
|
146
|
+
it 'stores the recipient variable on the exception when there is an ArgumentError' do
|
147
|
+
expressions = {apples: "NULL", more_apples: "1 + apples"}
|
148
|
+
exception = nil
|
149
|
+
described_class.new(expressions, calculator).solve do |ex|
|
150
|
+
exception = ex
|
151
|
+
end
|
152
|
+
expect(exception.recipient_variable).to eq('more_apples')
|
153
|
+
end
|
154
|
+
|
146
155
|
it 'safely handles argument errors' do
|
147
156
|
expressions = {i: "a / 5 + d", a: "m * 12", d: "a + b"}
|
148
157
|
result = described_class.new(expressions, calculator.store(m: 3)).solve
|
data/spec/calculator_spec.rb
CHANGED
@@ -40,6 +40,8 @@ describe Dentaku::Calculator do
|
|
40
40
|
expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
|
41
41
|
expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
|
42
42
|
expect(calculator.evaluate("5%")).to eq (0.05)
|
43
|
+
expect(calculator.evaluate('1 << 3')).to eq (8)
|
44
|
+
expect(calculator.evaluate('0xFF >> 6')).to eq (3)
|
43
45
|
end
|
44
46
|
|
45
47
|
describe 'evaluate' do
|
@@ -67,6 +69,7 @@ describe Dentaku::Calculator do
|
|
67
69
|
expect(calculator.evaluate('ROUNDDOWN(a)', a: nil)).to be_nil
|
68
70
|
expect(calculator.evaluate('ROUNDUP(a)', a: nil)).to be_nil
|
69
71
|
expect(calculator.evaluate('SUM(a,b)', a: nil, b: nil)).to be_nil
|
72
|
+
expect(calculator.evaluate('1.0 & "bar"')).to be_nil
|
70
73
|
end
|
71
74
|
|
72
75
|
it 'treats explicit nil as logical false' do
|
@@ -82,6 +85,13 @@ describe Dentaku::Calculator do
|
|
82
85
|
end
|
83
86
|
end
|
84
87
|
|
88
|
+
describe 'ast' do
|
89
|
+
it 'raises parsing errors' do
|
90
|
+
expect { calculator.ast('()') }.to raise_error(Dentaku::ParseError)
|
91
|
+
expect { calculator.ast('(}') }.to raise_error(Dentaku::TokenizerError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
85
95
|
describe 'evaluate!' do
|
86
96
|
it 'raises exception when formula has error' do
|
87
97
|
expect { calculator.evaluate!('1 + + 1') }.to raise_error(Dentaku::ParseError)
|
@@ -108,6 +118,9 @@ describe Dentaku::Calculator do
|
|
108
118
|
expect { calculator.evaluate!('ROUNDDOWN(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
|
109
119
|
expect { calculator.evaluate!('ROUNDUP(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
|
110
120
|
expect { calculator.evaluate!('SUM(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
|
121
|
+
expect { calculator.evaluate!('"foo" & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
122
|
+
expect { calculator.evaluate!('1.0 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
123
|
+
expect { calculator.evaluate!('1 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
111
124
|
end
|
112
125
|
|
113
126
|
it 'raises argument error if a function is called with incorrect arity' do
|
data/spec/tokenizer_spec.rb
CHANGED
@@ -89,6 +89,18 @@ describe Dentaku::Tokenizer do
|
|
89
89
|
expect(tokens.map(&:value)).to eq([2, :bitand, 3])
|
90
90
|
end
|
91
91
|
|
92
|
+
it 'tokenizes bitwise SHIFT LEFT' do
|
93
|
+
tokens = tokenizer.tokenize('2 << 3')
|
94
|
+
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
95
|
+
expect(tokens.map(&:value)).to eq([2, :bitshiftleft, 3])
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'tokenizes bitwise SHIFT RIGHT' do
|
99
|
+
tokens = tokenizer.tokenize('2 >> 3')
|
100
|
+
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
101
|
+
expect(tokens.map(&:value)).to eq([2, :bitshiftright, 3])
|
102
|
+
end
|
103
|
+
|
92
104
|
it 'ignores whitespace' do
|
93
105
|
tokens = tokenizer.tokenize('1 / 1 ')
|
94
106
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
data/spec/visitor_spec.rb
CHANGED
@@ -90,6 +90,7 @@ describe TestVisitor do
|
|
90
90
|
def generic_subclasses
|
91
91
|
[
|
92
92
|
:Arithmetic,
|
93
|
+
:Bitwise,
|
93
94
|
:Combinator,
|
94
95
|
:Comparator,
|
95
96
|
:Function,
|
@@ -111,7 +112,7 @@ describe TestVisitor do
|
|
111
112
|
visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
|
112
113
|
visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
|
113
114
|
visit_nodes('case (a % 5) when 0 then a else b end')
|
114
|
-
visit_nodes('0xCAFE & 0xDECAF | 0xBEEF')
|
115
|
+
visit_nodes('0xCAFE & (0xDECAF << 3) | (0xBEEF >> 5)')
|
115
116
|
visit_nodes('2017-12-24 23:59:59')
|
116
117
|
visit_nodes('ALL({1, 2, 3}, "val", val % 2 == 0)')
|
117
118
|
visit_nodes('ANY(vals, val, val > 1)')
|
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: 3.5.
|
4
|
+
version: 3.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -283,7 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
283
283
|
- !ruby/object:Gem::Version
|
284
284
|
version: '0'
|
285
285
|
requirements: []
|
286
|
-
rubygems_version: 3.3.
|
286
|
+
rubygems_version: 3.3.7
|
287
287
|
signing_key:
|
288
288
|
specification_version: 4
|
289
289
|
summary: A formula language parser and evaluator
|