dentaku_zevo 3.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.pryrc +2 -0
  4. data/.rubocop.yml +114 -0
  5. data/.travis.yml +10 -0
  6. data/CHANGELOG.md +281 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +21 -0
  9. data/README.md +342 -0
  10. data/Rakefile +31 -0
  11. data/dentaku.gemspec +32 -0
  12. data/lib/dentaku/ast/access.rb +47 -0
  13. data/lib/dentaku/ast/arithmetic.rb +241 -0
  14. data/lib/dentaku/ast/array.rb +41 -0
  15. data/lib/dentaku/ast/bitwise.rb +42 -0
  16. data/lib/dentaku/ast/case/case_conditional.rb +38 -0
  17. data/lib/dentaku/ast/case/case_else.rb +35 -0
  18. data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
  19. data/lib/dentaku/ast/case/case_then.rb +35 -0
  20. data/lib/dentaku/ast/case/case_when.rb +39 -0
  21. data/lib/dentaku/ast/case.rb +81 -0
  22. data/lib/dentaku/ast/combinators.rb +50 -0
  23. data/lib/dentaku/ast/comparators.rb +89 -0
  24. data/lib/dentaku/ast/datetime.rb +8 -0
  25. data/lib/dentaku/ast/function.rb +56 -0
  26. data/lib/dentaku/ast/function_registry.rb +98 -0
  27. data/lib/dentaku/ast/functions/all.rb +23 -0
  28. data/lib/dentaku/ast/functions/and.rb +25 -0
  29. data/lib/dentaku/ast/functions/any.rb +23 -0
  30. data/lib/dentaku/ast/functions/avg.rb +13 -0
  31. data/lib/dentaku/ast/functions/count.rb +26 -0
  32. data/lib/dentaku/ast/functions/duration.rb +51 -0
  33. data/lib/dentaku/ast/functions/enum.rb +37 -0
  34. data/lib/dentaku/ast/functions/filter.rb +23 -0
  35. data/lib/dentaku/ast/functions/if.rb +51 -0
  36. data/lib/dentaku/ast/functions/map.rb +23 -0
  37. data/lib/dentaku/ast/functions/max.rb +5 -0
  38. data/lib/dentaku/ast/functions/min.rb +5 -0
  39. data/lib/dentaku/ast/functions/mul.rb +12 -0
  40. data/lib/dentaku/ast/functions/not.rb +5 -0
  41. data/lib/dentaku/ast/functions/or.rb +25 -0
  42. data/lib/dentaku/ast/functions/pluck.rb +30 -0
  43. data/lib/dentaku/ast/functions/round.rb +5 -0
  44. data/lib/dentaku/ast/functions/rounddown.rb +8 -0
  45. data/lib/dentaku/ast/functions/roundup.rb +8 -0
  46. data/lib/dentaku/ast/functions/ruby_math.rb +55 -0
  47. data/lib/dentaku/ast/functions/string_functions.rb +212 -0
  48. data/lib/dentaku/ast/functions/sum.rb +12 -0
  49. data/lib/dentaku/ast/functions/switch.rb +8 -0
  50. data/lib/dentaku/ast/functions/xor.rb +44 -0
  51. data/lib/dentaku/ast/grouping.rb +23 -0
  52. data/lib/dentaku/ast/identifier.rb +52 -0
  53. data/lib/dentaku/ast/literal.rb +30 -0
  54. data/lib/dentaku/ast/logical.rb +8 -0
  55. data/lib/dentaku/ast/negation.rb +54 -0
  56. data/lib/dentaku/ast/nil.rb +13 -0
  57. data/lib/dentaku/ast/node.rb +28 -0
  58. data/lib/dentaku/ast/numeric.rb +8 -0
  59. data/lib/dentaku/ast/operation.rb +39 -0
  60. data/lib/dentaku/ast/string.rb +15 -0
  61. data/lib/dentaku/ast.rb +39 -0
  62. data/lib/dentaku/bulk_expression_solver.rb +128 -0
  63. data/lib/dentaku/calculator.rb +169 -0
  64. data/lib/dentaku/date_arithmetic.rb +45 -0
  65. data/lib/dentaku/dependency_resolver.rb +24 -0
  66. data/lib/dentaku/exceptions.rb +102 -0
  67. data/lib/dentaku/flat_hash.rb +38 -0
  68. data/lib/dentaku/parser.rb +349 -0
  69. data/lib/dentaku/print_visitor.rb +101 -0
  70. data/lib/dentaku/string_casing.rb +7 -0
  71. data/lib/dentaku/token.rb +36 -0
  72. data/lib/dentaku/token_matcher.rb +138 -0
  73. data/lib/dentaku/token_matchers.rb +29 -0
  74. data/lib/dentaku/token_scanner.rb +183 -0
  75. data/lib/dentaku/tokenizer.rb +110 -0
  76. data/lib/dentaku/version.rb +3 -0
  77. data/lib/dentaku/visitor/infix.rb +82 -0
  78. data/lib/dentaku.rb +69 -0
  79. data/spec/ast/addition_spec.rb +62 -0
  80. data/spec/ast/all_spec.rb +25 -0
  81. data/spec/ast/and_function_spec.rb +35 -0
  82. data/spec/ast/and_spec.rb +32 -0
  83. data/spec/ast/any_spec.rb +23 -0
  84. data/spec/ast/arithmetic_spec.rb +91 -0
  85. data/spec/ast/avg_spec.rb +37 -0
  86. data/spec/ast/case_spec.rb +84 -0
  87. data/spec/ast/comparator_spec.rb +87 -0
  88. data/spec/ast/count_spec.rb +40 -0
  89. data/spec/ast/division_spec.rb +35 -0
  90. data/spec/ast/filter_spec.rb +25 -0
  91. data/spec/ast/function_spec.rb +69 -0
  92. data/spec/ast/map_spec.rb +27 -0
  93. data/spec/ast/max_spec.rb +33 -0
  94. data/spec/ast/min_spec.rb +33 -0
  95. data/spec/ast/mul_spec.rb +43 -0
  96. data/spec/ast/negation_spec.rb +48 -0
  97. data/spec/ast/node_spec.rb +43 -0
  98. data/spec/ast/numeric_spec.rb +16 -0
  99. data/spec/ast/or_spec.rb +35 -0
  100. data/spec/ast/pluck_spec.rb +32 -0
  101. data/spec/ast/round_spec.rb +35 -0
  102. data/spec/ast/rounddown_spec.rb +35 -0
  103. data/spec/ast/roundup_spec.rb +35 -0
  104. data/spec/ast/string_functions_spec.rb +217 -0
  105. data/spec/ast/sum_spec.rb +43 -0
  106. data/spec/ast/switch_spec.rb +30 -0
  107. data/spec/ast/xor_spec.rb +35 -0
  108. data/spec/benchmark.rb +70 -0
  109. data/spec/bulk_expression_solver_spec.rb +201 -0
  110. data/spec/calculator_spec.rb +898 -0
  111. data/spec/dentaku_spec.rb +52 -0
  112. data/spec/exceptions_spec.rb +9 -0
  113. data/spec/external_function_spec.rb +106 -0
  114. data/spec/parser_spec.rb +166 -0
  115. data/spec/print_visitor_spec.rb +66 -0
  116. data/spec/spec_helper.rb +71 -0
  117. data/spec/token_matcher_spec.rb +134 -0
  118. data/spec/token_scanner_spec.rb +49 -0
  119. data/spec/token_spec.rb +16 -0
  120. data/spec/tokenizer_spec.rb +359 -0
  121. data/spec/visitor/infix_spec.rb +31 -0
  122. data/spec/visitor_spec.rb +138 -0
  123. metadata +335 -0
@@ -0,0 +1,138 @@
1
+ require 'dentaku/token'
2
+
3
+ module Dentaku
4
+ class TokenMatcher
5
+ attr_reader :children, :categories, :values
6
+
7
+ def initialize(categories = nil, values = nil, children = [])
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 }
11
+ @children = children.compact
12
+ @invert = false
13
+
14
+ @min = 1
15
+ @max = 1
16
+ @range = (@min..@max)
17
+ end
18
+
19
+ def |(other_matcher)
20
+ self.class.new(:nomatch, :nomatch, leaf_matchers + other_matcher.leaf_matchers)
21
+ end
22
+
23
+ def invert
24
+ @invert = ! @invert
25
+ self
26
+ end
27
+
28
+ def ==(token)
29
+ leaf_matcher? ? matches_token?(token) : any_child_matches_token?(token)
30
+ end
31
+
32
+ def match(token_stream, offset = 0)
33
+ matched_tokens = []
34
+ matched = false
35
+
36
+ while self == token_stream[matched_tokens.length + offset] && matched_tokens.length < @max
37
+ matched_tokens << token_stream[matched_tokens.length + offset]
38
+ end
39
+
40
+ if @range.cover?(matched_tokens.length)
41
+ matched = true
42
+ end
43
+
44
+ [matched, matched_tokens]
45
+ end
46
+
47
+ def caret
48
+ @caret = true
49
+ self
50
+ end
51
+
52
+ def caret?
53
+ @caret
54
+ end
55
+
56
+ def star
57
+ @min = 0
58
+ @max = Float::INFINITY
59
+ @range = (@min..@max)
60
+ self
61
+ end
62
+
63
+ def plus
64
+ @max = Float::INFINITY
65
+ @range = (@min..@max)
66
+ self
67
+ end
68
+
69
+ def leaf_matcher?
70
+ children.empty?
71
+ end
72
+
73
+ def leaf_matchers
74
+ leaf_matcher? ? [self] : children
75
+ end
76
+
77
+ private
78
+
79
+ def any_child_matches_token?(token)
80
+ children.any? { |child| child == token }
81
+ end
82
+
83
+ def matches_token?(token)
84
+ return false if token.nil?
85
+ (category_match(token.category) && value_match(token.value)) ^ @invert
86
+ end
87
+
88
+ def category_match(category)
89
+ @categories.empty? || @categories.key?(category)
90
+ end
91
+
92
+ def value_match(value)
93
+ @values.empty? || @values.key?(value)
94
+ end
95
+
96
+ def self.datetime; new(:datetime); end
97
+ def self.numeric; new(:numeric); end
98
+ def self.string; new(:string); end
99
+ def self.logical; new(:logical); end
100
+ def self.value
101
+ new(:datetime) | new(:numeric) | new(:string) | new(:logical)
102
+ end
103
+
104
+ def self.addsub; new(:operator, [:add, :subtract]); end
105
+ def self.subtract; new(:operator, :subtract); end
106
+ def self.anchored_minus; new(:operator, :subtract).caret; end
107
+ def self.muldiv; new(:operator, [:multiply, :divide]); end
108
+ def self.pow; new(:operator, :pow); end
109
+ def self.mod; new(:operator, :mod); end
110
+ def self.combinator; new(:combinator); end
111
+
112
+ def self.comparator; new(:comparator); end
113
+ def self.comp_gt; new(:comparator, [:gt, :ge]); end
114
+ def self.comp_lt; new(:comparator, [:lt, :le]); end
115
+
116
+ def self.open; new(:grouping, :open); end
117
+ def self.close; new(:grouping, :close); end
118
+ def self.comma; new(:grouping, :comma); end
119
+ def self.non_group; new(:grouping).invert; end
120
+ def self.non_group_star; new(:grouping).invert.star; end
121
+ def self.non_close_plus; new(:grouping, :close).invert.plus; end
122
+ def self.arguments; (value | comma).plus; end
123
+
124
+ def self.if; new(:function, :if); end
125
+ def self.round; new(:function, :round); end
126
+ def self.roundup; new(:function, :roundup); end
127
+ def self.rounddown; new(:function, :rounddown); end
128
+ def self.not; new(:function, :not); end
129
+
130
+ def self.method_missing(name, *args, &block)
131
+ new(:function, name)
132
+ end
133
+
134
+ def self.respond_to_missing?(name, include_priv)
135
+ true
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,29 @@
1
+ module Dentaku
2
+ module TokenMatchers
3
+ def self.token_matchers(*symbols)
4
+ symbols.map { |s| matcher(s) }
5
+ end
6
+
7
+ def self.function_token_matchers(function_name, *symbols)
8
+ token_matchers(:open, *symbols, :close).unshift(
9
+ TokenMatcher.send(function_name)
10
+ )
11
+ end
12
+
13
+ def self.matcher(symbol)
14
+ @matchers ||= [
15
+ :datetime, :numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
16
+ :comparator, :comp_gt, :comp_lt, :open, :close, :comma,
17
+ :non_close_plus, :non_group, :non_group_star, :arguments,
18
+ :logical, :combinator, :if, :round, :roundup, :rounddown, :not,
19
+ :anchored_minus, :math_neg_pow, :math_neg_mul
20
+ ].each_with_object({}) do |name, matchers|
21
+ matchers[name] = TokenMatcher.send(name)
22
+ end
23
+
24
+ @matchers.fetch(symbol) do
25
+ raise "Unknown token symbol #{ symbol }"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,183 @@
1
+ require 'bigdecimal'
2
+ require 'time'
3
+ require 'dentaku/string_casing'
4
+ require 'dentaku/token'
5
+
6
+ module Dentaku
7
+ class TokenScanner
8
+ extend StringCasing
9
+
10
+ def initialize(category, regexp, converter = nil, condition = nil)
11
+ @category = category
12
+ @regexp = %r{\A(#{ regexp })}i
13
+ @converter = converter
14
+ @condition = condition || ->(*) { true }
15
+ end
16
+
17
+ def scan(string, last_token = nil)
18
+ if (m = @regexp.match(string)) && @condition.call(last_token)
19
+ value = raw = m.to_s
20
+ value = @converter.call(raw) if @converter
21
+
22
+ return Array(value).map do |v|
23
+ Token === v ? v : Token.new(@category, v, raw)
24
+ end
25
+ end
26
+
27
+ false
28
+ end
29
+
30
+ class << self
31
+ attr_reader :case_sensitive
32
+
33
+ def available_scanners
34
+ [
35
+ :null,
36
+ :whitespace,
37
+ :datetime, # before numeric so it can pick up timestamps
38
+ :numeric,
39
+ :hexadecimal,
40
+ :double_quoted_string,
41
+ :single_quoted_string,
42
+ :negate,
43
+ :combinator,
44
+ :operator,
45
+ :grouping,
46
+ :array,
47
+ :access,
48
+ :case_statement,
49
+ :comparator,
50
+ :boolean,
51
+ :function,
52
+ :identifier
53
+ ]
54
+ end
55
+
56
+ def register_default_scanners
57
+ register_scanners(available_scanners)
58
+ end
59
+
60
+ def register_scanners(scanner_ids)
61
+ @scanners = scanner_ids.each_with_object({}) do |id, scanners|
62
+ scanners[id] = self.send(id)
63
+ end
64
+ end
65
+
66
+ def register_scanner(id, scanner)
67
+ @scanners[id] = scanner
68
+ end
69
+
70
+ def scanners=(scanner_ids)
71
+ @scanners.select! { |k, v| scanner_ids.include?(k) }
72
+ end
73
+
74
+ def scanners(options = {})
75
+ @case_sensitive = options.fetch(:case_sensitive, false)
76
+ @scanners.values
77
+ end
78
+
79
+ def whitespace
80
+ new(:whitespace, '\s+')
81
+ end
82
+
83
+ def null
84
+ new(:null, 'null\b')
85
+ end
86
+
87
+ # NOTE: Convert to DateTime as Array(Time) returns the parts of the time for some reason
88
+ def datetime
89
+ new(:datetime, /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?/, lambda { |raw| Time.parse(raw).to_datetime })
90
+ end
91
+
92
+ def numeric
93
+ new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
94
+ raw =~ /(\.|e|E)/ ? BigDecimal(raw) : raw.to_i
95
+ })
96
+ end
97
+
98
+ def hexadecimal
99
+ new(:numeric, '(0x[0-9a-f]+)\b', lambda { |raw| raw[2..-1].to_i(16) })
100
+ end
101
+
102
+ def double_quoted_string
103
+ new(:string, '"[^"]*"', lambda { |raw| raw.gsub(/^"|"$/, '') })
104
+ end
105
+
106
+ def single_quoted_string
107
+ new(:string, "'[^']*'", lambda { |raw| raw.gsub(/^'|'$/, '') })
108
+ end
109
+
110
+ def negate
111
+ new(:operator, '-', lambda { |raw| :negate }, lambda { |last_token|
112
+ last_token.nil? ||
113
+ last_token.is?(:operator) ||
114
+ last_token.is?(:comparator) ||
115
+ last_token.is?(:combinator) ||
116
+ last_token.value == :open ||
117
+ last_token.value == :comma
118
+ })
119
+ end
120
+
121
+ def operator
122
+ names = {
123
+ pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&', bitshiftleft: '<<', bitshiftright: '>>'
124
+ }.invert
125
+ new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
126
+ end
127
+
128
+ def grouping
129
+ names = { open: '(', close: ')', comma: ',' }.invert
130
+ new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
131
+ end
132
+
133
+ def array
134
+ names = { array_start: '{', array_end: '}', }.invert
135
+ new(:array, '\{|\}|,', lambda { |raw| names[raw] })
136
+ end
137
+
138
+ def access
139
+ names = { lbracket: '[', rbracket: ']' }.invert
140
+ new(:access, '\[|\]', lambda { |raw| names[raw] })
141
+ end
142
+
143
+ def case_statement
144
+ names = { open: 'case', close: 'end', then: 'then', when: 'when', else: 'else' }.invert
145
+ new(:case, '(case|end|then|when|else)\b', lambda { |raw| names[raw.downcase] })
146
+ end
147
+
148
+ def comparator
149
+ names = { le: '<=', ge: '>=', ne: '!=', lt: '<', gt: '>', eq: '=' }.invert
150
+ alternate = { ne: '<>', eq: '==' }.invert
151
+ new(:comparator, '<=|>=|!=|<>|<|>|==|=', lambda { |raw| names[raw] || alternate[raw] })
152
+ end
153
+
154
+ def combinator
155
+ names = { and: '&&', or: '||' }.invert
156
+ new(:combinator, '(and|or|&&|\|\|)\s', lambda { |raw|
157
+ norm = raw.strip.downcase
158
+ names.fetch(norm) { norm.to_sym }
159
+ })
160
+ end
161
+
162
+ def boolean
163
+ new(:logical, '(true|false)\b', lambda { |raw| raw.strip.downcase == 'true' })
164
+ end
165
+
166
+ def function
167
+ new(:function, '\w+!?\s*\(', lambda do |raw|
168
+ function_name = raw.gsub('(', '')
169
+ [
170
+ Token.new(:function, function_name.strip.downcase.to_sym, function_name),
171
+ Token.new(:grouping, :open, '(')
172
+ ]
173
+ end)
174
+ end
175
+
176
+ def identifier
177
+ new(:identifier, '[[[:word:]]\.]+\b', lambda { |raw| standardize_case(raw.strip) })
178
+ end
179
+ end
180
+
181
+ register_default_scanners
182
+ end
183
+ end
@@ -0,0 +1,110 @@
1
+ require 'dentaku/token'
2
+ require 'dentaku/token_matcher'
3
+ require 'dentaku/token_scanner'
4
+
5
+ module Dentaku
6
+ class Tokenizer
7
+ attr_reader :case_sensitive, :aliases
8
+
9
+ LPAREN = TokenMatcher.new(:grouping, :open)
10
+ RPAREN = TokenMatcher.new(:grouping, :close)
11
+
12
+ def tokenize(string, options = {})
13
+ @nesting = 0
14
+ @tokens = []
15
+ @aliases = options.fetch(:aliases, global_aliases)
16
+ input = strip_comments(string.to_s.dup)
17
+ input = replace_aliases(input)
18
+ @case_sensitive = options.fetch(:case_sensitive, false)
19
+
20
+ until input.empty?
21
+ scanned = TokenScanner.scanners(case_sensitive: case_sensitive).any? do |scanner|
22
+ scanned, input = scan(input, scanner)
23
+ scanned
24
+ end
25
+
26
+ unless scanned
27
+ fail! :parse_error, at: input
28
+ end
29
+ end
30
+
31
+ fail! :too_many_opening_parentheses if @nesting > 0
32
+
33
+ @tokens
34
+ end
35
+
36
+ def last_token
37
+ @tokens.last
38
+ end
39
+
40
+ def scan(string, scanner)
41
+ if tokens = scanner.scan(string, last_token)
42
+ tokens.each do |token|
43
+ if token.empty?
44
+ fail! :unexpected_zero_width_match,
45
+ token_category: token.category, at: string
46
+ end
47
+
48
+ @nesting += 1 if LPAREN == token
49
+ @nesting -= 1 if RPAREN == token
50
+ fail! :too_many_closing_parentheses if @nesting < 0
51
+
52
+ @tokens << token unless token.is?(:whitespace)
53
+ end
54
+
55
+ match_length = tokens.map(&:length).reduce(:+)
56
+ [true, string[match_length..-1]]
57
+ else
58
+ [false, string]
59
+ end
60
+ end
61
+
62
+ def strip_comments(input)
63
+ input.gsub(/\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//, '')
64
+ end
65
+
66
+ def replace_aliases(string)
67
+ return string unless @aliases.any?
68
+
69
+ string.gsub!(alias_regex) do |match|
70
+ match_regex = /^#{Regexp.escape(match)}$/i
71
+
72
+ @aliases.detect do |(_key, aliases)|
73
+ !aliases.grep(match_regex).empty?
74
+ end.first
75
+ end
76
+
77
+ string
78
+ end
79
+
80
+ def alias_regex
81
+ values = @aliases.values.flatten.join('|')
82
+ /(?<=\p{Punct}|[[:space:]]|\A)(#{values})(?=\()/i
83
+ end
84
+
85
+ private
86
+
87
+ def global_aliases
88
+ return {} unless Dentaku.respond_to?(:aliases)
89
+ Dentaku.aliases
90
+ end
91
+
92
+ def fail!(reason, **meta)
93
+ message =
94
+ case reason
95
+ when :parse_error
96
+ "parse error at: '#{meta.fetch(:at)}'"
97
+ when :too_many_opening_parentheses
98
+ "too many opening parentheses"
99
+ when :too_many_closing_parentheses
100
+ "too many closing parentheses"
101
+ when :unexpected_zero_width_match
102
+ "unexpected zero-width match (:#{meta.fetch(:category)}) at '#{meta.fetch(:at)}'"
103
+ else
104
+ raise ::ArgumentError, "Unhandled #{reason}"
105
+ end
106
+
107
+ raise TokenizerError.for(reason, **meta), message
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,3 @@
1
+ module Dentaku
2
+ VERSION = "3.5.2"
3
+ end
@@ -0,0 +1,82 @@
1
+ # infix visitor
2
+ #
3
+ # use this visitor in a processor to get infix visiting order
4
+ #
5
+ # visitor node deps
6
+ # accept -> visit left ->
7
+ # process
8
+ # visit right ->
9
+ module Dentaku
10
+ module Visitor
11
+ module Infix
12
+ def visit(ast)
13
+ ast.accept(self)
14
+ end
15
+
16
+ def process(_ast)
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def visit_function(node)
21
+ node.args.each do |arg|
22
+ visit(arg)
23
+ end
24
+ process(node)
25
+ end
26
+
27
+ def visit_identifier(node)
28
+ process(node)
29
+ end
30
+
31
+ def visit_operation(node)
32
+ visit(node.left) if node.left
33
+ process(node)
34
+ visit(node.right) if node.right
35
+ end
36
+
37
+ def visit_operand(node)
38
+ process(node)
39
+ end
40
+
41
+ def visit_case(node)
42
+ process(node)
43
+ end
44
+
45
+ def visit_switch(node)
46
+ process(node)
47
+ end
48
+
49
+ def visit_case_conditional(node)
50
+ process(node)
51
+ end
52
+
53
+ def visit_when(node)
54
+ process(node)
55
+ end
56
+
57
+ def visit_then(node)
58
+ process(node)
59
+ end
60
+
61
+ def visit_else(node)
62
+ process(node)
63
+ end
64
+
65
+ def visit_negation(node)
66
+ process(node)
67
+ end
68
+
69
+ def visit_access(node)
70
+ process(node)
71
+ end
72
+
73
+ def visit_literal(node)
74
+ process(node)
75
+ end
76
+
77
+ def visit_nil(node)
78
+ process(node)
79
+ end
80
+ end
81
+ end
82
+ end
data/lib/dentaku.rb ADDED
@@ -0,0 +1,69 @@
1
+ require "bigdecimal"
2
+ require "concurrent"
3
+ require "dentaku/calculator"
4
+ require "dentaku/version"
5
+
6
+ module Dentaku
7
+ @enable_ast_caching = false
8
+ @enable_dependency_order_caching = false
9
+ @enable_identifier_caching = false
10
+ @aliases = {}
11
+
12
+ def self.evaluate(expression, data = {}, &block)
13
+ calculator.value.evaluate(expression, data, &block)
14
+ end
15
+
16
+ def self.evaluate!(expression, data = {}, &block)
17
+ calculator.value.evaluate!(expression, data, &block)
18
+ end
19
+
20
+ def self.enable_caching!
21
+ enable_ast_cache!
22
+ enable_dependency_order_cache!
23
+ enable_identifier_cache!
24
+ end
25
+
26
+ def self.enable_ast_cache!
27
+ @enable_ast_caching = true
28
+ end
29
+
30
+ def self.cache_ast?
31
+ @enable_ast_caching
32
+ end
33
+
34
+ def self.enable_dependency_order_cache!
35
+ @enable_dependency_order_caching = true
36
+ end
37
+
38
+ def self.cache_dependency_order?
39
+ @enable_dependency_order_caching
40
+ end
41
+
42
+ def self.enable_identifier_cache!
43
+ @enable_identifier_caching = true
44
+ end
45
+
46
+ def self.cache_identifier?
47
+ @enable_identifier_caching
48
+ end
49
+
50
+ def self.aliases
51
+ @aliases
52
+ end
53
+
54
+ def self.aliases=(hash)
55
+ @aliases = hash
56
+ end
57
+
58
+ def self.calculator
59
+ @calculator ||= Concurrent::ThreadLocalVar.new { Dentaku::Calculator.new }
60
+ end
61
+ end
62
+
63
+ def Dentaku(expression, data = {})
64
+ Dentaku.evaluate(expression, data)
65
+ end
66
+
67
+ def Dentaku!(expression, data = {})
68
+ Dentaku.evaluate!(expression, data)
69
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/arithmetic'
3
+
4
+ require 'dentaku/token'
5
+
6
+ describe Dentaku::AST::Addition do
7
+ let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
8
+ let(:six) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 6) }
9
+
10
+ let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
11
+
12
+ it 'allows access to its sub-trees' do
13
+ node = described_class.new(five, six)
14
+ expect(node.left).to eq(five)
15
+ expect(node.right).to eq(six)
16
+ end
17
+
18
+ it 'performs addition' do
19
+ node = described_class.new(five, six)
20
+ expect(node.value).to eq(11)
21
+ end
22
+
23
+ it 'requires numeric operands' do
24
+ expect {
25
+ described_class.new(five, t)
26
+ }.to raise_error(Dentaku::NodeError, /requires numeric operands/)
27
+
28
+ expression = Dentaku::AST::Multiplication.new(five, five)
29
+ group = Dentaku::AST::Grouping.new(expression)
30
+
31
+ expect {
32
+ described_class.new(group, five)
33
+ }.not_to raise_error
34
+ end
35
+
36
+ it 'allows operands that respond to addition' do
37
+ # Sample struct that has a custom definition for addition
38
+
39
+ Operand = Struct.new(:value) do
40
+ def +(other)
41
+ case other
42
+ when Operand
43
+ value + other.value
44
+ when Numeric
45
+ value + other
46
+ end
47
+ end
48
+ end
49
+
50
+ operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(5))
51
+ operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(6))
52
+
53
+ expect {
54
+ described_class.new(operand_five, operand_six)
55
+ }.not_to raise_error
56
+
57
+ expect {
58
+ described_class.new(operand_five, six)
59
+ }.not_to raise_error
60
+
61
+ end
62
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/all'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::All do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+ it 'performs ALL operation' do
8
+ result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
9
+ expect(result).to eq(false)
10
+ end
11
+
12
+ it 'works with a single value if needed for some reason' do
13
+ result = Dentaku('ALL(vals, val, val > 1)', vals: 1)
14
+ expect(result).to eq(false)
15
+
16
+ result = Dentaku('ALL(vals, val, val > 1)', vals: 2)
17
+ expect(result).to eq(true)
18
+ end
19
+
20
+ it 'raises argument error if a string is passed as identifier' do
21
+ expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
22
+ Dentaku::ArgumentError, 'ALL() requires second argument to be an identifier'
23
+ )
24
+ end
25
+ end