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