net-imap 0.4.24 → 0.5.14

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -1
  3. data/README.md +10 -4
  4. data/docs/styles.css +75 -14
  5. data/lib/net/imap/authenticators.rb +2 -2
  6. data/lib/net/imap/command_data.rb +73 -78
  7. data/lib/net/imap/config/attr_type_coercion.rb +22 -10
  8. data/lib/net/imap/config/attr_version_defaults.rb +93 -0
  9. data/lib/net/imap/config.rb +70 -94
  10. data/lib/net/imap/connection_state.rb +48 -0
  11. data/lib/net/imap/data_encoding.rb +3 -3
  12. data/lib/net/imap/data_lite.rb +226 -0
  13. data/lib/net/imap/deprecated_client_options.rb +6 -3
  14. data/lib/net/imap/errors.rb +6 -0
  15. data/lib/net/imap/esearch_result.rb +219 -0
  16. data/lib/net/imap/fetch_data.rb +126 -47
  17. data/lib/net/imap/flags.rb +1 -1
  18. data/lib/net/imap/response_data.rb +120 -186
  19. data/lib/net/imap/response_parser/parser_utils.rb +5 -0
  20. data/lib/net/imap/response_parser.rb +155 -21
  21. data/lib/net/imap/response_reader.rb +9 -12
  22. data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
  23. data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
  24. data/lib/net/imap/sasl/authenticators.rb +8 -4
  25. data/lib/net/imap/sasl/client_adapter.rb +77 -26
  26. data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
  27. data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
  28. data/lib/net/imap/sasl/external_authenticator.rb +2 -2
  29. data/lib/net/imap/sasl/gs2_header.rb +7 -7
  30. data/lib/net/imap/sasl/login_authenticator.rb +4 -3
  31. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
  32. data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
  33. data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
  34. data/lib/net/imap/sasl/scram_authenticator.rb +10 -10
  35. data/lib/net/imap/sasl.rb +7 -4
  36. data/lib/net/imap/sasl_adapter.rb +0 -1
  37. data/lib/net/imap/search_result.rb +4 -5
  38. data/lib/net/imap/sequence_set.rb +529 -154
  39. data/lib/net/imap/stringprep/nameprep.rb +1 -1
  40. data/lib/net/imap/stringprep/trace.rb +4 -4
  41. data/lib/net/imap/uidplus_data.rb +2 -84
  42. data/lib/net/imap/vanished_data.rb +65 -0
  43. data/lib/net/imap.rb +996 -305
  44. data/net-imap.gemspec +1 -1
  45. data/rakelib/rfcs.rake +2 -0
  46. data/rakelib/string_prep_tables_generator.rb +6 -2
  47. metadata +7 -2
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set" unless defined?(::Set)
4
+
3
5
  module Net
4
6
  class IMAP
5
7
 
@@ -14,30 +16,12 @@ module Net
14
16
  # receive a SequenceSet as an argument, for example IMAP#search, IMAP#fetch,
15
17
  # and IMAP#store.
16
18
  #
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
19
  # == Creating sequence sets
25
20
  #
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
21
  # SequenceSet.new may receive a single optional argument: a non-zero 32 bit
39
22
  # unsigned integer, a range, a <tt>sequence-set</tt> formatted string,
40
- # another sequence set, or an enumerable containing any of these.
23
+ # another SequenceSet, a Set (containing only numbers or <tt>*</tt>), or an
24
+ # Array containing any of these (array inputs may be nested).
41
25
  #
42
26
  # set = Net::IMAP::SequenceSet.new(1)
43
27
  # set.valid_string #=> "1"
@@ -52,30 +36,114 @@ module Net
52
36
  # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
53
37
  # set.valid_string #=> "1:10,55,1024:2048"
54
38
  #
55
- # Use ::[] with one or more arguments to create a frozen SequenceSet. An
56
- # invalid (empty) set cannot be created with ::[].
39
+ # SequenceSet.new with no arguments creates an empty sequence set. Note
40
+ # that an empty sequence set is invalid in the \IMAP grammar.
41
+ #
42
+ # set = Net::IMAP::SequenceSet.new
43
+ # set.empty? #=> true
44
+ # set.valid? #=> false
45
+ # set.valid_string #!> raises DataFormatError
46
+ # set << 1..10
47
+ # set.empty? #=> false
48
+ # set.valid? #=> true
49
+ # set.valid_string #=> "1:10"
50
+ #
51
+ # Using SequenceSet.new with another SequenceSet input behaves the same as
52
+ # calling #dup on the other set. The input's #string will be preserved.
53
+ #
54
+ # input = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
55
+ # copy = Net::IMAP::SequenceSet.new(input)
56
+ # input.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
57
+ # copy.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
58
+ # copy2 = input.dup # same as calling new with a SequenceSet input
59
+ # copy == input #=> true, same set membership
60
+ # copy.eql? input #=> true, same string value
61
+ # copy.equal? input #=> false, different objects
62
+ #
63
+ # copy.normalize!
64
+ # copy.valid_string #=> "1:10,1024,2048"
65
+ # copy == input #=> true, same set membership
66
+ # copy.eql? input #=> false, different string value
67
+ #
68
+ # copy << 999
69
+ # copy.valid_string #=> "1:10,999,1024,2048"
70
+ # copy == input #=> false, different set membership
71
+ # copy.eql? input #=> false, different string value
72
+ #
73
+ # Use Net::IMAP::SequenceSet() to coerce a single (optional) input.
74
+ # A SequenceSet input is returned without duplication, even when frozen.
75
+ #
76
+ # set = Net::IMAP::SequenceSet()
77
+ # set.string #=> nil
78
+ # set.frozen? #=> false
57
79
  #
80
+ # # String order is preserved
81
+ # set = Net::IMAP::SequenceSet("1,2,3:7,5,6:10,2048,1024")
82
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
83
+ # set.frozen? #=> false
84
+ #
85
+ # # Other inputs are normalized
86
+ # set = Net::IMAP::SequenceSet([1, 2, [3..7, 5], 6..10, 2048, 1024])
87
+ # set.valid_string #=> "1:10,1024,2048"
88
+ # set.frozen? #=> false
89
+ #
90
+ # unfrozen = set
91
+ # frozen = set.dup.freeze
92
+ # unfrozen.equal? Net::IMAP::SequenceSet(unfrozen) #=> true
93
+ # frozen.equal? Net::IMAP::SequenceSet(frozen) #=> true
94
+ #
95
+ # Use ::[] to coerce one or more arguments into a valid frozen SequenceSet.
96
+ # A valid frozen SequenceSet is returned directly, without allocating a new
97
+ # object. ::[] will not create an invalid (empty) set.
98
+ #
99
+ # Net::IMAP::SequenceSet[] #!> raises ArgumentError
100
+ # Net::IMAP::SequenceSet[nil] #!> raises DataFormatError
101
+ # Net::IMAP::SequenceSet[""] #!> raises DataFormatError
102
+ #
103
+ # # String order is preserved
58
104
  # set = Net::IMAP::SequenceSet["1,2,3:7,5,6:10,2048,1024"]
59
105
  # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
106
+ # set.frozen? #=> true
107
+ #
108
+ # # Other inputs are normalized
60
109
  # set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
61
- # set.valid_string #=> "1:10,55,1024:2048"
110
+ # set.valid_string #=> "1:10,1024,2048"
111
+ # set.frozen? #=> true
112
+ #
113
+ # frozen = set
114
+ # unfrozen = set.dup
115
+ # frozen.equal? Net::IMAP::SequenceSet[frozen] #=> true
116
+ # unfrozen.equal? Net::IMAP::SequenceSet[unfrozen] #=> false
117
+ #
118
+ # Objects which respond to +to_sequence_set+ (such as SearchResult and
119
+ # ThreadMember) can be coerced to a SequenceSet with ::new, ::try_convert,
120
+ # ::[], or Net::IMAP::SequenceSet.
121
+ #
122
+ # search = imap.uid_search(["SUBJECT", "hello", "NOT", "SEEN"])
123
+ # seqset = Net::IMAP::SequenceSet(search) - already_fetched
124
+ # fetch = imap.uid_fetch(seqset, "FAST")
62
125
  #
63
126
  # == Ordered and Normalized sets
64
127
  #
65
128
  # Sometimes the order of the set's members is significant, such as with the
66
129
  # +ESORT+, <tt>CONTEXT=SORT</tt>, and +UIDPLUS+ extensions. So, when a
67
- # sequence set is created by the parser or with a single string value, that
68
- # #string representation is preserved.
130
+ # sequence set is created from a single string (such as by the parser), that
131
+ # #string representation is preserved. Assigning a string with #string= or
132
+ # #replace will also preserve that string. Use #each_entry, #entries, or
133
+ # #each_ordered_number to enumerate the entries in their #string order.
134
+ # Hash equality (using #eql?) is based on the string representation.
69
135
  #
70
- # Internally, SequenceSet stores a normalized representation which sorts all
71
- # entries, de-duplicates numbers, and coalesces adjacent or overlapping
72
- # ranges. Most methods use this normalized representation to achieve
73
- # <tt>O(lg n)</tt> porformance. Use #entries or #each_entry to enumerate
74
- # the set in its original order.
136
+ # Internally, SequenceSet uses a normalized uint32 set representation which
137
+ # sorts and de-duplicates all numbers and coalesces adjacent or overlapping
138
+ # entries. Many methods use this sorted set representation for <tt>O(lg
139
+ # n)</tt> searches. Use #each_element, #elements, #each_range, #ranges,
140
+ # #each_number, or #numbers to enumerate the set in sorted order. Basic
141
+ # object equality (using #==) is based on set membership, without regard to
142
+ # #entry order or #string normalization.
75
143
  #
76
- # Most modification methods convert #string to its normalized form. To
77
- # preserve #string order while modifying a set, use #append, #string=, or
78
- # #replace.
144
+ # Most modification methods reset #string to its #normalized form, so that
145
+ # #entries and #elements are identical. Use #append to preserve #entries
146
+ # order while modifying a set.
79
147
  #
80
148
  # == Using <tt>*</tt>
81
149
  #
@@ -112,11 +180,15 @@ module Net
112
180
  # When a set includes <tt>*</tt>, some methods may have surprising behavior.
113
181
  #
114
182
  # For example, #complement treats <tt>*</tt> as its own number. This way,
115
- # the #intersection of a set and its #complement will always be empty.
116
- # This is not how an \IMAP server interprets the set: it will convert
117
- # <tt>*</tt> to either the number of messages in the mailbox or +UIDNEXT+,
118
- # as appropriate. And there _will_ be overlap between a set and its
119
- # complement after #limit is applied to each:
183
+ # the #intersection of a set and its #complement will always be empty. And
184
+ # <tt>*</tt> is sorted as greater than any other number in the set. This is
185
+ # not how an \IMAP server interprets the set: it will convert <tt>*</tt> to
186
+ # the number of messages in the mailbox, the +UID+ of the last message in
187
+ # the mailbox, or +UIDNEXT+, as appropriate. Several methods have an
188
+ # argument for how <tt>*</tt> should be interpreted.
189
+ #
190
+ # But, for example, this means that there may be overlap between a set and
191
+ # its complement after #limit is applied to each:
120
192
  #
121
193
  # ~Net::IMAP::SequenceSet["*"] == Net::IMAP::SequenceSet[1..(2**32-1)]
122
194
  # ~Net::IMAP::SequenceSet[1..5] == Net::IMAP::SequenceSet["6:*"]
@@ -153,6 +225,7 @@ module Net
153
225
  # * ::new: Creates a new mutable sequence set, which may be empty (invalid).
154
226
  # * ::try_convert: Calls +to_sequence_set+ on an object and verifies that
155
227
  # the result is a SequenceSet.
228
+ # * Net::IMAP::SequenceSet(): Coerce an input using ::try_convert or ::new.
156
229
  # * ::empty: Returns a frozen empty (invalid) SequenceSet.
157
230
  # * ::full: Returns a frozen SequenceSet containing every possible number.
158
231
  #
@@ -178,14 +251,13 @@ module Net
178
251
  #
179
252
  # <i>Set membership:</i>
180
253
  # - #include? (aliased as #member?):
181
- # Returns whether a given element (nz-number, range, or <tt>*</tt>) is
182
- # contained by the set.
254
+ # Returns whether a given element is contained by the set.
183
255
  # - #include_star?: Returns whether the set contains <tt>*</tt>.
184
256
  #
185
257
  # <i>Minimum and maximum value elements:</i>
186
- # - #min: Returns the minimum number in the set.
187
- # - #max: Returns the maximum number in the set.
188
- # - #minmax: Returns the minimum and maximum numbers in the set.
258
+ # - #min: Returns one or more of the lowest numbers in the set.
259
+ # - #max: Returns one or more of the highest numbers in the set.
260
+ # - #minmax: Returns the lowest and highest numbers in the set.
189
261
  #
190
262
  # <i>Accessing value by offset in sorted set:</i>
191
263
  # - #[] (aliased as #slice): Returns the number or consecutive subset at a
@@ -252,6 +324,10 @@ module Net
252
324
  # +self+ and the other set except those common to both.
253
325
  # - #~ (aliased as #complement): Returns a new set containing all members
254
326
  # that are not in +self+
327
+ # - #above: Return a copy of +self+ which only contains numbers above a
328
+ # given number.
329
+ # - #below: Return a copy of +self+ which only contains numbers below a
330
+ # given value.
255
331
  # - #limit: Returns a copy of +self+ which has replaced <tt>*</tt> with a
256
332
  # given maximum value and removed all members over that maximum.
257
333
  #
@@ -318,11 +394,24 @@ module Net
318
394
 
319
395
  # valid inputs for "*"
320
396
  STARS = [:*, ?*, -1].freeze
321
- private_constant :STAR_INT, :STARS
397
+ private_constant :STARS
322
398
 
323
- COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
324
- ENUMABLE = ->{ _1.respond_to?(:each) && _1.respond_to?(:empty?) }
325
- private_constant :COERCIBLE, :ENUMABLE
399
+ INSPECT_MAX_LEN = 512
400
+ INSPECT_TRUNCATE_LEN = 16
401
+ private_constant :INSPECT_MAX_LEN, :INSPECT_TRUNCATE_LEN
402
+
403
+ # /(,\d+){100}\z/ is shockingly slow on huge strings.
404
+ # /(,\d{0,10}){100}\z/ is ok, but ironically, Regexp.linear_time? is false.
405
+ #
406
+ # This unrolls all nested quantifiers. It's much harder to read, but it's
407
+ # also the fastest out of all the versions I tested.
408
+ nz_uint32 = /[1-9](?:\d(?:\d(?:\d(?:\d(?:\d(?:\d(?:\d(?:\d(?:\d)?)?)?)?)?)?)?)?)?/
409
+ num_or_star = /#{nz_uint32}|\*/
410
+ entry = /#{num_or_star}(?::#{num_or_star})?/
411
+ entries = ([entry] * INSPECT_TRUNCATE_LEN).join(",")
412
+ INSPECT_ABRIDGED_HEAD_RE = /\A#{entries},/
413
+ INSPECT_ABRIDGED_TAIL_RE = /,#{entries}\z/
414
+ private_constant :INSPECT_ABRIDGED_HEAD_RE, :INSPECT_ABRIDGED_TAIL_RE
326
415
 
327
416
  class << self
328
417
 
@@ -337,13 +426,12 @@ module Net
337
426
  # An empty SequenceSet is invalid and will raise a DataFormatError.
338
427
  #
339
428
  # Use ::new to create a mutable or empty SequenceSet.
429
+ #
430
+ # Related: ::new, Net::IMAP::SequenceSet(), ::try_convert
340
431
  def [](first, *rest)
341
432
  if rest.empty?
342
- if first.is_a?(SequenceSet) && first.frozen? && first.valid?
343
- first
344
- else
345
- new(first).validate.freeze
346
- end
433
+ set = try_convert(first)&.validate
434
+ set&.frozen? ? set : (set&.dup || new(first).validate).freeze
347
435
  else
348
436
  new(first).merge(*rest).validate.freeze
349
437
  end
@@ -356,12 +444,14 @@ module Net
356
444
  # +to_sequence_set+, calls +obj.to_sequence_set+ and returns the result.
357
445
  # Otherwise returns +nil+.
358
446
  #
359
- # If +obj.to_sequence_set+ doesn't return a SequenceSet, an exception is
360
- # raised.
447
+ # If +obj.to_sequence_set+ doesn't return a SequenceSet or +nil+, an
448
+ # exception is raised.
449
+ #
450
+ # Related: Net::IMAP::SequenceSet(), ::new, ::[]
361
451
  def try_convert(obj)
362
452
  return obj if obj.is_a?(SequenceSet)
363
453
  return nil unless obj.respond_to?(:to_sequence_set)
364
- obj = obj.to_sequence_set
454
+ return nil unless obj = obj.to_sequence_set
365
455
  return obj if obj.is_a?(SequenceSet)
366
456
  raise DataFormatError, "invalid object returned from to_sequence_set"
367
457
  end
@@ -376,23 +466,91 @@ module Net
376
466
  end
377
467
 
378
468
  # Create a new SequenceSet object from +input+, which may be another
379
- # SequenceSet, an IMAP formatted +sequence-set+ string, a number, a
380
- # range, <tt>:*</tt>, or an enumerable of these.
381
- #
382
- # Use ::[] to create a frozen (non-empty) SequenceSet.
469
+ # SequenceSet, an IMAP formatted +sequence-set+ string, a non-zero 32 bit
470
+ # unsigned integer, a range, <tt>:*</tt>, a Set of numbers or <tt>*</tt>,
471
+ # an object that responds to +to_sequence_set+ (such as SearchResult) or
472
+ # an Array of these (array inputs may be nested).
473
+ #
474
+ # set = Net::IMAP::SequenceSet.new(1)
475
+ # set.valid_string #=> "1"
476
+ # set = Net::IMAP::SequenceSet.new(1..100)
477
+ # set.valid_string #=> "1:100"
478
+ # set = Net::IMAP::SequenceSet.new(1...100)
479
+ # set.valid_string #=> "1:99"
480
+ # set = Net::IMAP::SequenceSet.new([1, 2, 5..])
481
+ # set.valid_string #=> "1:2,5:*"
482
+ # set = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
483
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
484
+ # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
485
+ # set.valid_string #=> "1:10,1024,2048"
486
+ #
487
+ # With no arguments (or +nil+) creates an empty sequence set. Note that
488
+ # an empty sequence set is invalid in the \IMAP grammar.
489
+ #
490
+ # set = Net::IMAP::SequenceSet.new
491
+ # set.empty? #=> true
492
+ # set.valid? #=> false
493
+ # set.valid_string #!> raises DataFormatError
494
+ # set << 1..10
495
+ # set.empty? #=> false
496
+ # set.valid? #=> true
497
+ # set.valid_string #=> "1:10"
498
+ #
499
+ # When +input+ is a SequenceSet, ::new behaves the same as calling #dup on
500
+ # that other set. The input's #string will be preserved.
501
+ #
502
+ # input = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
503
+ # copy = Net::IMAP::SequenceSet.new(input)
504
+ # input.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
505
+ # copy.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
506
+ # copy2 = input.dup # same as calling new with a SequenceSet input
507
+ # copy == input #=> true, same set membership
508
+ # copy.eql? input #=> true, same string value
509
+ # copy.equal? input #=> false, different objects
510
+ #
511
+ # copy.normalize!
512
+ # copy.valid_string #=> "1:10,1024,2048"
513
+ # copy == input #=> true, same set membership
514
+ # copy.eql? input #=> false, different string value
515
+ #
516
+ # copy << 999
517
+ # copy.valid_string #=> "1:10,999,1024,2048"
518
+ # copy == input #=> false, different set membership
519
+ # copy.eql? input #=> false, different string value
520
+ #
521
+ # === Alternative set creation methods
522
+ #
523
+ # * ::[] returns a frozen validated (non-empty) SequenceSet, without
524
+ # allocating a new object when the input is already a valid frozen
525
+ # SequenceSet.
526
+ # * Net::IMAP::SequenceSet() coerces an input to SequenceSet, without
527
+ # allocating a new object when the input is already a SequenceSet.
528
+ # * ::try_convert calls +to_sequence_set+ on inputs that support it and
529
+ # returns +nil+ for inputs that don't.
530
+ # * ::empty and ::full both return frozen singleton sets which can be
531
+ # combined with set operations (#|, #&, #^, #-, etc) to make new sets.
532
+ #
533
+ # See SequenceSet@Creating+sequence+sets.
383
534
  def initialize(input = nil) input ? replace(input) : clear end
384
535
 
385
536
  # Removes all elements and returns self.
386
- def clear; @tuples, @string = [], nil; self end
537
+ def clear
538
+ modifying! # redundant check, to normalize the error message for JRuby
539
+ @tuples, @string = [], nil
540
+ self
541
+ end
387
542
 
388
543
  # Replace the contents of the set with the contents of +other+ and returns
389
544
  # +self+.
390
545
  #
391
- # +other+ may be another SequenceSet, or it may be an IMAP +sequence-set+
392
- # string, a number, a range, <tt>*</tt>, or an enumerable of these.
546
+ # +other+ may be another SequenceSet or any other object that would be
547
+ # accepted by ::new.
393
548
  def replace(other)
394
549
  case other
395
- when SequenceSet then initialize_dup(other)
550
+ when SequenceSet then
551
+ modifying! # short circuit before doing any work
552
+ @tuples = other.deep_copy_tuples
553
+ @string = other.instance_variable_get(:@string)
396
554
  when String then self.string = other
397
555
  else clear; merge other
398
556
  end
@@ -421,32 +579,40 @@ module Net
421
579
  # If the set was created from a single string, it is not normalized. If
422
580
  # the set is updated the string will be normalized.
423
581
  #
424
- # Related: #valid_string, #normalized_string, #to_s
582
+ # Related: #valid_string, #normalized_string, #to_s, #inspect
425
583
  def string; @string ||= normalized_string if valid? end
426
584
 
427
- # Assigns a new string to #string and resets #elements to match. It
428
- # cannot be set to an empty string—assign +nil+ or use #clear instead.
429
- # The string is validated but not normalized.
585
+ # Returns an array with #normalized_string when valid and an empty array
586
+ # otherwise.
587
+ def deconstruct; valid? ? [normalized_string] : [] end
588
+
589
+ # Assigns a new string to #string and resets #elements to match.
590
+ # Assigning +nil+ or an empty string are equivalent to calling #clear.
430
591
  #
431
- # Use #add or #merge to add a string to an existing set.
592
+ # Non-empty strings are validated but not normalized.
593
+ #
594
+ # Use #add, #merge, or #append to add a string to an existing set.
432
595
  #
433
596
  # Related: #replace, #clear
434
- def string=(str)
435
- if str.nil?
597
+ def string=(input)
598
+ if input.nil?
436
599
  clear
437
- else
438
- str = String.try_convert(str) or raise ArgumentError, "not a string"
600
+ elsif (str = String.try_convert(input))
601
+ modifying! # short-circuit before parsing the string
439
602
  tuples = str_to_tuples str
440
603
  @tuples, @string = [], -str
441
604
  tuples_add tuples
605
+ else
606
+ raise ArgumentError, "expected a string or nil, got #{input.class}"
442
607
  end
608
+ str
443
609
  end
444
610
 
445
611
  # Returns the \IMAP +sequence-set+ string representation, or an empty
446
612
  # string when the set is empty. Note that an empty set is invalid in the
447
613
  # \IMAP syntax.
448
614
  #
449
- # Related: #valid_string, #normalized_string, #to_s
615
+ # Related: #string, #valid_string, #normalized_string, #inspect
450
616
  def to_s; string || "" end
451
617
 
452
618
  # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
@@ -499,8 +665,9 @@ module Net
499
665
 
500
666
  # :call-seq: self === other -> true | false | nil
501
667
  #
502
- # Returns whether +other+ is contained within the set. Returns +nil+ if a
503
- # StandardError is raised while converting +other+ to a comparable type.
668
+ # Returns whether +other+ is contained within the set. +other+ may be any
669
+ # object that would be accepted by ::new. Returns +nil+ if StandardError
670
+ # is raised while converting +other+ to a comparable type.
504
671
  #
505
672
  # Related: #cover?, #include?, #include_star?
506
673
  def ===(other)
@@ -514,12 +681,12 @@ module Net
514
681
  # Returns whether +other+ is contained within the set. +other+ may be any
515
682
  # object that would be accepted by ::new.
516
683
  #
517
- # Related: #===, #include?, #include_star?
684
+ # Related: #===, #include?, #include_star?, #intersect?
518
685
  def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
519
686
 
520
687
  # Returns +true+ when a given number or range is in +self+, and +false+
521
- # otherwise. Returns +false+ unless +number+ is an Integer, Range, or
522
- # <tt>*</tt>.
688
+ # otherwise. Returns +nil+ when +number+ isn't a valid SequenceSet
689
+ # element (Integer, Range, <tt>*</tt>, +sequence-set+ string).
523
690
  #
524
691
  # set = Net::IMAP::SequenceSet["5:10,100,111:115"]
525
692
  # set.include? 1 #=> false
@@ -527,8 +694,8 @@ module Net
527
694
  # set.include? 11..20 #=> false
528
695
  # set.include? 100 #=> true
529
696
  # set.include? 6 #=> true, covered by "5:10"
530
- # set.include? 4..9 #=> true, covered by "5:10"
531
- # set.include? "4:9" #=> true, strings are parsed
697
+ # set.include? 6..9 #=> true, covered by "5:10"
698
+ # set.include? "6:9" #=> true, strings are parsed
532
699
  # set.include? 4..9 #=> false, intersection is not sufficient
533
700
  # set.include? "*" #=> false, use #limit to re-interpret "*"
534
701
  # set.include? -1 #=> false, -1 is interpreted as "*"
@@ -537,11 +704,14 @@ module Net
537
704
  # set.include? :* #=> true
538
705
  # set.include? "*" #=> true
539
706
  # set.include? -1 #=> true
540
- # set.include? 200.. #=> true
541
- # set.include? 100.. #=> false
707
+ # set.include?(200..) #=> true
708
+ # set.include?(100..) #=> false
542
709
  #
543
- # Related: #include_star?, #cover?, #===
544
- def include?(element) include_tuple? input_to_tuple element end
710
+ # Related: #include_star?, #cover?, #===, #intersect?
711
+ def include?(element)
712
+ tuple = input_to_tuple element rescue nil
713
+ !!include_tuple?(tuple) if tuple
714
+ end
545
715
 
546
716
  alias member? include?
547
717
 
@@ -554,7 +724,7 @@ module Net
554
724
  # Net::IMAP::SequenceSet["5:10"].intersect? "7,9,11" #=> true
555
725
  # Net::IMAP::SequenceSet["5:10"].intersect? "11:33" #=> false
556
726
  #
557
- # Related: #intersection, #disjoint?
727
+ # Related: #intersection, #disjoint?, #cover?, #include?
558
728
  def intersect?(other)
559
729
  valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
560
730
  end
@@ -571,26 +741,59 @@ module Net
571
741
  empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
572
742
  end
573
743
 
574
- # :call-seq: max(star: :*) => integer or star or nil
744
+ # :call-seq:
745
+ # max(star: :*) => integer or star or nil
746
+ # max(count) => SequenceSet
575
747
  #
576
748
  # Returns the maximum value in +self+, +star+ when the set includes
577
749
  # <tt>*</tt>, or +nil+ when the set is empty.
578
- def max(star: :*)
579
- (val = @tuples.last&.last) && val == STAR_INT ? star : val
750
+ #
751
+ # When +count+ is given, a new SequenceSet is returned, containing only
752
+ # the last +count+ numbers. An empty SequenceSet is returned when +self+
753
+ # is empty. (+star+ is ignored when +count+ is given.)
754
+ #
755
+ # Related: #min, #minmax, #slice
756
+ def max(count = nil, star: :*)
757
+ if count
758
+ # n.b: #cardinality has not been backported to 0.5
759
+ cardinality = @tuples.sum(@tuples.count) { _2 - _1 }
760
+ if cardinality <= count
761
+ frozen? ? self : dup
762
+ else
763
+ slice(-count..) || remain_frozen_empty
764
+ end
765
+ elsif (val = @tuples.last&.last)
766
+ val == STAR_INT ? star : val
767
+ end
580
768
  end
581
769
 
582
- # :call-seq: min(star: :*) => integer or star or nil
770
+ # :call-seq:
771
+ # min(star: :*) => integer or star or nil
772
+ # min(count) => SequenceSet
583
773
  #
584
774
  # Returns the minimum value in +self+, +star+ when the only value in the
585
775
  # set is <tt>*</tt>, or +nil+ when the set is empty.
586
- def min(star: :*)
587
- (val = @tuples.first&.first) && val == STAR_INT ? star : val
776
+ #
777
+ # When +count+ is given, a new SequenceSet is returned, containing only
778
+ # the first +count+ numbers. An empty SequenceSet is returned when +self+
779
+ # is empty. (+star+ is ignored when +count+ is given.)
780
+ #
781
+ # Related: #max, #minmax, #slice
782
+ def min(count = nil, star: :*)
783
+ if count
784
+ slice(0...count) || remain_frozen_empty
785
+ elsif (val = @tuples.first&.first)
786
+ val != STAR_INT ? val : star
787
+ end
588
788
  end
589
789
 
590
- # :call-seq: minmax(star: :*) => nil or [integer, integer or star]
790
+ # :call-seq: minmax(star: :*) => [min, max] or nil
591
791
  #
592
792
  # Returns a 2-element array containing the minimum and maximum numbers in
593
- # +self+, or +nil+ when the set is empty.
793
+ # +self+, or +nil+ when the set is empty. +star+ is handled the same way
794
+ # as by #min and #max.
795
+ #
796
+ # Related: #min, #max
594
797
  def minmax(star: :*); [min(star: star), max(star: star)] unless empty? end
595
798
 
596
799
  # Returns false when the set is empty.
@@ -610,14 +813,19 @@ module Net
610
813
  # Returns a new sequence set that has every number in the +other+ object
611
814
  # added.
612
815
  #
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.
816
+ # +other+ may be any object that would be accepted by ::new.
616
817
  #
617
818
  # Net::IMAP::SequenceSet["1:5"] | 2 | [4..6, 99]
618
819
  # #=> Net::IMAP::SequenceSet["1:6,99"]
619
820
  #
620
- # Related: #add, #merge
821
+ # Related: #add, #merge, #&, #-, #^, #~
822
+ #
823
+ # ==== Set identities
824
+ #
825
+ # <tt>lhs | rhs</tt> is equivalent to:
826
+ # * <tt>rhs | lhs</tt> (commutative)
827
+ # * <tt>~(~lhs & ~rhs)</tt> (De Morgan's Law)
828
+ # * <tt>(lhs & rhs) ^ (lhs ^ rhs)</tt>
621
829
  def |(other) remain_frozen dup.merge other end
622
830
  alias :+ :|
623
831
  alias union :|
@@ -629,14 +837,22 @@ module Net
629
837
  # Returns a new sequence set built by duplicating this set and removing
630
838
  # every number that appears in +other+.
631
839
  #
632
- # +other+ may be any object that would be accepted by ::new: a non-zero 32
633
- # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
634
- # another sequence set, or an enumerable containing any of these.
840
+ # +other+ may be any object that would be accepted by ::new.
635
841
  #
636
842
  # Net::IMAP::SequenceSet[1..5] - 2 - 4 - 6
637
843
  # #=> Net::IMAP::SequenceSet["1,3,5"]
638
844
  #
639
- # Related: #subtract
845
+ # Related: #subtract, #|, #&, #^, #~
846
+ #
847
+ # ==== Set identities
848
+ #
849
+ # <tt>lhs - rhs</tt> is equivalent to:
850
+ # * <tt>~rhs - ~lhs</tt>
851
+ # * <tt>lhs & ~rhs</tt>
852
+ # * <tt>~(~lhs | rhs)</tt>
853
+ # * <tt>lhs & (lhs ^ rhs)</tt>
854
+ # * <tt>lhs ^ (lhs & rhs)</tt>
855
+ # * <tt>rhs ^ (lhs | rhs)</tt>
640
856
  def -(other) remain_frozen dup.subtract other end
641
857
  alias difference :-
642
858
 
@@ -647,14 +863,22 @@ module Net
647
863
  # Returns a new sequence set containing only the numbers common to this
648
864
  # set and +other+.
649
865
  #
650
- # +other+ may be any object that would be accepted by ::new: a non-zero 32
651
- # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
652
- # another sequence set, or an enumerable containing any of these.
866
+ # +other+ may be any object that would be accepted by ::new.
653
867
  #
654
868
  # Net::IMAP::SequenceSet[1..5] & [2, 4, 6]
655
869
  # #=> Net::IMAP::SequenceSet["2,4"]
656
870
  #
657
- # <tt>(seqset & other)</tt> is equivalent to <tt>(seqset - ~other)</tt>.
871
+ # Related: #intersect?, #|, #-, #^, #~
872
+ #
873
+ # ==== Set identities
874
+ #
875
+ # <tt>lhs & rhs</tt> is equivalent to:
876
+ # * <tt>rhs & lhs</tt> (commutative)
877
+ # * <tt>~(~lhs | ~rhs)</tt> (De Morgan's Law)
878
+ # * <tt>lhs - ~rhs</tt>
879
+ # * <tt>lhs - (lhs - rhs)</tt>
880
+ # * <tt>lhs - (lhs ^ rhs)</tt>
881
+ # * <tt>lhs ^ (lhs - rhs)</tt>
658
882
  def &(other)
659
883
  remain_frozen dup.subtract SequenceSet.new(other).complement!
660
884
  end
@@ -667,15 +891,21 @@ module Net
667
891
  # Returns a new sequence set containing numbers that are exclusive between
668
892
  # this set and +other+.
669
893
  #
670
- # +other+ may be any object that would be accepted by ::new: a non-zero 32
671
- # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
672
- # another sequence set, or an enumerable containing any of these.
894
+ # +other+ may be any object that would be accepted by ::new.
673
895
  #
674
896
  # Net::IMAP::SequenceSet[1..5] ^ [2, 4, 6]
675
897
  # #=> Net::IMAP::SequenceSet["1,3,5:6"]
676
898
  #
677
- # <tt>(seqset ^ other)</tt> is equivalent to <tt>((seqset | other) -
678
- # (seqset & other))</tt>.
899
+ # Related: #|, #&, #-, #~
900
+ #
901
+ # ==== Set identities
902
+ #
903
+ # <tt>lhs ^ rhs</tt> is equivalent to:
904
+ # * <tt>rhs ^ lhs</tt> (commutative)
905
+ # * <tt>~lhs ^ ~rhs</tt>
906
+ # * <tt>(lhs | rhs) - (lhs & rhs)</tt>
907
+ # * <tt>(lhs - rhs) | (rhs - lhs)</tt>
908
+ # * <tt>(lhs ^ other) ^ (other ^ rhs)</tt>
679
909
  def ^(other) remain_frozen (dup | other).subtract(self & other) end
680
910
  alias xor :^
681
911
 
@@ -693,7 +923,12 @@ module Net
693
923
  # ~Net::IMAP::SequenceSet["6:99,223:*"]
694
924
  # #=> Net::IMAP::SequenceSet["1:5,100:222"]
695
925
  #
696
- # Related: #complement!
926
+ # Related: #complement!, #|, #&, #-, #^
927
+ #
928
+ # ==== Set identities
929
+ #
930
+ # <tt>~set</tt> is equivalent to:
931
+ # * <tt>full - set</tt>, where "full" is Net::IMAP::SequenceSet.full
697
932
  def ~; remain_frozen dup.complement! end
698
933
  alias complement :~
699
934
 
@@ -705,8 +940,12 @@ module Net
705
940
  #
706
941
  # #string will be regenerated. Use #merge to add many elements at once.
707
942
  #
708
- # Related: #add?, #merge, #union
943
+ # Use #append to append new elements to #string. See
944
+ # SequenceSet@Ordered+and+Normalized+sets.
945
+ #
946
+ # Related: #add?, #merge, #union, #append
709
947
  def add(element)
948
+ modifying! # short-circuit before input_to_tuple
710
949
  tuple_add input_to_tuple element
711
950
  normalize!
712
951
  end
@@ -716,8 +955,12 @@ module Net
716
955
  #
717
956
  # Unlike #add, #merge, or #union, the new value is appended to #string.
718
957
  # This may result in a #string which has duplicates or is out-of-order.
958
+ #
959
+ # See SequenceSet@Ordered+and+Normalized+sets.
960
+ #
961
+ # Related: #add, #merge, #union
719
962
  def append(entry)
720
- modifying!
963
+ modifying! # short-circuit before input_to_tuple
721
964
  tuple = input_to_tuple entry
722
965
  entry = tuple_to_str tuple
723
966
  string unless empty? # write @string before tuple_add
@@ -735,6 +978,7 @@ module Net
735
978
  #
736
979
  # Related: #add, #merge, #union, #include?
737
980
  def add?(element)
981
+ modifying! # short-circuit before include?
738
982
  add element unless include? element
739
983
  end
740
984
 
@@ -747,6 +991,7 @@ module Net
747
991
  #
748
992
  # Related: #delete?, #delete_at, #subtract, #difference
749
993
  def delete(element)
994
+ modifying! # short-circuit before input_to_tuple
750
995
  tuple_subtract input_to_tuple element
751
996
  normalize!
752
997
  end
@@ -784,8 +1029,10 @@ module Net
784
1029
  #
785
1030
  # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
786
1031
  def delete?(element)
1032
+ modifying! # short-circuit before input_to_tuple
1033
+ element = input_try_convert(element)
787
1034
  tuple = input_to_tuple element
788
- if tuple.first == tuple.last
1035
+ if number_input?(element)
789
1036
  return unless include_tuple? tuple
790
1037
  tuple_subtract tuple
791
1038
  normalize!
@@ -824,6 +1071,7 @@ module Net
824
1071
  #
825
1072
  # Related: #slice, #delete_at, #delete, #delete?, #subtract, #difference
826
1073
  def slice!(index, length = nil)
1074
+ modifying! # short-circuit before slice
827
1075
  deleted = slice(index, length) and subtract deleted
828
1076
  deleted
829
1077
  end
@@ -831,14 +1079,13 @@ module Net
831
1079
  # Merges all of the elements that appear in any of the +sets+ into the
832
1080
  # set, and returns +self+.
833
1081
  #
834
- # The +sets+ may be any objects that would be accepted by ::new: non-zero
835
- # 32 bit unsigned integers, ranges, <tt>sequence-set</tt> formatted
836
- # strings, other sequence sets, or enumerables containing any of these.
1082
+ # The +sets+ may be any objects that would be accepted by ::new.
837
1083
  #
838
1084
  # #string will be regenerated after all sets have been merged.
839
1085
  #
840
1086
  # Related: #add, #add?, #union
841
1087
  def merge(*sets)
1088
+ modifying! # short-circuit before input_to_tuples
842
1089
  tuples_add input_to_tuples sets
843
1090
  normalize!
844
1091
  end
@@ -846,9 +1093,7 @@ module Net
846
1093
  # Removes all of the elements that appear in any of the given +sets+ from
847
1094
  # the set, and returns +self+.
848
1095
  #
849
- # The +sets+ may be any objects that would be accepted by ::new: non-zero
850
- # 32 bit unsigned integers, ranges, <tt>sequence-set</tt> formatted
851
- # strings, other sequence sets, or enumerables containing any of these.
1096
+ # The +sets+ may be any objects that would be accepted by ::new.
852
1097
  #
853
1098
  # Related: #difference
854
1099
  def subtract(*sets)
@@ -864,21 +1109,21 @@ module Net
864
1109
  # This is useful when the given order is significant, for example in a
865
1110
  # ESEARCH response to IMAP#sort.
866
1111
  #
1112
+ # See SequenceSet@Ordered+and+Normalized+sets.
1113
+ #
867
1114
  # Related: #each_entry, #elements
868
1115
  def entries; each_entry.to_a end
869
1116
 
870
1117
  # Returns an array of ranges and integers and <tt>:*</tt>.
871
1118
  #
872
1119
  # The returned elements are sorted and coalesced, even when the input
873
- # #string is not. <tt>*</tt> will sort last. See #normalize.
1120
+ # #string is not. <tt>*</tt> will sort last. See #normalize,
1121
+ # SequenceSet@Ordered+and+Normalized+sets.
874
1122
  #
875
1123
  # By itself, <tt>*</tt> translates to <tt>:*</tt>. A range containing
876
1124
  # <tt>*</tt> translates to an endless range. Use #limit to translate both
877
1125
  # cases to a maximum value.
878
1126
  #
879
- # The returned elements will be sorted and coalesced, even when the input
880
- # #string is not. <tt>*</tt> will sort last. See #normalize.
881
- #
882
1127
  # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].elements
883
1128
  # #=> [2, 5..9, 11..12, :*]
884
1129
  #
@@ -889,15 +1134,13 @@ module Net
889
1134
  # Returns an array of ranges
890
1135
  #
891
1136
  # The returned elements are sorted and coalesced, even when the input
892
- # #string is not. <tt>*</tt> will sort last. See #normalize.
1137
+ # #string is not. <tt>*</tt> will sort last. See #normalize,
1138
+ # SequenceSet@Ordered+and+Normalized+sets.
893
1139
  #
894
1140
  # <tt>*</tt> translates to an endless range. By itself, <tt>*</tt>
895
1141
  # translates to <tt>:*..</tt>. Use #limit to set <tt>*</tt> to a maximum
896
1142
  # value.
897
1143
  #
898
- # The returned ranges will be sorted and coalesced, even when the input
899
- # #string is not. <tt>*</tt> will sort last. See #normalize.
900
- #
901
1144
  # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
902
1145
  # #=> [2..2, 5..9, 11..12, :*..]
903
1146
  # Net::IMAP::SequenceSet["123,999:*,456:789"].ranges
@@ -909,7 +1152,7 @@ module Net
909
1152
  # Returns a sorted array of all of the number values in the sequence set.
910
1153
  #
911
1154
  # The returned numbers are sorted and de-duplicated, even when the input
912
- # #string is not. See #normalize.
1155
+ # #string is not. See #normalize, SequenceSet@Ordered+and+Normalized+sets.
913
1156
  #
914
1157
  # Net::IMAP::SequenceSet["2,5:9,6,12:11"].numbers
915
1158
  # #=> [2, 5, 6, 7, 8, 9, 11, 12]
@@ -941,6 +1184,8 @@ module Net
941
1184
  # no sorting, deduplication, or coalescing. When #string is in its
942
1185
  # normalized form, this will yield the same values as #each_element.
943
1186
  #
1187
+ # See SequenceSet@Ordered+and+Normalized+sets.
1188
+ #
944
1189
  # Related: #entries, #each_element
945
1190
  def each_entry(&block) # :yields: integer or range or :*
946
1191
  return to_enum(__method__) unless block_given?
@@ -951,7 +1196,7 @@ module Net
951
1196
  # and returns self. Returns an enumerator when called without a block.
952
1197
  #
953
1198
  # The returned numbers are sorted and de-duplicated, even when the input
954
- # #string is not. See #normalize.
1199
+ # #string is not. See #normalize, SequenceSet@Ordered+and+Normalized+sets.
955
1200
  #
956
1201
  # Related: #elements, #each_entry
957
1202
  def each_element # :yields: integer or range or :*
@@ -1263,6 +1508,58 @@ module Net
1263
1508
 
1264
1509
  public
1265
1510
 
1511
+ # Returns a copy of +self+ which only contains the numbers above +num+.
1512
+ #
1513
+ # Net::IMAP::SequenceSet["5,10:22,50"].above(10) # to_s => "11:22,50"
1514
+ # Net::IMAP::SequenceSet["5,10:22,50"].above(20) # to_s => "21:22,50
1515
+ # Net::IMAP::SequenceSet["5,10:22,50"].above(30) # to_s => "50"
1516
+ #
1517
+ # This returns the same result as #intersection with <tt>((num+1)..)</tt>
1518
+ # or #difference with <tt>(..num)</tt>.
1519
+ #
1520
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (11..) # to_s => "11:22,50"
1521
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (..10) # to_s => "11:22,50"
1522
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (21..) # to_s => "21:22,50"
1523
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (..20) # to_s => "21:22,50"
1524
+ #
1525
+ # Related: #above, #-, #&
1526
+ def above(num)
1527
+ NumValidator.valid_nz_number?(num) or
1528
+ raise ArgumentError, "not a valid sequence set number"
1529
+ difference(..num)
1530
+ end
1531
+
1532
+ # Returns a copy of +self+ which only contains numbers below +num+.
1533
+ #
1534
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(10) # to_s => "5"
1535
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(20) # to_s => "5,10:19"
1536
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(30) # to_s => "5,10:22"
1537
+ #
1538
+ # This returns the same result as #intersection with <tt>(..(num-1))</tt>
1539
+ # or #difference with <tt>(num..)</tt>.
1540
+ #
1541
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (..9) # to_s => "5"
1542
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (10..) # to_s => "5"
1543
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (..19) # to_s => "5,10:19"
1544
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (20..) # to_s => "5,10:19"
1545
+ #
1546
+ # When the set does not contain <tt>*</tt>, #below is identical to #limit
1547
+ # with <tt>max: num - 1</tt>. When the set does contain <tt>*</tt>,
1548
+ # #below always drops it from the result. Use #limit when the IMAP
1549
+ # semantics for <tt>*</tt> must be enforced.
1550
+ #
1551
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(30) # to_s => "5,10:22"
1552
+ # Net::IMAP::SequenceSet["5,10:22,50"].limit(max: 29) # to_s => "5,10:22"
1553
+ # Net::IMAP::SequenceSet["5,10:22,*"].below(30) # to_s => "5,10:22"
1554
+ # Net::IMAP::SequenceSet["5,10:22,*"].limit(max: 29) # to_s => "5,10:22,29"
1555
+ #
1556
+ # Related: #above, #-, #&, #limit
1557
+ def below(num)
1558
+ NumValidator.valid_nz_number?(num) or
1559
+ raise ArgumentError, "not a valid sequence set number"
1560
+ difference(num..)
1561
+ end
1562
+
1266
1563
  # Returns a frozen SequenceSet with <tt>*</tt> converted to +max+, numbers
1267
1564
  # and ranges over +max+ removed, and ranges containing +max+ converted to
1268
1565
  # end at +max+.
@@ -1280,6 +1577,7 @@ module Net
1280
1577
  # Net::IMAP::SequenceSet["500:*"].limit(max: 37)
1281
1578
  # #=> Net::IMAP::SequenceSet["37"]
1282
1579
  #
1580
+ # Related: #limit!
1283
1581
  def limit(max:)
1284
1582
  max = to_tuple_int(max)
1285
1583
  if empty? then self.class.empty
@@ -1294,6 +1592,7 @@ module Net
1294
1592
  #
1295
1593
  # Related: #limit
1296
1594
  def limit!(max:)
1595
+ modifying! # short-circuit, and normalize the error message for JRuby
1297
1596
  star = include_star?
1298
1597
  max = to_tuple_int(max)
1299
1598
  tuple_subtract [max + 1, STAR_INT]
@@ -1308,6 +1607,7 @@ module Net
1308
1607
  #
1309
1608
  # Related: #complement
1310
1609
  def complement!
1610
+ modifying! # short-circuit, and normalize the error message for JRuby
1311
1611
  return replace(self.class.full) if empty?
1312
1612
  return clear if full?
1313
1613
  flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
@@ -1321,6 +1621,7 @@ module Net
1321
1621
  #
1322
1622
  # The returned set's #string is sorted and deduplicated. Adjacent or
1323
1623
  # overlapping elements will be merged into a single larger range.
1624
+ # See SequenceSet@Ordered+and+Normalized+sets.
1324
1625
  #
1325
1626
  # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalize
1326
1627
  # #=> Net::IMAP::SequenceSet["1:7,9:11"]
@@ -1333,37 +1634,87 @@ module Net
1333
1634
  end
1334
1635
 
1335
1636
  # Resets #string to be sorted, deduplicated, and coalesced. Returns
1336
- # +self+.
1637
+ # +self+. See SequenceSet@Ordered+and+Normalized+sets.
1337
1638
  #
1338
1639
  # Related: #normalize, #normalized_string
1339
1640
  def normalize!
1641
+ modifying! # redundant check, to normalize the error message for JRuby
1340
1642
  @string = nil
1341
1643
  self
1342
1644
  end
1343
1645
 
1344
1646
  # Returns a normalized +sequence-set+ string representation, sorted
1345
1647
  # and deduplicated. Adjacent or overlapping elements will be merged into
1346
- # a single larger range. Returns +nil+ when the set is empty.
1648
+ # a single larger range. See SequenceSet@Ordered+and+Normalized+sets.
1347
1649
  #
1348
1650
  # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalized_string
1349
1651
  # #=> "1:7,9:11"
1350
1652
  #
1351
- # Related: #normalize!, #normalize
1653
+ # Returns +nil+ when the set is empty.
1654
+ #
1655
+ # Related: #normalize!, #normalize, #string, #to_s
1352
1656
  def normalized_string
1353
1657
  @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
1354
1658
  end
1355
1659
 
1660
+ # Returns an inspection string for the SequenceSet.
1661
+ #
1662
+ # Net::IMAP::SequenceSet.new.inspect
1663
+ # #=> "Net::IMAP::SequenceSet()"
1664
+ #
1665
+ # Net::IMAP::SequenceSet(1..5, 1024, 15, 2000).inspect
1666
+ # #=> 'Net::IMAP::SequenceSet("1:5,15,1024,2000")'
1667
+ #
1668
+ # Frozen sets have slightly different output:
1669
+ #
1670
+ # Net::IMAP::SequenceSet.empty.inspect
1671
+ # #=> "Net::IMAP::SequenceSet.empty"
1672
+ #
1673
+ # Net::IMAP::SequenceSet[1..5, 1024, 15, 2000].inspect
1674
+ # #=> 'Net::IMAP::SequenceSet["1:5,15,1024,2000"]'
1675
+ #
1676
+ # Large sets (by number of #entries) have abridged output, with only the
1677
+ # first and last entries:
1678
+ #
1679
+ # Net::IMAP::SequenceSet(((1..5000) % 2).to_a).inspect
1680
+ # #=> #<Net::IMAP::SequenceSet 2500 entries "1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,...(2468 entries omitted)...,4969,4971,4973,4975,4977,4979,4981,4983,4985,4987,4989,4991,4993,4995,4997,4999">
1681
+ #
1682
+ # Related: #to_s, #string
1356
1683
  def inspect
1357
- if empty?
1358
- (frozen? ? "%s.empty" : "#<%s empty>") % [self.class]
1359
- elsif frozen?
1360
- "%s[%p]" % [self.class, to_s]
1684
+ case (count = count_entries)
1685
+ when 0
1686
+ (frozen? ? "%s.empty" : "%s()") % [self.class]
1687
+ when ..INSPECT_MAX_LEN
1688
+ (frozen? ? "%s[%p]" : "%s(%p)") % [self.class, to_s]
1361
1689
  else
1362
- "#<%s %p>" % [self.class, to_s]
1690
+ if @string
1691
+ head = @string[INSPECT_ABRIDGED_HEAD_RE]
1692
+ tail = @string[INSPECT_ABRIDGED_TAIL_RE]
1693
+ else
1694
+ head = export_string_entries(@tuples.first(INSPECT_TRUNCATE_LEN)) + ","
1695
+ tail = "," + export_string_entries(@tuples.last(INSPECT_TRUNCATE_LEN))
1696
+ end
1697
+ '#<%s %d entries "%s...(%d entries omitted)...%s"%s>' % [
1698
+ self.class, count,
1699
+ head, count - INSPECT_TRUNCATE_LEN * 2, tail,
1700
+ frozen? ? " (frozen)" : "",
1701
+ ]
1363
1702
  end
1364
1703
  end
1365
1704
 
1366
- # Returns self
1705
+ private def count_entries
1706
+ @string ? @string.count(",") + 1 : @tuples.count
1707
+ end
1708
+
1709
+ ##
1710
+ # :method: to_sequence_set
1711
+ # :call-seq: to_sequence_set -> self
1712
+ #
1713
+ # Returns +self+
1714
+ #
1715
+ # Related: ::try_convert
1716
+
1717
+ # :nodoc: (work around rdoc bug)
1367
1718
  alias to_sequence_set itself
1368
1719
 
1369
1720
  # Unstable API: currently for internal use only (Net::IMAP#validate_data)
@@ -1377,10 +1728,24 @@ module Net
1377
1728
  imap.__send__(:put_string, valid_string)
1378
1729
  end
1379
1730
 
1731
+ # For YAML serialization
1732
+ def encode_with(coder) # :nodoc:
1733
+ # we can perfectly reconstruct from the string
1734
+ coder['string'] = to_s
1735
+ end
1736
+
1737
+ # For YAML deserialization
1738
+ def init_with(coder) # :nodoc:
1739
+ @tuples = []
1740
+ self.string = coder['string']
1741
+ end
1742
+
1380
1743
  protected
1381
1744
 
1382
1745
  attr_reader :tuples # :nodoc:
1383
1746
 
1747
+ def deep_copy_tuples; @tuples.map { _1.dup } end # :nodoc:
1748
+
1384
1749
  private
1385
1750
 
1386
1751
  def remain_frozen(set) frozen? ? set.freeze : set end
@@ -1388,12 +1753,12 @@ module Net
1388
1753
 
1389
1754
  # frozen clones are shallow copied
1390
1755
  def initialize_clone(other)
1391
- other.frozen? ? super : initialize_dup(other)
1756
+ @tuples = other.deep_copy_tuples unless other.frozen?
1757
+ super
1392
1758
  end
1393
1759
 
1394
1760
  def initialize_dup(other)
1395
- @tuples = other.tuples.map(&:dup)
1396
- @string = other.string&.-@
1761
+ @tuples = other.deep_copy_tuples
1397
1762
  super
1398
1763
  end
1399
1764
 
@@ -1414,12 +1779,12 @@ module Net
1414
1779
  when *STARS, Integer, Range then [input_to_tuple(set)]
1415
1780
  when String then str_to_tuples set
1416
1781
  when SequenceSet then set.tuples
1417
- when ENUMABLE then set.flat_map { input_to_tuples _1 }
1782
+ when Set then set.map { [to_tuple_int(_1)] * 2 }
1783
+ when Array then set.flat_map { input_to_tuples _1 }
1418
1784
  when nil then []
1419
1785
  else
1420
- raise DataFormatError,
1421
- "expected nz-number, range, string, or enumerable; " \
1422
- "got %p" % [set]
1786
+ raise DataFormatError, "expected nz-number, range, '*', Set, Array; " \
1787
+ "got %p" % [set]
1423
1788
  end
1424
1789
  end
1425
1790
 
@@ -1427,12 +1792,19 @@ module Net
1427
1792
  # String, Set, Array, or... any type of object.
1428
1793
  def input_try_convert(input)
1429
1794
  SequenceSet.try_convert(input) ||
1430
- # Integer.try_convert(input) || # ruby 3.1+
1431
- input.respond_to?(:to_int) && Integer(input.to_int) ||
1795
+ Integer.try_convert(input) ||
1432
1796
  String.try_convert(input) ||
1433
1797
  input
1434
1798
  end
1435
1799
 
1800
+ # NOTE: input_try_convert must be called on input first
1801
+ def number_input?(input)
1802
+ case input
1803
+ when *STARS, Integer then true
1804
+ when String then !input.include?(/[:,]/)
1805
+ end
1806
+ end
1807
+
1436
1808
  def range_to_tuple(range)
1437
1809
  first = to_tuple_int(range.begin || 1)
1438
1810
  last = to_tuple_int(range.end || :*)
@@ -1446,6 +1818,10 @@ module Net
1446
1818
  def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1447
1819
  def from_tuple_int(num) num == STAR_INT ? :* : num end
1448
1820
 
1821
+ def export_string_entries(entries)
1822
+ -entries.map { tuple_to_str _1 }.join(",")
1823
+ end
1824
+
1449
1825
  def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
1450
1826
  def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
1451
1827
  def str_to_tuple(str)
@@ -1558,12 +1934,11 @@ module Net
1558
1934
  end
1559
1935
 
1560
1936
  def nz_number(num)
1561
- case num
1562
- when Integer, /\A[1-9]\d*\z/ then num = Integer(num)
1563
- else raise DataFormatError, "%p is not a valid nz-number" % [num]
1564
- end
1565
- NumValidator.ensure_nz_number(num)
1566
- num
1937
+ String === num && !/\A[1-9]\d*\z/.match?(num) and
1938
+ raise DataFormatError, "%p is not a valid nz-number" % [num]
1939
+ NumValidator.ensure_nz_number Integer num
1940
+ rescue TypeError # To catch errors from Integer()
1941
+ raise DataFormatError, $!.message
1567
1942
  end
1568
1943
 
1569
1944
  # intentionally defined after the class implementation