dentaku 2.0.5 → 2.0.6
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 +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:
|