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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c299535113d559ed61d819be474ced8128bf7e0f
4
- data.tar.gz: 80aa950604c573fcbee95cfa6bb820a1fa6af94b
3
+ metadata.gz: e5dd3288b6e13441c5e71ac914a527a9c4d97daa
4
+ data.tar.gz: 88c305d4010f094f2f59615fd830ea583274d9ca
5
5
  SHA512:
6
- metadata.gz: 616133f23a73885b22d1e53722d8482f9522e249ca03a76fe7a8685d1d365eebbde7b1677248ea6758cbf29e60f54504f9c4a4a9ee43735ab54cffc8cb9d11ac
7
- data.tar.gz: aea6a8919d66c838c704ba09b19787ddeb301f72ead03883ffc481ffe4ff7a54afa92108ed73794feaec4d83ea93cc17476dc108d0273401dba9c6077a603ec0
6
+ metadata.gz: 588690a66352277de04db87a048047c54d47bf8bd5ce92f3b1d841345a5cf721c903631a283d8912346ccead66242c13ae2f86e3ad186a1ab9b943e05988d095
7
+ data.tar.gz: 38b5f255f8e5bac861729589a71b7d7b6deec651bfc0fde17bfb07e96c2c710c758f58835dd9071bf695bb6dda45c532e6a25fc895e908ea10066a7450ac8123
@@ -1,7 +1,5 @@
1
1
  language: ruby
2
- before_install:
3
- - gem update --system
4
- - gem update bundler
2
+ sudo: false
5
3
  rvm:
6
4
  - 1.9.3
7
5
  - 2.0.0
@@ -11,4 +9,5 @@ rvm:
11
9
  - 2.2.1
12
10
  - 2.2.2
13
11
  - 2.2.3
12
+ - 2.3.0
14
13
  - rbx-2
@@ -1,6 +1,9 @@
1
1
  # Change Log
2
2
 
3
- ## [Unreleased]
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
- [Unreleased]: https://github.com/rubysolo/dentaku/compare/v2.0.7...HEAD
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 `evalutate` method will return `nil` if there is an error in the formula.
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: `< > <= >= <> != = AND OR`
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
- Math: all functions from Ruby's Math module, including `SIN, COS, TAN, ...`
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
  ----------------------
@@ -18,3 +18,4 @@ require_relative './ast/functions/round'
18
18
  require_relative './ast/functions/roundup'
19
19
  require_relative './ast/functions/rounddown'
20
20
  require_relative './ast/functions/ruby_math'
21
+ require_relative './ast/functions/string_functions'
@@ -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)
@@ -27,7 +27,7 @@ module Dentaku
27
27
 
28
28
  def evaluate(expression, data={})
29
29
  evaluate!(expression, data)
30
- rescue UnboundVariableError
30
+ rescue UnboundVariableError, ArgumentError
31
31
  yield expression if block_given?
32
32
  end
33
33
 
@@ -13,6 +13,9 @@ module Dentaku
13
13
  class ParseError < StandardError
14
14
  end
15
15
 
16
+ class ArgumentError < ::ArgumentError
17
+ end
18
+
16
19
  class ZeroDivisionError < ::ZeroDivisionError
17
20
  attr_accessor :recipient_variable
18
21
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "2.0.7"
2
+ VERSION = "2.0.8"
3
3
  end
@@ -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
@@ -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
@@ -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.7
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-02-25 00:00:00.000000000 Z
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: