dentaku 2.0.7 → 2.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -3
- data/CHANGELOG.md +5 -2
- data/README.md +9 -5
- data/lib/dentaku/ast.rb +1 -0
- data/lib/dentaku/ast/arithmetic.rb +7 -0
- data/lib/dentaku/ast/functions/string_functions.rb +104 -0
- data/lib/dentaku/calculator.rb +1 -1
- data/lib/dentaku/exceptions.rb +3 -0
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/string_functions_spec.rb +135 -0
- data/spec/calculator_spec.rb +15 -0
- data/spec/spec_helper.rb +8 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5dd3288b6e13441c5e71ac914a527a9c4d97daa
|
4
|
+
data.tar.gz: 88c305d4010f094f2f59615fd830ea583274d9ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 588690a66352277de04db87a048047c54d47bf8bd5ce92f3b1d841345a5cf721c903631a283d8912346ccead66242c13ae2f86e3ad186a1ab9b943e05988d095
|
7
|
+
data.tar.gz: 38b5f255f8e5bac861729589a71b7d7b6deec651bfc0fde17bfb07e96c2c710c758f58835dd9071bf695bb6dda45c532e6a25fc895e908ea10066a7450ac8123
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [v2.0.8] 2016-05-10
|
4
|
+
- numeric input validations
|
5
|
+
- fail with gem-specific error for invalid arithmetic operands
|
6
|
+
- add `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, and `CONCAT` string functions
|
4
7
|
|
5
8
|
## [v2.0.7] 2016-02-25
|
6
9
|
- fail with gem-specific error for parsing issues
|
@@ -103,7 +106,7 @@
|
|
103
106
|
## [v0.1.0] 2012-01-20
|
104
107
|
- initial release
|
105
108
|
|
106
|
-
[
|
109
|
+
[v2.0.8]: https://github.com/rubysolo/dentaku/compare/v2.0.7...v2.0.8
|
107
110
|
[v2.0.7]: https://github.com/rubysolo/dentaku/compare/v2.0.6...v2.0.7
|
108
111
|
[v2.0.6]: https://github.com/rubysolo/dentaku/compare/v2.0.5...v2.0.6
|
109
112
|
[v2.0.5]: https://github.com/rubysolo/dentaku/compare/v2.0.4...v2.0.5
|
data/README.md
CHANGED
@@ -57,7 +57,7 @@ calculator.evaluate('(5 + 3) * 2')
|
|
57
57
|
#=> 16
|
58
58
|
```
|
59
59
|
|
60
|
-
The `
|
60
|
+
The `evaluate` method will return `nil` if there is an error in the formula.
|
61
61
|
If this is not the desired behavior, use `evaluate!`, which will raise an
|
62
62
|
exception.
|
63
63
|
|
@@ -123,13 +123,17 @@ application, AST caching will consume more memory with each new formula.
|
|
123
123
|
BUILT-IN OPERATORS AND FUNCTIONS
|
124
124
|
---------------------------------
|
125
125
|
|
126
|
-
Math:
|
126
|
+
Math: `+`, `-`, `*`, `/`, `%`
|
127
127
|
|
128
|
-
Logic:
|
128
|
+
Logic: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`, `AND`, `OR`
|
129
129
|
|
130
|
-
Functions: `IF NOT MIN MAX ROUND ROUNDDOWN ROUNDUP`
|
130
|
+
Functions: `IF`, `NOT`, `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
|
131
131
|
|
132
|
-
|
132
|
+
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
|
133
|
+
|
134
|
+
Math: all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
|
135
|
+
|
136
|
+
String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`
|
133
137
|
|
134
138
|
RESOLVING DEPENDENCIES
|
135
139
|
----------------------
|
data/lib/dentaku/ast.rb
CHANGED
@@ -25,6 +25,7 @@ module Dentaku
|
|
25
25
|
private
|
26
26
|
|
27
27
|
def cast(value, prefer_integer=true)
|
28
|
+
validate_numeric(value)
|
28
29
|
v = BigDecimal.new(value, Float::DIG+1)
|
29
30
|
v = v.to_i if prefer_integer && v.frac.zero?
|
30
31
|
v
|
@@ -33,6 +34,12 @@ module Dentaku
|
|
33
34
|
def valid_node?(node)
|
34
35
|
node && (node.dependencies.any? || node.type == :numeric)
|
35
36
|
end
|
37
|
+
|
38
|
+
def validate_numeric(value)
|
39
|
+
Float(value)
|
40
|
+
rescue ::ArgumentError, ::TypeError
|
41
|
+
fail Dentaku::ArgumentError, "#{ self.class } requires numeric operands"
|
42
|
+
end
|
36
43
|
end
|
37
44
|
|
38
45
|
class Addition < Arithmetic
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
module AST
|
5
|
+
module StringFunctions
|
6
|
+
class Left < Function
|
7
|
+
def initialize(string, length)
|
8
|
+
@string = string
|
9
|
+
@length = length
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context={})
|
13
|
+
@string.value(context)[0, @length.value(context)]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Right < Function
|
18
|
+
def initialize(string, length)
|
19
|
+
@string = string
|
20
|
+
@length = length
|
21
|
+
end
|
22
|
+
|
23
|
+
def value(context={})
|
24
|
+
string = @string.value(context)
|
25
|
+
length = @length.value(context)
|
26
|
+
string[length * -1, length] || string
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Mid < Function
|
31
|
+
def initialize(string, offset, length)
|
32
|
+
@string = string
|
33
|
+
@offset = offset
|
34
|
+
@length = length
|
35
|
+
end
|
36
|
+
|
37
|
+
def value(context={})
|
38
|
+
string = @string.value(context)
|
39
|
+
offset = @offset.value(context)
|
40
|
+
length = @length.value(context)
|
41
|
+
string[offset - 1, length].to_s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Len < Function
|
46
|
+
def initialize(string)
|
47
|
+
@string = string
|
48
|
+
end
|
49
|
+
|
50
|
+
def value(context={})
|
51
|
+
@string.value(context).length
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Find < Function
|
56
|
+
def initialize(needle, haystack)
|
57
|
+
@needle = needle
|
58
|
+
@haystack = haystack
|
59
|
+
end
|
60
|
+
|
61
|
+
def value(context={})
|
62
|
+
needle = @needle.value(context)
|
63
|
+
haystack = @haystack.value(context)
|
64
|
+
pos = haystack.index(needle)
|
65
|
+
pos && pos + 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Substitute < Function
|
70
|
+
def initialize(original, search, replacement)
|
71
|
+
@original = original
|
72
|
+
@search = search
|
73
|
+
@replacement = replacement
|
74
|
+
end
|
75
|
+
|
76
|
+
def value(context={})
|
77
|
+
original = @original.value(context)
|
78
|
+
search = @search.value(context)
|
79
|
+
replacement = @replacement.value(context)
|
80
|
+
original.sub(search, replacement)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Concat < Function
|
85
|
+
def initialize(left, right)
|
86
|
+
@left = left
|
87
|
+
@right = right
|
88
|
+
end
|
89
|
+
|
90
|
+
def value(context={})
|
91
|
+
@left.value(context) + @right.value(context)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
Dentaku::AST::Function.register_class(:left, Dentaku::AST::StringFunctions::Left)
|
99
|
+
Dentaku::AST::Function.register_class(:right, Dentaku::AST::StringFunctions::Right)
|
100
|
+
Dentaku::AST::Function.register_class(:mid, Dentaku::AST::StringFunctions::Mid)
|
101
|
+
Dentaku::AST::Function.register_class(:len, Dentaku::AST::StringFunctions::Len)
|
102
|
+
Dentaku::AST::Function.register_class(:find, Dentaku::AST::StringFunctions::Find)
|
103
|
+
Dentaku::AST::Function.register_class(:substitute, Dentaku::AST::StringFunctions::Substitute)
|
104
|
+
Dentaku::AST::Function.register_class(:concat, Dentaku::AST::StringFunctions::Concat)
|
data/lib/dentaku/calculator.rb
CHANGED
data/lib/dentaku/exceptions.rb
CHANGED
data/lib/dentaku/version.rb
CHANGED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/string_functions'
|
3
|
+
|
4
|
+
describe Dentaku::AST::StringFunctions::Left do
|
5
|
+
let(:string) { identifier('string') }
|
6
|
+
let(:length) { identifier('length') }
|
7
|
+
|
8
|
+
subject { described_class.new(string, length) }
|
9
|
+
|
10
|
+
it 'returns the left N characters of the string' do
|
11
|
+
expect(subject.value('string' => 'ABCDEFG', 'length' => 4)).to eq 'ABCD'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'works correctly with literals' do
|
15
|
+
left = literal('ABCD')
|
16
|
+
len = literal(2)
|
17
|
+
fn = described_class.new(left, len)
|
18
|
+
expect(fn.value).to eq 'AB'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'handles an empty string correctly' do
|
22
|
+
expect(subject.value('string' => '', 'length' => 4)).to eq ''
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'handles size greater than input string length correctly' do
|
26
|
+
expect(subject.value('string' => 'abcdefg', 'length' => 40)).to eq 'abcdefg'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe Dentaku::AST::StringFunctions::Right do
|
31
|
+
it 'returns the right N characters of the string' do
|
32
|
+
subject = described_class.new(literal('ABCDEFG'), literal(4))
|
33
|
+
expect(subject.value).to eq 'DEFG'
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'handles an empty string correctly' do
|
37
|
+
subject = described_class.new(literal(''), literal(4))
|
38
|
+
expect(subject.value).to eq ''
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'handles size greater than input string length correctly' do
|
42
|
+
subject = described_class.new(literal('abcdefg'), literal(40))
|
43
|
+
expect(subject.value).to eq 'abcdefg'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe Dentaku::AST::StringFunctions::Mid do
|
48
|
+
it 'returns a substring from the middle of the string' do
|
49
|
+
subject = described_class.new(literal('ABCDEFG'), literal(4), literal(2))
|
50
|
+
expect(subject.value).to eq 'DE'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'handles an empty string correctly' do
|
54
|
+
subject = described_class.new(literal(''), literal(4), literal(2))
|
55
|
+
expect(subject.value).to eq ''
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'handles offset greater than input string length correctly' do
|
59
|
+
subject = described_class.new(literal('abcdefg'), literal(40), literal(4))
|
60
|
+
expect(subject.value).to eq ''
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'handles size greater than input string length correctly' do
|
64
|
+
subject = described_class.new(literal('abcdefg'), literal(4), literal(40))
|
65
|
+
expect(subject.value).to eq 'defg'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe Dentaku::AST::StringFunctions::Len do
|
70
|
+
it 'returns the length of a string' do
|
71
|
+
subject = described_class.new(literal('ABCDEFG'))
|
72
|
+
expect(subject.value).to eq 7
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'handles an empty string correctly' do
|
76
|
+
subject = described_class.new(literal(''))
|
77
|
+
expect(subject.value).to eq 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe Dentaku::AST::StringFunctions::Find do
|
82
|
+
it 'returns the position of a substring within a string' do
|
83
|
+
subject = described_class.new(literal('DE'), literal('ABCDEFG'))
|
84
|
+
expect(subject.value).to eq 4
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'handles an empty substring correctly' do
|
88
|
+
subject = described_class.new(literal(''), literal('ABCDEFG'))
|
89
|
+
expect(subject.value).to eq 1
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'handles an empty string correctly' do
|
93
|
+
subject = described_class.new(literal('DE'), literal(''))
|
94
|
+
expect(subject.value).to be_nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe Dentaku::AST::StringFunctions::Substitute do
|
99
|
+
it 'replaces a substring within a string' do
|
100
|
+
subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal('xy'))
|
101
|
+
expect(subject.value).to eq 'ABCxyFG'
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'handles an empty search string correctly' do
|
105
|
+
subject = described_class.new(literal('ABCDEFG'), literal(''), literal('xy'))
|
106
|
+
expect(subject.value).to eq 'xyABCDEFG'
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'handles an empty replacement string correctly' do
|
110
|
+
subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal(''))
|
111
|
+
expect(subject.value).to eq 'ABCFG'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe Dentaku::AST::StringFunctions::Concat do
|
116
|
+
it 'concatenates two strings' do
|
117
|
+
subject = described_class.new(literal('ABC'), literal('DEF'))
|
118
|
+
expect(subject.value).to eq 'ABCDEF'
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'concatenates a string onto an empty string' do
|
122
|
+
subject = described_class.new(literal(''), literal('ABC'))
|
123
|
+
expect(subject.value).to eq 'ABC'
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'concatenates an empty string onto a string' do
|
127
|
+
subject = described_class.new(literal('ABC'), literal(''))
|
128
|
+
expect(subject.value).to eq 'ABC'
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'concatenates two empty strings' do
|
132
|
+
subject = described_class.new(literal(''), literal(''))
|
133
|
+
expect(subject.value).to eq ''
|
134
|
+
end
|
135
|
+
end
|
data/spec/calculator_spec.rb
CHANGED
@@ -31,6 +31,7 @@ describe Dentaku::Calculator do
|
|
31
31
|
expect(calculator.evaluate('(((695759/735000)^(1/(1981-1991)))-1)*1000').round(4)).to eq(5.5018)
|
32
32
|
expect(calculator.evaluate('0.253/0.253')).to eq(1)
|
33
33
|
expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
|
34
|
+
expect(calculator.evaluate('10 + x', x: 'abc')).to be_nil
|
34
35
|
end
|
35
36
|
|
36
37
|
describe 'memory' do
|
@@ -280,6 +281,12 @@ describe Dentaku::Calculator do
|
|
280
281
|
foo: nil
|
281
282
|
)
|
282
283
|
end
|
284
|
+
|
285
|
+
it 'raises errors when used in arithmetic operation' do
|
286
|
+
expect {
|
287
|
+
calculator.solve!(more_apples: "apples + 1", apples: nil)
|
288
|
+
}.to raise_error(Dentaku::ArgumentError)
|
289
|
+
end
|
283
290
|
end
|
284
291
|
|
285
292
|
describe 'case statements' do
|
@@ -371,4 +378,12 @@ describe Dentaku::Calculator do
|
|
371
378
|
end
|
372
379
|
end
|
373
380
|
end
|
381
|
+
|
382
|
+
describe 'string functions' do
|
383
|
+
it 'concatenates two strings' do
|
384
|
+
expect(
|
385
|
+
calculator.evaluate('CONCAT(s1, s2)', 's1' => 'abc', 's2' => 'def')
|
386
|
+
).to eq 'abcdef'
|
387
|
+
end
|
388
|
+
end
|
374
389
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -31,3 +31,11 @@ def type_for(value)
|
|
31
31
|
:identifier
|
32
32
|
end
|
33
33
|
end
|
34
|
+
|
35
|
+
def identifier(name)
|
36
|
+
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, name))
|
37
|
+
end
|
38
|
+
|
39
|
+
def literal(value)
|
40
|
+
Dentaku::AST::Literal.new(Dentaku::Token.new(type_for(value), value))
|
41
|
+
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: 2.0.
|
4
|
+
version: 2.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -87,6 +87,7 @@ files:
|
|
87
87
|
- lib/dentaku/ast/functions/rounddown.rb
|
88
88
|
- lib/dentaku/ast/functions/roundup.rb
|
89
89
|
- lib/dentaku/ast/functions/ruby_math.rb
|
90
|
+
- lib/dentaku/ast/functions/string_functions.rb
|
90
91
|
- lib/dentaku/ast/grouping.rb
|
91
92
|
- lib/dentaku/ast/identifier.rb
|
92
93
|
- lib/dentaku/ast/literal.rb
|
@@ -115,6 +116,7 @@ files:
|
|
115
116
|
- spec/ast/function_spec.rb
|
116
117
|
- spec/ast/node_spec.rb
|
117
118
|
- spec/ast/numeric_spec.rb
|
119
|
+
- spec/ast/string_functions_spec.rb
|
118
120
|
- spec/benchmark.rb
|
119
121
|
- spec/bulk_expression_solver_spec.rb
|
120
122
|
- spec/calculator_spec.rb
|
@@ -159,6 +161,7 @@ test_files:
|
|
159
161
|
- spec/ast/function_spec.rb
|
160
162
|
- spec/ast/node_spec.rb
|
161
163
|
- spec/ast/numeric_spec.rb
|
164
|
+
- spec/ast/string_functions_spec.rb
|
162
165
|
- spec/benchmark.rb
|
163
166
|
- spec/bulk_expression_solver_spec.rb
|
164
167
|
- spec/calculator_spec.rb
|
@@ -171,4 +174,3 @@ test_files:
|
|
171
174
|
- spec/token_scanner_spec.rb
|
172
175
|
- spec/token_spec.rb
|
173
176
|
- spec/tokenizer_spec.rb
|
174
|
-
has_rdoc:
|