dentaku 2.0.7 → 2.0.8
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 +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:
|