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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a0b093e29b178197c0f92c1f63ec56342716c1fa84a4d12a73a7d355d42bc76
4
- data.tar.gz: 480ccf9248568006227518363a0ef6350843007389f2e94ac5610ae62028e87e
3
+ metadata.gz: d5592654ee45adeb24167b584374fb2d69bc67d1db4d5f4ac99050130f187f8f
4
+ data.tar.gz: 31d3952b08887ae934661f4c0ecc5c298b2f36f0f8365cc1503495eb044eb558
5
5
  SHA512:
6
- metadata.gz: 904292b2d2fd834701fd18900d689b9125579d58421fc58aaad03cd75c0fb556eeaeb5635dcd034a3f29e9f70f5c8fe0370e605acefd599c3dd75eae64436fdd
7
- data.tar.gz: 3b6ed8763b9241e55e85f1a1e5a09d2652ec91eee21c080ca825029e04867b8fe65b128ddfc557cab3ef4fae76abb30b936f5971326355ea763081f90ba66638
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
- begin
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
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, prefer_integer = true)
43
+ def cast(val)
45
44
  validate_value(val)
46
- numeric(val, prefer_integer)
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 numeric(val, prefer_integer)
50
- v = BigDecimal(val, Float::DIG + 1)
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), false)
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)
@@ -2,23 +2,40 @@ require_relative './operation'
2
2
 
3
3
  module Dentaku
4
4
  module AST
5
- class BitwiseOr < Operation
5
+ class Bitwise < Operation
6
6
  def value(context = {})
7
- left.value(context) | right.value(context)
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 < Operation
16
- def value(context = {})
17
- left.value(context) & right.value(context)
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, Dentaku::ZeroDivisionError => ex
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]
@@ -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
@@ -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, '\^|\+|-|\*|\/|%|\||&', lambda { |raw| names[raw] })
125
+ new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
126
126
  end
127
127
 
128
128
  def grouping
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.0"
2
+ VERSION = "3.5.1"
3
3
  end
@@ -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
@@ -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
@@ -6,7 +6,7 @@ describe 'Dentaku::AST::Or' do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
7
 
8
8
  it 'returns false if all of the arguments are false' do
9
- result = Dentaku('OR(1 = "1", 0 = 1)')
9
+ result = Dentaku('OR(1 = "2", 0 = 1)')
10
10
  expect(result).to eq(false)
11
11
  end
12
12
 
@@ -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
@@ -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
@@ -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.0
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-03-17 00:00:00.000000000 Z
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.9
286
+ rubygems_version: 3.3.7
287
287
  signing_key:
288
288
  specification_version: 4
289
289
  summary: A formula language parser and evaluator