dentaku 2.0.5 → 2.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +5 -0
- data/README.md +1 -15
- data/lib/dentaku/ast.rb +1 -0
- data/lib/dentaku/ast/case.rb +52 -0
- data/lib/dentaku/ast/case/case_conditional.rb +23 -0
- data/lib/dentaku/ast/case/case_else.rb +21 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +21 -0
- data/lib/dentaku/ast/case/case_then.rb +21 -0
- data/lib/dentaku/ast/case/case_when.rb +21 -0
- data/lib/dentaku/ast/function.rb +1 -1
- data/lib/dentaku/ast/functions/rounddown.rb +4 -2
- data/lib/dentaku/ast/functions/roundup.rb +4 -2
- data/lib/dentaku/calculator.rb +8 -3
- data/lib/dentaku/parser.rb +87 -3
- data/lib/dentaku/token_scanner.rb +6 -0
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/case_spec.rb +80 -0
- data/spec/calculator_spec.rb +109 -0
- data/spec/external_function_spec.rb +13 -0
- data/spec/parser_spec.rb +29 -0
- data/spec/token_scanner_spec.rb +1 -1
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 813c0e0ea4c6a021ca324515d26386d2f2f28962
|
4
|
+
data.tar.gz: 943958582e98916af345553a343b261b978c97c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d281ac5672cb9badc8ba74033d5624bd2e23d12f37785a6fc47595be0de51c8da9a2419eb0701de273e6ed710212988f47a16a72fef9fcbc119a8c39b38ebad4
|
7
|
+
data.tar.gz: 8f3ca27e26d5a603c99d2473a3115fb87c00407127b7d1613f0c0634057f37318583de7a7f8192d47db0ea8236648e916ea6dbc6084add0b591b4939e95c48f1
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [Unreleased]
|
4
|
+
|
5
|
+
## [v2.0.6] 2016-01-26
|
6
|
+
- support array parameters for external functions
|
7
|
+
- support case statements
|
8
|
+
- support precision for `ROUNDUP` and `ROUNDDOWN` functions
|
9
|
+
- prevent errors from corrupting calculator memory
|
10
|
+
|
3
11
|
## [v2.0.5] 2015-09-03
|
4
12
|
- fix bug with detecting unbound nodes
|
5
13
|
- silence warnings
|
@@ -90,6 +98,8 @@
|
|
90
98
|
## [v0.1.0] 2012-01-20
|
91
99
|
- initial release
|
92
100
|
|
101
|
+
[Unreleased]: https://github.com/rubysolo/dentaku/compare/v2.0.6...HEAD
|
102
|
+
[v2.0.6]: https://github.com/rubysolo/dentaku/compare/v2.0.5...v2.0.6
|
93
103
|
[v2.0.5]: https://github.com/rubysolo/dentaku/compare/v2.0.4...v2.0.5
|
94
104
|
[v2.0.4]: https://github.com/rubysolo/dentaku/compare/v2.0.3...v2.0.4
|
95
105
|
[v2.0.3]: https://github.com/rubysolo/dentaku/compare/v2.0.1...v2.0.3
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -260,21 +260,7 @@ THANKS
|
|
260
260
|
|
261
261
|
Big thanks to [ElkStone Basements](http://www.elkstonebasements.com/) for
|
262
262
|
allowing me to extract and open source this code. Thanks also to all the
|
263
|
-
contributors
|
264
|
-
|
265
|
-
* [0xCCD](https://github.com/0xCCD)
|
266
|
-
* [AlexeyMK](https://github.com/AlexeyMK)
|
267
|
-
* [antonversal](https://github.com/antonversal)
|
268
|
-
* [arnaudl](https://github.com/arnaudl)
|
269
|
-
* [bernardofire](https://github.com/bernardofire)
|
270
|
-
* [brixen](https://github.com/brixen)
|
271
|
-
* [CraigCottingham](https://github.com/CraigCottingham)
|
272
|
-
* [glanotte](https://github.com/glanotte)
|
273
|
-
* [jasonhutchens](https://github.com/jasonhutchens)
|
274
|
-
* [jmangs](https://github.com/jmangs)
|
275
|
-
* [mvbrocato](https://github.com/mvbrocato)
|
276
|
-
* [schneidmaster](https://github.com/schneidmaster)
|
277
|
-
* [thbar](https://github.com/thbar) / [BoxCar](https://www.boxcar.io)
|
263
|
+
[contributors](https://github.com/rubysolo/dentaku/graphs/contributors)!
|
278
264
|
|
279
265
|
|
280
266
|
LICENSE
|
data/lib/dentaku/ast.rb
CHANGED
@@ -9,6 +9,7 @@ require_relative './ast/negation'
|
|
9
9
|
require_relative './ast/comparators'
|
10
10
|
require_relative './ast/combinators'
|
11
11
|
require_relative './ast/grouping'
|
12
|
+
require_relative './ast/case'
|
12
13
|
require_relative './ast/functions/if'
|
13
14
|
require_relative './ast/functions/max'
|
14
15
|
require_relative './ast/functions/min'
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative './case/case_conditional'
|
2
|
+
require_relative './case/case_when'
|
3
|
+
require_relative './case/case_then'
|
4
|
+
require_relative './case/case_switch_variable'
|
5
|
+
require_relative './case/case_else'
|
6
|
+
|
7
|
+
module Dentaku
|
8
|
+
module AST
|
9
|
+
class Case < Node
|
10
|
+
def initialize(*nodes)
|
11
|
+
@switch = nodes.shift
|
12
|
+
|
13
|
+
unless @switch.is_a?(AST::CaseSwitchVariable)
|
14
|
+
raise 'Case missing switch variable'
|
15
|
+
end
|
16
|
+
|
17
|
+
@conditions = nodes
|
18
|
+
|
19
|
+
@else = @conditions.pop if @conditions.last.is_a?(AST::CaseElse)
|
20
|
+
|
21
|
+
@conditions.each do |condition|
|
22
|
+
unless condition.is_a?(AST::CaseConditional)
|
23
|
+
raise "#{condition} is not a CaseConditional"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def value(context={})
|
29
|
+
switch_value = @switch.value(context)
|
30
|
+
@conditions.each do |condition|
|
31
|
+
if condition.when.value(context) == switch_value
|
32
|
+
return condition.then.value(context)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if @else
|
37
|
+
return @else.value(context)
|
38
|
+
else
|
39
|
+
raise "No block matched the switch value '#{switch_value}'"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def dependencies(context={})
|
44
|
+
# TODO: should short-circuit
|
45
|
+
@switch.dependencies(context) +
|
46
|
+
@conditions.flat_map do |condition|
|
47
|
+
condition.dependencies(context)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class CaseConditional < Node
|
4
|
+
attr_reader :when,
|
5
|
+
:then
|
6
|
+
|
7
|
+
def initialize(when_statement, then_statement)
|
8
|
+
@when = when_statement
|
9
|
+
unless @when.is_a?(AST::CaseWhen)
|
10
|
+
raise 'Expected first argument to be a CaseWhen'
|
11
|
+
end
|
12
|
+
@then = then_statement
|
13
|
+
unless @then.is_a?(AST::CaseThen)
|
14
|
+
raise 'Expected second argument to be a CaseThen'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def dependencies(context={})
|
19
|
+
@when.dependencies(context) + @then.dependencies(context)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class CaseElse < Node
|
4
|
+
def self.arity
|
5
|
+
1
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(node)
|
9
|
+
@node = node
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context={})
|
13
|
+
@node.value(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def dependencies(context={})
|
17
|
+
@node.dependencies(context)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class CaseSwitchVariable < Node
|
4
|
+
def initialize(node)
|
5
|
+
@node = node
|
6
|
+
end
|
7
|
+
|
8
|
+
def value(context={})
|
9
|
+
@node.value(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def dependencies(context={})
|
13
|
+
@node.dependencies(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.arity
|
17
|
+
1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class CaseThen < Node
|
4
|
+
def self.arity
|
5
|
+
1
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(node)
|
9
|
+
@node = node
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context={})
|
13
|
+
@node.value(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def dependencies(context={})
|
17
|
+
@node.dependencies(context)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class CaseWhen < Operation
|
4
|
+
def self.arity
|
5
|
+
1
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(node)
|
9
|
+
@node = node
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context={})
|
13
|
+
@node.value(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def dependencies(context={})
|
17
|
+
@node.dependencies(context)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/dentaku/ast/function.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative '../function'
|
2
2
|
|
3
|
-
Dentaku::AST::Function.register(:rounddown, :numeric, ->(numeric) {
|
4
|
-
|
3
|
+
Dentaku::AST::Function.register(:rounddown, :numeric, ->(numeric, precision=0) {
|
4
|
+
tens = 10.0**precision
|
5
|
+
result = (numeric * tens).floor / tens
|
6
|
+
precision <= 0 ? result.to_i : result
|
5
7
|
})
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative '../function'
|
2
2
|
|
3
|
-
Dentaku::AST::Function.register(:roundup, :numeric, ->(numeric) {
|
4
|
-
|
3
|
+
Dentaku::AST::Function.register(:roundup, :numeric, ->(numeric, precision=0) {
|
4
|
+
tens = 10.0**precision
|
5
|
+
result = (numeric * tens).ceil / tens
|
6
|
+
precision <= 0 ? result.to_i : result
|
5
7
|
})
|
data/lib/dentaku/calculator.rb
CHANGED
data/lib/dentaku/parser.rb
CHANGED
@@ -4,11 +4,11 @@ module Dentaku
|
|
4
4
|
class Parser
|
5
5
|
attr_reader :input, :output, :operations, :arities
|
6
6
|
|
7
|
-
def initialize(tokens)
|
7
|
+
def initialize(tokens, options={})
|
8
8
|
@input = tokens.dup
|
9
9
|
@output = []
|
10
|
-
@operations = []
|
11
|
-
@arities = []
|
10
|
+
@operations = options.fetch(:operations, [])
|
11
|
+
@arities = options.fetch(:arities, [])
|
12
12
|
end
|
13
13
|
|
14
14
|
def get_args(count)
|
@@ -58,6 +58,90 @@ module Dentaku
|
|
58
58
|
arities.push 0
|
59
59
|
operations.push function(token)
|
60
60
|
|
61
|
+
when :case
|
62
|
+
case token.value
|
63
|
+
when :open
|
64
|
+
# special handling for case nesting: strip out inner case
|
65
|
+
# statements and parse their AST segments recursively
|
66
|
+
if operations.include?(AST::Case)
|
67
|
+
last_case_close_index = nil
|
68
|
+
first_nested_case_close_index = nil
|
69
|
+
input.each_with_index do |token, index|
|
70
|
+
first_nested_case_close_index = last_case_close_index
|
71
|
+
if token.category == :case && token.value == :close
|
72
|
+
last_case_close_index = index
|
73
|
+
end
|
74
|
+
end
|
75
|
+
inner_case_inputs = input.slice!(0..first_nested_case_close_index)
|
76
|
+
subparser = Parser.new(
|
77
|
+
inner_case_inputs,
|
78
|
+
operations: [AST::Case],
|
79
|
+
arities: [0]
|
80
|
+
)
|
81
|
+
subparser.parse
|
82
|
+
output.concat(subparser.output)
|
83
|
+
else
|
84
|
+
operations.push AST::Case
|
85
|
+
arities.push(0)
|
86
|
+
end
|
87
|
+
when :close
|
88
|
+
if operations[1] == AST::CaseThen
|
89
|
+
while operations.last != AST::Case
|
90
|
+
consume
|
91
|
+
end
|
92
|
+
|
93
|
+
operations.push(AST::CaseConditional)
|
94
|
+
consume(2)
|
95
|
+
arities[-1] += 1
|
96
|
+
elsif operations[1] == AST::CaseElse
|
97
|
+
while operations.last != AST::Case
|
98
|
+
consume
|
99
|
+
end
|
100
|
+
|
101
|
+
arities[-1] += 1
|
102
|
+
end
|
103
|
+
|
104
|
+
unless operations.count == 1 && operations.last == AST::Case
|
105
|
+
fail "Unprocessed token #{ token.value }"
|
106
|
+
end
|
107
|
+
consume(arities.pop.succ)
|
108
|
+
when :when
|
109
|
+
if operations[1] == AST::CaseThen
|
110
|
+
while ![AST::CaseWhen, AST::Case].include?(operations.last)
|
111
|
+
consume
|
112
|
+
end
|
113
|
+
operations.push(AST::CaseConditional)
|
114
|
+
consume(2)
|
115
|
+
arities[-1] += 1
|
116
|
+
elsif operations.last == AST::Case
|
117
|
+
operations.push(AST::CaseSwitchVariable)
|
118
|
+
consume
|
119
|
+
end
|
120
|
+
|
121
|
+
operations.push(AST::CaseWhen)
|
122
|
+
when :then
|
123
|
+
if operations[1] == AST::CaseWhen
|
124
|
+
while ![AST::CaseThen, AST::Case].include?(operations.last)
|
125
|
+
consume
|
126
|
+
end
|
127
|
+
end
|
128
|
+
operations.push(AST::CaseThen)
|
129
|
+
when :else
|
130
|
+
if operations[1] == AST::CaseThen
|
131
|
+
while operations.last != AST::Case
|
132
|
+
consume
|
133
|
+
end
|
134
|
+
|
135
|
+
operations.push(AST::CaseConditional)
|
136
|
+
consume(2)
|
137
|
+
arities[-1] += 1
|
138
|
+
end
|
139
|
+
|
140
|
+
operations.push(AST::CaseElse)
|
141
|
+
else
|
142
|
+
fail "Unknown case token #{ token.value }"
|
143
|
+
end
|
144
|
+
|
61
145
|
when :grouping
|
62
146
|
case token.value
|
63
147
|
when :open
|
@@ -33,6 +33,7 @@ module Dentaku
|
|
33
33
|
:negate,
|
34
34
|
:operator,
|
35
35
|
:grouping,
|
36
|
+
:case_statement,
|
36
37
|
:comparator,
|
37
38
|
:combinator,
|
38
39
|
:boolean,
|
@@ -100,6 +101,11 @@ module Dentaku
|
|
100
101
|
new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
|
101
102
|
end
|
102
103
|
|
104
|
+
def case_statement
|
105
|
+
names = { open: 'case', close: 'end', then: 'then', when: 'when', else: 'else' }.invert
|
106
|
+
new(:case, '(case|end|then|when|else)\b', lambda { |raw| names[raw.downcase] })
|
107
|
+
end
|
108
|
+
|
103
109
|
def comparator
|
104
110
|
names = { le: '<=', ge: '>=', ne: '!=', lt: '<', gt: '>', eq: '=' }.invert
|
105
111
|
alternate = { ne: '<>', eq: '==' }.invert
|
data/lib/dentaku/version.rb
CHANGED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/operation'
|
3
|
+
require 'dentaku/ast/logical'
|
4
|
+
require 'dentaku/ast/identifier'
|
5
|
+
require 'dentaku/ast/arithmetic'
|
6
|
+
require 'dentaku/ast/case'
|
7
|
+
|
8
|
+
require 'dentaku/token'
|
9
|
+
|
10
|
+
describe Dentaku::AST::Case do
|
11
|
+
let!(:one) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 1) }
|
12
|
+
let!(:two) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 2) }
|
13
|
+
let!(:apple) do
|
14
|
+
Dentaku::AST::Logical.new Dentaku::Token.new(:string, 'apple')
|
15
|
+
end
|
16
|
+
let!(:banana) do
|
17
|
+
Dentaku::AST::Logical.new Dentaku::Token.new(:string, 'banana')
|
18
|
+
end
|
19
|
+
let!(:identifier) do
|
20
|
+
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :fruit))
|
21
|
+
end
|
22
|
+
let!(:switch) { Dentaku::AST::CaseSwitchVariable.new(identifier) }
|
23
|
+
|
24
|
+
let!(:when1) { Dentaku::AST::CaseWhen.new(apple) }
|
25
|
+
let!(:then1) { Dentaku::AST::CaseThen.new(one) }
|
26
|
+
let!(:conditional1) { Dentaku::AST::CaseConditional.new(when1, then1) }
|
27
|
+
|
28
|
+
let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
|
29
|
+
let!(:then2) { Dentaku::AST::CaseThen.new(two) }
|
30
|
+
let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
|
31
|
+
|
32
|
+
describe '#value' do
|
33
|
+
it 'raises an exception if there is no switch variable' do
|
34
|
+
expect { described_class.new(conditional1, conditional2) }
|
35
|
+
.to raise_error('Case missing switch variable')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'raises an exception if a non-conditional is passed' do
|
39
|
+
expect { described_class.new(switch, conditional1, when2) }
|
40
|
+
.to raise_error(/is not a CaseConditional/)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'tests each conditional against the switch variable' do
|
44
|
+
node = described_class.new(switch, conditional1, conditional2)
|
45
|
+
expect(node.value(fruit: 'banana')).to eq(2)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises an exception if the conditional is not matched' do
|
49
|
+
node = described_class.new(switch, conditional1, conditional2)
|
50
|
+
expect { node.value(fruit: 'orange') }
|
51
|
+
.to raise_error("No block matched the switch value 'orange'")
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'uses the else value if provided and conditional is not matched' do
|
55
|
+
three = Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 3)
|
56
|
+
else_statement = Dentaku::AST::CaseElse.new(three)
|
57
|
+
node = described_class.new(
|
58
|
+
switch,
|
59
|
+
conditional1,
|
60
|
+
conditional2,
|
61
|
+
else_statement)
|
62
|
+
expect(node.value(fruit: 'orange')).to eq(3)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#dependencies' do
|
67
|
+
let!(:tax) do
|
68
|
+
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :tax))
|
69
|
+
end
|
70
|
+
let!(:addition) { Dentaku::AST::Addition.new(two, tax) }
|
71
|
+
let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
|
72
|
+
let!(:then2) { Dentaku::AST::CaseThen.new(addition) }
|
73
|
+
let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
|
74
|
+
|
75
|
+
it 'gathers dependencies from switch and conditionals' do
|
76
|
+
node = described_class.new(switch, conditional1, conditional2)
|
77
|
+
expect(node.dependencies).to eq([:fruit, :tax])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/spec/calculator_spec.rb
CHANGED
@@ -130,6 +130,17 @@ describe Dentaku::Calculator do
|
|
130
130
|
expect(calculator.solve(expressions) { :foo })
|
131
131
|
.to eq(more_apples: :foo)
|
132
132
|
end
|
133
|
+
|
134
|
+
it "solves remainder of expressions with unbound variable" do
|
135
|
+
calculator.store(peaches: 1, oranges: 1)
|
136
|
+
expressions = { more_apples: "apples + 1", more_peaches: "peaches + 1" }
|
137
|
+
result = calculator.solve(expressions)
|
138
|
+
expect(calculator.memory).to eq("peaches" => 1, "oranges" => 1)
|
139
|
+
expect(result).to eq(
|
140
|
+
more_apples: :undefined,
|
141
|
+
more_peaches: 2
|
142
|
+
)
|
143
|
+
end
|
133
144
|
end
|
134
145
|
|
135
146
|
it 'evaluates a statement with no variables' do
|
@@ -227,6 +238,104 @@ describe Dentaku::Calculator do
|
|
227
238
|
result = calculator.evaluate('number_of_sheets / if(multi_color, sheets_per_minute_color, sheets_per_minute_black)')
|
228
239
|
expect(result).to eq(5)
|
229
240
|
end
|
241
|
+
|
242
|
+
describe 'roundup' do
|
243
|
+
it 'should work with one argument' do
|
244
|
+
expect(calculator.evaluate('roundup(1.234)')).to eq(2)
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'should accept second precision argument like in Office formula' do
|
248
|
+
expect(calculator.evaluate('roundup(1.234, 2)')).to eq(1.24)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe 'rounddown' do
|
253
|
+
it 'should work with one argument' do
|
254
|
+
expect(calculator.evaluate('rounddown(1.234)')).to eq(1)
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'should accept second precision argument like in Office formula' do
|
258
|
+
expect(calculator.evaluate('rounddown(1.234, 2)')).to eq(1.23)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe 'case statements' do
|
264
|
+
it 'handles complex then statements' do
|
265
|
+
formula = <<-FORMULA
|
266
|
+
CASE fruit
|
267
|
+
WHEN 'apple'
|
268
|
+
THEN (1 * quantity)
|
269
|
+
WHEN 'banana'
|
270
|
+
THEN (2 * quantity)
|
271
|
+
END
|
272
|
+
FORMULA
|
273
|
+
expect(calculator.evaluate(formula, quantity: 3, fruit: 'apple')).to eq(3)
|
274
|
+
expect(calculator.evaluate(formula, quantity: 3, fruit: 'banana')).to eq(6)
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'handles complex when statements' do
|
278
|
+
formula = <<-FORMULA
|
279
|
+
CASE number
|
280
|
+
WHEN (2 * 2)
|
281
|
+
THEN 1
|
282
|
+
WHEN (2 * 3)
|
283
|
+
THEN 2
|
284
|
+
END
|
285
|
+
FORMULA
|
286
|
+
expect(calculator.evaluate(formula, number: 4)).to eq(1)
|
287
|
+
expect(calculator.evaluate(formula, number: 6)).to eq(2)
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'throws an exception when no match and there is no default value' do
|
291
|
+
formula = <<-FORMULA
|
292
|
+
CASE number
|
293
|
+
WHEN 42
|
294
|
+
THEN 1
|
295
|
+
END
|
296
|
+
FORMULA
|
297
|
+
expect { calculator.evaluate(formula, number: 2) }
|
298
|
+
.to raise_error("No block matched the switch value '2'")
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'handles a default else statement' do
|
302
|
+
formula = <<-FORMULA
|
303
|
+
CASE fruit
|
304
|
+
WHEN 'apple'
|
305
|
+
THEN 1 * quantity
|
306
|
+
WHEN 'banana'
|
307
|
+
THEN 2 * quantity
|
308
|
+
ELSE
|
309
|
+
3 * quantity
|
310
|
+
END
|
311
|
+
FORMULA
|
312
|
+
expect(calculator.evaluate(formula, quantity: 1, fruit: 'banana')).to eq(2)
|
313
|
+
expect(calculator.evaluate(formula, quantity: 1, fruit: 'orange')).to eq(3)
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'handles nested case statements' do
|
317
|
+
formula = <<-FORMULA
|
318
|
+
CASE fruit
|
319
|
+
WHEN 'apple'
|
320
|
+
THEN 1 * quantity
|
321
|
+
WHEN 'banana'
|
322
|
+
THEN
|
323
|
+
CASE quantity
|
324
|
+
WHEN 1 THEN 2
|
325
|
+
WHEN 10 THEN
|
326
|
+
CASE type
|
327
|
+
WHEN 'organic' THEN 5
|
328
|
+
END
|
329
|
+
END
|
330
|
+
END
|
331
|
+
FORMULA
|
332
|
+
value = calculator.evaluate(
|
333
|
+
formula,
|
334
|
+
type: 'organic',
|
335
|
+
quantity: 10,
|
336
|
+
fruit: 'banana')
|
337
|
+
expect(value).to eq(5)
|
338
|
+
end
|
230
339
|
end
|
231
340
|
|
232
341
|
describe 'math functions' do
|
@@ -38,6 +38,19 @@ describe Dentaku::Calculator do
|
|
38
38
|
it 'includes SMALLEST' do
|
39
39
|
expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
|
40
40
|
end
|
41
|
+
|
42
|
+
it 'supports array parameters' do
|
43
|
+
calculator = described_class.new
|
44
|
+
calculator.add_function(
|
45
|
+
:includes,
|
46
|
+
:logical,
|
47
|
+
->(haystack, needle) {
|
48
|
+
haystack.include?(needle)
|
49
|
+
}
|
50
|
+
)
|
51
|
+
|
52
|
+
expect(calculator.evaluate("INCLUDES(list, 2)", list: [1,2,3])).to eq(true)
|
53
|
+
end
|
41
54
|
end
|
42
55
|
end
|
43
56
|
end
|
data/spec/parser_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'dentaku/token'
|
2
3
|
require 'dentaku/parser'
|
3
4
|
|
4
5
|
describe Dentaku::Parser do
|
@@ -94,4 +95,32 @@ describe Dentaku::Parser do
|
|
94
95
|
node = described_class.new([d_true, d_and, d_false]).parse
|
95
96
|
expect(node.value).to eq false
|
96
97
|
end
|
98
|
+
|
99
|
+
it 'evaluates a case statement' do
|
100
|
+
case_start = Dentaku::Token.new(:case, :open)
|
101
|
+
x = Dentaku::Token.new(:identifier, :x)
|
102
|
+
case_when1 = Dentaku::Token.new(:case, :when)
|
103
|
+
one = Dentaku::Token.new(:numeric, 1)
|
104
|
+
case_then1 = Dentaku::Token.new(:case, :then)
|
105
|
+
two = Dentaku::Token.new(:numeric, 2)
|
106
|
+
case_when2 = Dentaku::Token.new(:case, :when)
|
107
|
+
three = Dentaku::Token.new(:numeric, 3)
|
108
|
+
case_then2 = Dentaku::Token.new(:case, :then)
|
109
|
+
four = Dentaku::Token.new(:numeric, 4)
|
110
|
+
case_close = Dentaku::Token.new(:case, :close)
|
111
|
+
|
112
|
+
node = described_class.new(
|
113
|
+
[case_start,
|
114
|
+
x,
|
115
|
+
case_when1,
|
116
|
+
one,
|
117
|
+
case_then1,
|
118
|
+
two,
|
119
|
+
case_when2,
|
120
|
+
three,
|
121
|
+
case_then2,
|
122
|
+
four,
|
123
|
+
case_close]).parse
|
124
|
+
expect(node.value(x: 3)).to eq(4)
|
125
|
+
end
|
97
126
|
end
|
data/spec/token_scanner_spec.rb
CHANGED
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: 2.0.
|
4
|
+
version: 2.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -71,6 +71,12 @@ files:
|
|
71
71
|
- lib/dentaku.rb
|
72
72
|
- lib/dentaku/ast.rb
|
73
73
|
- lib/dentaku/ast/arithmetic.rb
|
74
|
+
- lib/dentaku/ast/case.rb
|
75
|
+
- lib/dentaku/ast/case/case_conditional.rb
|
76
|
+
- lib/dentaku/ast/case/case_else.rb
|
77
|
+
- lib/dentaku/ast/case/case_switch_variable.rb
|
78
|
+
- lib/dentaku/ast/case/case_then.rb
|
79
|
+
- lib/dentaku/ast/case/case_when.rb
|
74
80
|
- lib/dentaku/ast/combinators.rb
|
75
81
|
- lib/dentaku/ast/comparators.rb
|
76
82
|
- lib/dentaku/ast/function.rb
|
@@ -105,6 +111,7 @@ files:
|
|
105
111
|
- lib/dentaku/version.rb
|
106
112
|
- spec/ast/addition_spec.rb
|
107
113
|
- spec/ast/and_spec.rb
|
114
|
+
- spec/ast/case_spec.rb
|
108
115
|
- spec/ast/division_spec.rb
|
109
116
|
- spec/ast/function_spec.rb
|
110
117
|
- spec/ast/node_spec.rb
|
@@ -148,6 +155,7 @@ summary: A formula language parser and evaluator
|
|
148
155
|
test_files:
|
149
156
|
- spec/ast/addition_spec.rb
|
150
157
|
- spec/ast/and_spec.rb
|
158
|
+
- spec/ast/case_spec.rb
|
151
159
|
- spec/ast/division_spec.rb
|
152
160
|
- spec/ast/function_spec.rb
|
153
161
|
- spec/ast/node_spec.rb
|
@@ -164,4 +172,3 @@ test_files:
|
|
164
172
|
- spec/token_scanner_spec.rb
|
165
173
|
- spec/token_spec.rb
|
166
174
|
- spec/tokenizer_spec.rb
|
167
|
-
has_rdoc:
|