net-imap 0.4.6 → 0.4.8

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.

Potentially problematic release.


This version of net-imap might be problematic. Click here for more details.

@@ -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