hayadentaku 3.5.7

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 (132) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/.gitignore +14 -0
  5. data/.pryrc +2 -0
  6. data/.rubocop.yml +114 -0
  7. data/.travis.yml +10 -0
  8. data/CHANGELOG.md +328 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE +21 -0
  11. data/README.md +352 -0
  12. data/Rakefile +31 -0
  13. data/hayadentaku.gemspec +35 -0
  14. data/lib/dentaku/ast/access.rb +44 -0
  15. data/lib/dentaku/ast/arithmetic.rb +292 -0
  16. data/lib/dentaku/ast/array.rb +38 -0
  17. data/lib/dentaku/ast/bitwise.rb +42 -0
  18. data/lib/dentaku/ast/case/case_conditional.rb +38 -0
  19. data/lib/dentaku/ast/case/case_else.rb +35 -0
  20. data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
  21. data/lib/dentaku/ast/case/case_then.rb +35 -0
  22. data/lib/dentaku/ast/case/case_when.rb +39 -0
  23. data/lib/dentaku/ast/case.rb +93 -0
  24. data/lib/dentaku/ast/combinators.rb +50 -0
  25. data/lib/dentaku/ast/comparators.rb +88 -0
  26. data/lib/dentaku/ast/datetime.rb +8 -0
  27. data/lib/dentaku/ast/function.rb +56 -0
  28. data/lib/dentaku/ast/function_registry.rb +107 -0
  29. data/lib/dentaku/ast/functions/abs.rb +5 -0
  30. data/lib/dentaku/ast/functions/all.rb +19 -0
  31. data/lib/dentaku/ast/functions/and.rb +25 -0
  32. data/lib/dentaku/ast/functions/any.rb +19 -0
  33. data/lib/dentaku/ast/functions/avg.rb +13 -0
  34. data/lib/dentaku/ast/functions/count.rb +26 -0
  35. data/lib/dentaku/ast/functions/duration.rb +51 -0
  36. data/lib/dentaku/ast/functions/enum.rb +54 -0
  37. data/lib/dentaku/ast/functions/filter.rb +21 -0
  38. data/lib/dentaku/ast/functions/if.rb +47 -0
  39. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  40. data/lib/dentaku/ast/functions/map.rb +19 -0
  41. data/lib/dentaku/ast/functions/max.rb +5 -0
  42. data/lib/dentaku/ast/functions/min.rb +5 -0
  43. data/lib/dentaku/ast/functions/mul.rb +12 -0
  44. data/lib/dentaku/ast/functions/not.rb +5 -0
  45. data/lib/dentaku/ast/functions/or.rb +25 -0
  46. data/lib/dentaku/ast/functions/pluck.rb +34 -0
  47. data/lib/dentaku/ast/functions/reduce.rb +60 -0
  48. data/lib/dentaku/ast/functions/round.rb +5 -0
  49. data/lib/dentaku/ast/functions/rounddown.rb +8 -0
  50. data/lib/dentaku/ast/functions/roundup.rb +8 -0
  51. data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
  52. data/lib/dentaku/ast/functions/string_functions.rb +212 -0
  53. data/lib/dentaku/ast/functions/sum.rb +12 -0
  54. data/lib/dentaku/ast/functions/switch.rb +8 -0
  55. data/lib/dentaku/ast/functions/xor.rb +44 -0
  56. data/lib/dentaku/ast/grouping.rb +23 -0
  57. data/lib/dentaku/ast/identifier.rb +52 -0
  58. data/lib/dentaku/ast/literal.rb +30 -0
  59. data/lib/dentaku/ast/logical.rb +8 -0
  60. data/lib/dentaku/ast/negation.rb +54 -0
  61. data/lib/dentaku/ast/nil.rb +13 -0
  62. data/lib/dentaku/ast/node.rb +29 -0
  63. data/lib/dentaku/ast/numeric.rb +8 -0
  64. data/lib/dentaku/ast/operation.rb +44 -0
  65. data/lib/dentaku/ast/string.rb +15 -0
  66. data/lib/dentaku/ast.rb +42 -0
  67. data/lib/dentaku/bulk_expression_solver.rb +158 -0
  68. data/lib/dentaku/calculator.rb +192 -0
  69. data/lib/dentaku/date_arithmetic.rb +60 -0
  70. data/lib/dentaku/dependency_resolver.rb +29 -0
  71. data/lib/dentaku/exceptions.rb +116 -0
  72. data/lib/dentaku/flat_hash.rb +161 -0
  73. data/lib/dentaku/parser.rb +318 -0
  74. data/lib/dentaku/print_visitor.rb +112 -0
  75. data/lib/dentaku/string_casing.rb +7 -0
  76. data/lib/dentaku/token.rb +48 -0
  77. data/lib/dentaku/token_matcher.rb +138 -0
  78. data/lib/dentaku/token_matchers.rb +29 -0
  79. data/lib/dentaku/token_scanner.rb +240 -0
  80. data/lib/dentaku/tokenizer.rb +127 -0
  81. data/lib/dentaku/version.rb +3 -0
  82. data/lib/dentaku/visitor/infix.rb +86 -0
  83. data/lib/dentaku.rb +69 -0
  84. data/spec/ast/abs_spec.rb +26 -0
  85. data/spec/ast/addition_spec.rb +67 -0
  86. data/spec/ast/all_spec.rb +38 -0
  87. data/spec/ast/and_function_spec.rb +35 -0
  88. data/spec/ast/and_spec.rb +32 -0
  89. data/spec/ast/any_spec.rb +36 -0
  90. data/spec/ast/arithmetic_spec.rb +147 -0
  91. data/spec/ast/avg_spec.rb +42 -0
  92. data/spec/ast/case_spec.rb +84 -0
  93. data/spec/ast/comparator_spec.rb +87 -0
  94. data/spec/ast/count_spec.rb +40 -0
  95. data/spec/ast/division_spec.rb +64 -0
  96. data/spec/ast/filter_spec.rb +25 -0
  97. data/spec/ast/function_spec.rb +69 -0
  98. data/spec/ast/intercept_spec.rb +30 -0
  99. data/spec/ast/map_spec.rb +40 -0
  100. data/spec/ast/max_spec.rb +33 -0
  101. data/spec/ast/min_spec.rb +33 -0
  102. data/spec/ast/mul_spec.rb +43 -0
  103. data/spec/ast/negation_spec.rb +48 -0
  104. data/spec/ast/node_spec.rb +43 -0
  105. data/spec/ast/numeric_spec.rb +16 -0
  106. data/spec/ast/or_spec.rb +35 -0
  107. data/spec/ast/pluck_spec.rb +49 -0
  108. data/spec/ast/reduce_spec.rb +22 -0
  109. data/spec/ast/round_spec.rb +35 -0
  110. data/spec/ast/rounddown_spec.rb +35 -0
  111. data/spec/ast/roundup_spec.rb +35 -0
  112. data/spec/ast/string_functions_spec.rb +217 -0
  113. data/spec/ast/sum_spec.rb +43 -0
  114. data/spec/ast/switch_spec.rb +30 -0
  115. data/spec/ast/xor_spec.rb +35 -0
  116. data/spec/benchmark.rb +70 -0
  117. data/spec/bulk_expression_solver_spec.rb +241 -0
  118. data/spec/calculator_spec.rb +1003 -0
  119. data/spec/dentaku_spec.rb +52 -0
  120. data/spec/dependency_resolver_spec.rb +18 -0
  121. data/spec/exceptions_spec.rb +9 -0
  122. data/spec/external_function_spec.rb +177 -0
  123. data/spec/parser_spec.rb +183 -0
  124. data/spec/print_visitor_spec.rb +77 -0
  125. data/spec/spec_helper.rb +69 -0
  126. data/spec/token_matcher_spec.rb +134 -0
  127. data/spec/token_scanner_spec.rb +49 -0
  128. data/spec/token_spec.rb +16 -0
  129. data/spec/tokenizer_spec.rb +375 -0
  130. data/spec/visitor/infix_spec.rb +52 -0
  131. data/spec/visitor_spec.rb +139 -0
  132. metadata +353 -0
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/token_matcher'
3
+
4
+ describe Dentaku::TokenMatcher do
5
+ it 'with single category matches token category' do
6
+ matcher = described_class.new(:numeric)
7
+ token = Dentaku::Token.new(:numeric, 5)
8
+
9
+ expect(matcher).to eq(token)
10
+ end
11
+
12
+ it 'with multiple categories matches any included token category' do
13
+ matcher = described_class.new([:comparator, :operator])
14
+ numeric = Dentaku::Token.new(:numeric, 5)
15
+ comparator = Dentaku::Token.new(:comparator, :lt)
16
+ operator = Dentaku::Token.new(:operator, :add)
17
+
18
+ expect(matcher).to eq(comparator)
19
+ expect(matcher).to eq(operator)
20
+ expect(matcher).not_to eq(numeric)
21
+ end
22
+
23
+ it 'with single category and value matches token category and value' do
24
+ matcher = described_class.new(:operator, :add)
25
+ addition = Dentaku::Token.new(:operator, :add)
26
+ subtraction = Dentaku::Token.new(:operator, :subtract)
27
+
28
+ expect(matcher).to eq(addition)
29
+ expect(matcher).not_to eq(subtraction)
30
+ end
31
+
32
+ it 'with multiple values matches any included token value' do
33
+ matcher = described_class.new(:operator, [:add, :subtract])
34
+ add = Dentaku::Token.new(:operator, :add)
35
+ sub = Dentaku::Token.new(:operator, :subtract)
36
+ mul = Dentaku::Token.new(:operator, :multiply)
37
+ div = Dentaku::Token.new(:operator, :divide)
38
+
39
+ expect(matcher).to eq(add)
40
+ expect(matcher).to eq(sub)
41
+ expect(matcher).not_to eq(mul)
42
+ expect(matcher).not_to eq(div)
43
+ end
44
+
45
+ it 'is invertible' do
46
+ matcher = described_class.new(:operator, [:add, :subtract]).invert
47
+ add = Dentaku::Token.new(:operator, :add)
48
+ mul = Dentaku::Token.new(:operator, :multiply)
49
+ cmp = Dentaku::Token.new(:comparator, :lt)
50
+
51
+ expect(matcher).not_to eq(add)
52
+ expect(matcher).to eq(mul)
53
+ expect(matcher).to eq(cmp)
54
+ end
55
+
56
+ describe 'combining multiple tokens' do
57
+ let(:numeric) { described_class.new(:numeric) }
58
+ let(:string) { described_class.new(:string) }
59
+
60
+ it 'matches either' do
61
+ either = numeric | string
62
+ expect(either).to eq(Dentaku::Token.new(:numeric, 5))
63
+ expect(either).to eq(Dentaku::Token.new(:string, 'rhubarb'))
64
+ end
65
+
66
+ it 'matches any value' do
67
+ value = described_class.value
68
+ expect(value).to eq(Dentaku::Token.new(:numeric, 8))
69
+ expect(value).to eq(Dentaku::Token.new(:string, 'apricot'))
70
+ expect(value).to eq(Dentaku::Token.new(:logical, false))
71
+ expect(value).not_to eq(Dentaku::Token.new(:function, :round))
72
+ expect(value).not_to eq(Dentaku::Token.new(:identifier, :hello))
73
+ end
74
+ end
75
+
76
+ describe 'stream matching' do
77
+ let(:stream) { token_stream(5, 11, 9, 24, :hello, 8) }
78
+
79
+ describe 'standard' do
80
+ let(:standard) { described_class.new(:numeric) }
81
+
82
+ it 'matches zero or more occurrences in a token stream' do
83
+ matched, substream = standard.match(stream)
84
+ expect(matched).to be_truthy
85
+ expect(substream.length).to eq(1)
86
+ expect(substream.map(&:value)).to eq([5])
87
+
88
+ matched, substream = standard.match(stream, 4)
89
+ expect(substream).to be_empty
90
+ expect(matched).not_to be_truthy
91
+ end
92
+ end
93
+
94
+ describe 'star' do
95
+ let(:star) { described_class.new(:numeric).star }
96
+
97
+ it 'matches zero or more occurrences in a token stream' do
98
+ matched, substream = star.match(stream)
99
+ expect(matched).to be_truthy
100
+ expect(substream.length).to eq(4)
101
+ expect(substream.map(&:value)).to eq([5, 11, 9, 24])
102
+
103
+ matched, substream = star.match(stream, 4)
104
+ expect(substream).to be_empty
105
+ expect(matched).to be_truthy
106
+ end
107
+ end
108
+
109
+ describe 'plus' do
110
+ let(:plus) { described_class.new(:numeric).plus }
111
+
112
+ it 'matches one or more occurrences in a token stream' do
113
+ matched, substream = plus.match(stream)
114
+ expect(matched).to be_truthy
115
+ expect(substream.length).to eq(4)
116
+ expect(substream.map(&:value)).to eq([5, 11, 9, 24])
117
+
118
+ matched, substream = plus.match(stream, 4)
119
+ expect(substream).to be_empty
120
+ expect(matched).not_to be_truthy
121
+ end
122
+ end
123
+
124
+ describe 'arguments' do
125
+ it 'matches comma-separated values' do
126
+ stream = token_stream(1, :comma, 2, :comma, true, :comma, 'olive', :comma, :'(')
127
+ matched, substream = described_class.arguments.match(stream)
128
+ expect(matched).to be_truthy
129
+ expect(substream.length).to eq(8)
130
+ expect(substream.map(&:value)).to eq([1, :comma, 2, :comma, true, :comma, 'olive', :comma])
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,49 @@
1
+ require 'dentaku/token_scanner'
2
+
3
+ describe Dentaku::TokenScanner do
4
+ let(:whitespace) { described_class.new(:whitespace, '\s') }
5
+ let(:numeric) { described_class.new(:numeric, '(\d+(\.\d+)?|\.\d+)',
6
+ ->(raw) { raw =~ /\./ ? BigDecimal(raw) : raw.to_i })
7
+ }
8
+ let(:custom) { described_class.new(:identifier, '#\w+\b',
9
+ ->(raw) { raw.gsub('#', '').to_sym })
10
+ }
11
+
12
+ after { described_class.register_default_scanners }
13
+
14
+ it 'returns a token for a matching string' do
15
+ token = whitespace.scan(' ').first
16
+ expect(token.category).to eq(:whitespace)
17
+ expect(token.value).to eq(' ')
18
+ end
19
+
20
+ it 'returns falsy for a non-matching string' do
21
+ expect(whitespace.scan('A')).not_to be
22
+ end
23
+
24
+ it 'performs raw value conversion' do
25
+ token = numeric.scan('5').first
26
+ expect(token.category).to eq(:numeric)
27
+ expect(token.value).to eq(5)
28
+ end
29
+
30
+ it 'allows customizing available scanners' do
31
+ described_class.scanners = [:whitespace, :numeric]
32
+ expect(described_class.scanners.length).to eq(2)
33
+ end
34
+
35
+ it 'ignores invalid scanners' do
36
+ described_class.scanners = [:whitespace, :numeric, :fake]
37
+ expect(described_class.scanners.length).to eq(2)
38
+ end
39
+
40
+ it 'uses a custom scanner' do
41
+ described_class.scanners = [:whitespace, :numeric]
42
+ described_class.register_scanner(:custom, custom)
43
+ expect(described_class.scanners.length).to eq(3)
44
+
45
+ token = custom.scan('#apple + #pear').first
46
+ expect(token.category).to eq(:identifier)
47
+ expect(token.value).to eq(:apple)
48
+ end
49
+ end
@@ -0,0 +1,16 @@
1
+ require 'dentaku/token'
2
+
3
+ describe Dentaku::Token do
4
+ it 'has a category and a value' do
5
+ token = Dentaku::Token.new(:numeric, 5)
6
+ expect(token.category).to eq(:numeric)
7
+ expect(token.value).to eq(5)
8
+ expect(token.is?(:numeric)).to be_truthy
9
+ end
10
+
11
+ it 'compares category and value to determine equality' do
12
+ t1 = Dentaku::Token.new(:numeric, 5)
13
+ t2 = Dentaku::Token.new(:numeric, 5)
14
+ expect(t1 == t2).to be_truthy
15
+ end
16
+ end
@@ -0,0 +1,375 @@
1
+ require 'dentaku/exceptions'
2
+ require 'dentaku/tokenizer'
3
+
4
+ describe Dentaku::Tokenizer do
5
+ let(:tokenizer) { described_class.new }
6
+
7
+ it 'handles an empty expression' do
8
+ expect(tokenizer.tokenize('')).to be_empty
9
+ end
10
+
11
+ it 'tokenizes numeric literal in decimal' do
12
+ token = tokenizer.tokenize('80').first
13
+ expect(token.category).to eq(:numeric)
14
+ expect(token.value).to eq(80)
15
+ end
16
+
17
+ it 'tokenizes numeric literal in hexadecimal' do
18
+ token = tokenizer.tokenize('0x80').first
19
+ expect(token.category).to eq(:numeric)
20
+ expect(token.value).to eq(128)
21
+ end
22
+
23
+ it 'tokenizes numeric literal in scientific notation' do
24
+ %w( 6.02e23 .602E+24 ).each do |s|
25
+ tokens = tokenizer.tokenize(s)
26
+ expect(tokens.map(&:category)).to eq([:numeric])
27
+ expect(tokens.map(&:value)).to eq([6.02e23])
28
+ end
29
+
30
+ tokens = tokenizer.tokenize('6E23')
31
+ expect(tokens.map(&:value)).to eq([0.6e24])
32
+
33
+ tokens = tokenizer.tokenize('6e-23')
34
+ expect(tokens.map(&:value)).to eq([0.6e-22])
35
+ end
36
+
37
+ it 'tokenizes addition' do
38
+ tokens = tokenizer.tokenize('1+1')
39
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
40
+ expect(tokens.map(&:value)).to eq([1, :add, 1])
41
+ end
42
+
43
+ it 'tokenizes unary minus' do
44
+ tokens = tokenizer.tokenize('-5')
45
+ expect(tokens.map(&:category)).to eq([:operator, :numeric])
46
+ expect(tokens.map(&:value)).to eq([:negate, 5])
47
+
48
+ tokens = tokenizer.tokenize('(-5)')
49
+ expect(tokens.map(&:category)).to eq([:grouping, :operator, :numeric, :grouping])
50
+ expect(tokens.map(&:value)).to eq([:open, :negate, 5, :close])
51
+
52
+ tokens = tokenizer.tokenize('{-5, -2}[-1]')
53
+ expect(tokens.map(&:category)).to eq([
54
+ :array, # {
55
+ :operator, :numeric, :grouping, # -5,
56
+ :operator, :numeric, :array, # -2}
57
+ :access, :operator, :numeric, :access # [-1]
58
+ ])
59
+ expect(tokens.map(&:value)).to eq([
60
+ :array_start, # {
61
+ :negate, 5, :comma, # -5,
62
+ :negate, 2, :array_end, # -2}
63
+ :lbracket, :negate, 1, :rbracket # [-1]
64
+ ])
65
+
66
+ tokens = tokenizer.tokenize('if(-5 > x, -7, -8) - 9')
67
+ expect(tokens.map(&:category)).to eq([
68
+ :function, :grouping, # if(
69
+ :operator, :numeric, :comparator, :identifier, :grouping, # -5 > x,
70
+ :operator, :numeric, :grouping, # -7,
71
+ :operator, :numeric, :grouping, # -8)
72
+ :operator, :numeric # - 9
73
+ ])
74
+ expect(tokens.map(&:value)).to eq([
75
+ :if, :open, # if(
76
+ :negate, 5, :gt, 'x', :comma, # -5 > x,
77
+ :negate, 7, :comma, # -7,
78
+ :negate, 8, :close, # -8)
79
+ :subtract, 9 # - 9
80
+ ])
81
+ end
82
+
83
+ it 'tokenizes comparison with =' do
84
+ tokens = tokenizer.tokenize('number = 5')
85
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
86
+ expect(tokens.map(&:value)).to eq(['number', :eq, 5])
87
+ end
88
+
89
+ it 'tokenizes comparison with alternate ==' do
90
+ tokens = tokenizer.tokenize('number == 5')
91
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
92
+ expect(tokens.map(&:value)).to eq(['number', :eq, 5])
93
+ end
94
+
95
+ it 'tokenizes bitwise OR' do
96
+ tokens = tokenizer.tokenize('2 | 3')
97
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
98
+ expect(tokens.map(&:value)).to eq([2, :bitor, 3])
99
+ end
100
+
101
+ it 'tokenizes bitwise AND' do
102
+ tokens = tokenizer.tokenize('2 & 3')
103
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
104
+ expect(tokens.map(&:value)).to eq([2, :bitand, 3])
105
+ end
106
+
107
+ it 'tokenizes bitwise SHIFT LEFT' do
108
+ tokens = tokenizer.tokenize('2 << 3')
109
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
110
+ expect(tokens.map(&:value)).to eq([2, :bitshiftleft, 3])
111
+ end
112
+
113
+ it 'tokenizes bitwise SHIFT RIGHT' do
114
+ tokens = tokenizer.tokenize('2 >> 3')
115
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
116
+ expect(tokens.map(&:value)).to eq([2, :bitshiftright, 3])
117
+ end
118
+
119
+ it 'ignores whitespace' do
120
+ tokens = tokenizer.tokenize('1 / 1 ')
121
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
122
+ expect(tokens.map(&:value)).to eq([1, :divide, 1])
123
+ end
124
+
125
+ it 'tokenizes power operations in simple expressions' do
126
+ tokens = tokenizer.tokenize('10 ^ 2')
127
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
128
+ expect(tokens.map(&:value)).to eq([10, :pow, 2])
129
+ end
130
+
131
+ it 'tokenizes power operations in complex expressions' do
132
+ tokens = tokenizer.tokenize('0 * 10 ^ -5')
133
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric, :operator, :operator, :numeric])
134
+ expect(tokens.map(&:value)).to eq([0, :multiply, 10, :pow, :negate, 5])
135
+ end
136
+
137
+ it 'handles floating point operands' do
138
+ tokens = tokenizer.tokenize('1.5 * 3.7')
139
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
140
+ expect(tokens.map(&:value)).to eq([1.5, :multiply, 3.7])
141
+ end
142
+
143
+ it 'does not require leading zero' do
144
+ tokens = tokenizer.tokenize('.5 * 3.7')
145
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
146
+ expect(tokens.map(&:value)).to eq([0.5, :multiply, 3.7])
147
+ end
148
+
149
+ it 'accepts arbitrary identifiers' do
150
+ tokens = tokenizer.tokenize('sea_monkeys > 1500')
151
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
152
+ expect(tokens.map(&:value)).to eq(['sea_monkeys', :gt, 1500])
153
+ end
154
+
155
+ it 'recognizes double-quoted strings' do
156
+ tokens = tokenizer.tokenize('animal = "giraffe"')
157
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :string])
158
+ expect(tokens.map(&:value)).to eq(['animal', :eq, 'giraffe'])
159
+ end
160
+
161
+ it 'recognizes single-quoted strings' do
162
+ tokens = tokenizer.tokenize("animal = 'giraffe'")
163
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :string])
164
+ expect(tokens.map(&:value)).to eq(['animal', :eq, 'giraffe'])
165
+ end
166
+
167
+ it 'recognizes binary minus operator' do
168
+ tokens = tokenizer.tokenize('2 - 3')
169
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
170
+ expect(tokens.map(&:value)).to eq([2, :subtract, 3])
171
+ end
172
+
173
+ it 'recognizes unary minus operator applied to left operand' do
174
+ tokens = tokenizer.tokenize('-2 + 3')
175
+ expect(tokens.map(&:category)).to eq([:operator, :numeric, :operator, :numeric])
176
+ expect(tokens.map(&:value)).to eq([:negate, 2, :add, 3])
177
+ end
178
+
179
+ it 'recognizes unary minus operator applied to right operand' do
180
+ tokens = tokenizer.tokenize('2 - -3')
181
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :operator, :numeric])
182
+ expect(tokens.map(&:value)).to eq([2, :subtract, :negate, 3])
183
+ end
184
+
185
+ it 'matches "<=" before "<"' do
186
+ tokens = tokenizer.tokenize('perimeter <= 7500')
187
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
188
+ expect(tokens.map(&:value)).to eq(['perimeter', :le, 7500])
189
+ end
190
+
191
+ it 'tokenizes "and" for logical expressions' do
192
+ tokens = tokenizer.tokenize('octopi <= 7500 AND sharks > 1500')
193
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric, :combinator, :identifier, :comparator, :numeric])
194
+ expect(tokens.map(&:value)).to eq(['octopi', :le, 7500, :and, 'sharks', :gt, 1500])
195
+ end
196
+
197
+ it 'tokenizes "or" for logical expressions' do
198
+ tokens = tokenizer.tokenize('size < 3 or admin = 1')
199
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric, :combinator, :identifier, :comparator, :numeric])
200
+ expect(tokens.map(&:value)).to eq(['size', :lt, 3, :or, 'admin', :eq, 1])
201
+ end
202
+
203
+ it 'tokenizes "&&" for logical expressions' do
204
+ tokens = tokenizer.tokenize('octopi <= 7500 && sharks > 1500')
205
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric, :combinator, :identifier, :comparator, :numeric])
206
+ expect(tokens.map(&:value)).to eq(['octopi', :le, 7500, :and, 'sharks', :gt, 1500])
207
+ end
208
+
209
+ it 'tokenizes "||" for logical expressions' do
210
+ tokens = tokenizer.tokenize('size < 3 || admin = 1')
211
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric, :combinator, :identifier, :comparator, :numeric])
212
+ expect(tokens.map(&:value)).to eq(['size', :lt, 3, :or, 'admin', :eq, 1])
213
+ end
214
+
215
+ it 'tokenizes curly brackets for array literals' do
216
+ tokens = tokenizer.tokenize('{}')
217
+ expect(tokens.map(&:category)).to eq(%i(array array))
218
+ expect(tokens.map(&:value)).to eq(%i(array_start array_end))
219
+ end
220
+
221
+ it 'tokenizes square brackets for data structure access' do
222
+ tokens = tokenizer.tokenize('a[1]')
223
+ expect(tokens.map(&:category)).to eq(%i(identifier access numeric access))
224
+ expect(tokens.map(&:value)).to eq(['a', :lbracket, 1, :rbracket])
225
+ end
226
+
227
+ it 'detects unbalanced parentheses' do
228
+ expect { tokenizer.tokenize('(5+3') }.to raise_error(Dentaku::TokenizerError, /too many opening parentheses/)
229
+ expect { tokenizer.tokenize(')') }.to raise_error(Dentaku::TokenizerError, /too many closing parentheses/)
230
+ end
231
+
232
+ it 'recognizes identifiers that share initial substrings with combinators' do
233
+ tokens = tokenizer.tokenize('andover < 10')
234
+ expect(tokens.length).to eq(3)
235
+ expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
236
+ expect(tokens.map(&:value)).to eq(['andover', :lt, 10])
237
+ end
238
+
239
+ it 'tokenizes TRUE and FALSE literals' do
240
+ tokens = tokenizer.tokenize('true and false')
241
+ expect(tokens.length).to eq(3)
242
+ expect(tokens.map(&:category)).to eq([:logical, :combinator, :logical])
243
+ expect(tokens.map(&:value)).to eq([true, :and, false])
244
+
245
+ tokens = tokenizer.tokenize('true_lies and falsehoods')
246
+ expect(tokens.length).to eq(3)
247
+ expect(tokens.map(&:category)).to eq([:identifier, :combinator, :identifier])
248
+ expect(tokens.map(&:value)).to eq(['true_lies', :and, 'falsehoods'])
249
+ end
250
+
251
+ it 'tokenizes Time literals' do
252
+ tokens = tokenizer.tokenize('2017-01-01 2017-01-2 2017-1-03 2017-01-04 12:23:42 2017-1-5 1:2:3 2017-1-06 1:02:30 2017-01-07 12:34:56 Z 2017-01-08 1:2:3 +0800 2017-01-08T01:02:03.456Z')
253
+ expect(tokens.length).to eq(9)
254
+ expect(tokens.map(&:category)).to eq([:datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime])
255
+ expect(tokens.map(&:value)).to eq([
256
+ Time.local(2017, 1, 1).to_datetime,
257
+ Time.local(2017, 1, 2).to_datetime,
258
+ Time.local(2017, 1, 3).to_datetime,
259
+ Time.local(2017, 1, 4, 12, 23, 42).to_datetime,
260
+ Time.local(2017, 1, 5, 1, 2, 3).to_datetime,
261
+ Time.local(2017, 1, 6, 1, 2, 30).to_datetime,
262
+ Time.utc(2017, 1, 7, 12, 34, 56).to_datetime,
263
+ Time.new(2017, 1, 8, 1, 2, 3, "+08:00").to_datetime,
264
+ Time.utc(2017, 1, 8, 1, 2, 3, 456000).to_datetime
265
+ ])
266
+ end
267
+
268
+ describe 'tokenizing function calls' do
269
+ it 'handles IF' do
270
+ tokens = tokenizer.tokenize('if(x < 10, y, z)')
271
+ expect(tokens.length).to eq(10)
272
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :identifier, :comparator, :numeric, :grouping, :identifier, :grouping, :identifier, :grouping])
273
+ expect(tokens.map(&:value)).to eq([:if, :open, 'x', :lt, 10, :comma, 'y', :comma, 'z', :close])
274
+ end
275
+
276
+ it 'handles ROUND/UP/DOWN' do
277
+ tokens = tokenizer.tokenize('round(8.2)')
278
+ expect(tokens.length).to eq(4)
279
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping])
280
+ expect(tokens.map(&:value)).to eq([:round, :open, BigDecimal('8.2'), :close])
281
+
282
+ tokens = tokenizer.tokenize('round(8.75, 1)')
283
+ expect(tokens.length).to eq(6)
284
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping, :numeric, :grouping])
285
+ expect(tokens.map(&:value)).to eq([:round, :open, BigDecimal('8.75'), :comma, 1, :close])
286
+
287
+ tokens = tokenizer.tokenize('ROUNDUP(8.2)')
288
+ expect(tokens.length).to eq(4)
289
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping])
290
+ expect(tokens.map(&:value)).to eq([:roundup, :open, BigDecimal('8.2'), :close])
291
+
292
+ tokens = tokenizer.tokenize('RoundDown(8.2)')
293
+ expect(tokens.length).to eq(4)
294
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping])
295
+ expect(tokens.map(&:value)).to eq([:rounddown, :open, BigDecimal('8.2'), :close])
296
+ end
297
+
298
+ it 'handles NOT' do
299
+ tokens = tokenizer.tokenize('not(8 < 5)')
300
+ expect(tokens.length).to eq(6)
301
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :comparator, :numeric, :grouping])
302
+ expect(tokens.map(&:value)).to eq([:not, :open, 8, :lt, 5, :close])
303
+ end
304
+
305
+ it 'handles ANY/ALL' do
306
+ %i( any all ).each do |fn|
307
+ tokens = tokenizer.tokenize("#{fn}(users, u, u.age > 18)")
308
+ expect(tokens.length).to eq(10)
309
+ expect(tokens.map { |t| [t.category, t.value] }).to eq([
310
+ [:function, fn ], # function call (any/all)
311
+ [:grouping, :open ], # (
312
+ [:identifier, "users"], # users
313
+ [:grouping, :comma ], # ,
314
+ [:identifier, "u" ], # u
315
+ [:grouping, :comma ], # ,
316
+ [:identifier, "u.age"], # u.age
317
+ [:comparator, :gt ], # >
318
+ [:numeric, 18 ], # 18
319
+ [:grouping, :close ] # )
320
+ ])
321
+ end
322
+ end
323
+
324
+ it 'handles whitespace after function name' do
325
+ tokens = tokenizer.tokenize('not (8 < 5)')
326
+ expect(tokens.length).to eq(6)
327
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :comparator, :numeric, :grouping])
328
+ expect(tokens.map(&:value)).to eq([:not, :open, 8, :lt, 5, :close])
329
+ end
330
+
331
+ it 'handles when function ends with a bang' do
332
+ tokens = tokenizer.tokenize('exp!(5 * 3)')
333
+ expect(tokens.length).to eq(6)
334
+ expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :operator, :numeric, :grouping])
335
+ expect(tokens.map(&:value)).to eq([:exp!, :open, 5, :multiply, 3, :close])
336
+ end
337
+ end
338
+
339
+ describe 'aliases' do
340
+ let(:aliases) {
341
+ {
342
+ round: ['rrrrround!', 'redond'],
343
+ roundup: ['округлвверх'],
344
+ rounddown: ['округлвниз'],
345
+ if: ['если', 'cuando', '如果'],
346
+ max: ['макс', 'maximo'],
347
+ min: ['мин', 'minimo']
348
+ }
349
+ }
350
+
351
+ it 'replaced with function name' do
352
+ input = 'rrrrround!(8.2) + minimo(4,6,2)'
353
+ tokenizer.tokenize(input, aliases: aliases)
354
+ expect(tokenizer.replace_aliases(input)).to eq('round(8.2) + min(4,6,2)')
355
+ end
356
+
357
+ it 'case insensitive' do
358
+ input = 'MinImO(4,6,2)'
359
+ tokenizer.tokenize(input, aliases: aliases)
360
+ expect(tokenizer.replace_aliases(input)).to eq('min(4,6,2)')
361
+ end
362
+
363
+ it 'replace only whole aliases without word parts' do
364
+ input = 'maximo(2,minimoooo())' # `minimoooo` doesn't match `minimo`
365
+ tokenizer.tokenize(input, aliases: aliases)
366
+ expect(tokenizer.replace_aliases(input)).to eq('max(2,minimoooo())')
367
+ end
368
+
369
+ it 'work with non-latin symbols' do
370
+ input = '如果(1,2,3)'
371
+ tokenizer.tokenize(input, aliases: aliases)
372
+ expect(tokenizer.replace_aliases(input)).to eq('if(1,2,3)')
373
+ end
374
+ end
375
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ require 'dentaku/visitor/infix'
4
+
5
+ class ArrayProcessor
6
+ attr_reader :expression
7
+ include Dentaku::Visitor::Infix
8
+
9
+ def initialize
10
+ @expression = []
11
+ end
12
+
13
+ def visit_array(node)
14
+ @expression << "{"
15
+
16
+ head, *tail = node.value
17
+
18
+ process(head) if head
19
+
20
+ tail.each do |v|
21
+ @expression << ","
22
+ process(v)
23
+ end
24
+
25
+ @expression << "}"
26
+ end
27
+
28
+ def process(node)
29
+ @expression << node.to_s
30
+ end
31
+ end
32
+
33
+ RSpec.describe Dentaku::Visitor::Infix do
34
+ it 'generates array representation of operation' do
35
+ processor = ArrayProcessor.new
36
+ processor.visit(ast('5 + 3'))
37
+ expect(processor.expression).to eq ['5', '+', '3']
38
+ end
39
+
40
+ it 'supports array nodes' do
41
+ processor = ArrayProcessor.new
42
+ processor.visit(ast('{1, 2, 3}'))
43
+ expect(processor.expression).to eq ['{', '1', ',', '2', ',', '3', '}']
44
+ end
45
+
46
+ private
47
+
48
+ def ast(expression)
49
+ tokens = Dentaku::Tokenizer.new.tokenize(expression)
50
+ Dentaku::Parser.new(tokens).parse
51
+ end
52
+ end