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
@@ -10,7 +10,7 @@ module Dentaku
10
10
  @right = right
11
11
  end
12
12
 
13
- def dependencies(context={})
13
+ def dependencies(context = {})
14
14
  (left.dependencies(context) + right.dependencies(context)).uniq
15
15
  end
16
16
 
@@ -1,13 +1,14 @@
1
1
  require 'dentaku/dependency_resolver'
2
2
  require 'dentaku/exceptions'
3
+ require 'dentaku/flat_hash'
3
4
  require 'dentaku/parser'
4
5
  require 'dentaku/tokenizer'
5
6
 
6
7
  module Dentaku
7
8
  class BulkExpressionSolver
8
- def initialize(expression_hash, calculator)
9
- self.expression_hash = expression_hash
10
- self.calculator = calculator
9
+ def initialize(expressions, calculator)
10
+ @expression_hash = FlatHash.from_hash(expressions)
11
+ @calculator = calculator
11
12
  end
12
13
 
13
14
  def solve!
@@ -18,8 +19,19 @@ module Dentaku
18
19
  error_handler = block || return_undefined_handler
19
20
  results = load_results(&error_handler)
20
21
 
21
- expression_hash.each_with_object({}) do |(k, _), r|
22
- r[k] = results[k.to_s]
22
+ FlatHash.expand(
23
+ expression_hash.each_with_object({}) do |(k, _), r|
24
+ r[k] = results[k.to_s]
25
+ end
26
+ )
27
+ end
28
+
29
+ def dependencies
30
+ Hash[expression_deps].tap do |d|
31
+ d.values.each do |deps|
32
+ unresolved = deps.reject { |ud| d.has_key?(ud) }
33
+ unresolved.each { |u| add_dependencies(d, u) }
34
+ end
23
35
  end
24
36
  end
25
37
 
@@ -29,7 +41,7 @@ module Dentaku
29
41
  @dep_cache ||= {}
30
42
  end
31
43
 
32
- attr_accessor :expression_hash, :calculator
44
+ attr_reader :expression_hash, :calculator
33
45
 
34
46
  def return_undefined_handler
35
47
  ->(*) { :undefined }
@@ -39,38 +51,45 @@ module Dentaku
39
51
  ->(ex) { raise ex }
40
52
  end
41
53
 
54
+ def expression_with_exception_handler(&block)
55
+ ->(expr, ex) { block.call(ex) }
56
+ end
57
+
42
58
  def load_results(&block)
43
59
  variables_in_resolve_order.each_with_object({}) do |var_name, r|
44
60
  begin
45
- value_from_memory = calculator.memory[var_name]
61
+ solved = calculator.memory
62
+ value_from_memory = solved[var_name.downcase]
46
63
 
47
64
  if value_from_memory.nil? &&
48
65
  expressions[var_name].nil? &&
49
- !calculator.memory.has_key?(var_name)
66
+ !solved.has_key?(var_name)
50
67
  next
51
68
  end
52
69
 
53
- value = value_from_memory ||
54
- evaluate!(expressions[var_name], expressions.merge(r))
70
+ value = value_from_memory || evaluate!(
71
+ expressions[var_name],
72
+ expressions.merge(r).merge(solved),
73
+ &expression_with_exception_handler(&block)
74
+ )
55
75
 
56
76
  r[var_name] = value
57
77
  rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
58
78
  ex.recipient_variable = var_name
59
79
  r[var_name] = block.call(ex)
80
+ rescue Dentaku::ArgumentError => ex
81
+ r[var_name] = block.call(ex)
60
82
  end
61
83
  end
62
84
  end
63
85
 
64
86
  def expressions
65
- @expressions ||= Hash[expression_hash.map { |k,v| [k.to_s, v] }]
87
+ @expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
66
88
  end
67
89
 
68
- def expression_dependencies
69
- Hash[expressions.map { |var, expr| [var, calculator.dependencies(expr)] }].tap do |d|
70
- d.values.each do |deps|
71
- unresolved = deps.reject { |ud| d.has_key?(ud) }
72
- unresolved.each { |u| add_dependencies(d, u) }
73
- end
90
+ def expression_deps
91
+ expressions.map do |var, expr|
92
+ [var, calculator.dependencies(expr)]
74
93
  end
75
94
  end
76
95
 
@@ -85,14 +104,14 @@ module Dentaku
85
104
  def variables_in_resolve_order
86
105
  cache_key = expressions.keys.map(&:to_s).sort.join("|")
87
106
  @ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
88
- DependencyResolver.find_resolve_order(expression_dependencies).tap do |d|
107
+ DependencyResolver.find_resolve_order(dependencies).tap do |d|
89
108
  self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
90
109
  end
91
110
  }
92
111
  end
93
112
 
94
- def evaluate!(expression, results)
95
- calculator.evaluate!(expression, results)
113
+ def evaluate!(expression, results, &block)
114
+ calculator.evaluate!(expression, results, &block)
96
115
  end
97
116
  end
98
117
  end
@@ -1,18 +1,24 @@
1
1
  require 'dentaku/bulk_expression_solver'
2
- require 'dentaku/exceptions'
3
- require 'dentaku/token'
4
2
  require 'dentaku/dependency_resolver'
3
+ require 'dentaku/exceptions'
4
+ require 'dentaku/flat_hash'
5
5
  require 'dentaku/parser'
6
-
6
+ require 'dentaku/string_casing'
7
+ require 'dentaku/token'
7
8
 
8
9
  module Dentaku
9
10
  class Calculator
10
- attr_reader :result, :memory, :tokenizer
11
+ include StringCasing
12
+ attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases, :nested_data_support
11
13
 
12
- def initialize(ast_cache={})
14
+ def initialize(options = {})
13
15
  clear
14
16
  @tokenizer = Tokenizer.new
15
- @ast_cache = ast_cache
17
+ @case_sensitive = options.delete(:case_sensitive)
18
+ @aliases = options.delete(:aliases) || Dentaku.aliases
19
+ @nested_data_support = options.fetch(:nested_data_support, true)
20
+ options.delete(:nested_data_support)
21
+ @ast_cache = options
16
22
  @disable_ast_cache = false
17
23
  @function_registry = Dentaku::AST::FunctionRegistry.new
18
24
  end
@@ -38,13 +44,17 @@ module Dentaku
38
44
  @disable_ast_cache = false
39
45
  end
40
46
 
41
- def evaluate(expression, data={})
47
+ def evaluate(expression, data = {}, &block)
42
48
  evaluate!(expression, data)
43
- rescue UnboundVariableError, Dentaku::ArgumentError
44
- yield expression if block_given?
49
+ rescue UnboundVariableError, Dentaku::ArgumentError => ex
50
+ block.call(expression, ex) if block_given?
45
51
  end
46
52
 
47
- def evaluate!(expression, data={})
53
+ def evaluate!(expression, data = {}, &block)
54
+ return expression.map { |e|
55
+ evaluate(e, data, &block)
56
+ } if expression.is_a? Array
57
+
48
58
  store(data) do
49
59
  node = expression
50
60
  node = ast(node) unless node.is_a?(AST::Node)
@@ -66,39 +76,50 @@ module Dentaku
66
76
  end
67
77
 
68
78
  def dependencies(expression)
79
+ if expression.is_a? Array
80
+ return expression.flat_map { |e| dependencies(e) }
81
+ end
69
82
  ast(expression).dependencies(memory)
70
83
  end
71
84
 
72
85
  def ast(expression)
73
86
  @ast_cache.fetch(expression) {
74
- Parser.new(tokenizer.tokenize(expression), function_registry: @function_registry).parse.tap do |node|
87
+ options = {
88
+ case_sensitive: case_sensitive,
89
+ function_registry: @function_registry,
90
+ aliases: aliases
91
+ }
92
+
93
+ tokens = tokenizer.tokenize(expression, options)
94
+ Parser.new(tokens, options).parse.tap do |node|
75
95
  @ast_cache[expression] = node if cache_ast?
76
96
  end
77
97
  }
78
98
  end
79
99
 
80
- def clear_cache(pattern=:all)
100
+ def clear_cache(pattern = :all)
81
101
  case pattern
82
102
  when :all
83
103
  @ast_cache = {}
84
104
  when String
85
105
  @ast_cache.delete(pattern)
86
106
  when Regexp
87
- @ast_cache.delete_if { |k,_| k =~ pattern }
107
+ @ast_cache.delete_if { |k, _| k =~ pattern }
88
108
  else
89
109
  raise ::ArgumentError
90
110
  end
91
111
  end
92
112
 
93
- def store(key_or_hash, value=nil)
113
+ def store(key_or_hash, value = nil)
94
114
  restore = Hash[memory]
95
115
 
96
116
  if value.nil?
97
- _flat_hash(key_or_hash).each do |key, val|
98
- memory[key.to_s.downcase] = val
117
+ key_or_hash = FlatHash.from_hash(key_or_hash) if nested_data_support
118
+ key_or_hash.each do |key, val|
119
+ memory[standardize_case(key.to_s)] = val
99
120
  end
100
121
  else
101
- memory[key_or_hash.to_s.downcase] = value
122
+ memory[standardize_case(key_or_hash.to_s)] = value
102
123
  end
103
124
 
104
125
  if block_given?
@@ -131,16 +152,5 @@ module Dentaku
131
152
  def cache_ast?
132
153
  Dentaku.cache_ast? && !@disable_ast_cache
133
154
  end
134
-
135
- private
136
-
137
- def _flat_hash(hash, k = [])
138
- if hash.is_a?(Hash)
139
- hash.inject({}) { |h, v| h.merge! _flat_hash(v[-1], k + [v[0]]) }
140
- else
141
- return { k.join('.') => hash } if k.is_a?(Array)
142
- { k => hash }
143
- end
144
- end
145
155
  end
146
156
  end
@@ -10,7 +10,7 @@ module Dentaku
10
10
 
11
11
  def initialize(vars_to_dependencies_hash)
12
12
  # ensure variables are strings
13
- @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v]}]
13
+ @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v] }]
14
14
  end
15
15
 
16
16
  def tsort_each_node(&block)
@@ -0,0 +1,31 @@
1
+ module Dentaku
2
+ class FlatHash
3
+ def self.from_hash(h, key = [], acc = {})
4
+ return acc.update(key => h) unless h.is_a? Hash
5
+ h.each { |k, v| from_hash(v, key + [k], acc) }
6
+ flatten_keys(acc)
7
+ end
8
+
9
+ def self.flatten_keys(hash)
10
+ hash.each_with_object({}) do |(k, v), h|
11
+ h[flatten_key(k)] = v
12
+ end
13
+ end
14
+
15
+ def self.flatten_key(segments)
16
+ return segments.first if segments.length == 1
17
+ key = segments.join('.')
18
+ key = key.to_sym if segments.first.is_a?(Symbol)
19
+ key
20
+ end
21
+
22
+ def self.expand(h)
23
+ h.each_with_object({}) do |(k, v), r|
24
+ hash_levels = k.to_s.split('.')
25
+ hash_levels = hash_levels.map(&:to_sym) if k.is_a?(Symbol)
26
+ child_hash = hash_levels[0...-1].reduce(r) { |h, n| h[n] ||= {} }
27
+ child_hash[hash_levels.last] = v
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,17 +2,18 @@ require_relative './ast'
2
2
 
3
3
  module Dentaku
4
4
  class Parser
5
- attr_reader :input, :output, :operations, :arities
5
+ attr_reader :input, :output, :operations, :arities, :case_sensitive
6
6
 
7
- def initialize(tokens, options={})
8
- @input = tokens.dup
9
- @output = []
7
+ def initialize(tokens, options = {})
8
+ @input = tokens.dup
9
+ @output = []
10
10
  @operations = options.fetch(:operations, [])
11
11
  @arities = options.fetch(:arities, [])
12
12
  @function_registry = options.fetch(:function_registry, nil)
13
+ @case_sensitive = options.fetch(:case_sensitive, false)
13
14
  end
14
15
 
15
- def consume(count=2)
16
+ def consume(count = 2)
16
17
  operator = operations.pop
17
18
  operator.peek(output)
18
19
 
@@ -45,7 +46,7 @@ module Dentaku
45
46
  output.push AST::String.new(token)
46
47
 
47
48
  when :identifier
48
- output.push AST::Identifier.new(token)
49
+ output.push AST::Identifier.new(token, case_sensitive: case_sensitive)
49
50
 
50
51
  when :operator, :comparator, :combinator
51
52
  op_class = operation(token)
@@ -0,0 +1,7 @@
1
+ module Dentaku
2
+ module StringCasing
3
+ def standardize_case(value)
4
+ case_sensitive ? value : value.downcase
5
+ end
6
+ end
7
+ end
@@ -2,7 +2,7 @@ module Dentaku
2
2
  class Token
3
3
  attr_reader :category, :raw_value, :value
4
4
 
5
- def initialize(category, value, raw_value=nil)
5
+ def initialize(category, value, raw_value = nil)
6
6
  @category = category
7
7
  @value = value
8
8
  @raw_value = raw_value
@@ -4,10 +4,10 @@ module Dentaku
4
4
  class TokenMatcher
5
5
  attr_reader :children, :categories, :values
6
6
 
7
- def initialize(categories=nil, values=nil, children=[])
7
+ def initialize(categories = nil, values = nil, children = [])
8
8
  # store categories and values as hash to optimize key lookup, h/t @jan-mangs
9
- @categories = [categories].compact.flatten.each_with_object({}) { |c,h| h[c] = 1 }
10
- @values = [values].compact.flatten.each_with_object({}) { |v,h| h[v] = 1 }
9
+ @categories = [categories].compact.flatten.each_with_object({}) { |c, h| h[c] = 1 }
10
+ @values = [values].compact.flatten.each_with_object({}) { |v, h| h[v] = 1 }
11
11
  @children = children.compact
12
12
  @invert = false
13
13
 
@@ -29,7 +29,7 @@ module Dentaku
29
29
  leaf_matcher? ? matches_token?(token) : any_child_matches_token?(token)
30
30
  end
31
31
 
32
- def match(token_stream, offset=0)
32
+ def match(token_stream, offset = 0)
33
33
  matched_tokens = []
34
34
  matched = false
35
35
 
@@ -1,17 +1,20 @@
1
1
  require 'bigdecimal'
2
2
  require 'time'
3
+ require 'dentaku/string_casing'
3
4
  require 'dentaku/token'
4
5
 
5
6
  module Dentaku
6
7
  class TokenScanner
7
- def initialize(category, regexp, converter=nil, condition=nil)
8
+ extend StringCasing
9
+
10
+ def initialize(category, regexp, converter = nil, condition = nil)
8
11
  @category = category
9
12
  @regexp = %r{\A(#{ regexp })}i
10
13
  @converter = converter
11
14
  @condition = condition || ->(*) { true }
12
15
  end
13
16
 
14
- def scan(string, last_token=nil)
17
+ def scan(string, last_token = nil)
15
18
  if (m = @regexp.match(string)) && @condition.call(last_token)
16
19
  value = raw = m.to_s
17
20
  value = @converter.call(raw) if @converter
@@ -25,6 +28,8 @@ module Dentaku
25
28
  end
26
29
 
27
30
  class << self
31
+ attr_reader :case_sensitive
32
+
28
33
  def available_scanners
29
34
  [
30
35
  :null,
@@ -62,10 +67,11 @@ module Dentaku
62
67
  end
63
68
 
64
69
  def scanners=(scanner_ids)
65
- @scanners.select! { |k,v| scanner_ids.include?(k) }
70
+ @scanners.select! { |k, v| scanner_ids.include?(k) }
66
71
  end
67
72
 
68
- def scanners
73
+ def scanners(options = {})
74
+ @case_sensitive = options.fetch(:case_sensitive, false)
69
75
  @scanners.values
70
76
  end
71
77
 
@@ -112,7 +118,9 @@ module Dentaku
112
118
  end
113
119
 
114
120
  def operator
115
- names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&' }.invert
121
+ names = {
122
+ pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&'
123
+ }.invert
116
124
  new(:operator, '\^|\+|-|\*|\/|%|\||&', lambda { |raw| names[raw] })
117
125
  end
118
126
 
@@ -152,12 +160,15 @@ module Dentaku
152
160
  def function
153
161
  new(:function, '\w+!?\s*\(', lambda do |raw|
154
162
  function_name = raw.gsub('(', '')
155
- [Token.new(:function, function_name.strip.downcase.to_sym, function_name), Token.new(:grouping, :open, '(')]
163
+ [
164
+ Token.new(:function, function_name.strip.downcase.to_sym, function_name),
165
+ Token.new(:grouping, :open, '(')
166
+ ]
156
167
  end)
157
168
  end
158
169
 
159
170
  def identifier
160
- new(:identifier, '[\w\.]+\b', lambda { |raw| raw.strip.downcase })
171
+ new(:identifier, '[\w\.]+\b', lambda { |raw| standardize_case(raw.strip) })
161
172
  end
162
173
  end
163
174