net-imap 0.4.6 → 0.4.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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