dentaku 3.5.0 → 3.5.1

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 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