net-imap 0.4.7 → 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,64 +4,1346 @@ module Net
4
4
  class IMAP
5
5
 
6
6
  ##
7
- # An IMAP {sequence
8
- # set}[https://www.rfc-editor.org/rfc/rfc9051.html#section-4.1.1],
9
- # is a set of message sequence numbers or unique identifier numbers
10
- # ("UIDs"). It contains numbers and ranges of numbers. The numbers are all
11
- # non-zero unsigned 32-bit integers and one special value, <tt>*</tt>, that
12
- # represents the largest value in the mailbox.
13
- #
14
- # *NOTE:* This SequenceSet class is currently a placeholder for unhandled
15
- # extension data. All it does now is validate. It will be expanded to a
16
- # full API in a future release.
7
+ # An \IMAP sequence set is a set of message sequence numbers or unique
8
+ # identifier numbers ("UIDs"). It contains numbers and ranges of numbers.
9
+ # The numbers are all non-zero unsigned 32-bit integers and one special
10
+ # value (<tt>"*"</tt>) that represents the largest value in the mailbox.
11
+ #
12
+ # Certain types of \IMAP responses will contain a SequenceSet, for example
13
+ # the data for a <tt>"MODIFIED"</tt> ResponseCode. Some \IMAP commands may
14
+ # receive a SequenceSet as an argument, for example IMAP#search, IMAP#fetch,
15
+ # and IMAP#store.
16
+ #
17
+ # == EXPERIMENTAL API
18
+ #
19
+ # SequenceSet is currently experimental. Only two methods, ::[] and
20
+ # #valid_string, are considered stable. Although the API isn't expected to
21
+ # change much, any other methods may be removed or changed without
22
+ # deprecation.
23
+ #
24
+ # == Creating sequence sets
25
+ #
26
+ # SequenceSet.new with no arguments creates an empty sequence set. Note
27
+ # that an empty sequence set is invalid in the \IMAP grammar.
28
+ #
29
+ # set = Net::IMAP::SequenceSet.new
30
+ # set.empty? #=> true
31
+ # set.valid? #=> false
32
+ # set.valid_string #!> raises DataFormatError
33
+ # set << 1..10
34
+ # set.empty? #=> false
35
+ # set.valid? #=> true
36
+ # set.valid_string #=> "1:10"
37
+ #
38
+ # SequenceSet.new may receive a single optional argument: a non-zero 32 bit
39
+ # unsigned integer, a range, a <tt>sequence-set</tt> formatted string,
40
+ # another sequence set, or an enumerable containing any of these.
41
+ #
42
+ # set = Net::IMAP::SequenceSet.new(1)
43
+ # set.valid_string #=> "1"
44
+ # set = Net::IMAP::SequenceSet.new(1..100)
45
+ # set.valid_string #=> "1:100"
46
+ # set = Net::IMAP::SequenceSet.new(1...100)
47
+ # set.valid_string #=> "1:99"
48
+ # set = Net::IMAP::SequenceSet.new([1, 2, 5..])
49
+ # set.valid_string #=> "1:2,5:*"
50
+ # set = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
51
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
52
+ # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
53
+ # set.valid_string #=> "1:10,55,1024:2048"
54
+ #
55
+ # Use ::[] with one or more arguments to create a frozen SequenceSet. An
56
+ # invalid (empty) set cannot be created with ::[].
57
+ #
58
+ # set = Net::IMAP::SequenceSet["1,2,3:7,5,6:10,2048,1024"]
59
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
60
+ # set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
61
+ # set.valid_string #=> "1:10,55,1024:2048"
62
+ #
63
+ # == Using <tt>*</tt>
64
+ #
65
+ # \IMAP sequence sets may contain a special value <tt>"*"</tt>, which
66
+ # represents the largest number in use. From +seq-number+ in
67
+ # {RFC9051 §9}[https://www.rfc-editor.org/rfc/rfc9051.html#section-9-5]:
68
+ # >>>
69
+ # In the case of message sequence numbers, it is the number of messages
70
+ # in a non-empty mailbox. In the case of unique identifiers, it is the
71
+ # unique identifier of the last message in the mailbox or, if the
72
+ # mailbox is empty, the mailbox's current UIDNEXT value.
73
+ #
74
+ # When creating a SequenceSet, <tt>*</tt> may be input as <tt>-1</tt>,
75
+ # <tt>"*"</tt>, <tt>:*</tt>, an endless range, or a range ending in
76
+ # <tt>-1</tt>. When converting to #elements, #ranges, or #numbers, it will
77
+ # output as either <tt>:*</tt> or an endless range. For example:
78
+ #
79
+ # Net::IMAP::SequenceSet["1,3,*"].to_a #=> [1, 3, :*]
80
+ # Net::IMAP::SequenceSet["1,234:*"].to_a #=> [1, 234..]
81
+ # Net::IMAP::SequenceSet[1234..-1].to_a #=> [1234..]
82
+ # Net::IMAP::SequenceSet[1234..].to_a #=> [1234..]
83
+ #
84
+ # Net::IMAP::SequenceSet[1234..].to_s #=> "1234:*"
85
+ # Net::IMAP::SequenceSet[1234..-1].to_s #=> "1234:*"
86
+ #
87
+ # Use #limit to convert <tt>"*"</tt> to a maximum value. When a range
88
+ # includes <tt>"*"</tt>, the maximum value will always be matched:
89
+ #
90
+ # Net::IMAP::SequenceSet["9999:*"].limit(max: 25)
91
+ # #=> Net::IMAP::SequenceSet["25"]
92
+ #
93
+ # === Surprising <tt>*</tt> behavior
94
+ #
95
+ # When a set includes <tt>*</tt>, some methods may have surprising behavior.
96
+ #
97
+ # For example, #complement treats <tt>*</tt> as its own number. This way,
98
+ # the #intersection of a set and its #complement will always be empty.
99
+ # This is not how an \IMAP server interprets the set: it will convert
100
+ # <tt>*</tt> to either the number of messages in the mailbox or +UIDNEXT+,
101
+ # as appropriate. And there _will_ be overlap between a set and its
102
+ # complement after #limit is applied to each:
103
+ #
104
+ # ~Net::IMAP::SequenceSet["*"] == Net::IMAP::SequenceSet[1..(2**32-1)]
105
+ # ~Net::IMAP::SequenceSet[1..5] == Net::IMAP::SequenceSet["6:*"]
106
+ #
107
+ # set = Net::IMAP::SequenceSet[1..5]
108
+ # (set & ~set).empty? => true
109
+ #
110
+ # (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4]
111
+ #
112
+ # When counting the number of numbers in a set, <tt>*</tt> will be counted
113
+ # _except_ when UINT32_MAX is also in the set:
114
+ # UINT32_MAX = 2**32 - 1
115
+ # Net::IMAP::SequenceSet["*"].count => 1
116
+ # Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX
117
+ #
118
+ # Net::IMAP::SequenceSet["1:*"].count => UINT32_MAX
119
+ # Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
120
+ # Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
121
+ #
122
+ # == What's here?
123
+ #
124
+ # SequenceSet provides methods for:
125
+ # * {Creating a SequenceSet}[rdoc-ref:SequenceSet@Methods+for+Creating+a+SequenceSet]
126
+ # * {Comparing}[rdoc-ref:SequenceSet@Methods+for+Comparing]
127
+ # * {Querying}[rdoc-ref:SequenceSet@Methods+for+Querying]
128
+ # * {Iterating}[rdoc-ref:SequenceSet@Methods+for+Iterating]
129
+ # * {Set Operations}[rdoc-ref:SequenceSet@Methods+for+Set+Operations]
130
+ # * {Assigning}[rdoc-ref:SequenceSet@Methods+for+Assigning]
131
+ # * {Deleting}[rdoc-ref:SequenceSet@Methods+for+Deleting]
132
+ # * {IMAP String Formatting}[rdoc-ref:SequenceSet@Methods+for+IMAP+String+Formatting]
133
+ #
134
+ # === Methods for Creating a \SequenceSet
135
+ # * ::[]: Creates a validated frozen sequence set from one or more inputs.
136
+ # * ::new: Creates a new mutable sequence set, which may be empty (invalid).
137
+ # * ::try_convert: Calls +to_sequence_set+ on an object and verifies that
138
+ # the result is a SequenceSet.
139
+ # * ::empty: Returns a frozen empty (invalid) SequenceSet.
140
+ # * ::full: Returns a frozen SequenceSet containing every possible number.
141
+ #
142
+ # === Methods for Comparing
143
+ #
144
+ # <i>Comparison to another \SequenceSet:</i>
145
+ # - #==: Returns whether a given set contains the same numbers as +self+.
146
+ # - #eql?: Returns whether a given set uses the same #string as +self+.
147
+ #
148
+ # <i>Comparison to objects which are convertible to \SequenceSet:</i>
149
+ # - #===:
150
+ # Returns whether a given object is fully contained within +self+, or
151
+ # +nil+ if the object cannot be converted to a compatible type.
152
+ # - #cover? (aliased as #===):
153
+ # Returns whether a given object is fully contained within +self+.
154
+ # - #intersect?:
155
+ # Returns whether +self+ and a given object have any common elements.
156
+ # - #disjoint?:
157
+ # Returns whether +self+ and a given object have no common elements.
158
+ #
159
+ # === Methods for Querying
160
+ # These methods do not modify +self+.
161
+ #
162
+ # <i>Set membership:</i>
163
+ # - #include? (aliased as #member?):
164
+ # Returns whether a given object (nz-number, range, or <tt>*</tt>) is
165
+ # contained by the set.
166
+ # - #include_star?: Returns whether the set contains <tt>*</tt>.
167
+ #
168
+ # <i>Minimum and maximum value elements:</i>
169
+ # - #min: Returns the minimum number in the set.
170
+ # - #max: Returns the maximum number in the set.
171
+ # - #minmax: Returns the minimum and maximum numbers in the set.
172
+ #
173
+ # <i>Accessing value by offset:</i>
174
+ # - #[] (aliased as #slice): Returns the number or consecutive subset at a
175
+ # given offset or range of offsets.
176
+ # - #at: Returns the number at a given offset.
177
+ # - #find_index: Returns the given number's offset in the set
178
+ #
179
+ # <i>Set cardinality:</i>
180
+ # - #count (aliased as #size): Returns the count of numbers in the set.
181
+ # - #empty?: Returns whether the set has no members. \IMAP syntax does not
182
+ # allow empty sequence sets.
183
+ # - #valid?: Returns whether the set has any members.
184
+ # - #full?: Returns whether the set contains every possible value, including
185
+ # <tt>*</tt>.
186
+ #
187
+ # === Methods for Iterating
188
+ #
189
+ # - #each_element: Yields each number and range in the set and returns
190
+ # +self+.
191
+ # - #elements (aliased as #to_a):
192
+ # Returns an Array of every number and range in the set.
193
+ # - #each_range:
194
+ # Yields each element in the set as a Range and returns +self+.
195
+ # - #ranges: Returns an Array of every element in the set, converting
196
+ # numbers into ranges of a single value.
197
+ # - #each_number: Yields each number in the set and returns +self+.
198
+ # - #numbers: Returns an Array with every number in the set, expanding
199
+ # ranges into all of their contained numbers.
200
+ # - #to_set: Returns a Set containing all of the #numbers in the set.
201
+ #
202
+ # === Methods for \Set Operations
203
+ # These methods do not modify +self+.
204
+ #
205
+ # - #| (aliased as #union and #+): Returns a new set combining all members
206
+ # from +self+ with all members from the other object.
207
+ # - #& (aliased as #intersection): Returns a new set containing all members
208
+ # common to +self+ and the other object.
209
+ # - #- (aliased as #difference): Returns a copy of +self+ with all members
210
+ # in the other object removed.
211
+ # - #^ (aliased as #xor): Returns a new set containing all members from
212
+ # +self+ and the other object except those common to both.
213
+ # - #~ (aliased as #complement): Returns a new set containing all members
214
+ # that are not in +self+
215
+ # - #limit: Returns a copy of +self+ which has replaced <tt>*</tt> with a
216
+ # given maximum value and removed all members over that maximum.
217
+ #
218
+ # === Methods for Assigning
219
+ # These methods add or replace elements in +self+.
220
+ #
221
+ # - #add (aliased as #<<): Adds a given object to the set; returns +self+.
222
+ # - #add?: If the given object is not an element in the set, adds it and
223
+ # returns +self+; otherwise, returns +nil+.
224
+ # - #merge: Merges multiple elements into the set; returns +self+.
225
+ # - #string=: Assigns a new #string value and replaces #elements to match.
226
+ # - #replace: Replaces the contents of the set with the contents
227
+ # of a given object.
228
+ # - #complement!: Replaces the contents of the set with its own #complement.
229
+ #
230
+ # === Methods for Deleting
231
+ # These methods remove elements from +self+.
232
+ #
233
+ # - #clear: Removes all elements in the set; returns +self+.
234
+ # - #delete: Removes a given object from the set; returns +self+.
235
+ # - #delete?: If the given object is an element in the set, removes it and
236
+ # returns it; otherwise, returns +nil+.
237
+ # - #delete_at: Removes the number at a given offset.
238
+ # - #slice!: Removes the number or consecutive numbers at a given offset or
239
+ # range of offsets.
240
+ # - #subtract: Removes each given object from the set; returns +self+.
241
+ # - #limit!: Replaces <tt>*</tt> with a given maximum value and removes all
242
+ # members over that maximum; returns +self+.
243
+ #
244
+ # === Methods for \IMAP String Formatting
245
+ #
246
+ # - #to_s: Returns the +sequence-set+ string, or an empty string when the
247
+ # set is empty.
248
+ # - #string: Returns the +sequence-set+ string, or nil when empty.
249
+ # - #valid_string: Returns the +sequence-set+ string, or raises
250
+ # DataFormatError when the set is empty.
251
+ # - #normalized_string: Returns a <tt>sequence-set</tt> string with its
252
+ # elements sorted and coalesced, or nil when the set is empty.
253
+ # - #normalize: Returns a new set with this set's normalized +sequence-set+
254
+ # representation.
255
+ # - #normalize!: Updates #string to its normalized +sequence-set+
256
+ # representation and returns +self+.
257
+ #
17
258
  class SequenceSet
259
+ # The largest possible non-zero unsigned 32-bit integer
260
+ UINT32_MAX = 2**32 - 1
261
+
262
+ # represents "*" internally, to simplify sorting (etc)
263
+ STAR_INT = UINT32_MAX + 1
264
+ private_constant :STAR_INT
265
+
266
+ # valid inputs for "*"
267
+ STARS = [:*, ?*, -1].freeze
268
+ private_constant :STAR_INT, :STARS
269
+
270
+ COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
271
+ ENUMABLE = ->{ _1.respond_to?(:each) && _1.respond_to?(:empty?) }
272
+ private_constant :COERCIBLE, :ENUMABLE
273
+
274
+ class << self
275
+
276
+ # :call-seq:
277
+ # SequenceSet[*values] -> valid frozen sequence set
278
+ #
279
+ # Returns a frozen SequenceSet, constructed from +values+.
280
+ #
281
+ # An empty SequenceSet is invalid and will raise a DataFormatError.
282
+ #
283
+ # Use ::new to create a mutable or empty SequenceSet.
284
+ def [](first, *rest)
285
+ if rest.empty?
286
+ if first.is_a?(SequenceSet) && set.frozen? && set.valid?
287
+ first
288
+ else
289
+ new(first).validate.freeze
290
+ end
291
+ else
292
+ new(first).merge(*rest).validate.freeze
293
+ end
294
+ end
295
+
296
+ # :call-seq:
297
+ # SequenceSet.try_convert(obj) -> sequence set or nil
298
+ #
299
+ # If +obj+ is a SequenceSet, returns +obj+. If +obj+ responds_to
300
+ # +to_sequence_set+, calls +obj.to_sequence_set+ and returns the result.
301
+ # Otherwise returns +nil+.
302
+ #
303
+ # If +obj.to_sequence_set+ doesn't return a SequenceSet, an exception is
304
+ # raised.
305
+ def try_convert(obj)
306
+ return obj if obj.is_a?(SequenceSet)
307
+ return nil unless respond_to?(:to_sequence_set)
308
+ obj = obj.to_sequence_set
309
+ return obj if obj.is_a?(SequenceSet)
310
+ raise DataFormatError, "invalid object returned from to_sequence_set"
311
+ end
312
+
313
+ # Returns a frozen empty set singleton. Note that valid \IMAP sequence
314
+ # sets cannot be empty, so this set is _invalid_.
315
+ def empty; EMPTY end
316
+
317
+ # Returns a frozen full set singleton: <tt>"1:*"</tt>
318
+ def full; FULL end
319
+
320
+ end
321
+
322
+ # Create a new SequenceSet object from +input+, which may be another
323
+ # SequenceSet, an IMAP formatted +sequence-set+ string, a number, a
324
+ # range, <tt>:*</tt>, or an enumerable of these.
325
+ #
326
+ # Use ::[] to create a frozen (non-empty) SequenceSet.
327
+ def initialize(input = nil) input ? replace(input) : clear end
328
+
329
+ # Removes all elements and returns self.
330
+ def clear; @tuples, @string = [], nil; self end
18
331
 
19
- def self.[](str) new(str).freeze end
332
+ # Replace the contents of the set with the contents of +other+ and returns
333
+ # +self+.
334
+ #
335
+ # +other+ may be another SequenceSet, or it may be an IMAP +sequence-set+
336
+ # string, a number, a range, <tt>*</tt>, or an enumerable of these.
337
+ def replace(other)
338
+ case other
339
+ when SequenceSet then initialize_dup(other)
340
+ when String then self.string = other
341
+ else clear; merge other
342
+ end
343
+ self
344
+ end
345
+
346
+ # Returns the \IMAP +sequence-set+ string representation, or raises a
347
+ # DataFormatError when the set is empty.
348
+ #
349
+ # Use #string to return +nil+ or #to_s to return an empty string without
350
+ # error.
351
+ #
352
+ # Related: #string, #normalized_string, #to_s
353
+ def valid_string
354
+ raise DataFormatError, "empty sequence-set" if empty?
355
+ string
356
+ end
357
+
358
+ # Returns the \IMAP +sequence-set+ string representation, or +nil+ when
359
+ # the set is empty. Note that an empty set is invalid in the \IMAP
360
+ # syntax.
361
+ #
362
+ # Use #valid_string to raise an exception when the set is empty, or #to_s
363
+ # to return an empty string.
364
+ #
365
+ # If the set was created from a single string, it is not normalized. If
366
+ # the set is updated the string will be normalized.
367
+ #
368
+ # Related: #valid_string, #normalized_string, #to_s
369
+ def string; @string ||= normalized_string if valid? end
20
370
 
21
- def initialize(input)
22
- @atom = -String.try_convert(input)
23
- validate
371
+ # Assigns a new string to #string and resets #elements to match. It
372
+ # cannot be set to an empty string—assign +nil+ or use #clear instead.
373
+ # The string is validated but not normalized.
374
+ #
375
+ # Use #add or #merge to add a string to an existing set.
376
+ #
377
+ # Related: #replace, #clear
378
+ def string=(str)
379
+ if str.nil?
380
+ clear
381
+ else
382
+ str = String.try_convert(str) or raise ArgumentError, "not a string"
383
+ tuples = str_to_tuples str
384
+ @tuples, @string = [], -str
385
+ tuples_add tuples
386
+ end
24
387
  end
25
388
 
26
- # Returns the IMAP string representation. In the IMAP grammar,
27
- # +sequence-set+ is a subset of +atom+ which is a subset of +astring+.
28
- attr_accessor :atom
389
+ # Returns the \IMAP +sequence-set+ string representation, or an empty
390
+ # string when the set is empty. Note that an empty set is invalid in the
391
+ # \IMAP syntax.
392
+ #
393
+ # Related: #valid_string, #normalized_string, #to_s
394
+ def to_s; string || "" end
29
395
 
30
- # Returns #atom. In the IMAP grammar, +atom+ is a subset of +astring+.
31
- alias astring atom
396
+ # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
397
+ def freeze
398
+ return self if frozen?
399
+ string
400
+ @tuples.each(&:freeze).freeze
401
+ super
402
+ end
32
403
 
33
- # Returns the value of #atom
34
- alias to_s atom
404
+ # :call-seq: self == other -> true or false
405
+ #
406
+ # Returns true when the other SequenceSet represents the same message
407
+ # identifiers. Encoding difference—such as order, overlaps, or
408
+ # duplicates—are ignored.
409
+ #
410
+ # Net::IMAP::SequenceSet["1:3"] == Net::IMAP::SequenceSet["1:3"]
411
+ # #=> true
412
+ # Net::IMAP::SequenceSet["1,2,3"] == Net::IMAP::SequenceSet["1:3"]
413
+ # #=> true
414
+ # Net::IMAP::SequenceSet["1,3"] == Net::IMAP::SequenceSet["3,1"]
415
+ # #=> true
416
+ # Net::IMAP::SequenceSet["9,1:*"] == Net::IMAP::SequenceSet["1:*"]
417
+ # #=> true
418
+ #
419
+ # Related: #eql?, #normalize
420
+ def ==(other)
421
+ self.class == other.class &&
422
+ (to_s == other.to_s || tuples == other.tuples)
423
+ end
35
424
 
36
- # Hash equality requires the same encoded #atom representation.
425
+ # :call-seq: eql?(other) -> true or false
37
426
  #
38
- # Net::IMAP::SequenceSet["1:3"] .eql? Net::IMAP::SequenceSet["1:3"] # => true
39
- # Net::IMAP::SequenceSet["1,2,3"].eql? Net::IMAP::SequenceSet["1:3"] # => false
40
- # Net::IMAP::SequenceSet["1,3"] .eql? Net::IMAP::SequenceSet["3,1"] # => false
41
- # Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"] # => false
427
+ # Hash equality requires the same encoded #string representation.
42
428
  #
43
- def eql?(other) self.class == other.class && atom == other.atom end
44
- alias == eql?
429
+ # Net::IMAP::SequenceSet["1:3"] .eql? Net::IMAP::SequenceSet["1:3"]
430
+ # #=> true
431
+ # Net::IMAP::SequenceSet["1,2,3"].eql? Net::IMAP::SequenceSet["1:3"]
432
+ # #=> false
433
+ # Net::IMAP::SequenceSet["1,3"] .eql? Net::IMAP::SequenceSet["3,1"]
434
+ # #=> false
435
+ # Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"]
436
+ # #=> false
437
+ #
438
+ # Related: #==, #normalize
439
+ def eql?(other) self.class == other.class && string == other.string end
45
440
 
46
441
  # See #eql?
47
- def hash; [self.class. atom].hash end
442
+ def hash; [self.class, string].hash end
443
+
444
+ # :call-seq: self === other -> true | false | nil
445
+ #
446
+ # Returns whether +other+ is contained within the set. Returns +nil+ if a
447
+ # StandardError is raised while converting +other+ to a comparable type.
448
+ #
449
+ # Related: #cover?, #include?, #include_star?
450
+ def ===(other)
451
+ cover?(other)
452
+ rescue
453
+ nil
454
+ end
455
+
456
+ # :call-seq: cover?(other) -> true | false | nil
457
+ #
458
+ # Returns whether +other+ is contained within the set. +other+ may be any
459
+ # object that would be accepted by ::new.
460
+ #
461
+ # Related: #===, #include?, #include_star?
462
+ def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
463
+
464
+ # Returns +true+ when a given number or range is in +self+, and +false+
465
+ # otherwise. Returns +false+ unless +number+ is an Integer, Range, or
466
+ # <tt>*</tt>.
467
+ #
468
+ # set = Net::IMAP::SequenceSet["5:10,100,111:115"]
469
+ # set.include? 1 #=> false
470
+ # set.include? 5..10 #=> true
471
+ # set.include? 11..20 #=> false
472
+ # set.include? 100 #=> true
473
+ # set.include? 6 #=> true, covered by "5:10"
474
+ # set.include? 4..9 #=> true, covered by "5:10"
475
+ # set.include? "4:9" #=> true, strings are parsed
476
+ # set.include? 4..9 #=> false, intersection is not sufficient
477
+ # set.include? "*" #=> false, use #limit to re-interpret "*"
478
+ # set.include? -1 #=> false, -1 is interpreted as "*"
479
+ #
480
+ # set = Net::IMAP::SequenceSet["5:10,100,111:*"]
481
+ # set.include? :* #=> true
482
+ # set.include? "*" #=> true
483
+ # set.include? -1 #=> true
484
+ # set.include? 200.. #=> true
485
+ # set.include? 100.. #=> false
486
+ #
487
+ # Related: #include_star?, #cover?, #===
488
+ def include?(element) include_tuple? input_to_tuple element end
489
+
490
+ alias member? include?
491
+
492
+ # Returns +true+ when the set contains <tt>*</tt>.
493
+ def include_star?; @tuples.last&.last == STAR_INT end
494
+
495
+ # Returns +true+ if the set and a given object have any common elements,
496
+ # +false+ otherwise.
497
+ #
498
+ # Net::IMAP::SequenceSet["5:10"].intersect? "7,9,11" #=> true
499
+ # Net::IMAP::SequenceSet["5:10"].intersect? "11:33" #=> false
500
+ #
501
+ # Related: #intersection, #disjoint?
502
+ def intersect?(other)
503
+ valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
504
+ end
505
+
506
+ # Returns +true+ if the set and a given object have no common elements,
507
+ # +false+ otherwise.
508
+ #
509
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "7,9,11" #=> false
510
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "11:33" #=> true
511
+ #
512
+ # Related: #intersection, #intersect?
513
+ def disjoint?(other)
514
+ empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
515
+ end
516
+
517
+ # :call-seq: max(star: :*) => integer or star or nil
518
+ #
519
+ # Returns the maximum value in +self+, +star+ when the set includes
520
+ # <tt>*</tt>, or +nil+ when the set is empty.
521
+ def max(star: :*)
522
+ (val = @tuples.last&.last) && val == STAR_INT ? star : val
523
+ end
524
+
525
+ # :call-seq: min(star: :*) => integer or star or nil
526
+ #
527
+ # Returns the minimum value in +self+, +star+ when the only value in the
528
+ # set is <tt>*</tt>, or +nil+ when the set is empty.
529
+ def min(star: :*)
530
+ (val = @tuples.first&.first) && val == STAR_INT ? star : val
531
+ end
532
+
533
+ # :call-seq: minmax(star: :*) => nil or [integer, integer or star]
534
+ #
535
+ # Returns a 2-element array containing the minimum and maximum numbers in
536
+ # +self+, or +nil+ when the set is empty.
537
+ def minmax(star: :*); [min(star: star), max(star: star)] unless empty? end
538
+
539
+ # Returns false when the set is empty.
540
+ def valid?; !empty? end
541
+
542
+ # Returns true if the set contains no elements
543
+ def empty?; @tuples.empty? end
544
+
545
+ # Returns true if the set contains every possible element.
546
+ def full?; @tuples == [[1, STAR_INT]] end
547
+
548
+ # :call-seq:
549
+ # self + other -> sequence set
550
+ # self | other -> sequence set
551
+ # union(other) -> sequence set
552
+ #
553
+ # Returns a new sequence set that has every number in the +other+ object
554
+ # added.
555
+ #
556
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
557
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
558
+ # another sequence set, or an enumerable containing any of these.
559
+ #
560
+ # Net::IMAP::SequenceSet["1:5"] | 2 | [4..6, 99]
561
+ # #=> Net::IMAP::SequenceSet["1:6,99"]
562
+ #
563
+ # Related: #add, #merge
564
+ def |(other) remain_frozen dup.merge other end
565
+ alias :+ :|
566
+ alias union :|
567
+
568
+ # :call-seq:
569
+ # self - other -> sequence set
570
+ # difference(other) -> sequence set
571
+ #
572
+ # Returns a new sequence set built by duplicating this set and removing
573
+ # every number that appears in +other+.
574
+ #
575
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
576
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
577
+ # another sequence set, or an enumerable containing any of these.
578
+ #
579
+ # Net::IMAP::SequenceSet[1..5] - 2 - 4 - 6
580
+ # #=> Net::IMAP::SequenceSet["1,3,5"]
581
+ #
582
+ # Related: #subtract
583
+ def -(other) remain_frozen dup.subtract other end
584
+ alias difference :-
585
+
586
+ # :call-seq:
587
+ # self & other -> sequence set
588
+ # intersection(other) -> sequence set
589
+ #
590
+ # Returns a new sequence set containing only the numbers common to this
591
+ # set and +other+.
592
+ #
593
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
594
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
595
+ # another sequence set, or an enumerable containing any of these.
596
+ #
597
+ # Net::IMAP::SequenceSet[1..5] & [2, 4, 6]
598
+ # #=> Net::IMAP::SequenceSet["2,4"]
599
+ #
600
+ # <tt>(seqset & other)</tt> is equivalent to <tt>(seqset - ~other)</tt>.
601
+ def &(other)
602
+ remain_frozen dup.subtract SequenceSet.new(other).complement!
603
+ end
604
+ alias intersection :&
605
+
606
+ # :call-seq:
607
+ # self ^ other -> sequence set
608
+ # xor(other) -> sequence set
609
+ #
610
+ # Returns a new sequence set containing numbers that are exclusive between
611
+ # this set and +other+.
612
+ #
613
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
614
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
615
+ # another sequence set, or an enumerable containing any of these.
616
+ #
617
+ # Net::IMAP::SequenceSet[1..5] ^ [2, 4, 6]
618
+ # #=> Net::IMAP::SequenceSet["1,3,5:6"]
619
+ #
620
+ # <tt>(seqset ^ other)</tt> is equivalent to <tt>((seqset | other) -
621
+ # (seqset & other))</tt>.
622
+ def ^(other) remain_frozen (self | other).subtract(self & other) end
623
+ alias xor :^
624
+
625
+ # :call-seq:
626
+ # ~ self -> sequence set
627
+ # complement -> sequence set
628
+ #
629
+ # Returns the complement of self, a SequenceSet which contains all numbers
630
+ # _except_ for those in this set.
631
+ #
632
+ # ~Net::IMAP::SequenceSet.full #=> Net::IMAP::SequenceSet.empty
633
+ # ~Net::IMAP::SequenceSet.empty #=> Net::IMAP::SequenceSet.full
634
+ # ~Net::IMAP::SequenceSet["1:5,100:222"]
635
+ # #=> Net::IMAP::SequenceSet["6:99,223:*"]
636
+ # ~Net::IMAP::SequenceSet["6:99,223:*"]
637
+ # #=> Net::IMAP::SequenceSet["1:5,100:222"]
638
+ #
639
+ # Related: #complement!
640
+ def ~; remain_frozen dup.complement! end
641
+ alias complement :~
642
+
643
+ # :call-seq:
644
+ # add(object) -> self
645
+ # self << other -> self
646
+ #
647
+ # Adds a range or number to the set and returns +self+.
648
+ #
649
+ # #string will be regenerated. Use #merge to add many elements at once.
650
+ #
651
+ # Related: #add?, #merge, #union
652
+ def add(object)
653
+ tuple_add input_to_tuple object
654
+ normalize!
655
+ end
656
+ alias << add
657
+
658
+ # :call-seq: add?(object) -> self or nil
659
+ #
660
+ # Adds a range or number to the set and returns +self+. Returns +nil+
661
+ # when the object is already included in the set.
662
+ #
663
+ # #string will be regenerated. Use #merge to add many elements at once.
664
+ #
665
+ # Related: #add, #merge, #union, #include?
666
+ def add?(object)
667
+ add object unless include? object
668
+ end
669
+
670
+ # :call-seq: delete(object) -> self
671
+ #
672
+ # Deletes the given range or number from the set and returns +self+.
673
+ #
674
+ # #string will be regenerated after deletion. Use #subtract to remove
675
+ # many elements at once.
676
+ #
677
+ # Related: #delete?, #delete_at, #subtract, #difference
678
+ def delete(object)
679
+ tuple_subtract input_to_tuple object
680
+ normalize!
681
+ end
682
+
683
+ # :call-seq:
684
+ # delete?(number) -> integer or nil
685
+ # delete?(star) -> :* or nil
686
+ # delete?(range) -> sequence set or nil
687
+ #
688
+ # Removes a specified value from the set, and returns the removed value.
689
+ # Returns +nil+ if nothing was removed.
690
+ #
691
+ # Returns an integer when the specified +number+ argument was removed:
692
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
693
+ # set.delete?(7) #=> 7
694
+ # set #=> #<Net::IMAP::SequenceSet "5:6,8:10,20">
695
+ # set.delete?("20") #=> 20
696
+ # set #=> #<Net::IMAP::SequenceSet "5:6,8:10">
697
+ # set.delete?(30) #=> nil
698
+ #
699
+ # Returns <tt>:*</tt> when <tt>*</tt> or <tt>-1</tt> is specified and
700
+ # removed:
701
+ # set = Net::IMAP::SequenceSet.new "5:9,20,35,*"
702
+ # set.delete?(-1) #=> :*
703
+ # set #=> #<Net::IMAP::SequenceSet "5:9,20,35">
704
+ #
705
+ # And returns a new SequenceSet when a range is specified:
706
+ #
707
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
708
+ # set.delete?(9..) #=> #<Net::IMAP::SequenceSet "9:10,20">
709
+ # set #=> #<Net::IMAP::SequenceSet "5:8">
710
+ # set.delete?(21..) #=> nil
711
+ #
712
+ # #string will be regenerated after deletion.
713
+ #
714
+ # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
715
+ def delete?(object)
716
+ tuple = input_to_tuple object
717
+ if tuple.first == tuple.last
718
+ return unless include_tuple? tuple
719
+ tuple_subtract tuple
720
+ normalize!
721
+ from_tuple_int tuple.first
722
+ else
723
+ copy = dup
724
+ tuple_subtract tuple
725
+ normalize!
726
+ copy if copy.subtract(self).valid?
727
+ end
728
+ end
729
+
730
+ # :call-seq: delete_at(index) -> number or :* or nil
731
+ #
732
+ # Deletes a number the set, indicated by the given +index+. Returns the
733
+ # number that was removed, or +nil+ if nothing was removed.
734
+ #
735
+ # #string will be regenerated after deletion.
736
+ #
737
+ # Related: #delete, #delete?, #slice!, #subtract, #difference
738
+ def delete_at(index)
739
+ slice! Integer(index.to_int)
740
+ end
741
+
742
+ # :call-seq:
743
+ # slice!(index) -> integer or :* or nil
744
+ # slice!(start, length) -> sequence set or nil
745
+ # slice!(range) -> sequence set or nil
746
+ #
747
+ # Deletes a number or consecutive numbers from the set, indicated by the
748
+ # given +index+, +start+ and +length+, or +range+ of offsets. Returns the
749
+ # number or sequence set that was removed, or +nil+ if nothing was
750
+ # removed. Arguments are interpreted the same as for #slice or #[].
751
+ #
752
+ # #string will be regenerated after deletion.
753
+ #
754
+ # Related: #slice, #delete_at, #delete, #delete?, #subtract, #difference
755
+ def slice!(index, length = nil)
756
+ deleted = slice(index, length) and subtract deleted
757
+ deleted
758
+ end
759
+
760
+ # Merges all of the elements that appear in any of the +inputs+ into the
761
+ # set, and returns +self+.
762
+ #
763
+ # The +inputs+ may be any objects that would be accepted by ::new:
764
+ # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
765
+ # formatted strings, other sequence sets, or enumerables containing any of
766
+ # these.
767
+ #
768
+ # #string will be regenerated after all inputs have been merged.
769
+ #
770
+ # Related: #add, #add?, #union
771
+ def merge(*inputs)
772
+ tuples_add input_to_tuples inputs
773
+ normalize!
774
+ end
775
+
776
+ # Removes all of the elements that appear in any of the given +objects+
777
+ # from the set, and returns +self+.
778
+ #
779
+ # The +objects+ may be any objects that would be accepted by ::new:
780
+ # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
781
+ # formatted strings, other sequence sets, or enumerables containing any of
782
+ # these.
783
+ #
784
+ # Related: #difference
785
+ def subtract(*objects)
786
+ tuples_subtract input_to_tuples objects
787
+ normalize!
788
+ end
789
+
790
+ # Returns an array of ranges and integers.
791
+ #
792
+ # The returned elements are sorted and coalesced, even when the input
793
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
794
+ #
795
+ # By itself, <tt>*</tt> translates to <tt>:*</tt>. A range containing
796
+ # <tt>*</tt> translates to an endless range. Use #limit to translate both
797
+ # cases to a maximum value.
798
+ #
799
+ # If the original input was unordered or contains overlapping ranges, the
800
+ # returned ranges will be ordered and coalesced.
801
+ #
802
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].elements
803
+ # #=> [2, 5..9, 11..12, :*]
804
+ #
805
+ # Related: #each_element, #ranges, #numbers
806
+ def elements; each_element.to_a end
807
+ alias to_a elements
808
+
809
+ # Returns an array of ranges
810
+ #
811
+ # The returned elements are sorted and coalesced, even when the input
812
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
813
+ #
814
+ # <tt>*</tt> translates to an endless range. By itself, <tt>*</tt>
815
+ # translates to <tt>:*..</tt>. Use #limit to set <tt>*</tt> to a maximum
816
+ # value.
817
+ #
818
+ # The returned ranges will be ordered and coalesced, even when the input
819
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
820
+ #
821
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
822
+ # #=> [2..2, 5..9, 11..12, :*..]
823
+ # Net::IMAP::SequenceSet["123,999:*,456:789"].ranges
824
+ # #=> [123..123, 456..789, 999..]
825
+ #
826
+ # Related: #each_range, #elements, #numbers, #to_set
827
+ def ranges; each_range.to_a end
828
+
829
+ # Returns a sorted array of all of the number values in the sequence set.
830
+ #
831
+ # The returned numbers are sorted and de-duplicated, even when the input
832
+ # #string is not. See #normalize.
833
+ #
834
+ # Net::IMAP::SequenceSet["2,5:9,6,12:11"].numbers
835
+ # #=> [2, 5, 6, 7, 8, 9, 11, 12]
836
+ #
837
+ # If the set contains a <tt>*</tt>, RangeError is raised. See #limit.
838
+ #
839
+ # Net::IMAP::SequenceSet["10000:*"].numbers
840
+ # #!> RangeError
841
+ #
842
+ # *WARNING:* Even excluding sets with <tt>*</tt>, an enormous result can
843
+ # easily be created. An array with over 4 billion integers could be
844
+ # returned, requiring up to 32GiB of memory on a 64-bit architecture.
845
+ #
846
+ # Net::IMAP::SequenceSet[10000..2**32-1].numbers
847
+ # # ...probably freezes the process for a while...
848
+ # #!> NoMemoryError (probably)
849
+ #
850
+ # For safety, consider using #limit or #intersection to set an upper
851
+ # bound. Alternatively, use #each_element, #each_range, or even
852
+ # #each_number to avoid allocation of a result array.
853
+ #
854
+ # Related: #elements, #ranges, #to_set
855
+ def numbers; each_number.to_a end
856
+
857
+ # Yields each number or range in #elements to the block and returns self.
858
+ # Returns an enumerator when called without a block.
859
+ #
860
+ # Related: #elements
861
+ def each_element # :yields: integer or range or :*
862
+ return to_enum(__method__) unless block_given?
863
+ @tuples.each do |min, max|
864
+ if min == STAR_INT then yield :*
865
+ elsif max == STAR_INT then yield min..
866
+ elsif min == max then yield min
867
+ else yield min..max
868
+ end
869
+ end
870
+ self
871
+ end
872
+
873
+ # Yields each range in #ranges to the block and returns self.
874
+ # Returns an enumerator when called without a block.
875
+ #
876
+ # Related: #ranges
877
+ def each_range # :yields: range
878
+ return to_enum(__method__) unless block_given?
879
+ @tuples.each do |min, max|
880
+ if min == STAR_INT then yield :*..
881
+ elsif max == STAR_INT then yield min..
882
+ else yield min..max
883
+ end
884
+ end
885
+ self
886
+ end
887
+
888
+ # Yields each number in #numbers to the block and returns self.
889
+ # If the set contains a <tt>*</tt>, RangeError will be raised.
890
+ #
891
+ # Returns an enumerator when called without a block (even if the set
892
+ # contains <tt>*</tt>).
893
+ #
894
+ # Related: #numbers
895
+ def each_number(&block) # :yields: integer
896
+ return to_enum(__method__) unless block_given?
897
+ raise RangeError, '%s contains "*"' % [self.class] if include_star?
898
+ each_element do |elem|
899
+ case elem
900
+ when Range then elem.each(&block)
901
+ when Integer then block.(elem)
902
+ end
903
+ end
904
+ self
905
+ end
906
+
907
+ # Returns a Set with all of the #numbers in the sequence set.
908
+ #
909
+ # If the set contains a <tt>*</tt>, RangeError will be raised.
910
+ #
911
+ # See #numbers for the warning about very large sets.
912
+ #
913
+ # Related: #elements, #ranges, #numbers
914
+ def to_set; Set.new(numbers) end
915
+
916
+ # Returns the count of #numbers in the set.
917
+ #
918
+ # If <tt>*</tt> and <tt>2**32 - 1</tt> (the maximum 32-bit unsigned
919
+ # integer value) are both in the set, they will only be counted once.
920
+ def count
921
+ @tuples.sum(@tuples.count) { _2 - _1 } +
922
+ (include_star? && include?(UINT32_MAX) ? -1 : 0)
923
+ end
924
+
925
+ alias size count
926
+
927
+ # Returns the index of +number+ in the set, or +nil+ if +number+ isn't in
928
+ # the set.
929
+ #
930
+ # Related: #[]
931
+ def find_index(number)
932
+ number = to_tuple_int number
933
+ each_tuple_with_index do |min, max, idx_min|
934
+ number < min and return nil
935
+ number <= max and return from_tuple_int(idx_min + (number - min))
936
+ end
937
+ nil
938
+ end
939
+
940
+ private def each_tuple_with_index
941
+ idx_min = 0
942
+ @tuples.each do |min, max|
943
+ yield min, max, idx_min, (idx_max = idx_min + (max - min))
944
+ idx_min = idx_max + 1
945
+ end
946
+ idx_min
947
+ end
948
+
949
+ private def reverse_each_tuple_with_index
950
+ idx_max = -1
951
+ @tuples.reverse_each do |min, max|
952
+ yield min, max, (idx_min = idx_max - (max - min)), idx_max
953
+ idx_max = idx_min - 1
954
+ end
955
+ idx_max
956
+ end
957
+
958
+ # :call-seq: at(index) -> integer or nil
959
+ #
960
+ # Returns a number from +self+, without modifying the set. Behaves the
961
+ # same as #[], except that #at only allows a single integer argument.
962
+ #
963
+ # Related: #[], #slice
964
+ def at(index)
965
+ index = Integer(index.to_int)
966
+ if index.negative?
967
+ reverse_each_tuple_with_index do |min, max, idx_min, idx_max|
968
+ idx_min <= index and return from_tuple_int(min + (index - idx_min))
969
+ end
970
+ else
971
+ each_tuple_with_index do |min, _, idx_min, idx_max|
972
+ index <= idx_max and return from_tuple_int(min + (index - idx_min))
973
+ end
974
+ end
975
+ nil
976
+ end
977
+
978
+ # :call-seq:
979
+ # seqset[index] -> integer or :* or nil
980
+ # slice(index) -> integer or :* or nil
981
+ # seqset[start, length] -> sequence set or nil
982
+ # slice(start, length) -> sequence set or nil
983
+ # seqset[range] -> sequence set or nil
984
+ # slice(range) -> sequence set or nil
985
+ #
986
+ # Returns a number or a subset from +self+, without modifying the set.
987
+ #
988
+ # When an Integer argument +index+ is given, the number at offset +index+
989
+ # is returned:
990
+ #
991
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
992
+ # set[0] #=> 10
993
+ # set[5] #=> 15
994
+ # set[10] #=> 26
995
+ #
996
+ # If +index+ is negative, it counts relative to the end of +self+:
997
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
998
+ # set[-1] #=> 26
999
+ # set[-3] #=> 22
1000
+ # set[-6] #=> 15
1001
+ #
1002
+ # If +index+ is out of range, +nil+ is returned.
1003
+ #
1004
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
1005
+ # set[11] #=> nil
1006
+ # set[-12] #=> nil
1007
+ #
1008
+ # The result is based on the normalized set—sorted and de-duplicated—not
1009
+ # on the assigned value of #string.
1010
+ #
1011
+ # set = Net::IMAP::SequenceSet["12,20:23,11:16,21"]
1012
+ # set[0] #=> 11
1013
+ # set[-1] #=> 23
1014
+ #
1015
+ def [](index, length = nil)
1016
+ if length then slice_length(index, length)
1017
+ elsif index.is_a?(Range) then slice_range(index)
1018
+ else at(index)
1019
+ end
1020
+ end
1021
+
1022
+ alias slice :[]
1023
+
1024
+ private def slice_length(start, length)
1025
+ start = Integer(start.to_int)
1026
+ length = Integer(length.to_int)
1027
+ raise ArgumentError, "length must be positive" unless length.positive?
1028
+ last = start + length - 1 unless start.negative? && start.abs <= length
1029
+ slice_range(start..last)
1030
+ end
1031
+
1032
+ private def slice_range(range)
1033
+ first = range.begin || 0
1034
+ last = range.end || -1
1035
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1036
+ if (first * last).positive? && last < first
1037
+ SequenceSet.empty
1038
+ elsif (min = at(first))
1039
+ max = at(last)
1040
+ if max == :* then self & (min..)
1041
+ elsif min <= max then self & (min..max)
1042
+ else SequenceSet.empty
1043
+ end
1044
+ end
1045
+ end
1046
+
1047
+ # Returns a frozen SequenceSet with <tt>*</tt> converted to +max+, numbers
1048
+ # and ranges over +max+ removed, and ranges containing +max+ converted to
1049
+ # end at +max+.
1050
+ #
1051
+ # Net::IMAP::SequenceSet["5,10:22,50"].limit(max: 20).to_s
1052
+ # #=> "5,10:20"
1053
+ #
1054
+ # <tt>*</tt> is always interpreted as the maximum value. When the set
1055
+ # contains <tt>*</tt>, it will be set equal to the limit.
1056
+ #
1057
+ # Net::IMAP::SequenceSet["*"].limit(max: 37)
1058
+ # #=> Net::IMAP::SequenceSet["37"]
1059
+ # Net::IMAP::SequenceSet["5:*"].limit(max: 37)
1060
+ # #=> Net::IMAP::SequenceSet["5:37"]
1061
+ # Net::IMAP::SequenceSet["500:*"].limit(max: 37)
1062
+ # #=> Net::IMAP::SequenceSet["37"]
1063
+ #
1064
+ def limit(max:)
1065
+ max = to_tuple_int(max)
1066
+ if empty? then self.class.empty
1067
+ elsif !include_star? && max < min then self.class.empty
1068
+ elsif max(star: STAR_INT) <= max then frozen? ? self : dup.freeze
1069
+ else dup.limit!(max: max).freeze
1070
+ end
1071
+ end
1072
+
1073
+ # Removes all members over +max+ and returns self. If <tt>*</tt> is a
1074
+ # member, it will be converted to +max+.
1075
+ #
1076
+ # Related: #limit
1077
+ def limit!(max:)
1078
+ star = include_star?
1079
+ max = to_tuple_int(max)
1080
+ tuple_subtract [max + 1, STAR_INT]
1081
+ tuple_add [max, max ] if star
1082
+ normalize!
1083
+ end
1084
+
1085
+ # :call-seq: complement! -> self
1086
+ #
1087
+ # Converts the SequenceSet to its own #complement. It will contain all
1088
+ # possible values _except_ for those currently in the set.
1089
+ #
1090
+ # Related: #complement
1091
+ def complement!
1092
+ return replace(self.class.full) if empty?
1093
+ return clear if full?
1094
+ flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
1095
+ if flat.first < 1 then flat.shift else flat.unshift 1 end
1096
+ if STAR_INT < flat.last then flat.pop else flat.push STAR_INT end
1097
+ @tuples = flat.each_slice(2).to_a
1098
+ normalize!
1099
+ end
1100
+
1101
+ # Returns a new SequenceSet with a normalized string representation.
1102
+ #
1103
+ # The returned set's #string is sorted and deduplicated. Adjacent or
1104
+ # overlapping elements will be merged into a single larger range.
1105
+ #
1106
+ # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalize
1107
+ # #=> Net::IMAP::SequenceSet["1:7,9:11"]
1108
+ #
1109
+ # Related: #normalize!, #normalized_string
1110
+ def normalize
1111
+ str = normalized_string
1112
+ return self if frozen? && str == string
1113
+ remain_frozen dup.instance_exec { @string = str&.-@; self }
1114
+ end
1115
+
1116
+ # Resets #string to be sorted, deduplicated, and coalesced. Returns
1117
+ # +self+.
1118
+ #
1119
+ # Related: #normalize, #normalized_string
1120
+ def normalize!
1121
+ @string = nil
1122
+ self
1123
+ end
1124
+
1125
+ # Returns a normalized +sequence-set+ string representation, sorted
1126
+ # and deduplicated. Adjacent or overlapping elements will be merged into
1127
+ # a single larger range. Returns +nil+ when the set is empty.
1128
+ #
1129
+ # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalized_string
1130
+ # #=> "1:7,9:11"
1131
+ #
1132
+ # Related: #normalize!, #normalize
1133
+ def normalized_string
1134
+ @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
1135
+ end
48
1136
 
49
1137
  def inspect
50
- (frozen? ? "%s[%p]" : "#<%s %p>") % [self.class, to_s]
1138
+ if empty?
1139
+ (frozen? ? "%s.empty" : "#<%s empty>") % [self.class]
1140
+ elsif frozen?
1141
+ "%s[%p]" % [self.class, to_s]
1142
+ else
1143
+ "#<%s %p>" % [self.class, to_s]
1144
+ end
51
1145
  end
52
1146
 
53
- # Unstable API, for internal use only (Net::IMAP#validate_data)
1147
+ # Returns self
1148
+ alias to_sequence_set itself
1149
+
1150
+ # Unstable API: currently for internal use only (Net::IMAP#validate_data)
54
1151
  def validate # :nodoc:
55
- ResponseParser::Patterns::SEQUENCE_SET_STR.match?(@atom) or
56
- raise ArgumentError, "invalid sequence-set: %p" % [input]
57
- true
1152
+ empty? and raise DataFormatError, "empty sequence-set is invalid"
1153
+ self
58
1154
  end
59
1155
 
60
- # Unstable API, for internal use only (Net::IMAP#send_data)
1156
+ # Unstable API: for internal use only (Net::IMAP#send_data)
61
1157
  def send_data(imap, tag) # :nodoc:
62
- imap.__send__(:put_string, atom)
1158
+ imap.__send__(:put_string, valid_string)
1159
+ end
1160
+
1161
+ protected
1162
+
1163
+ attr_reader :tuples # :nodoc:
1164
+
1165
+ private
1166
+
1167
+ def remain_frozen(set) frozen? ? set.freeze : set end
1168
+
1169
+ # frozen clones are shallow copied
1170
+ def initialize_clone(other)
1171
+ other.frozen? ? super : initialize_dup(other)
63
1172
  end
64
1173
 
1174
+ def initialize_dup(other)
1175
+ @tuples = other.tuples.map(&:dup)
1176
+ @string = other.string&.-@
1177
+ super
1178
+ end
1179
+
1180
+ def input_to_tuple(obj)
1181
+ obj = input_try_convert obj
1182
+ case obj
1183
+ when *STARS, Integer then [int = to_tuple_int(obj), int]
1184
+ when Range then range_to_tuple(obj)
1185
+ when String then str_to_tuple(obj)
1186
+ else
1187
+ raise DataFormatError, "expected number or range, got %p" % [obj]
1188
+ end
1189
+ end
1190
+
1191
+ def input_to_tuples(obj)
1192
+ obj = input_try_convert obj
1193
+ case obj
1194
+ when *STARS, Integer, Range then [input_to_tuple(obj)]
1195
+ when String then str_to_tuples obj
1196
+ when SequenceSet then obj.tuples
1197
+ when ENUMABLE then obj.flat_map { input_to_tuples _1 }
1198
+ when nil then []
1199
+ else
1200
+ raise DataFormatError,
1201
+ "expected nz-number, range, string, or enumerable; " \
1202
+ "got %p" % [obj]
1203
+ end
1204
+ end
1205
+
1206
+ # unlike SequenceSet#try_convert, this returns an Integer, Range,
1207
+ # String, Set, Array, or... any type of object.
1208
+ def input_try_convert(input)
1209
+ SequenceSet.try_convert(input) ||
1210
+ # Integer.try_convert(input) || # ruby 3.1+
1211
+ input.respond_to?(:to_int) && Integer(input.to_int) ||
1212
+ String.try_convert(input) ||
1213
+ input
1214
+ end
1215
+
1216
+ def range_to_tuple(range)
1217
+ first = to_tuple_int(range.begin || 1)
1218
+ last = to_tuple_int(range.end || :*)
1219
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1220
+ unless first <= last
1221
+ raise DataFormatError, "invalid range for sequence-set: %p" % [range]
1222
+ end
1223
+ [first, last]
1224
+ end
1225
+
1226
+ def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1227
+ def from_tuple_int(num) num == STAR_INT ? :* : num end
1228
+
1229
+ def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
1230
+ def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
1231
+ def str_to_tuple(str)
1232
+ raise DataFormatError, "invalid sequence set string" if str.empty?
1233
+ str.split(":", 2).map! { to_tuple_int _1 }.minmax
1234
+ end
1235
+
1236
+ def include_tuple?((min, max)) range_gte_to(min)&.cover?(min..max) end
1237
+
1238
+ def intersect_tuple?((min, max))
1239
+ range = range_gte_to(min) and
1240
+ range.include?(min) || range.include?(max) || (min..max).cover?(range)
1241
+ end
1242
+
1243
+ def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
1244
+ def tuples_subtract(tuples) tuples.each do tuple_subtract _1 end; self end
1245
+
1246
+ #
1247
+ # --|=====| |=====new tuple=====| append
1248
+ # ?????????-|=====new tuple=====|-|===lower===|-- insert
1249
+ #
1250
+ # |=====new tuple=====|
1251
+ # ---------??=======lower=======??--------------- noop
1252
+ #
1253
+ # ---------??===lower==|--|==| join remaining
1254
+ # ---------??===lower==|--|==|----|===upper===|-- join until upper
1255
+ # ---------??===lower==|--|==|--|=====upper===|-- join to upper
1256
+ def tuple_add(tuple)
1257
+ min, max = tuple
1258
+ lower, lower_idx = tuple_gte_with_index(min - 1)
1259
+ if lower.nil? then tuples << tuple
1260
+ elsif (max + 1) < lower.first then tuples.insert(lower_idx, tuple)
1261
+ else tuple_coalesce(lower, lower_idx, min, max)
1262
+ end
1263
+ end
1264
+
1265
+ def tuple_coalesce(lower, lower_idx, min, max)
1266
+ return if lower.first <= min && max <= lower.last
1267
+ lower[0] = [min, lower.first].min
1268
+ lower[1] = [max, lower.last].max
1269
+ lower_idx += 1
1270
+ return if lower_idx == tuples.count
1271
+ tmax_adj = lower.last + 1
1272
+ upper, upper_idx = tuple_gte_with_index(tmax_adj)
1273
+ if upper
1274
+ tmax_adj < upper.first ? (upper_idx -= 1) : (lower[1] = upper.last)
1275
+ end
1276
+ tuples.slice!(lower_idx..upper_idx)
1277
+ end
1278
+
1279
+ # |====tuple================|
1280
+ # --|====| no more 1. noop
1281
+ # --|====|---------------------------|====lower====|-- 2. noop
1282
+ # -------|======lower================|---------------- 3. split
1283
+ # --------|=====lower================|---------------- 4. trim beginning
1284
+ #
1285
+ # -------|======lower====????????????----------------- trim lower
1286
+ # --------|=====lower====????????????----------------- delete lower
1287
+ #
1288
+ # -------??=====lower===============|----------------- 5. trim/delete one
1289
+ # -------??=====lower====|--|====| no more 6. delete rest
1290
+ # -------??=====lower====|--|====|---|====upper====|-- 7. delete until
1291
+ # -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
1292
+ def tuple_subtract(tuple)
1293
+ min, max = tuple
1294
+ lower, idx = tuple_gte_with_index(min)
1295
+ if lower.nil? then nil # case 1.
1296
+ elsif max < lower.first then nil # case 2.
1297
+ elsif max < lower.last then tuple_trim_or_split lower, idx, min, max
1298
+ else tuples_trim_or_delete lower, idx, min, max
1299
+ end
1300
+ end
1301
+
1302
+ def tuple_trim_or_split(lower, idx, tmin, tmax)
1303
+ if lower.first < tmin # split
1304
+ tuples.insert(idx, [lower.first, tmin - 1])
1305
+ end
1306
+ lower[0] = tmax + 1
1307
+ end
1308
+
1309
+ def tuples_trim_or_delete(lower, lower_idx, tmin, tmax)
1310
+ if lower.first < tmin # trim lower
1311
+ lower[1] = tmin - 1
1312
+ lower_idx += 1
1313
+ end
1314
+ if tmax == lower.last # case 5
1315
+ upper_idx = lower_idx
1316
+ elsif (upper, upper_idx = tuple_gte_with_index(tmax + 1))
1317
+ upper_idx -= 1 # cases 7 and 8
1318
+ upper[0] = tmax + 1 if upper.first <= tmax # case 8 (else case 7)
1319
+ end
1320
+ tuples.slice!(lower_idx..upper_idx)
1321
+ end
1322
+
1323
+ def tuple_gte_with_index(num)
1324
+ idx = tuples.bsearch_index { _2 >= num } and [tuples[idx], idx]
1325
+ end
1326
+
1327
+ def range_gte_to(num)
1328
+ first, last = tuples.bsearch { _2 >= num }
1329
+ first..last if first
1330
+ end
1331
+
1332
+ def nz_number(num)
1333
+ case num
1334
+ when Integer, /\A[1-9]\d*\z/ then num = Integer(num)
1335
+ else raise DataFormatError, "%p is not a valid nz-number" % [num]
1336
+ end
1337
+ NumValidator.ensure_nz_number(num)
1338
+ num
1339
+ end
1340
+
1341
+ # intentionally defined after the class implementation
1342
+
1343
+ EMPTY = new.freeze
1344
+ FULL = self["1:*"]
1345
+ private_constant :EMPTY, :FULL
1346
+
65
1347
  end
66
1348
  end
67
1349
  end