dentaku 3.1.0 → 3.2.0

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
  SHA1:
3
- metadata.gz: 32158c76337de5c5f07140c71cb70fa6ea1d2963
4
- data.tar.gz: 42bc70de0b8208fda14f82850c5362d34410bd16
3
+ metadata.gz: 402c5a79a4d28d7323c53de8852007fa1c49c087
4
+ data.tar.gz: e5bd251b569708d740a7aeedde619e3bbfa34037
5
5
  SHA512:
6
- metadata.gz: 7362ea2c8a48f4e218807c6a5d979c64460e19bd4cd26a356433b77200fec7871b8d13343c99404979179d2362ec8028f4205990a38c48cb70ed101c06a3b57f
7
- data.tar.gz: b399f646e1cf8671041d11a07db4f65d7b21f3c4bd31cd91ad643aa8ad6266272b75713de501c5307395bcf67867209554e085a8e0a5aafd088a9dca8364cc3f
6
+ metadata.gz: a7dda0285c345400228dd7ec606958a95b9f57bd54f87a4333b4533d8f671b9ac66485ec8d29f66360b0dc026b00bb57ca842f4cf07e763c5840a2d84fa5a89e
7
+ data.tar.gz: d58d8f1451b0092feb6a8038f0d1efe8b6957c5de0a67badf53538395963098912853ea848c5fab03581ffeeddf89e4bb8812a735ec8ba6be48a76db35760e97
@@ -3,9 +3,10 @@ sudo: false
3
3
  rvm:
4
4
  - 2.0.0-p648
5
5
  - 2.1.10
6
- - 2.2.8
7
- - 2.3.5
8
- - 2.4.2
6
+ - 2.2.9
7
+ - 2.3.6
8
+ - 2.4.3
9
+ - 2.5.0
9
10
  before_install:
10
11
  - gem update bundler
11
12
  - gem update --system
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## [v3.2.0] Unreleased
4
+ - add `COUNT` and `AVG` functions
5
+ - add unicode support 😎
6
+ - fix CASE parsing bug
7
+ - allow dependency filtering based on context
8
+ - add variadic MUL function
9
+ - performance optimization
10
+
3
11
  ## [v3.1.0] 2017-01-10
4
12
  - allow decimals with no leading zero
5
13
  - nested hash and array support in bulk expression solver
@@ -151,6 +159,7 @@
151
159
  ## [v0.1.0] 2012-01-20
152
160
  - initial release
153
161
 
162
+ [v3.2.0]: https://github.com/rubysolo/dentaku/compare/v3.1.0...v3.2.0
154
163
  [v3.1.0]: https://github.com/rubysolo/dentaku/compare/v3.0.0...v3.1.0
155
164
  [v3.0.0]: https://github.com/rubysolo/dentaku/compare/v2.0.11...v3.0.0
156
165
  [v2.0.11]: https://github.com/rubysolo/dentaku/compare/v2.0.10...v2.0.11
data/README.md CHANGED
@@ -146,7 +146,7 @@ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
146
146
 
147
147
  Logic: `IF`, `AND`, `OR`, `NOT`, `SWITCH`
148
148
 
149
- Numeric: `MIN`, `MAX`, `SUM`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
149
+ Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
150
150
 
151
151
  Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
152
152
 
@@ -321,7 +321,7 @@ LICENSE
321
321
 
322
322
  (The MIT License)
323
323
 
324
- Copyright © 2012-2017 Solomon White
324
+ Copyright © 2012-2018 Solomon White
325
325
 
326
326
  Permission is hereby granted, free of charge, to any person obtaining a copy of
327
327
  this software and associated documentation files (the ‘Software’), to deal in
@@ -15,14 +15,16 @@ require_relative './ast/grouping'
15
15
  require_relative './ast/case'
16
16
  require_relative './ast/function_registry'
17
17
  require_relative './ast/functions/and'
18
+ require_relative './ast/functions/avg'
19
+ require_relative './ast/functions/count'
18
20
  require_relative './ast/functions/if'
19
21
  require_relative './ast/functions/max'
20
22
  require_relative './ast/functions/min'
21
23
  require_relative './ast/functions/not'
22
24
  require_relative './ast/functions/or'
23
25
  require_relative './ast/functions/round'
24
- require_relative './ast/functions/roundup'
25
26
  require_relative './ast/functions/rounddown'
27
+ require_relative './ast/functions/roundup'
26
28
  require_relative './ast/functions/ruby_math'
27
29
  require_relative './ast/functions/string_functions'
28
30
  require_relative './ast/functions/sum'
@@ -0,0 +1,13 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
4
+ if args.empty?
5
+ raise Dentaku::ArgumentError.for(
6
+ :too_few_arguments,
7
+ function_name: 'AVG()', at_least: 1, given: 0
8
+ ), 'AVG() requires at least one argument'
9
+ end
10
+
11
+ flatten_args = args.flatten
12
+ flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / flatten_args.length
13
+ })
@@ -0,0 +1,18 @@
1
+ require_relative '../function'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Count < Function
6
+ def value(context = {})
7
+ if @args.length == 1
8
+ first_arg = @args[0].value(context)
9
+ return first_arg.length if first_arg.respond_to?(:length)
10
+ end
11
+
12
+ @args.length
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Dentaku::AST::Function.register_class(:count, Dentaku::AST::Count)
@@ -0,0 +1,12 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
4
+ if args.empty?
5
+ raise Dentaku::ArgumentError.for(
6
+ :too_few_arguments,
7
+ function_name: 'MUL()', at_least: 1, given: 0
8
+ ), 'MUL() requires at least one argument'
9
+ end
10
+
11
+ args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
12
+ })
@@ -75,11 +75,11 @@ module Dentaku
75
75
  BulkExpressionSolver.new(expression_hash, self).solve(&block)
76
76
  end
77
77
 
78
- def dependencies(expression)
78
+ def dependencies(expression, context = {})
79
79
  if expression.is_a? Array
80
- return expression.flat_map { |e| dependencies(e) }
80
+ return expression.flat_map { |e| dependencies(e, context) }
81
81
  end
82
- ast(expression).dependencies(memory)
82
+ store(context) { ast(expression).dependencies(memory) }
83
83
  end
84
84
 
85
85
  def ast(expression)
@@ -2,6 +2,28 @@ require_relative './ast'
2
2
 
3
3
  module Dentaku
4
4
  class Parser
5
+ AST_OPERATIONS = {
6
+ add: AST::Addition,
7
+ subtract: AST::Subtraction,
8
+ multiply: AST::Multiplication,
9
+ divide: AST::Division,
10
+ pow: AST::Exponentiation,
11
+ negate: AST::Negation,
12
+ mod: AST::Modulo,
13
+ bitor: AST::BitwiseOr,
14
+ bitand: AST::BitwiseAnd,
15
+
16
+ lt: AST::LessThan,
17
+ gt: AST::GreaterThan,
18
+ le: AST::LessThanOrEqual,
19
+ ge: AST::GreaterThanOrEqual,
20
+ ne: AST::NotEqual,
21
+ eq: AST::Equal,
22
+
23
+ and: AST::And,
24
+ or: AST::Or,
25
+ }.freeze
26
+
5
27
  attr_reader :input, :output, :operations, :arities, :case_sensitive
6
28
 
7
29
  def initialize(tokens, options = {})
@@ -78,6 +100,9 @@ module Dentaku
78
100
  operations.push func
79
101
 
80
102
  when :case
103
+ case_index = operations.index { |o| o == AST::Case } || -1
104
+ token_index = case_index + 1
105
+
81
106
  case token.value
82
107
  when :open
83
108
  # special handling for case nesting: strip out inner case
@@ -113,7 +138,7 @@ module Dentaku
113
138
  arities.push(0)
114
139
  end
115
140
  when :close
116
- if operations[1] == AST::CaseThen
141
+ if operations[token_index] == AST::CaseThen
117
142
  while operations.last != AST::Case
118
143
  consume
119
144
  end
@@ -121,7 +146,7 @@ module Dentaku
121
146
  operations.push(AST::CaseConditional)
122
147
  consume(2)
123
148
  arities[-1] += 1
124
- elsif operations[1] == AST::CaseElse
149
+ elsif operations[token_index] == AST::CaseElse
125
150
  while operations.last != AST::Case
126
151
  consume
127
152
  end
@@ -129,12 +154,12 @@ module Dentaku
129
154
  arities[-1] += 1
130
155
  end
131
156
 
132
- unless operations.count == 1 && operations.last == AST::Case
157
+ unless operations.count >= 1 && operations.last == AST::Case
133
158
  fail! :unprocessed_token, token_name: token.value
134
159
  end
135
160
  consume(arities.pop.succ)
136
161
  when :when
137
- if operations[1] == AST::CaseThen
162
+ if operations[token_index] == AST::CaseThen
138
163
  while ![AST::CaseWhen, AST::Case].include?(operations.last)
139
164
  consume
140
165
  end
@@ -148,14 +173,14 @@ module Dentaku
148
173
 
149
174
  operations.push(AST::CaseWhen)
150
175
  when :then
151
- if operations[1] == AST::CaseWhen
176
+ if operations[token_index] == AST::CaseWhen
152
177
  while ![AST::CaseThen, AST::Case].include?(operations.last)
153
178
  consume
154
179
  end
155
180
  end
156
181
  operations.push(AST::CaseThen)
157
182
  when :else
158
- if operations[1] == AST::CaseThen
183
+ if operations[token_index] == AST::CaseThen
159
184
  while operations.last != AST::Case
160
185
  consume
161
186
  end
@@ -237,27 +262,7 @@ module Dentaku
237
262
  end
238
263
 
239
264
  def operation(token)
240
- {
241
- add: AST::Addition,
242
- subtract: AST::Subtraction,
243
- multiply: AST::Multiplication,
244
- divide: AST::Division,
245
- pow: AST::Exponentiation,
246
- negate: AST::Negation,
247
- mod: AST::Modulo,
248
- bitor: AST::BitwiseOr,
249
- bitand: AST::BitwiseAnd,
250
-
251
- lt: AST::LessThan,
252
- gt: AST::GreaterThan,
253
- le: AST::LessThanOrEqual,
254
- ge: AST::GreaterThanOrEqual,
255
- ne: AST::NotEqual,
256
- eq: AST::Equal,
257
-
258
- and: AST::And,
259
- or: AST::Or,
260
- }.fetch(token.value)
265
+ AST_OPERATIONS.fetch(token.value)
261
266
  end
262
267
 
263
268
  def function(token)
@@ -168,7 +168,7 @@ module Dentaku
168
168
  end
169
169
 
170
170
  def identifier
171
- new(:identifier, '[\w\.]+\b', lambda { |raw| standardize_case(raw.strip) })
171
+ new(:identifier, '[[[:word:]]\.]+\b', lambda { |raw| standardize_case(raw.strip) })
172
172
  end
173
173
  end
174
174
 
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.1.0"
2
+ VERSION = "3.2.0"
3
3
  end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/avg'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Avg' do
6
+ it 'returns the average of an array of Numeric values' do
7
+ result = Dentaku('AVG(1, x, 1.8)', x: 2.3)
8
+ expect(result).to eq 1.7
9
+ end
10
+
11
+ it 'returns the average of a single entry array of a Numeric value' do
12
+ result = Dentaku('AVG(x)', x: 2.3)
13
+ expect(result).to eq 2.3
14
+ end
15
+
16
+ it 'returns the average even if a String is passed' do
17
+ result = Dentaku('AVG(1, x, 1.8)', x: '2.3')
18
+ expect(result).to eq 1.7
19
+ end
20
+
21
+ it 'returns the average even if an array is passed' do
22
+ result = Dentaku('AVG(1, x, 2.3)', x: [4, 5])
23
+ expect(result).to eq 3.075
24
+ end
25
+
26
+ context 'checking errors' do
27
+ let(:calculator) { Dentaku::Calculator.new }
28
+
29
+ it 'raises an error if no arguments are passed' do
30
+ expect { calculator.evaluate!('AVG()') }.to raise_error(ArgumentError)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/count'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Count' do
6
+ it 'returns the length of an array' do
7
+ result = Dentaku('COUNT(1, x, 1.8)', x: 2.3)
8
+ expect(result).to eq 3
9
+ end
10
+
11
+ it 'returns the length of a single number object' do
12
+ result = Dentaku('COUNT(x)', x: 2.3)
13
+ expect(result).to eq 1
14
+ end
15
+
16
+ it 'returns the length if a single String is passed' do
17
+ result = Dentaku('COUNT(x)', x: 'dentaku')
18
+ expect(result).to eq 7
19
+ end
20
+
21
+ it 'returns the length if an array is passed' do
22
+ result = Dentaku('COUNT(x)', x: [4, 5])
23
+ expect(result).to eq 2
24
+ end
25
+
26
+ it 'returns the length if an array with one element is passed' do
27
+ result = Dentaku('COUNT(x)', x: [4])
28
+ expect(result).to eq 1
29
+ end
30
+
31
+ it 'returns the length if an array even if it has nested array' do
32
+ result = Dentaku('COUNT(1, x, 3)', x: [4, 5])
33
+ expect(result).to eq 3
34
+ end
35
+
36
+ it 'returns the length if an array is passed' do
37
+ result = Dentaku('COUNT()')
38
+ expect(result).to eq 0
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/mul'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Mul' do
6
+ it 'returns the product of an array of Numeric values' do
7
+ result = Dentaku('MUL(1, x, 1.8)', x: 2.3)
8
+ expect(result).to eq 4.14
9
+ end
10
+
11
+ it 'returns the product of a single entry array of a Numeric value' do
12
+ result = Dentaku('MUL(x)', x: 2.3)
13
+ expect(result).to eq 2.3
14
+ end
15
+
16
+ it 'coerces string inputs to numeric' do
17
+ result = Dentaku('mul(1, x, 1.8)', x: '2.3')
18
+ expect(result).to eq 4.14
19
+ end
20
+
21
+ it 'returns the product even if an array is passed' do
22
+ result = Dentaku('mul(1, x, 2.3)', x: [4, 5])
23
+ expect(result).to eq 46
24
+ end
25
+
26
+ it 'handles nested calls' do
27
+ result = Dentaku('mul(1, x, mul(4, 5))', x: '2.3')
28
+ expect(result).to eq 46
29
+ end
30
+
31
+ context 'checking errors' do
32
+ let(:calculator) { Dentaku::Calculator.new }
33
+
34
+ it 'raises an error if no arguments are passed' do
35
+ expect { calculator.evaluate!('MUL()') }.to raise_error(ArgumentError)
36
+ end
37
+ end
38
+ end
@@ -41,6 +41,10 @@ describe Dentaku::Calculator do
41
41
  expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
42
42
  end
43
43
 
44
+ it 'supports unicode characters in identifiers' do
45
+ expect(calculator.evaluate("ρ * 2", ρ: 2)).to eq (4)
46
+ end
47
+
44
48
  describe 'memory' do
45
49
  it { expect(calculator).to be_empty }
46
50
  it { expect(with_memory).not_to be_empty }
@@ -90,6 +94,10 @@ describe Dentaku::Calculator do
90
94
  expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
91
95
  end
92
96
 
97
+ it "ignores dependencies passed in context" do
98
+ expect(calculator.dependencies("a + b", a: 1)).to eq(['b'])
99
+ end
100
+
93
101
  it "finds dependencies in formula arguments" do
94
102
  allow(Dentaku).to receive(:cache_ast?) { true }
95
103
 
@@ -371,19 +379,33 @@ describe Dentaku::Calculator do
371
379
  end
372
380
 
373
381
  describe 'case statements' do
374
- it 'handles complex then statements' do
375
- formula = <<-FORMULA
382
+ let(:formula) {
383
+ <<-FORMULA
376
384
  CASE fruit
377
385
  WHEN 'apple'
378
- THEN (1 * quantity)
386
+ THEN 1 * quantity
379
387
  WHEN 'banana'
380
- THEN (2 * quantity)
388
+ THEN 2 * quantity
389
+ ELSE
390
+ 3 * quantity
381
391
  END
382
392
  FORMULA
393
+ }
394
+
395
+ it 'handles complex then statements' do
383
396
  expect(calculator.evaluate(formula, quantity: 3, fruit: 'apple')).to eq(3)
384
397
  expect(calculator.evaluate(formula, quantity: 3, fruit: 'banana')).to eq(6)
385
398
  end
386
399
 
400
+ it 'evaluates case statement as part of a larger expression' do
401
+ expect(calculator.evaluate("2 + #{formula}", quantity: 3, fruit: 'apple')).to eq(5)
402
+ expect(calculator.evaluate("2 + #{formula}", quantity: 3, fruit: 'banana')).to eq(8)
403
+ expect(calculator.evaluate("2 + #{formula}", quantity: 3, fruit: 'kiwi')).to eq(11)
404
+ expect(calculator.evaluate("#{formula} + 2", quantity: 3, fruit: 'apple')).to eq(5)
405
+ expect(calculator.evaluate("#{formula} + 2", quantity: 3, fruit: 'banana')).to eq(8)
406
+ expect(calculator.evaluate("#{formula} + 2", quantity: 3, fruit: 'kiwi')).to eq(11)
407
+ end
408
+
387
409
  it 'handles complex when statements' do
388
410
  formula = <<-FORMULA
389
411
  CASE number
@@ -409,16 +431,6 @@ describe Dentaku::Calculator do
409
431
  end
410
432
 
411
433
  it 'handles a default else statement' do
412
- formula = <<-FORMULA
413
- CASE fruit
414
- WHEN 'apple'
415
- THEN 1 * quantity
416
- WHEN 'banana'
417
- THEN 2 * quantity
418
- ELSE
419
- 3 * quantity
420
- END
421
- FORMULA
422
434
  expect(calculator.evaluate(formula, quantity: 1, fruit: 'banana')).to eq(2)
423
435
  expect(calculator.evaluate(formula, quantity: 1, fruit: 'orange')).to eq(3)
424
436
  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: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-11 00:00:00.000000000 Z
11
+ date: 2018-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: codecov
@@ -156,9 +156,12 @@ files:
156
156
  - lib/dentaku/ast/function.rb
157
157
  - lib/dentaku/ast/function_registry.rb
158
158
  - lib/dentaku/ast/functions/and.rb
159
+ - lib/dentaku/ast/functions/avg.rb
160
+ - lib/dentaku/ast/functions/count.rb
159
161
  - lib/dentaku/ast/functions/if.rb
160
162
  - lib/dentaku/ast/functions/max.rb
161
163
  - lib/dentaku/ast/functions/min.rb
164
+ - lib/dentaku/ast/functions/mul.rb
162
165
  - lib/dentaku/ast/functions/not.rb
163
166
  - lib/dentaku/ast/functions/or.rb
164
167
  - lib/dentaku/ast/functions/round.rb
@@ -195,12 +198,15 @@ files:
195
198
  - spec/ast/and_function_spec.rb
196
199
  - spec/ast/and_spec.rb
197
200
  - spec/ast/arithmetic_spec.rb
201
+ - spec/ast/avg_spec.rb
198
202
  - spec/ast/case_spec.rb
199
203
  - spec/ast/comparator_spec.rb
204
+ - spec/ast/count_spec.rb
200
205
  - spec/ast/division_spec.rb
201
206
  - spec/ast/function_spec.rb
202
207
  - spec/ast/max_spec.rb
203
208
  - spec/ast/min_spec.rb
209
+ - spec/ast/mul_spec.rb
204
210
  - spec/ast/node_spec.rb
205
211
  - spec/ast/numeric_spec.rb
206
212
  - spec/ast/or_spec.rb
@@ -242,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
248
  version: '0'
243
249
  requirements: []
244
250
  rubyforge_project: dentaku
245
- rubygems_version: 2.5.2.1
251
+ rubygems_version: 2.6.13
246
252
  signing_key:
247
253
  specification_version: 4
248
254
  summary: A formula language parser and evaluator
@@ -251,12 +257,15 @@ test_files:
251
257
  - spec/ast/and_function_spec.rb
252
258
  - spec/ast/and_spec.rb
253
259
  - spec/ast/arithmetic_spec.rb
260
+ - spec/ast/avg_spec.rb
254
261
  - spec/ast/case_spec.rb
255
262
  - spec/ast/comparator_spec.rb
263
+ - spec/ast/count_spec.rb
256
264
  - spec/ast/division_spec.rb
257
265
  - spec/ast/function_spec.rb
258
266
  - spec/ast/max_spec.rb
259
267
  - spec/ast/min_spec.rb
268
+ - spec/ast/mul_spec.rb
260
269
  - spec/ast/node_spec.rb
261
270
  - spec/ast/numeric_spec.rb
262
271
  - spec/ast/or_spec.rb