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,52 @@
1
+ require 'dentaku'
2
+
3
+ describe Dentaku do
4
+ it 'evaulates an expression' do
5
+ expect(Dentaku('5+3')).to eql(8)
6
+ end
7
+
8
+ it 'binds values to variables' do
9
+ expect(Dentaku('oranges > 7', oranges: 10)).to be_truthy
10
+ end
11
+
12
+ it 'evaulates a nested function' do
13
+ expect(Dentaku('roundup(roundup(3 * cherries) + raspberries)', cherries: 1.5, raspberries: 0.9)).to eql(6)
14
+ end
15
+
16
+ it 'treats variables as case-insensitive' do
17
+ expect(Dentaku('40 + N', 'n' => 2)).to eql(42)
18
+ expect(Dentaku('40 + N', 'N' => 2)).to eql(42)
19
+ expect(Dentaku('40 + n', 'N' => 2)).to eql(42)
20
+ expect(Dentaku('40 + n', 'n' => 2)).to eql(42)
21
+ end
22
+
23
+ it 'raises a parse error for bad logic expressions' do
24
+ expect {
25
+ Dentaku!('true AND')
26
+ }.to raise_error(Dentaku::ParseError)
27
+ end
28
+
29
+ it 'evaluates with class-level shortcut functions' do
30
+ expect(described_class.evaluate('2+2')).to eq(4)
31
+ expect(described_class.evaluate!('2+2')).to eq(4)
32
+ expect { described_class.evaluate!('a+1') }.to raise_error(Dentaku::UnboundVariableError)
33
+ end
34
+
35
+ it 'accepts a block for custom handling of unbound variables' do
36
+ unbound = 'apples * 1.5'
37
+ expect(described_class.evaluate(unbound) { :bar }).to eq(:bar)
38
+ expect(described_class.evaluate(unbound) { |e| e }).to eq(unbound)
39
+ end
40
+
41
+ it 'evaluates with class-level aliases' do
42
+ described_class.aliases = { roundup: ['roundupup'] }
43
+ expect(described_class.evaluate('roundupup(6.1)')).to eq(7)
44
+ end
45
+
46
+ it 'sets caching opt-in flags' do
47
+ expect {
48
+ described_class.enable_caching!
49
+ }.to change { described_class.cache_ast? }.from(false).to(true)
50
+ .and change { described_class.cache_dependency_order? }.from(false).to(true)
51
+ end
52
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/exceptions'
3
+
4
+ describe Dentaku::UnboundVariableError do
5
+ it 'includes variable name(s) in message' do
6
+ exception = described_class.new(['length'])
7
+ expect(exception.unbound_variables).to include('length')
8
+ end
9
+ end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/calculator'
4
+
5
+ describe Dentaku::Calculator do
6
+ describe 'functions' do
7
+ describe 'external functions' do
8
+
9
+ let(:with_external_funcs) do
10
+ c = described_class.new
11
+
12
+ c.add_function(:now, :string, -> { Time.now.to_s })
13
+
14
+ fns = [
15
+ [:pow, :numeric, ->(mantissa, exponent) { mantissa**exponent }],
16
+ [:biggest, :numeric, ->(*args) { args.max }],
17
+ [:smallest, :numeric, ->(*args) { args.min }],
18
+ [:optional, :numeric, ->(x, y, z = 0) { x + y + z }],
19
+ ]
20
+
21
+ c.add_functions(fns)
22
+ end
23
+
24
+ it 'includes NOW' do
25
+ now = with_external_funcs.evaluate('NOW()')
26
+ expect(now).not_to be_nil
27
+ expect(now).not_to be_empty
28
+ end
29
+
30
+ it 'includes POW' do
31
+ expect(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
32
+ expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
33
+ expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
34
+ end
35
+
36
+ it 'includes BIGGEST' do
37
+ expect(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
38
+ end
39
+
40
+ it 'includes SMALLEST' do
41
+ expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
42
+ end
43
+
44
+ it 'includes OPTIONAL' do
45
+ expect(with_external_funcs.evaluate('OPTIONAL(1,2)')).to eq(3)
46
+ expect(with_external_funcs.evaluate('OPTIONAL(1,2,3)')).to eq(6)
47
+ expect { with_external_funcs.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
48
+ expect { with_external_funcs.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
49
+ end
50
+
51
+ it 'supports array parameters' do
52
+ calculator = described_class.new
53
+ calculator.add_function(
54
+ :includes,
55
+ :logical,
56
+ ->(haystack, needle) {
57
+ haystack.include?(needle)
58
+ }
59
+ )
60
+
61
+ expect(calculator.evaluate("INCLUDES(list, 2)", list: [1, 2, 3])).to eq(true)
62
+ end
63
+ end
64
+
65
+ it 'allows registering "bang" functions' do
66
+ calculator = described_class.new
67
+ calculator.add_function(:hey!, :string, -> { "hey!" })
68
+ expect(calculator.evaluate("hey!()")).to eq("hey!")
69
+ end
70
+
71
+ it 'defines for a given function a properly named class that represents it to support AST marshaling' do
72
+ calculator = described_class.new
73
+ expect {
74
+ calculator.add_function(:ho, :string, -> {})
75
+ }.to change {
76
+ Dentaku::AST::Function.const_defined?("Ho")
77
+ }.from(false).to(true)
78
+
79
+ expect {
80
+ Marshal.dump(calculator.ast('MAX(1, 2)'))
81
+ }.not_to raise_error
82
+ end
83
+
84
+ it 'does not store functions across all calculators' do
85
+ calculator1 = Dentaku::Calculator.new
86
+ calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
87
+
88
+ calculator2 = Dentaku::Calculator.new
89
+ calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
90
+
91
+ expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
92
+ expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
93
+
94
+ expect {
95
+ Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
96
+ }.to raise_error(Dentaku::ParseError)
97
+ end
98
+
99
+ describe 'Dentaku::Calculator.add_function' do
100
+ it 'adds to default/global function registry' do
101
+ Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
102
+ expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/token'
3
+ require 'dentaku/tokenizer'
4
+ require 'dentaku/parser'
5
+
6
+ describe Dentaku::Parser do
7
+ it 'parses an integer literal' do
8
+ node = parse('5')
9
+ expect(node.value).to eq(5)
10
+ end
11
+
12
+ it 'performs simple addition' do
13
+ node = parse('5 + 4')
14
+ expect(node.value).to eq(9)
15
+ end
16
+
17
+ it 'compares two numbers' do
18
+ node = parse('5 < 4')
19
+ expect(node.value).to eq(false)
20
+ end
21
+
22
+ it 'calculates unary percentage' do
23
+ node = parse('5%')
24
+ expect(node.value).to eq(0.05)
25
+ end
26
+
27
+ it 'calculates bitwise OR' do
28
+ node = parse('2|3')
29
+ expect(node.value).to eq(3)
30
+ end
31
+
32
+ it 'performs multiple operations in one stream' do
33
+ node = parse('5 * 4 + 3')
34
+ expect(node.value).to eq(23)
35
+ end
36
+
37
+ it 'respects order of operations' do
38
+ node = parse('5 + 4*3')
39
+ expect(node.value).to eq(17)
40
+ end
41
+
42
+ it 'respects grouping by parenthesis' do
43
+ node = parse('(5 + 4) * 3')
44
+ expect(node.value).to eq(27)
45
+ end
46
+
47
+ it 'evaluates functions' do
48
+ node = parse('IF(5 < 4, 3, 2)')
49
+ expect(node.value).to eq(2)
50
+ end
51
+
52
+ it 'represents formulas with variables' do
53
+ node = parse('5 * x')
54
+ expect { node.value }.to raise_error(Dentaku::UnboundVariableError)
55
+ expect(node.value("x" => 3)).to eq(15)
56
+ end
57
+
58
+ it 'evaluates access into data structures' do
59
+ node = parse('a[1]')
60
+ expect { node.value }.to raise_error(Dentaku::UnboundVariableError)
61
+ expect(node.value("a" => [1, 2, 3])).to eq(2)
62
+ end
63
+
64
+ it 'evaluates boolean expressions' do
65
+ node = parse('true AND false')
66
+ expect(node.value).to eq(false)
67
+ end
68
+
69
+ it 'evaluates a case statement' do
70
+ node = parse('CASE x WHEN 1 THEN 2 WHEN 3 THEN 4 END')
71
+ expect(node.value("x" => 3)).to eq(4)
72
+ end
73
+
74
+ it 'evaluates a nested case statement with case-sensitivity' do
75
+ node = parse('CASE x WHEN 1 THEN CASE Y WHEN "A" THEN 2 WHEN "B" THEN 3 END END', { case_sensitive: true }, { case_sensitive: true })
76
+ expect(node.value("x" => 1, "y" => "A", "Y" => "B")).to eq(3)
77
+ end
78
+
79
+ it 'evaluates arrays' do
80
+ node = parse('{1, 2, 3}')
81
+ expect(node.value).to eq([1, 2, 3])
82
+ end
83
+
84
+ context 'invalid expression' do
85
+ it 'raises a parse error for bad math' do
86
+ expect {
87
+ parse("5 * -")
88
+ }.to raise_error(Dentaku::ParseError)
89
+ end
90
+
91
+ it 'raises a parse error for bad logic' do
92
+ expect {
93
+ parse("TRUE AND")
94
+ }.to raise_error(Dentaku::ParseError)
95
+ end
96
+
97
+ it 'raises a parse error for too many operands' do
98
+ expect {
99
+ parse("IF(1, 0, IF(1, 2, 3, 4))")
100
+ }.to raise_error(Dentaku::ParseError)
101
+
102
+ expect {
103
+ parse("CASE a WHEN 1 THEN true ELSE THEN IF(1, 2, 3, 4) END")
104
+ }.to raise_error(Dentaku::ParseError)
105
+ end
106
+
107
+ it 'raises a parse error for bad grouping structure' do
108
+ expect {
109
+ parse(",")
110
+ }.to raise_error(Dentaku::ParseError)
111
+
112
+ expect {
113
+ parse("5, x")
114
+ described_class.new([five, comma, x]).parse
115
+ }.to raise_error(Dentaku::ParseError)
116
+
117
+ expect {
118
+ parse("5 + 5, x")
119
+ }.to raise_error(Dentaku::ParseError)
120
+
121
+ expect {
122
+ parse("{1, 2, }")
123
+ }.to raise_error(Dentaku::ParseError)
124
+
125
+ expect {
126
+ parse("CONCAT('1', '2', )")
127
+ }.to raise_error(Dentaku::ParseError)
128
+ end
129
+
130
+ it 'raises parse errors for malformed case statements' do
131
+ expect {
132
+ parse("CASE a when 'one' then 1")
133
+ }.to raise_error(Dentaku::ParseError)
134
+
135
+ expect {
136
+ parse("case a whend 'one' then 1 end")
137
+ }.to raise_error(Dentaku::ParseError)
138
+
139
+ expect {
140
+ parse("CASE a WHEN 'one' THEND 1 END")
141
+ }.to raise_error(Dentaku::ParseError)
142
+
143
+ expect {
144
+ parse("CASE a when 'one' then end")
145
+ }.to raise_error(Dentaku::ParseError)
146
+ end
147
+
148
+ it 'raises a parse error when trying to access an undefined function' do
149
+ expect {
150
+ parse("undefined()")
151
+ }.to raise_error(Dentaku::ParseError)
152
+ end
153
+ end
154
+
155
+ it "evaluates explicit 'NULL' as nil" do
156
+ node = parse("NULL")
157
+ expect(node.value).to eq(nil)
158
+ end
159
+
160
+ private
161
+
162
+ def parse(expr, parser_options = {}, tokenizer_options = {})
163
+ tokens = Dentaku::Tokenizer.new.tokenize(expr, tokenizer_options)
164
+ described_class.new(tokens, parser_options).parse
165
+ end
166
+ end
@@ -0,0 +1,66 @@
1
+ require 'dentaku/print_visitor'
2
+ require 'dentaku/tokenizer'
3
+ require 'dentaku/parser'
4
+
5
+ describe Dentaku::PrintVisitor do
6
+ it 'prints a representation of an AST' do
7
+ repr = roundtrip('5+4')
8
+ expect(repr).to eq('5 + 4')
9
+ end
10
+
11
+ it 'quotes string literals' do
12
+ repr = roundtrip('Concat(\'a\', "B")')
13
+ expect(repr).to eq('CONCAT("a", "B")')
14
+ end
15
+
16
+ it 'handles unary operations on literals' do
17
+ repr = roundtrip('- 4')
18
+ expect(repr).to eq('-4')
19
+ end
20
+
21
+ it 'handles unary operations on trees' do
22
+ repr = roundtrip('- (5 + 5)')
23
+ expect(repr).to eq('-(5 + 5)')
24
+ end
25
+
26
+ it 'handles a complex arithmetic expression' do
27
+ repr = roundtrip('(((1 + 7) * (8 ^ 2)) / - (3.0 - apples))')
28
+ expect(repr).to eq('(1 + 7) * 8 ^ 2 / -(3.0 - apples)')
29
+ end
30
+
31
+ it 'handles a complex logical expression' do
32
+ repr = roundtrip('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
33
+ expect(repr).to eq('1 < 2 and 3 <= 4 or 5 > 6 and 7 >= 8 or 9 != 10 and true')
34
+ end
35
+
36
+ it 'handles a function call' do
37
+ repr = roundtrip('IF(a[0] = NULL, "five", \'seven\')')
38
+ expect(repr).to eq('IF(a[0] = NULL, "five", "seven")')
39
+ end
40
+
41
+ it 'handles a case statement' do
42
+ repr = roundtrip('case (a % 5) when 0 then a else b end')
43
+ expect(repr).to eq('CASE a % 5 WHEN 0 THEN a ELSE b END')
44
+ end
45
+
46
+ it 'handles a bitwise operators' do
47
+ repr = roundtrip('0xCAFE & 0xDECAF | 0xBEEF')
48
+ expect(repr).to eq('0xCAFE & 0xDECAF | 0xBEEF')
49
+ end
50
+
51
+ it 'handles a datetime literal' do
52
+ repr = roundtrip('2017-12-24 23:59:59')
53
+ expect(repr).to eq('2017-12-24 23:59:59')
54
+ end
55
+
56
+ private
57
+
58
+ def roundtrip(string)
59
+ described_class.new(parsed(string)).to_s
60
+ end
61
+
62
+ def parsed(string)
63
+ tokens = Dentaku::Tokenizer.new.tokenize(string)
64
+ Dentaku::Parser.new(tokens).parse
65
+ end
66
+ end
@@ -0,0 +1,71 @@
1
+ require 'pry'
2
+ require 'simplecov'
3
+ require 'codecov'
4
+
5
+ SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
6
+ SimpleCov::Formatter::HTMLFormatter,
7
+ SimpleCov::Formatter::Codecov,
8
+ ])
9
+
10
+ SimpleCov.minimum_coverage 90
11
+ SimpleCov.minimum_coverage_by_file 80
12
+
13
+ SimpleCov.start do
14
+ add_filter "spec/"
15
+ end
16
+
17
+ RSpec.configure do |c|
18
+ c.before(:all) {
19
+ if Dentaku.respond_to?(:aliases=)
20
+ # add example for alias because we can set aliases just once
21
+ # before `calculator` method called
22
+ Dentaku.aliases = { roundup: ['roundupup'] }
23
+ end
24
+ }
25
+ end
26
+
27
+ # automatically create a token stream from bare values
28
+ def token_stream(*args)
29
+ args.map do |value|
30
+ type = type_for(value)
31
+ Dentaku::Token.new(type, value)
32
+ end
33
+ end
34
+
35
+ # make a (hopefully intelligent) guess about type
36
+ def type_for(value)
37
+ case value
38
+ when Numeric
39
+ :numeric
40
+ when String
41
+ :string
42
+ when true, false
43
+ :logical
44
+ when :add, :subtract, :multiply, :divide, :mod, :pow
45
+ :operator
46
+ when :open, :close, :comma
47
+ :grouping
48
+ when :lbracket, :rbracket
49
+ :access
50
+ when :le, :ge, :ne, :lt, :gt, :eq
51
+ :comparator
52
+ when :and, :or
53
+ :combinator
54
+ when :if, :round, :roundup, :rounddown, :not
55
+ :function
56
+ else
57
+ :identifier
58
+ end
59
+ end
60
+
61
+ def identifier(name)
62
+ Dentaku::AST::Identifier.new(token(name))
63
+ end
64
+
65
+ def literal(value)
66
+ Dentaku::AST::Literal.new(token(value))
67
+ end
68
+
69
+ def token(value)
70
+ Dentaku::Token.new(type_for(value), value)
71
+ end
@@ -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