dentaku 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +119 -0
  3. data/.travis.yml +8 -9
  4. data/CHANGELOG.md +9 -0
  5. data/Gemfile +0 -5
  6. data/LICENSE +21 -0
  7. data/README.md +44 -3
  8. data/Rakefile +4 -1
  9. data/dentaku.gemspec +8 -4
  10. data/lib/dentaku.rb +15 -2
  11. data/lib/dentaku/ast.rb +1 -0
  12. data/lib/dentaku/ast/access.rb +2 -2
  13. data/lib/dentaku/ast/arithmetic.rb +7 -7
  14. data/lib/dentaku/ast/bitwise.rb +2 -2
  15. data/lib/dentaku/ast/case.rb +5 -5
  16. data/lib/dentaku/ast/case/case_conditional.rb +1 -1
  17. data/lib/dentaku/ast/case/case_else.rb +2 -2
  18. data/lib/dentaku/ast/case/case_switch_variable.rb +2 -2
  19. data/lib/dentaku/ast/case/case_then.rb +2 -2
  20. data/lib/dentaku/ast/case/case_when.rb +2 -2
  21. data/lib/dentaku/ast/combinators.rb +10 -2
  22. data/lib/dentaku/ast/comparators.rb +34 -6
  23. data/lib/dentaku/ast/function.rb +1 -1
  24. data/lib/dentaku/ast/function_registry.rb +1 -1
  25. data/lib/dentaku/ast/functions/if.rb +6 -2
  26. data/lib/dentaku/ast/functions/max.rb +1 -1
  27. data/lib/dentaku/ast/functions/min.rb +1 -1
  28. data/lib/dentaku/ast/functions/ruby_math.rb +1 -1
  29. data/lib/dentaku/ast/functions/string_functions.rb +8 -8
  30. data/lib/dentaku/ast/functions/sum.rb +12 -0
  31. data/lib/dentaku/ast/grouping.rb +2 -2
  32. data/lib/dentaku/ast/identifier.rb +8 -5
  33. data/lib/dentaku/ast/negation.rb +2 -2
  34. data/lib/dentaku/ast/node.rb +1 -1
  35. data/lib/dentaku/ast/operation.rb +1 -1
  36. data/lib/dentaku/bulk_expression_solver.rb +39 -20
  37. data/lib/dentaku/calculator.rb +38 -28
  38. data/lib/dentaku/dependency_resolver.rb +1 -1
  39. data/lib/dentaku/flat_hash.rb +31 -0
  40. data/lib/dentaku/parser.rb +7 -6
  41. data/lib/dentaku/string_casing.rb +7 -0
  42. data/lib/dentaku/token.rb +1 -1
  43. data/lib/dentaku/token_matcher.rb +4 -4
  44. data/lib/dentaku/token_scanner.rb +18 -7
  45. data/lib/dentaku/tokenizer.rb +26 -2
  46. data/lib/dentaku/version.rb +1 -1
  47. data/spec/ast/arithmetic_spec.rb +2 -2
  48. data/spec/ast/comparator_spec.rb +57 -0
  49. data/spec/ast/function_spec.rb +1 -1
  50. data/spec/ast/max_spec.rb +5 -0
  51. data/spec/ast/min_spec.rb +5 -0
  52. data/spec/ast/sum_spec.rb +38 -0
  53. data/spec/benchmark.rb +2 -2
  54. data/spec/bulk_expression_solver_spec.rb +89 -1
  55. data/spec/calculator_spec.rb +40 -7
  56. data/spec/dentaku_spec.rb +11 -0
  57. data/spec/external_function_spec.rb +7 -7
  58. data/spec/parser_spec.rb +11 -11
  59. data/spec/spec_helper.rb +21 -3
  60. data/spec/token_matcher_spec.rb +0 -1
  61. data/spec/token_spec.rb +6 -0
  62. data/spec/tokenizer_spec.rb +37 -0
  63. metadata +70 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 99f7f7efeefb4c1096618e028100c52bb15ca134
4
- data.tar.gz: 1afd93ef547f630fb0ea121a6e8c813ea8abeeac
3
+ metadata.gz: 32158c76337de5c5f07140c71cb70fa6ea1d2963
4
+ data.tar.gz: 42bc70de0b8208fda14f82850c5362d34410bd16
5
5
  SHA512:
6
- metadata.gz: 31227f42fb7f0a262e82b2ccdc334adc4458ba7339c87f0fbf81eb49ffb51bac72654e05de9495d8dad73c9669b8d82bf9bdf8c4f7ee1a5b5830aba123dc6f17
7
- data.tar.gz: 2f78631eb082dc6c2d52ccac04d057c11c05b212ab05ffaa197e4f5ebf80aafc23113691d1dc0653c6af5df2827d6a4b45af58d4bfac73aa0156e3e91fbdb063
6
+ metadata.gz: 7362ea2c8a48f4e218807c6a5d979c64460e19bd4cd26a356433b77200fec7871b8d13343c99404979179d2362ec8028f4205990a38c48cb70ed101c06a3b57f
7
+ data.tar.gz: b399f646e1cf8671041d11a07db4f65d7b21f3c4bd31cd91ad643aa8ad6266272b75713de501c5307395bcf67867209554e085a8e0a5aafd088a9dca8364cc3f
@@ -0,0 +1,119 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.4
3
+ # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
4
+ # to ignore them, so only the ones explicitly set in this file are enabled.
5
+ DisabledByDefault: true
6
+
7
+ # Prefer &&/|| over and/or.
8
+ Style/AndOr:
9
+ Enabled: true
10
+
11
+ # Do not use braces for hash literals when they are the last argument of a
12
+ # method call.
13
+ Style/BracesAroundHashParameters:
14
+ Enabled: true
15
+
16
+ # Align `when` with `case`.
17
+ Layout/CaseIndentation:
18
+ Enabled: true
19
+
20
+ # Align comments with method definitions.
21
+ Layout/CommentIndentation:
22
+ Enabled: true
23
+
24
+ # No extra empty lines.
25
+ Layout/EmptyLines:
26
+ Enabled: true
27
+
28
+ # In a regular class definition, no empty lines around the body.
29
+ Layout/EmptyLinesAroundClassBody:
30
+ Enabled: true
31
+
32
+ # In a regular method definition, no empty lines around the body.
33
+ Layout/EmptyLinesAroundMethodBody:
34
+ Enabled: true
35
+
36
+ # In a regular module definition, no empty lines around the body.
37
+ Layout/EmptyLinesAroundModuleBody:
38
+ Enabled: true
39
+
40
+ # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
41
+ Style/HashSyntax:
42
+ Enabled: true
43
+
44
+ # Method definitions after `private` or `protected` isolated calls need one
45
+ # extra level of indentation.
46
+ Layout/IndentationConsistency:
47
+ Enabled: false
48
+
49
+ # Two spaces, no tabs (for indentation).
50
+ Layout/IndentationWidth:
51
+ Enabled: true
52
+
53
+ Layout/SpaceAfterColon:
54
+ Enabled: true
55
+
56
+ Layout/SpaceAfterComma:
57
+ Enabled: true
58
+
59
+ Layout/SpaceAroundEqualsInParameterDefault:
60
+ Enabled: true
61
+
62
+ Layout/SpaceAroundKeyword:
63
+ Enabled: true
64
+
65
+ Layout/SpaceAroundOperators:
66
+ Enabled: true
67
+
68
+ Layout/SpaceBeforeFirstArg:
69
+ Enabled: true
70
+
71
+ # Defining a method with parameters needs parentheses.
72
+ Style/MethodDefParentheses:
73
+ Enabled: true
74
+
75
+ # Use `foo {}` not `foo{}`.
76
+ Layout/SpaceBeforeBlockBraces:
77
+ Enabled: true
78
+
79
+ # Use `foo { bar }` not `foo {bar}`.
80
+ Layout/SpaceInsideBlockBraces:
81
+ Enabled: true
82
+
83
+ # Use `{a: 1}` not `{ a:1 }`.
84
+ Layout/SpaceInsideHashLiteralBraces:
85
+ Enabled: false
86
+
87
+ Layout/SpaceInsideParens:
88
+ Enabled: true
89
+
90
+ # Check quotes usage according to lint rule below.
91
+ Style/StringLiterals:
92
+ Enabled: false
93
+ EnforcedStyle: double_quotes
94
+
95
+ # Detect hard tabs, no hard tabs.
96
+ Layout/Tab:
97
+ Enabled: true
98
+
99
+ # Blank lines should not have any spaces.
100
+ Layout/TrailingBlankLines:
101
+ Enabled: true
102
+
103
+ # No trailing whitespace.
104
+ Layout/TrailingWhitespace:
105
+ Enabled: true
106
+
107
+ # Use quotes for string literals when they are enough.
108
+ Style/UnneededPercentQ:
109
+ Enabled: true
110
+
111
+ # Align `end` with the matching keyword or starting expression except for
112
+ # assignments, where it should be aligned with the LHS.
113
+ Lint/EndAlignment:
114
+ Enabled: true
115
+ EnforcedStyleAlignWith: variable
116
+
117
+ # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
118
+ Lint/RequireParentheses:
119
+ Enabled: true
@@ -1,12 +1,11 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - 2.0.0
5
- - 2.1.0
6
- - 2.1.1
7
- - 2.2.0
8
- - 2.2.1
9
- - 2.2.2
10
- - 2.2.3
11
- - 2.3.0
12
- - 2.4.0
4
+ - 2.0.0-p648
5
+ - 2.1.10
6
+ - 2.2.8
7
+ - 2.3.5
8
+ - 2.4.2
9
+ before_install:
10
+ - gem update bundler
11
+ - gem update --system
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## [v3.1.0] 2017-01-10
4
+ - allow decimals with no leading zero
5
+ - nested hash and array support in bulk expression solver
6
+ - add a variadic SUM function
7
+ - support array arguments to min, max, sum, and Math functions
8
+ - add case-sensitive variable support
9
+ - allow opt-out of nested data for performance boost
10
+
3
11
  ## [v3.0.0] 2017-10-11
4
12
  - add && and || as aliases for AND and OR
5
13
  - add hexadecimal literal support
@@ -143,6 +151,7 @@
143
151
  ## [v0.1.0] 2012-01-20
144
152
  - initial release
145
153
 
154
+ [v3.1.0]: https://github.com/rubysolo/dentaku/compare/v3.0.0...v3.1.0
146
155
  [v3.0.0]: https://github.com/rubysolo/dentaku/compare/v2.0.11...v3.0.0
147
156
  [v2.0.11]: https://github.com/rubysolo/dentaku/compare/v2.0.10...v2.0.11
148
157
  [v2.0.10]: https://github.com/rubysolo/dentaku/compare/v2.0.9...v2.0.10
data/Gemfile CHANGED
@@ -2,8 +2,3 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in dentaku.gemspec
4
4
  gemspec
5
-
6
- if RUBY_VERSION.to_f >= 2.0 && RUBY_ENGINE == 'ruby'
7
- gem 'pry-byebug'
8
- gem 'pry-stack_explorer'
9
- end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2012 Solomon White
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -6,7 +6,8 @@ Dentaku
6
6
  [![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
7
7
  [![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
8
8
  [![Hakiri](https://hakiri.io/github/rubysolo/dentaku/master.svg)](https://hakiri.io/github/rubysolo/dentaku)
9
- [![Coverage Status](https://coveralls.io/repos/github/rubysolo/dentaku/badge.svg)](https://coveralls.io/github/rubysolo/dentaku)
9
+ [![Coverage](https://codecov.io/gh/rubysolo/dentaku/branch/master/graph/badge.svg)](https://codecov.io/gh/rubysolo/dentaku)
10
+
10
11
 
11
12
  DESCRIPTION
12
13
  -----------
@@ -35,6 +36,16 @@ calculator.evaluate('kiwi + 5', kiwi: 2)
35
36
  #=> 7
36
37
  ```
37
38
 
39
+ To enter a case sensitive mode, just pass an option to the calculator instance:
40
+
41
+ ```ruby
42
+ calculator.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2)
43
+ #=> 7
44
+ calculator = Dentaku::Calculator.new(case_sensitive: true)
45
+ calculator.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2)
46
+ #=> 3
47
+ ```
48
+
38
49
  You can also store the variable values in the calculator's memory and then
39
50
  evaluate expressions against those stored values:
40
51
 
@@ -69,11 +80,14 @@ calculator.evaluate!('10 * x')
69
80
  Dentaku::UnboundVariableError: Dentaku::UnboundVariableError
70
81
  ```
71
82
 
72
- Dentaku has built-in functions (including `if`, `not`, `min`, `max`, and
83
+ Dentaku has built-in functions (including `if`, `not`, `min`, `max`, `sum`, and
73
84
  `round`) and the ability to define custom functions (see below). Functions
74
85
  generally work like their counterparts in Excel:
75
86
 
76
87
  ```ruby
88
+ calculator.evaluate('SUM(1, 1, 2, 3, 5, 8)')
89
+ #=> 20
90
+
77
91
  calculator.evaluate('if (pears < 10, 10, 20)', pears: 5)
78
92
  #=> 10
79
93
  calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
@@ -132,7 +146,7 @@ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
132
146
 
133
147
  Logic: `IF`, `AND`, `OR`, `NOT`, `SWITCH`
134
148
 
135
- Numeric: `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
149
+ Numeric: `MIN`, `MAX`, `SUM`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
136
150
 
137
151
  Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
138
152
 
@@ -267,6 +281,33 @@ function)
267
281
  Functions can be added individually using Calculator#add_function, or en masse
268
282
  using Calculator#add_functions.
269
283
 
284
+ FUNCTION ALIASES
285
+ ----------------
286
+
287
+ Every function can be aliased by synonyms. For example, it can be useful if
288
+ your application is multilingual.
289
+
290
+ ```ruby
291
+ Dentaku.aliases = {
292
+ round: ['rrrrround!', 'округлить']
293
+ }
294
+
295
+ Dentaku('rrrrround!(8.2) + округлить(8.4)') # the same as round(8.2) + round(8.4)
296
+ # 16
297
+ ```
298
+
299
+ Also, if you need thread-safe aliases you can pass them to `Dentaku::Calculator`
300
+ initializer:
301
+
302
+ ```ruby
303
+ aliases = {
304
+ round: ['rrrrround!', 'округлить']
305
+ }
306
+ c = Dentaku::Calculator.new(aliases: aliases)
307
+ c.evaluate('rrrrround!(8.2) + округлить(8.4)')
308
+ # 16
309
+ ```
310
+
270
311
  THANKS
271
312
  ------
272
313
 
data/Rakefile CHANGED
@@ -1,5 +1,8 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RuboCop::RakeTask.new
3
6
 
4
7
  desc "Run specs"
5
8
  task :spec do
@@ -10,7 +13,7 @@ task :spec do
10
13
  end
11
14
 
12
15
  desc "Default: run specs."
13
- task default: :spec
16
+ task(:default).clear.enhance [:spec, :rubocop]
14
17
 
15
18
  task :console do
16
19
  begin
@@ -9,20 +9,24 @@ Gem::Specification.new do |s|
9
9
  s.email = ["rubysolo@gmail.com"]
10
10
  s.homepage = "http://github.com/rubysolo/dentaku"
11
11
  s.licenses = %w(MIT)
12
- s.summary = %q{A formula language parser and evaluator}
12
+ s.summary = 'A formula language parser and evaluator'
13
13
  s.description = <<-DESC
14
14
  Dentaku is a parser and evaluator for mathematical formulas
15
15
  DESC
16
16
 
17
17
  s.rubyforge_project = "dentaku"
18
18
 
19
+ s.add_development_dependency('codecov')
20
+ s.add_development_dependency('pry')
21
+ s.add_development_dependency('pry-byebug')
22
+ s.add_development_dependency('pry-stack_explorer')
19
23
  s.add_development_dependency('rake')
20
24
  s.add_development_dependency('rspec')
21
- s.add_development_dependency('pry')
22
- s.add_development_dependency('coveralls')
25
+ s.add_development_dependency('rubocop')
26
+ s.add_development_dependency('simplecov')
23
27
 
24
28
  s.files = `git ls-files`.split("\n")
25
29
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
30
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
27
31
  s.require_paths = ["lib"]
28
32
  end
@@ -5,11 +5,16 @@ require "dentaku/version"
5
5
  module Dentaku
6
6
  @enable_ast_caching = false
7
7
  @enable_dependency_order_caching = false
8
+ @aliases = {}
8
9
 
9
- def self.evaluate(expression, data={})
10
+ def self.evaluate(expression, data = {})
10
11
  calculator.evaluate(expression, data)
11
12
  end
12
13
 
14
+ def self.evaluate!(expression, data = {})
15
+ calculator.evaluate!(expression, data)
16
+ end
17
+
13
18
  def self.enable_caching!
14
19
  enable_ast_cache!
15
20
  enable_dependency_order_cache!
@@ -31,6 +36,14 @@ module Dentaku
31
36
  @enable_dependency_order_caching
32
37
  end
33
38
 
39
+ def self.aliases
40
+ @aliases
41
+ end
42
+
43
+ def self.aliases=(hash)
44
+ @aliases = hash
45
+ end
46
+
34
47
  private
35
48
 
36
49
  def self.calculator
@@ -38,6 +51,6 @@ module Dentaku
38
51
  end
39
52
  end
40
53
 
41
- def Dentaku(expression, data={})
54
+ def Dentaku(expression, data = {})
42
55
  Dentaku.evaluate(expression, data)
43
56
  end
@@ -25,4 +25,5 @@ require_relative './ast/functions/roundup'
25
25
  require_relative './ast/functions/rounddown'
26
26
  require_relative './ast/functions/ruby_math'
27
27
  require_relative './ast/functions/string_functions'
28
+ require_relative './ast/functions/sum'
28
29
  require_relative './ast/functions/switch'
@@ -13,13 +13,13 @@ module Dentaku
13
13
  @index = index
14
14
  end
15
15
 
16
- def value(context={})
16
+ def value(context = {})
17
17
  structure = @structure.value(context)
18
18
  index = @index.value(context)
19
19
  structure[index]
20
20
  end
21
21
 
22
- def dependencies(context={})
22
+ def dependencies(context = {})
23
23
  @structure.dependencies(context) + @index.dependencies(context)
24
24
  end
25
25
  end
@@ -26,7 +26,7 @@ module Dentaku
26
26
  raise NotImplementedError
27
27
  end
28
28
 
29
- def value(context={})
29
+ def value(context = {})
30
30
  l = cast(left.value(context))
31
31
  r = cast(right.value(context))
32
32
  l.public_send(operator, r)
@@ -34,13 +34,13 @@ module Dentaku
34
34
 
35
35
  private
36
36
 
37
- def cast(val, prefer_integer=true)
37
+ def cast(val, prefer_integer = true)
38
38
  validate_value(val)
39
39
  numeric(val, prefer_integer)
40
40
  end
41
41
 
42
42
  def numeric(val, prefer_integer)
43
- v = BigDecimal.new(val, Float::DIG+1)
43
+ v = BigDecimal.new(val, Float::DIG + 1)
44
44
  v = v.to_i if prefer_integer && v.frac.zero?
45
45
  v
46
46
  rescue ::TypeError
@@ -77,7 +77,7 @@ module Dentaku
77
77
  end
78
78
 
79
79
  def validate_format(string)
80
- unless string =~ /\A-?\d+(\.\d+)?\z/
80
+ unless string =~ /\A-?\d*(\.\d+)?\z/
81
81
  raise Dentaku::ArgumentError.for(:invalid_value, value: string, for: BigDecimal),
82
82
  "String input '#{string}' is not coercible to numeric"
83
83
  end
@@ -119,7 +119,7 @@ module Dentaku
119
119
  :/
120
120
  end
121
121
 
122
- def value(context={})
122
+ def value(context = {})
123
123
  r = cast(right.value(context), false)
124
124
  raise Dentaku::ZeroDivisionError if r.zero?
125
125
 
@@ -141,7 +141,7 @@ module Dentaku
141
141
  @arity = 2 if input.length > 1
142
142
  end
143
143
 
144
- def initialize(left, right=nil)
144
+ def initialize(left, right = nil)
145
145
  if right
146
146
  @left = left
147
147
  @right = right
@@ -163,7 +163,7 @@ module Dentaku
163
163
  left.nil?
164
164
  end
165
165
 
166
- def value(context={})
166
+ def value(context = {})
167
167
  if percent?
168
168
  cast(right.value(context)) * 0.01
169
169
  else