character_set 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ class CharacterSet
2
+ class Character
3
+ ENCODING = 'utf-8'.freeze
4
+ SAFELY_PRINTABLE = (0x21..0x7E).to_a - ['-', '[', '\\', ']', '^'].map(&:ord)
5
+
6
+ attr_accessor :codepoint
7
+
8
+ def initialize(codepoint)
9
+ case codepoint
10
+ when Integer then self.codepoint = codepoint
11
+ when String then self.codepoint = codepoint.ord
12
+ else raise ArgumentError, 'pass an Integer or String'
13
+ end
14
+ end
15
+
16
+ def to_s
17
+ codepoint.chr(ENCODING)
18
+ end
19
+
20
+ def hex
21
+ codepoint.to_s(16).upcase
22
+ end
23
+
24
+ def escape(opts = {})
25
+ return to_s if SAFELY_PRINTABLE.include?(codepoint) && !opts[:escape_all]
26
+
27
+ return yield(self) if block_given?
28
+
29
+ # https://billposer.org/Software/ListOfRepresentations.html
30
+ case opts[:format].to_s.downcase.delete('-_ ')
31
+ when '', 'default', 'es6', 'esnext', 'rb', 'ruby'
32
+ default_escape(opts)
33
+ when 'java', 'javascript', 'js'
34
+ default_escape(opts, false)
35
+ when 'capitalizableu', 'c#', 'csharp', 'd', 'python'
36
+ capitalizable_u_escape
37
+ when 'u+', 'uplus'
38
+ u_plus_escape
39
+ when 'literal', 'raw'
40
+ to_s
41
+ else
42
+ raise ArgumentError, "unsupported format: #{opts[:format].inspect}"
43
+ end
44
+ end
45
+
46
+ def plane
47
+ codepoint / 0x10000
48
+ end
49
+
50
+ private
51
+
52
+ def default_escape(opts, support_wide_hex = true)
53
+ if hex.length <= 2
54
+ '\\x' + hex.rjust(2, '0')
55
+ elsif hex.length <= 4
56
+ '\\u' + hex.rjust(4, '0')
57
+ elsif support_wide_hex
58
+ '\\u{' + hex + '}'
59
+ else
60
+ raise "#{opts[:format]} does not support escaping astral value #{hex}"
61
+ end
62
+ end
63
+
64
+ def capitalizable_u_escape
65
+ if hex.length <= 4
66
+ '\\u' + hex.rjust(4, '0')
67
+ else
68
+ '\\U' + hex.rjust(8, '0')
69
+ end
70
+ end
71
+
72
+ def u_plus_escape
73
+ 'U+' + hex.rjust(4, '0')
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,258 @@
1
+ class CharacterSet
2
+ module CommonSets
3
+ def ascii
4
+ @ascii ||= from_ranges(0..0x7F).freeze
5
+ end
6
+
7
+ # basic multilingual plane
8
+ def bmp
9
+ @bmp ||= from_ranges(0..0xD7FF, 0xE000..0xFFFF).freeze
10
+ end
11
+
12
+ # ./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
13
+ def crypt
14
+ @crypt ||= from_ranges(0x2E..0x5A, 0x61..0x7A).freeze
15
+ end
16
+
17
+ def newline
18
+ @newline ||= from_ranges(0xA..0xD, 0x85..0x85, 0x2028..0x2029).freeze
19
+ end
20
+
21
+ def unicode
22
+ @unicode ||= from_ranges(0..0xD7FF, 0xE000..0x10FFFF).freeze
23
+ end
24
+
25
+ def url_fragment
26
+ @url_fragment ||= from_ranges(
27
+ 0x21..0x21,
28
+ 0x24..0x24,
29
+ 0x26..0x3B,
30
+ 0x3D..0x3D,
31
+ 0x3F..0x5A,
32
+ 0x5F..0x5F,
33
+ 0x61..0x7A,
34
+ 0x7E..0x7E
35
+ ).freeze
36
+ end
37
+
38
+ def url_host
39
+ @url_host ||= from_ranges(
40
+ 0x21..0x21,
41
+ 0x24..0x24,
42
+ 0x26..0x2E,
43
+ 0x30..0x3B,
44
+ 0x3D..0x3D,
45
+ 0x41..0x5B,
46
+ 0x5D..0x5D,
47
+ 0x5F..0x5F,
48
+ 0x61..0x7A,
49
+ 0x7E..0x7E
50
+ ).freeze
51
+ end
52
+
53
+ def url_path
54
+ @url_path ||= from_ranges(
55
+ 0x21..0x21,
56
+ 0x24..0x3A,
57
+ 0x3D..0x3D,
58
+ 0x40..0x5A,
59
+ 0x5F..0x5F,
60
+ 0x61..0x7A,
61
+ 0x7E..0x7E
62
+ ).freeze
63
+ end
64
+
65
+ def url_query
66
+ @url_query ||= from_ranges(
67
+ 0x21..0x21,
68
+ 0x24..0x24,
69
+ 0x26..0x3B,
70
+ 0x3D..0x3D,
71
+ 0x3F..0x5A,
72
+ 0x5F..0x5F,
73
+ 0x61..0x7A,
74
+ 0x7E..0x7E
75
+ ).freeze
76
+ end
77
+
78
+ def whitespace
79
+ @whitespace ||= from_ranges(
80
+ 0x9..0x9,
81
+ 0xA..0xD,
82
+ 0x20..0x20,
83
+ 0x85..0x85,
84
+ 0xA0..0xA0,
85
+ 0x1680..0x1680,
86
+ 0x180E..0x180E,
87
+ 0x2000..0x200A,
88
+ 0x2028..0x2029,
89
+ 0x202F..0x202F,
90
+ 0x205F..0x205F,
91
+ 0x3000..0x3000
92
+ ).freeze
93
+ end
94
+
95
+ def emoji
96
+ @emoji ||= from_ranges(
97
+ 0x23..0x23,
98
+ 0x2A..0x2A,
99
+ 0x30..0x39,
100
+ 0xA9..0xA9,
101
+ 0xAE..0xAE,
102
+ 0x203C..0x203C,
103
+ 0x2049..0x2049,
104
+ 0x2122..0x2122,
105
+ 0x2139..0x2139,
106
+ 0x2194..0x2199,
107
+ 0x21A9..0x21AA,
108
+ 0x231A..0x231B,
109
+ 0x2328..0x2328,
110
+ 0x23CF..0x23CF,
111
+ 0x23E9..0x23F3,
112
+ 0x23F8..0x23FA,
113
+ 0x24C2..0x24C2,
114
+ 0x25AA..0x25AB,
115
+ 0x25B6..0x25B6,
116
+ 0x25C0..0x25C0,
117
+ 0x25FB..0x25FE,
118
+ 0x2600..0x2604,
119
+ 0x260E..0x260E,
120
+ 0x2611..0x2611,
121
+ 0x2614..0x2615,
122
+ 0x2618..0x2618,
123
+ 0x261D..0x261D,
124
+ 0x2620..0x2620,
125
+ 0x2622..0x2623,
126
+ 0x2626..0x2626,
127
+ 0x262A..0x262A,
128
+ 0x262E..0x262F,
129
+ 0x2638..0x263A,
130
+ 0x2640..0x2640,
131
+ 0x2642..0x2642,
132
+ 0x2648..0x2653,
133
+ 0x2660..0x2660,
134
+ 0x2663..0x2663,
135
+ 0x2665..0x2666,
136
+ 0x2668..0x2668,
137
+ 0x267B..0x267B,
138
+ 0x267F..0x267F,
139
+ 0x2692..0x2697,
140
+ 0x2699..0x2699,
141
+ 0x269B..0x269C,
142
+ 0x26A0..0x26A1,
143
+ 0x26AA..0x26AB,
144
+ 0x26B0..0x26B1,
145
+ 0x26BD..0x26BE,
146
+ 0x26C4..0x26C5,
147
+ 0x26C8..0x26C8,
148
+ 0x26CE..0x26CF,
149
+ 0x26D1..0x26D1,
150
+ 0x26D3..0x26D4,
151
+ 0x26E9..0x26EA,
152
+ 0x26F0..0x26F5,
153
+ 0x26F7..0x26FA,
154
+ 0x26FD..0x26FD,
155
+ 0x2702..0x2702,
156
+ 0x2705..0x2705,
157
+ 0x2708..0x270D,
158
+ 0x270F..0x270F,
159
+ 0x2712..0x2712,
160
+ 0x2714..0x2714,
161
+ 0x2716..0x2716,
162
+ 0x271D..0x271D,
163
+ 0x2721..0x2721,
164
+ 0x2728..0x2728,
165
+ 0x2733..0x2734,
166
+ 0x2744..0x2744,
167
+ 0x2747..0x2747,
168
+ 0x274C..0x274C,
169
+ 0x274E..0x274E,
170
+ 0x2753..0x2755,
171
+ 0x2757..0x2757,
172
+ 0x2763..0x2764,
173
+ 0x2795..0x2797,
174
+ 0x27A1..0x27A1,
175
+ 0x27B0..0x27B0,
176
+ 0x27BF..0x27BF,
177
+ 0x2934..0x2935,
178
+ 0x2B05..0x2B07,
179
+ 0x2B1B..0x2B1C,
180
+ 0x2B50..0x2B50,
181
+ 0x2B55..0x2B55,
182
+ 0x3030..0x3030,
183
+ 0x303D..0x303D,
184
+ 0x3297..0x3297,
185
+ 0x3299..0x3299,
186
+ 0x1F004..0x1F004,
187
+ 0x1F0CF..0x1F0CF,
188
+ 0x1F170..0x1F171,
189
+ 0x1F17E..0x1F17F,
190
+ 0x1F18E..0x1F18E,
191
+ 0x1F191..0x1F19A,
192
+ 0x1F1E6..0x1F1FF,
193
+ 0x1F201..0x1F202,
194
+ 0x1F21A..0x1F21A,
195
+ 0x1F22F..0x1F22F,
196
+ 0x1F232..0x1F23A,
197
+ 0x1F250..0x1F251,
198
+ 0x1F300..0x1F321,
199
+ 0x1F324..0x1F393,
200
+ 0x1F396..0x1F397,
201
+ 0x1F399..0x1F39B,
202
+ 0x1F39E..0x1F3F0,
203
+ 0x1F3F3..0x1F3F5,
204
+ 0x1F3F7..0x1F4FD,
205
+ 0x1F4FF..0x1F53D,
206
+ 0x1F549..0x1F54E,
207
+ 0x1F550..0x1F567,
208
+ 0x1F56F..0x1F570,
209
+ 0x1F573..0x1F57A,
210
+ 0x1F587..0x1F587,
211
+ 0x1F58A..0x1F58D,
212
+ 0x1F590..0x1F590,
213
+ 0x1F595..0x1F596,
214
+ 0x1F5A4..0x1F5A5,
215
+ 0x1F5A8..0x1F5A8,
216
+ 0x1F5B1..0x1F5B2,
217
+ 0x1F5BC..0x1F5BC,
218
+ 0x1F5C2..0x1F5C4,
219
+ 0x1F5D1..0x1F5D3,
220
+ 0x1F5DC..0x1F5DE,
221
+ 0x1F5E1..0x1F5E1,
222
+ 0x1F5E3..0x1F5E3,
223
+ 0x1F5E8..0x1F5E8,
224
+ 0x1F5EF..0x1F5EF,
225
+ 0x1F5F3..0x1F5F3,
226
+ 0x1F5FA..0x1F64F,
227
+ 0x1F680..0x1F6C5,
228
+ 0x1F6CB..0x1F6D2,
229
+ 0x1F6E0..0x1F6E5,
230
+ 0x1F6E9..0x1F6E9,
231
+ 0x1F6EB..0x1F6EC,
232
+ 0x1F6F0..0x1F6F0,
233
+ 0x1F6F3..0x1F6F8,
234
+ 0x1F910..0x1F93A,
235
+ 0x1F93C..0x1F93E,
236
+ 0x1F940..0x1F945,
237
+ 0x1F947..0x1F94C,
238
+ 0x1F950..0x1F96B,
239
+ 0x1F980..0x1F997,
240
+ 0x1F9C0..0x1F9C0,
241
+ 0x1F9D0..0x1F9E6
242
+ ).freeze
243
+ end
244
+
245
+ def respond_to_missing?(method_name, include_private = false)
246
+ (base = method_name[/^non_(.*)/, 1]) && respond_to?(base) || super
247
+ end
248
+
249
+ def method_missing(method_name, *args, &block)
250
+ if (base = method_name[/^non_(.*)/, 1])
251
+ ivar_name = "@#{method_name}"
252
+ return instance_variable_get(ivar_name) ||
253
+ instance_variable_set(ivar_name, send(base).inversion.freeze)
254
+ end
255
+ super
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,11 @@
1
+ class CharacterSet
2
+ module CoreExt
3
+ module RegexpExt
4
+ def character_set
5
+ CharacterSet.of_regexp(self)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ ::Regexp.send(:include, CharacterSet::CoreExt::RegexpExt)
@@ -0,0 +1,35 @@
1
+ class CharacterSet
2
+ module CoreExt
3
+ module StringExt
4
+ def character_set
5
+ CharacterSet.of(self)
6
+ end
7
+
8
+ def covered_by_character_set?(set)
9
+ set.cover?(self)
10
+ end
11
+
12
+ def uses_character_set?(set)
13
+ set.used_by?(self)
14
+ end
15
+
16
+ def delete_character_set(set)
17
+ set.delete_in(self)
18
+ end
19
+
20
+ def delete_character_set!(set)
21
+ set.delete_in!(self)
22
+ end
23
+
24
+ def keep_character_set(set)
25
+ set.keep_in(self)
26
+ end
27
+
28
+ def keep_character_set!(set)
29
+ set.keep_in!(self)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ ::String.send(:include, CharacterSet::CoreExt::StringExt)
@@ -0,0 +1,3 @@
1
+ require 'character_set'
2
+ require 'character_set/core_ext/regexp_ext'
3
+ require 'character_set/core_ext/string_ext'
@@ -0,0 +1,106 @@
1
+ class CharacterSet
2
+ module ExpressionConverter
3
+ module_function
4
+
5
+ Error = Class.new(ArgumentError)
6
+
7
+ def convert(expression)
8
+ @regexp_parser_required ||= require 'regexp_parser'
9
+
10
+ case expression
11
+ when Regexp::Expression::Root
12
+ if expression.count != 1
13
+ raise Error, 'Pass a Regexp with exactly one expression, e.g. /[a-z]/'
14
+ end
15
+ convert(expression[0])
16
+
17
+ when Regexp::Expression::CharacterSet
18
+ content = expression.map { |subexp| convert(subexp) }.reduce(:+)
19
+ expression.negative? ? content.inversion : content
20
+
21
+ when Regexp::Expression::CharacterSet::Intersection
22
+ expression.map { |subexp| convert(subexp) }.reduce(:&)
23
+
24
+ when Regexp::Expression::CharacterSet::IntersectedSequence
25
+ expression.map { |subexp| convert(subexp) }.reduce(:+)
26
+
27
+ when Regexp::Expression::CharacterSet::Range
28
+ start, finish = expression.map { |subexp| convert(subexp) }
29
+ CharacterSet.from_ranges((start.min)..(finish.max))
30
+
31
+ when Regexp::Expression::CharacterType::Any
32
+ CharacterSet.unicode
33
+
34
+ when Regexp::Expression::CharacterType::Digit
35
+ CharacterSet.from_ranges(48..57)
36
+
37
+ when Regexp::Expression::CharacterType::NonDigit
38
+ CharacterSet.from_ranges(48..57).inversion
39
+
40
+ when Regexp::Expression::CharacterType::Hex
41
+ CharacterSet.from_ranges(48..57, 65..70, 97..102)
42
+
43
+ when Regexp::Expression::CharacterType::NonHex
44
+ CharacterSet.from_ranges(48..57, 65..70, 97..102).inversion
45
+
46
+ when Regexp::Expression::CharacterType::Space
47
+ CharacterSet["\t", "\n", "\v", "\f", "\r", "\x20"]
48
+
49
+ when Regexp::Expression::CharacterType::NonSpace
50
+ CharacterSet["\t", "\n", "\v", "\f", "\r", "\x20"].inversion
51
+
52
+ when Regexp::Expression::CharacterType::Word
53
+ CharacterSet.from_ranges(48..57, 65..90, 95..95, 97..122)
54
+
55
+ when Regexp::Expression::CharacterType::NonWord
56
+ CharacterSet.from_ranges(48..57, 65..90, 95..95, 97..122).inversion
57
+
58
+ when Regexp::Expression::EscapeSequence::CodepointList
59
+ CharacterSet.new(expression.codepoints)
60
+
61
+ when Regexp::Expression::EscapeSequence::Base
62
+ CharacterSet[expression.codepoint]
63
+
64
+ when Regexp::Expression::Group::Capture,
65
+ Regexp::Expression::Group::Passive,
66
+ Regexp::Expression::Group::Named,
67
+ Regexp::Expression::Group::Atomic,
68
+ Regexp::Expression::Group::Options
69
+ case expression.count
70
+ when 0 then CharacterSet[]
71
+ when 1 then convert(expression.first)
72
+ else
73
+ raise Error, 'Groups must contain exactly one expression, e.g. ([a-z])'
74
+ end
75
+
76
+ when Regexp::Expression::Alternation
77
+ expression.map { |subexp| convert(subexp) }.reduce(:+)
78
+
79
+ when Regexp::Expression::Alternative
80
+ case expression.count
81
+ when 0 then CharacterSet[]
82
+ when 1 then convert(expression.first)
83
+ else
84
+ raise Error, 'Alternatives must contain exactly one expression'
85
+ end
86
+
87
+ when Regexp::Expression::Literal
88
+ if expression.set_level == 0 && expression.text.size != 1
89
+ raise Error, 'Literal runs outside of sets are codepoint *sequences*'
90
+ end
91
+ CharacterSet[expression.text.ord]
92
+
93
+ when Regexp::Expression::UnicodeProperty::Base,
94
+ Regexp::Expression::PosixClass
95
+ content = CharacterSet.of_property(expression.token)
96
+ expression.negative? ? content.inversion : content
97
+
98
+ when Regexp::Expression::Base
99
+ raise Error, "Unsupported expression class `#{expression.class}`"
100
+
101
+ else
102
+ raise Error, "Pass an expression (result of Regexp::Parser.parse)"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,48 @@
1
+ class CharacterSet
2
+ module Parser
3
+ module_function
4
+
5
+ def codepoints_from_enumerable(object)
6
+ raise ArgumentError, 'pass an Enumerable' unless object.respond_to?(:each)
7
+ # Use #each to check first element (only this works for all Enumerables)
8
+ object.each do |e|
9
+ return object if e.is_a?(Integer) && e >= 0 && e < 0x110000
10
+ return object.map(&:ord) if e.is_a?(String) && e.length == 1
11
+ raise ArgumentError, "#{e.inspect} is not valid as a codepoint"
12
+ end
13
+ end
14
+
15
+ def codepoints_from_bracket_expression(string)
16
+ raise ArgumentError, 'pass a String' unless string.is_a?(String)
17
+ raise ArgumentError, 'advanced syntax' if string =~ /\\[^uUx]|[^\\]\[|&&/
18
+
19
+ content = strip_brackets(string)
20
+ literal_content = eval_escapes(content)
21
+
22
+ prev_chr = nil
23
+ in_range = false
24
+
25
+ literal_content.each_char.map do |chr|
26
+ if chr == '-' && prev_chr && prev_chr != '\\' && prev_chr != '-'
27
+ in_range = true
28
+ nil
29
+ else
30
+ result = in_range ? ((prev_chr.ord + 1)..(chr.ord)).to_a : chr.ord
31
+ in_range = false
32
+ prev_chr = chr
33
+ result
34
+ end
35
+ end.compact.flatten
36
+ end
37
+
38
+ def strip_brackets(string)
39
+ string[/\A\[\^?(.*)\]\z/, 1] || string.dup
40
+ end
41
+
42
+ def eval_escapes(string)
43
+ string.gsub(/\\U(\h{8})|\\u(\h{4})|U\+(\h+)|\\x(\h{2})|\\u\{(\h+)\}/) do
44
+ ($1 || $2 || $3 || $4 || $5).to_i(16).chr('utf-8')
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ require 'character_set'
2
+ require 'character_set/ruby_fallback'
3
+
4
+ # CharacterSet::Pure uses only Ruby implementations.
5
+ # It is equal to CharacterSet if the C ext can't be loaded.
6
+ class CharacterSet
7
+ class Pure
8
+ prepend CharacterSet::RubyFallback
9
+ prepend CharacterSet::SetMethodAdapters
10
+ include CharacterSet::SharedMethods
11
+ extend CharacterSet::CommonSets
12
+ end
13
+ end
@@ -0,0 +1,83 @@
1
+ class CharacterSet
2
+ module RubyFallback
3
+ module CharacterSetMethods
4
+ module ClassMethods
5
+ def from_ranges(*ranges)
6
+ new(Array(ranges).flat_map(&:to_a))
7
+ end
8
+
9
+ def of(string)
10
+ raise ArgumentError, 'pass a String' unless string.is_a?(String)
11
+ new(string.codepoints)
12
+ end
13
+ end
14
+
15
+ def inversion(include_surrogates: false, upto: 0x10FFFF)
16
+ new_set = self.class.new
17
+ 0.upto(upto) do |cp|
18
+ next unless include_surrogates || cp > 0xDFFF || cp < 0xD800
19
+ new_set << cp unless include?(cp)
20
+ end
21
+ new_set
22
+ end
23
+
24
+ def case_insensitive
25
+ new_set = dup
26
+ each do |cp|
27
+ swapped_cps = cp.chr('utf-8').swapcase.codepoints
28
+ swapped_cps.size == 1 && new_set << swapped_cps[0]
29
+ end
30
+ new_set
31
+ end
32
+
33
+ def ranges
34
+ @range_compressor_required ||= require 'range_compressor'
35
+ RangeCompressor.compress(self)
36
+ end
37
+
38
+ def sample(count = nil)
39
+ count.nil? ? to_a(true).sample : to_a(true).sample(count)
40
+ end
41
+
42
+ def used_by?(string)
43
+ str!(string).each_codepoint { |cp| return true if include?(cp) }
44
+ false
45
+ end
46
+
47
+ def cover?(string)
48
+ str!(string).each_codepoint { |cp| return false unless include?(cp) }
49
+ true
50
+ end
51
+
52
+ def delete_in(string)
53
+ make_new_str(string) { |cp, new_str| include?(cp) || (new_str << cp) }
54
+ end
55
+
56
+ def delete_in!(string)
57
+ result = delete_in(string)
58
+ result.size == string.size ? nil : string.replace(result)
59
+ end
60
+
61
+ def keep_in(string)
62
+ make_new_str(string) { |cp, new_str| include?(cp) && (new_str << cp) }
63
+ end
64
+
65
+ def keep_in!(string)
66
+ result = keep_in(string)
67
+ result.size == string.size ? nil : string.replace(result)
68
+ end
69
+
70
+ private
71
+
72
+ def str!(obj)
73
+ raise ArgumentError, 'pass a String' unless obj.respond_to?(:codepoints)
74
+ obj
75
+ end
76
+
77
+ def make_new_str(original, &block)
78
+ new_string = str!(original).each_codepoint.each_with_object('', &block)
79
+ original.tainted? ? new_string.taint : new_string
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,27 @@
1
+ class CharacterSet
2
+ module RubyFallback
3
+ module PlaneMethods
4
+ def bmp_part
5
+ dup.keep_if { |cp| cp < 0x10000 }
6
+ end
7
+
8
+ def astral_part
9
+ dup.keep_if { |cp| cp >= 0x10000 }
10
+ end
11
+
12
+ def planes
13
+ plane_set = {}
14
+ plane_size = 0x10000.to_f
15
+ each do |cp|
16
+ plane = (cp / plane_size).floor
17
+ plane_set[plane] = true
18
+ end
19
+ plane_set.keys
20
+ end
21
+
22
+ def member_in_plane?(num)
23
+ ((num * 0x10000)...((num + 1) * 0x10000)).any? { |cp| include?(cp) }
24
+ end
25
+ end
26
+ end
27
+ end