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 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: