attr_searchable 0.0.1

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.
@@ -0,0 +1,136 @@
1
+
2
+ require "attr_searchable_grammar/attributes"
3
+ require "attr_searchable_grammar/nodes"
4
+
5
+ module AttrSearchableGrammar
6
+ class BaseNode < Treetop::Runtime::SyntaxNode
7
+ attr_writer :model
8
+
9
+ def model
10
+ @model || parent.model
11
+ end
12
+
13
+ def to_arel
14
+ elements.collect(&:to_arel).inject(:and)
15
+ end
16
+
17
+ def elements
18
+ super.select { |element| element.class != Treetop::Runtime::SyntaxNode }
19
+ end
20
+
21
+ def arel_collection_for(key)
22
+ raise AttrSearchable::UnknownColumn, "Unknown key: #{key}" if model.searchable_attributes[key].nil?
23
+
24
+ Attributes::Collection.new model, key
25
+ end
26
+ end
27
+
28
+ class OperatorNode < Treetop::Runtime::SyntaxNode
29
+ def to_arel
30
+ text_value
31
+ end
32
+ end
33
+
34
+ class ComplexExpression < BaseNode; end
35
+ class ParenthesesExpression < BaseNode; end
36
+
37
+ class ComparativeExpression < BaseNode
38
+ def to_arel
39
+ elements[0].arel_collection.send elements[1].to_arel_method, elements[2].text_value
40
+ end
41
+ end
42
+
43
+ class IncludesOperator < OperatorNode
44
+ def to_arel_method
45
+ :matches
46
+ end
47
+ end
48
+
49
+ class EqualOperator < OperatorNode
50
+ def to_arel_method
51
+ :eq
52
+ end
53
+ end
54
+
55
+ class UnequalOperator < OperatorNode
56
+ def to_arel_method
57
+ :not_eq
58
+ end
59
+ end
60
+
61
+ class GreaterEqualOperator < OperatorNode
62
+ def to_arel_method
63
+ :gteq
64
+ end
65
+ end
66
+
67
+ class GreaterOperator < OperatorNode
68
+ def to_arel_method
69
+ :gt
70
+ end
71
+ end
72
+
73
+ class LessEqualOperator < OperatorNode
74
+ def to_arel_method
75
+ :lteq
76
+ end
77
+ end
78
+
79
+ class LessOperator < OperatorNode
80
+ def to_arel_method
81
+ :lt
82
+ end
83
+ end
84
+
85
+ class AnywhereExpression < BaseNode
86
+ def to_arel
87
+ keys = model.searchable_attribute_options.select { |key, value| value[:default] == true }.keys
88
+ keys = model.searchable_attributes.keys if keys.empty?
89
+
90
+ queries = keys.collect { |key| arel_collection_for key }.select { |attribute| attribute.compatible? text_value }.collect { |attribute| attribute.matches text_value }
91
+
92
+ raise AttrSearchable::NoSearchableAttributes unless model.searchable_attributes
93
+
94
+ queries.flatten.inject(:or)
95
+ end
96
+ end
97
+
98
+ class AndExpression < BaseNode
99
+ def to_arel
100
+ [elements.first.to_arel, elements.last.to_arel].inject(:and)
101
+ end
102
+ end
103
+
104
+ class OrExpression < BaseNode
105
+ def to_arel
106
+ [elements.first.to_arel, elements.last.to_arel].inject(:or)
107
+ end
108
+ end
109
+
110
+ class NotExpression < BaseNode
111
+ def to_arel
112
+ elements.first.to_arel.not
113
+ end
114
+ end
115
+
116
+ class Column < BaseNode
117
+ def arel_collection
118
+ arel_collection_for text_value
119
+ end
120
+ end
121
+
122
+ class SingleQuotedValue < BaseNode
123
+ def text_value
124
+ super.gsub /^'|'$/, ""
125
+ end
126
+ end
127
+
128
+ class DoubleQuotedValue < BaseNode
129
+ def text_value
130
+ super.gsub /^"|"$/, ""
131
+ end
132
+ end
133
+
134
+ class Value < BaseNode; end
135
+ end
136
+
@@ -0,0 +1,55 @@
1
+
2
+ grammar AttrSearchableGrammar
3
+ rule complex_expression
4
+ space? (boolean_expression / expression) space? <ComplexExpression>
5
+ end
6
+
7
+ rule boolean_expression
8
+ and_expression
9
+ end
10
+
11
+ rule and_expression
12
+ or_expression (space? ('AND' / 'and') space? / space !('OR' / 'or')) complex_expression <AndExpression> / or_expression
13
+ end
14
+
15
+ rule or_expression
16
+ expression space? ('OR' / 'or') space? (or_expression / expression) <OrExpression> / expression
17
+ end
18
+
19
+ rule expression
20
+ parentheses_expression / not_expression / comparative_expression / anywhere_expression
21
+ end
22
+
23
+ rule parentheses_expression
24
+ '(' complex_expression ')' <ParenthesesExpression>
25
+ end
26
+
27
+ rule not_expression
28
+ ('NOT' space / 'not' space / '-') (comparative_expression / anywhere_expression) <NotExpression>
29
+ end
30
+
31
+ rule comparative_expression
32
+ simple_column space? comparison_operator space? value <ComparativeExpression>
33
+ end
34
+
35
+ rule comparison_operator
36
+ ':' <IncludesOperator> / '=' <EqualOperator> / '!=' <UnequalOperator> / '>=' <GreaterEqualOperator> / '>' <GreaterOperator> / '<=' <LessEqualOperator> / '<' <LessOperator>
37
+ end
38
+
39
+ rule anywhere_expression
40
+ ("'" ([^\']* <AnywhereExpression>) "'") / ('"' ([^\"]* <AnywhereExpression>) '"') / [^\s()]+ <AnywhereExpression>
41
+ end
42
+
43
+ rule simple_column
44
+ [a-zA-Z0-9_.]+ <Column>
45
+ end
46
+
47
+ rule value
48
+ "'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [^\s()]+ <Value>
49
+ end
50
+
51
+ rule space
52
+ [\s]+
53
+ end
54
+ end
55
+
data/test/and_test.rb ADDED
@@ -0,0 +1,27 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class AndTest < AttrSearchable::TestCase
5
+ def test_and_string
6
+ expected = FactoryGirl.create(:product, :title => "Expected title", :description => "Description")
7
+ rejected = FactoryGirl.create(:product, :title => "Rejected title", :description => "Description")
8
+
9
+ results = Product.search("title: 'Expected title' description: Description")
10
+
11
+ assert_includes results, expected
12
+ refute_includes results, rejected
13
+
14
+ assert_equal results, Product.search("title: 'Expected title' AND description: Description")
15
+ end
16
+
17
+ def test_and_hash
18
+ expected = FactoryGirl.create(:product, :title => "Expected title", :description => "Description")
19
+ rejected = FactoryGirl.create(:product, :title => "Rejected title", :description => "Description")
20
+
21
+ results = Product.search(:and => [{:title => "Expected title"}, {:description => "Description"}])
22
+
23
+ assert_includes results, expected
24
+ refute_includes results, rejected
25
+ end
26
+ end
27
+
@@ -0,0 +1,44 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class AttrSearchableTest < AttrSearchable::TestCase
5
+ def test_associations
6
+ product = FactoryGirl.create(:product, :comments => [
7
+ FactoryGirl.create(:comment, :title => "Title1", :message => "Message1"),
8
+ FactoryGirl.create(:comment, :title => "Title2", :message => "Message2")
9
+ ])
10
+
11
+ assert_includes Product.search("comment: Title1 comment: Message1"), product
12
+ assert_includes Product.search("comment: Title2 comment: Message2"), product
13
+ end
14
+
15
+ def test_multiple
16
+ product = FactoryGirl.create(:product, :comments => [FactoryGirl.create(:comment, :title => "Title", :message => "Message")])
17
+
18
+ assert_includes Product.search("comment: Title"), product
19
+ assert_includes Product.search("comment: Message"), product
20
+ end
21
+
22
+ def test_default
23
+ product1 = FactoryGirl.create(:product, :title => "Expected")
24
+ product2 = FactoryGirl.create(:product, :description => "Expected")
25
+
26
+ results = Product.search("Expected")
27
+
28
+ assert_includes results, product1
29
+ assert_includes results, product2
30
+ end
31
+
32
+ def test_custom_default
33
+ product1 = FactoryGirl.create(:product, :title => "Expected")
34
+ product2 = FactoryGirl.create(:product, :description => "Expected")
35
+ product3 = FactoryGirl.create(:product, :brand => "Expected")
36
+
37
+ results = with_attr_searchable_options(Product, :primary, :default => true) { Product.search "Expected" }
38
+
39
+ assert_includes results, product1
40
+ assert_includes results, product2
41
+ refute_includes results, product3
42
+ end
43
+ end
44
+
@@ -0,0 +1,47 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class BooleanTest < AttrSearchable::TestCase
5
+ def test_mapping
6
+ product = FactoryGirl.create(:product, :available => true)
7
+
8
+ assert_includes Product.search("available: 1"), product
9
+ assert_includes Product.search("available: true"), product
10
+ assert_includes Product.search("available: yes"), product
11
+
12
+ product = FactoryGirl.create(:product, :available => false)
13
+
14
+ assert_includes Product.search("available: 0"), product
15
+ assert_includes Product.search("available: false"), product
16
+ assert_includes Product.search("available: no"), product
17
+ end
18
+
19
+ def test_anywhere
20
+ product = FactoryGirl.create(:product, :available => true)
21
+
22
+ assert_includes Product.search("true"), product
23
+ refute_includes Product.search("false"), product
24
+ end
25
+
26
+ def test_includes
27
+ product = FactoryGirl.create(:product, :available => true)
28
+
29
+ assert_includes Product.search("available: true"), product
30
+ refute_includes Product.search("available: false"), product
31
+ end
32
+
33
+ def test_equals
34
+ product = FactoryGirl.create(:product, :available => true)
35
+
36
+ assert_includes Product.search("available = true"), product
37
+ refute_includes Product.search("available = false"), product
38
+ end
39
+
40
+ def test_equals_not
41
+ product = FactoryGirl.create(:product, :available => false)
42
+
43
+ assert_includes Product.search("available != true"), product
44
+ refute_includes Product.search("available != false"), product
45
+ end
46
+ end
47
+
data/test/database.yml ADDED
@@ -0,0 +1,17 @@
1
+
2
+ sqlite:
3
+ adapter: sqlite3
4
+ database: ":memory:"
5
+
6
+ mysql:
7
+ adapter: mysql2
8
+ database: attr_searchable
9
+ username: root
10
+ encoding: utf8
11
+
12
+ postgres:
13
+ adapter: postgresql
14
+ database: attr_searchable
15
+ username: postgres
16
+ encoding: utf8
17
+
@@ -0,0 +1,70 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class DatetimeTest < AttrSearchable::TestCase
5
+ def test_mapping
6
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01 12:30:30"))
7
+
8
+ assert_includes Product.search("created_at: 2014"), product
9
+ assert_includes Product.search("created_at: 2014-05"), product
10
+ assert_includes Product.search("created_at: 2014-05-01"), product
11
+ assert_includes Product.search("created_at: '2014-05-01 12:30:30'"), product
12
+ end
13
+
14
+ def test_anywhere
15
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
16
+
17
+ assert_includes Product.search("2014-05-01"), product
18
+ refute_includes Product.search("2014-05-02"), product
19
+ end
20
+
21
+ def test_includes
22
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
23
+
24
+ assert_includes Product.search("created_at: 2014-05-01"), product
25
+ refute_includes Product.search("created_at: 2014-05-02"), product
26
+ end
27
+
28
+ def test_equals
29
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
30
+
31
+ assert_includes Product.search("created_at = 2014-05-01"), product
32
+ refute_includes Product.search("created_at = 2014-05-02"), product
33
+ end
34
+
35
+ def test_equals_not
36
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
37
+
38
+ assert_includes Product.search("created_at != 2014-05-02"), product
39
+ refute_includes Product.search("created_at != 2014-05-01"), product
40
+ end
41
+
42
+ def test_greater
43
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
44
+
45
+ assert_includes Product.search("created_at < 2014-05-02"), product
46
+ refute_includes Product.search("created_at < 2014-05-01"), product
47
+ end
48
+
49
+ def test_greater_equals
50
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
51
+
52
+ assert_includes Product.search("created_at >= 2014-05-01"), product
53
+ refute_includes Product.search("created_at >= 2014-05-02"), product
54
+ end
55
+
56
+ def test_less
57
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
58
+
59
+ assert_includes Product.search("created_at < 2014-05-02"), product
60
+ refute_includes Product.search("created_at < 2014-05-01"), product
61
+ end
62
+
63
+ def test_less_equals
64
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-02"))
65
+
66
+ assert_includes Product.search("created_at <= 2014-05-02"), product
67
+ refute_includes Product.search("created_at <= 2014-05-01"), product
68
+ end
69
+ end
70
+
@@ -0,0 +1,61 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class FloatTest < AttrSearchable::TestCase
5
+ def test_anywhere
6
+ product = FactoryGirl.create(:product, :price => 10.5, :created_at => Time.now - 1.day)
7
+
8
+ assert_includes Product.search("10.5"), product
9
+ refute_includes Product.search("11.5"), product
10
+ end
11
+
12
+ def test_includes
13
+ product = FactoryGirl.create(:product, :price => 10.5)
14
+
15
+ assert_includes Product.search("price: 10.5"), product
16
+ refute_includes Product.search("price: 11.5"), product
17
+ end
18
+
19
+ def test_equals
20
+ product = FactoryGirl.create(:product, :price => 10.5)
21
+
22
+ assert_includes Product.search("price = 10.5"), product
23
+ refute_includes Product.search("price = 11.5"), product
24
+ end
25
+
26
+ def test_equals_not
27
+ product = FactoryGirl.create(:product, :price => 10.5)
28
+
29
+ assert_includes Product.search("price != 11.5"), product
30
+ refute_includes Product.search("price != 10.5"), product
31
+ end
32
+
33
+ def test_greater
34
+ product = FactoryGirl.create(:product, :price => 10.5)
35
+
36
+ assert_includes Product.search("price > 10.4"), product
37
+ refute_includes Product.search("price < 10.5"), product
38
+ end
39
+
40
+ def test_greater_equals
41
+ product = FactoryGirl.create(:product, :price => 10.5)
42
+
43
+ assert_includes Product.search("price >= 10.5"), product
44
+ refute_includes Product.search("price >= 10.6"), product
45
+ end
46
+
47
+ def test_less
48
+ product = FactoryGirl.create(:product, :price => 10.5)
49
+
50
+ assert_includes Product.search("price < 10.6"), product
51
+ refute_includes Product.search("price < 10.5"), product
52
+ end
53
+
54
+ def test_less_equals
55
+ product = FactoryGirl.create(:product, :price => 10.5)
56
+
57
+ assert_includes Product.search("price <= 10.5"), product
58
+ refute_includes Product.search("price <= 10.4"), product
59
+ end
60
+ end
61
+
@@ -0,0 +1,27 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class FulltextTest < AttrSearchable::TestCase
5
+ def test_complex
6
+ product1 = FactoryGirl.create(:product, :title => "word1")
7
+ product2 = FactoryGirl.create(:product, :title => "word2 word3")
8
+ product3 = FactoryGirl.create(:product, :title => "word2")
9
+
10
+ results = Product.search("title:word1 OR (title:word2 -title:word3)")
11
+
12
+ assert_includes results, product1
13
+ refute_includes results, product2
14
+ assert_includes results, product3
15
+ end
16
+
17
+ def test_mixed
18
+ expected = FactoryGirl.create(:product, :title => "Expected title", :stock => 1)
19
+ rejected = FactoryGirl.create(:product, :title => "Expected title", :stock => 0)
20
+
21
+ results = Product.search("title:Expected title:Title stock > 0")
22
+
23
+ assert_includes results, expected
24
+ refute_includes results, rejected
25
+ end
26
+ end
27
+
@@ -0,0 +1,61 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class IntegerTest < AttrSearchable::TestCase
5
+ def test_anywhere
6
+ product = FactoryGirl.create(:product, :stock => 1)
7
+
8
+ assert_includes Product.search("1"), product
9
+ refute_includes Product.search("0"), product
10
+ end
11
+
12
+ def test_includes
13
+ product = FactoryGirl.create(:product, :stock => 1)
14
+
15
+ assert_includes Product.search("stock: 1"), product
16
+ refute_includes Product.search("stock: 10"), product
17
+ end
18
+
19
+ def test_equals
20
+ product = FactoryGirl.create(:product, :stock => 1)
21
+
22
+ assert_includes Product.search("stock = 1"), product
23
+ refute_includes Product.search("stock = 0"), product
24
+ end
25
+
26
+ def test_equals_not
27
+ product = FactoryGirl.create(:product, :stock => 1)
28
+
29
+ assert_includes Product.search("stock != 0"), product
30
+ refute_includes Product.search("stock != 1"), product
31
+ end
32
+
33
+ def test_greater
34
+ product = FactoryGirl.create(:product, :stock => 1)
35
+
36
+ assert_includes Product.search("stock > 0"), product
37
+ refute_includes Product.search("stock < 1"), product
38
+ end
39
+
40
+ def test_greater_equals
41
+ product = FactoryGirl.create(:product, :stock => 1)
42
+
43
+ assert_includes Product.search("stock >= 1"), product
44
+ refute_includes Product.search("stock >= 2"), product
45
+ end
46
+
47
+ def test_less
48
+ product = FactoryGirl.create(:product, :stock => 1)
49
+
50
+ assert_includes Product.search("stock < 2"), product
51
+ refute_includes Product.search("stock < 1"), product
52
+ end
53
+
54
+ def test_less_equals
55
+ product = FactoryGirl.create(:product, :stock => 1)
56
+
57
+ assert_includes Product.search("stock <= 1"), product
58
+ refute_includes Product.search("stock <= 0"), product
59
+ end
60
+ end
61
+
data/test/not_test.rb ADDED
@@ -0,0 +1,27 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class NotTest < AttrSearchable::TestCase
5
+ def test_not_string
6
+ expected = FactoryGirl.create(:product, :title => "Expected title")
7
+ rejected = FactoryGirl.create(:product, :title => "Rejected title")
8
+
9
+ results = Product.search("title: Title NOT title: Rejected")
10
+
11
+ assert_includes results, expected
12
+ refute_includes results, rejected
13
+
14
+ assert_equal results, Product.search("title: Title -title: Rejected")
15
+ end
16
+
17
+ def test_not_hash
18
+ expected = FactoryGirl.create(:product, :title => "Expected title")
19
+ rejected = FactoryGirl.create(:product, :title => "Rejected title")
20
+
21
+ results = Product.search(:and => [{:title => "Title"}, {:not => {:title => "Rejected"}}])
22
+
23
+ assert_includes results, expected
24
+ refute_includes results, rejected
25
+ end
26
+ end
27
+
data/test/or_test.rb ADDED
@@ -0,0 +1,29 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class OrTest < AttrSearchable::TestCase
5
+ def test_or_string
6
+ product1 = FactoryGirl.create(:product, :title => "Title1")
7
+ product2 = FactoryGirl.create(:product, :title => "Title2")
8
+ product3 = FactoryGirl.create(:product, :title => "Title3")
9
+
10
+ results = Product.search("title: Title1 OR title: Title2")
11
+
12
+ assert_includes results, product1
13
+ assert_includes results, product2
14
+ refute_includes results, product3
15
+ end
16
+
17
+ def test_or_hash
18
+ product1 = FactoryGirl.create(:product, :title => "Title1")
19
+ product2 = FactoryGirl.create(:product, :title => "Title2")
20
+ product3 = FactoryGirl.create(:product, :title => "Title3")
21
+
22
+ results = Product.search(:or => [{:title => "Title1"}, {:title => "Title2"}])
23
+
24
+ assert_includes results, product1
25
+ assert_includes results, product2
26
+ refute_includes results, product3
27
+ end
28
+ end
29
+
@@ -0,0 +1,84 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class StringTest < AttrSearchable::TestCase
5
+ def test_anywhere
6
+ product = FactoryGirl.create(:product, :title => "Expected title")
7
+
8
+ assert_includes Product.search("Expected"), product
9
+ refute_includes Product.search("Rejected"), product
10
+ end
11
+
12
+ def test_multiple
13
+ product = FactoryGirl.create(:product, :comments => [FactoryGirl.create(:comment, :title => "Expected title", :message => "Expected message")])
14
+
15
+ assert_includes Product.search("Expected"), product
16
+ refute_includes Product.search("Rejected"), product
17
+ end
18
+
19
+ def test_includes
20
+ product = FactoryGirl.create(:product, :title => "Expected")
21
+
22
+ assert_includes Product.search("title: Expected"), product
23
+ refute_includes Product.search("title: Rejected"), product
24
+ end
25
+
26
+ def test_includes_with_left_wildcard
27
+ product = FactoryGirl.create(:product, :title => "Some title")
28
+
29
+ assert_includes Product.search("title: Title"), product
30
+ end
31
+
32
+ def test_includes_without_left_wildcard
33
+ expected = FactoryGirl.create(:product, :brand => "Brand")
34
+ rejected = FactoryGirl.create(:product, :brand => "Rejected brand")
35
+
36
+ results = with_attr_searchable_options(Product, :brand, :left_wildcard => false) { Product.search "brand: Brand" }
37
+
38
+ assert_includes results, expected
39
+ refute_includes results, rejected
40
+ end
41
+
42
+ def test_equals
43
+ product = FactoryGirl.create(:product, :title => "Expected title")
44
+
45
+ assert_includes Product.search("title = 'Expected title'"), product
46
+ refute_includes Product.search("title = Expected"), product
47
+ end
48
+
49
+ def test_equals_not
50
+ product = FactoryGirl.create(:product, :title => "Expected")
51
+
52
+ assert_includes Product.search("title != Rejected"), product
53
+ refute_includes Product.search("title != Expected"), product
54
+ end
55
+
56
+ def test_greater
57
+ product = FactoryGirl.create(:product, :title => "Title B")
58
+
59
+ assert_includes Product.search("title > 'Title A'"), product
60
+ refute_includes Product.search("title > 'Title B'"), product
61
+ end
62
+
63
+ def test_greater_equals
64
+ product = FactoryGirl.create(:product, :title => "Title A")
65
+
66
+ assert_includes Product.search("title >= 'Title A'"), product
67
+ refute_includes Product.search("title >= 'Title B'"), product
68
+ end
69
+
70
+ def test_less
71
+ product = FactoryGirl.create(:product, :title => "Title A")
72
+
73
+ assert_includes Product.search("title < 'Title B'"), product
74
+ refute_includes Product.search("title < 'Title A'"), product
75
+ end
76
+
77
+ def test_less_or_greater
78
+ product = FactoryGirl.create(:product, :title => "Title B")
79
+
80
+ assert_includes Product.search("title <= 'Title B'"), product
81
+ refute_includes Product.search("title <= 'Title A'"), product
82
+ end
83
+ end
84
+