dentaku 3.0.0 → 3.1.0

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