dentaku 3.1.0 → 3.2.0

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