net-imap 0.4.22 → 0.6.3

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +12 -2
  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 +40 -95
  7. data/lib/net/imap/config/attr_accessors.rb +8 -9
  8. data/lib/net/imap/config/attr_inheritance.rb +64 -1
  9. data/lib/net/imap/config/attr_type_coercion.rb +22 -10
  10. data/lib/net/imap/config/attr_version_defaults.rb +90 -0
  11. data/lib/net/imap/config.rb +241 -125
  12. data/lib/net/imap/connection_state.rb +48 -0
  13. data/lib/net/imap/data_encoding.rb +80 -31
  14. data/lib/net/imap/deprecated_client_options.rb +6 -3
  15. data/lib/net/imap/errors.rb +158 -0
  16. data/lib/net/imap/esearch_result.rb +225 -0
  17. data/lib/net/imap/fetch_data.rb +126 -47
  18. data/lib/net/imap/flags.rb +1 -1
  19. data/lib/net/imap/response_data.rb +123 -187
  20. data/lib/net/imap/response_parser/parser_utils.rb +19 -23
  21. data/lib/net/imap/response_parser.rb +182 -38
  22. data/lib/net/imap/response_reader.rb +10 -12
  23. data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
  24. data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
  25. data/lib/net/imap/sasl/authenticators.rb +8 -4
  26. data/lib/net/imap/sasl/client_adapter.rb +77 -26
  27. data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
  28. data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
  29. data/lib/net/imap/sasl/external_authenticator.rb +2 -2
  30. data/lib/net/imap/sasl/gs2_header.rb +7 -7
  31. data/lib/net/imap/sasl/login_authenticator.rb +4 -3
  32. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
  33. data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
  34. data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
  35. data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
  36. data/lib/net/imap/sasl.rb +7 -4
  37. data/lib/net/imap/sasl_adapter.rb +0 -1
  38. data/lib/net/imap/search_result.rb +10 -5
  39. data/lib/net/imap/sequence_set.rb +1104 -421
  40. data/lib/net/imap/stringprep/nameprep.rb +1 -1
  41. data/lib/net/imap/stringprep/trace.rb +4 -4
  42. data/lib/net/imap/uidplus_data.rb +4 -147
  43. data/lib/net/imap/vanished_data.rb +65 -0
  44. data/lib/net/imap.rb +1002 -313
  45. data/net-imap.gemspec +1 -1
  46. data/rakelib/rfcs.rake +2 -0
  47. data/rakelib/string_prep_tables_generator.rb +6 -2
  48. metadata +7 -3
@@ -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,118 @@ 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
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
57
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.
135
+ #
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.
69
143
  #
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.
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.
75
147
  #
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.
148
+ # Non-normalized sets store both representations of the set, which can more
149
+ # than double memory usage. Very large sequence sets should avoid
150
+ # denormalizing methods (such as #append) unless order is significant.
79
151
  #
80
152
  # == Using <tt>*</tt>
81
153
  #
@@ -111,12 +183,16 @@ module Net
111
183
  #
112
184
  # When a set includes <tt>*</tt>, some methods may have surprising behavior.
113
185
  #
114
- # 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:
186
+ # For example, #complement treats <tt>*</tt> as its own member. This way,
187
+ # the #intersection of a set and its #complement will always be empty. And
188
+ # <tt>*</tt> is sorted as greater than any other number in the set. This is
189
+ # not how an \IMAP server interprets the set: it will convert <tt>*</tt> to
190
+ # the number of messages in the mailbox, the +UID+ of the last message in
191
+ # the mailbox, or +UIDNEXT+, as appropriate. Several methods have an
192
+ # argument for how <tt>*</tt> should be interpreted.
193
+ #
194
+ # But, for example, this means that there may be overlap between a set and
195
+ # its complement after #limit is applied to each:
120
196
  #
121
197
  # ~Net::IMAP::SequenceSet["*"] == Net::IMAP::SequenceSet[1..(2**32-1)]
122
198
  # ~Net::IMAP::SequenceSet[1..5] == Net::IMAP::SequenceSet["6:*"]
@@ -127,7 +203,7 @@ module Net
127
203
  # (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4]
128
204
  #
129
205
  # When counting the number of numbers in a set, <tt>*</tt> will be counted
130
- # _except_ when UINT32_MAX is also in the set:
206
+ # as if it were equal to UINT32_MAX:
131
207
  # UINT32_MAX = 2**32 - 1
132
208
  # Net::IMAP::SequenceSet["*"].count => 1
133
209
  # Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX
@@ -136,6 +212,12 @@ module Net
136
212
  # Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
137
213
  # Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
138
214
  #
215
+ # Use #cardinality to count the set members wxth <tt>*</tt> counted as a
216
+ # distinct member:
217
+ # Net::IMAP::SequenceSet[1..].cardinality #=> UINT32_MAX + 1
218
+ # Net::IMAP::SequenceSet[UINT32_MAX, :*].cardinality #=> 2
219
+ # Net::IMAP::SequenceSet[UINT32_MAX..].cardinality #=> 2
220
+ #
139
221
  # == What's here?
140
222
  #
141
223
  # SequenceSet provides methods for:
@@ -153,6 +235,7 @@ module Net
153
235
  # * ::new: Creates a new mutable sequence set, which may be empty (invalid).
154
236
  # * ::try_convert: Calls +to_sequence_set+ on an object and verifies that
155
237
  # the result is a SequenceSet.
238
+ # * Net::IMAP::SequenceSet(): Coerce an input using ::try_convert or ::new.
156
239
  # * ::empty: Returns a frozen empty (invalid) SequenceSet.
157
240
  # * ::full: Returns a frozen SequenceSet containing every possible number.
158
241
  #
@@ -178,14 +261,13 @@ module Net
178
261
  #
179
262
  # <i>Set membership:</i>
180
263
  # - #include? (aliased as #member?):
181
- # Returns whether a given element (nz-number, range, or <tt>*</tt>) is
182
- # contained by the set.
264
+ # Returns whether a given element is contained by the set.
183
265
  # - #include_star?: Returns whether the set contains <tt>*</tt>.
184
266
  #
185
267
  # <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.
268
+ # - #min: Returns one or more of the lowest numbers in the set.
269
+ # - #max: Returns one or more of the highest numbers in the set.
270
+ # - #minmax: Returns the lowest and highest numbers in the set.
189
271
  #
190
272
  # <i>Accessing value by offset in sorted set:</i>
191
273
  # - #[] (aliased as #slice): Returns the number or consecutive subset at a
@@ -199,8 +281,10 @@ module Net
199
281
  # occurrence in entries.
200
282
  #
201
283
  # <i>Set cardinality:</i>
202
- # - #count (aliased as #size): Returns the count of numbers in the set.
203
- # Duplicated numbers are not counted.
284
+ # - #cardinality: Returns the number of distinct members in the set.
285
+ # <tt>*</tt> is counted as its own member, distinct from UINT32_MAX.
286
+ # - #count: Returns the count of distinct numbers in the set.
287
+ # <tt>*</tt> is counted as equal to UINT32_MAX.
204
288
  # - #empty?: Returns whether the set has no members. \IMAP syntax does not
205
289
  # allow empty sequence sets.
206
290
  # - #valid?: Returns whether the set has any members.
@@ -208,12 +292,18 @@ module Net
208
292
  # <tt>*</tt>.
209
293
  #
210
294
  # <i>Denormalized properties:</i>
295
+ # - #normalized?: Returns whether #entries are sorted, deduplicated, and
296
+ # coalesced, and all #string entries are in normalized form.
211
297
  # - #has_duplicates?: Returns whether the ordered entries repeat any
212
298
  # numbers.
213
- # - #count_duplicates: Returns the count of repeated numbers in the ordered
214
- # entries.
299
+ # - #size: Returns the total size of all #entries, including repeated
300
+ # numbers. <tt>*</tt> is counted as its own member, distinct from
301
+ # UINT32_MAX.
215
302
  # - #count_with_duplicates: Returns the count of numbers in the ordered
216
- # entries, including any repeated numbers.
303
+ # #entries, including repeated numbers. <tt>*</tt> is counted as
304
+ # equal to UINT32_MAX.
305
+ # - #count_duplicates: Returns the count of repeated numbers in the ordered
306
+ # #entries. <tt>*</tt> is counted as equal to UINT32_MAX.
217
307
  #
218
308
  # === Methods for Iterating
219
309
  #
@@ -252,11 +342,15 @@ module Net
252
342
  # +self+ and the other set except those common to both.
253
343
  # - #~ (aliased as #complement): Returns a new set containing all members
254
344
  # that are not in +self+
345
+ # - #above: Return a copy of +self+ which only contains numbers above a
346
+ # given number.
347
+ # - #below: Return a copy of +self+ which only contains numbers below a
348
+ # given value.
255
349
  # - #limit: Returns a copy of +self+ which has replaced <tt>*</tt> with a
256
350
  # given maximum value and removed all members over that maximum.
257
351
  #
258
352
  # === Methods for Assigning
259
- # These methods add or replace elements in +self+.
353
+ # These methods add or replace numbers in +self+.
260
354
  #
261
355
  # <i>Normalized (sorted and coalesced):</i>
262
356
  #
@@ -265,8 +359,12 @@ module Net
265
359
  # - #add (aliased as #<<): Adds a given element to the set; returns +self+.
266
360
  # - #add?: If the given element is not fully included the set, adds it and
267
361
  # returns +self+; otherwise, returns +nil+.
268
- # - #merge: Adds all members of the given sets into this set; returns +self+.
269
- # - #complement!: Replaces the contents of the set with its own #complement.
362
+ # - #merge: In-place set #union. Adds all members of the given sets into
363
+ # this set; returns +self+.
364
+ # - #complement!: In-place set #complement. Replaces the contents of this
365
+ # set with its own #complement; returns +self+.
366
+ # - #xor!: In-place +XOR+ operation. Adds numbers that are unique to the
367
+ # other set and removes numbers that are common to both; returns +self+.
270
368
  #
271
369
  # <i>Order preserving:</i>
272
370
  #
@@ -279,7 +377,7 @@ module Net
279
377
  # of a given object.
280
378
  #
281
379
  # === Methods for Deleting
282
- # These methods remove elements from +self+, and update #string to be fully
380
+ # These methods remove numbers from +self+, and update #string to be fully
283
381
  # sorted and coalesced.
284
382
  #
285
383
  # - #clear: Removes all elements in the set; returns +self+.
@@ -287,10 +385,12 @@ module Net
287
385
  # - #delete?: If the given element is included in the set, removes it and
288
386
  # returns it; otherwise, returns +nil+.
289
387
  # - #delete_at: Removes the number at a given offset.
388
+ # - #intersect!: In-place set #intersection. Removes numbers that are not
389
+ # in the given set; returns +self+.
290
390
  # - #slice!: Removes the number or consecutive numbers at a given offset or
291
391
  # range of offsets.
292
- # - #subtract: Removes all members of the given sets from this set; returns
293
- # +self+.
392
+ # - #subtract: In-place set #difference. Removes all members of the given
393
+ # sets from this set; returns +self+.
294
394
  # - #limit!: Replaces <tt>*</tt> with a given maximum value and removes all
295
395
  # members over that maximum; returns +self+.
296
396
  #
@@ -318,11 +418,24 @@ module Net
318
418
 
319
419
  # valid inputs for "*"
320
420
  STARS = [:*, ?*, -1].freeze
321
- private_constant :STAR_INT, :STARS
421
+ private_constant :STARS
322
422
 
323
- COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
324
- ENUMABLE = ->{ _1.respond_to?(:each) && _1.respond_to?(:empty?) }
325
- private_constant :COERCIBLE, :ENUMABLE
423
+ INSPECT_MAX_LEN = 512
424
+ INSPECT_TRUNCATE_LEN = 16
425
+ private_constant :INSPECT_MAX_LEN, :INSPECT_TRUNCATE_LEN
426
+
427
+ # /(,\d+){100}\z/ is shockingly slow on huge strings.
428
+ # /(,\d{0,10}){100}\z/ is ok, but ironically, Regexp.linear_time? is false.
429
+ #
430
+ # This unrolls all nested quantifiers. It's much harder to read, but it's
431
+ # also the fastest out of all the versions I tested.
432
+ nz_uint32 = /[1-9](?:\d(?:\d(?:\d(?:\d(?:\d(?:\d(?:\d(?:\d(?:\d)?)?)?)?)?)?)?)?)?/
433
+ num_or_star = /#{nz_uint32}|\*/
434
+ entry = /#{num_or_star}(?::#{num_or_star})?/
435
+ entries = ([entry] * INSPECT_TRUNCATE_LEN).join(",")
436
+ INSPECT_ABRIDGED_HEAD_RE = /\A#{entries},/
437
+ INSPECT_ABRIDGED_TAIL_RE = /,#{entries}\z/
438
+ private_constant :INSPECT_ABRIDGED_HEAD_RE, :INSPECT_ABRIDGED_TAIL_RE
326
439
 
327
440
  class << self
328
441
 
@@ -337,13 +450,12 @@ module Net
337
450
  # An empty SequenceSet is invalid and will raise a DataFormatError.
338
451
  #
339
452
  # Use ::new to create a mutable or empty SequenceSet.
453
+ #
454
+ # Related: ::new, Net::IMAP::SequenceSet(), ::try_convert
340
455
  def [](first, *rest)
341
456
  if rest.empty?
342
- if first.is_a?(SequenceSet) && first.frozen? && first.valid?
343
- first
344
- else
345
- new(first).validate.freeze
346
- end
457
+ set = try_convert(first)&.validate
458
+ set&.frozen? ? set : (set&.dup || new(first).validate).freeze
347
459
  else
348
460
  new(first).merge(*rest).validate.freeze
349
461
  end
@@ -356,12 +468,14 @@ module Net
356
468
  # +to_sequence_set+, calls +obj.to_sequence_set+ and returns the result.
357
469
  # Otherwise returns +nil+.
358
470
  #
359
- # If +obj.to_sequence_set+ doesn't return a SequenceSet, an exception is
360
- # raised.
471
+ # If +obj.to_sequence_set+ doesn't return a SequenceSet or +nil+, an
472
+ # exception is raised.
473
+ #
474
+ # Related: Net::IMAP::SequenceSet(), ::new, ::[]
361
475
  def try_convert(obj)
362
476
  return obj if obj.is_a?(SequenceSet)
363
477
  return nil unless obj.respond_to?(:to_sequence_set)
364
- obj = obj.to_sequence_set
478
+ return nil unless obj = obj.to_sequence_set
365
479
  return obj if obj.is_a?(SequenceSet)
366
480
  raise DataFormatError, "invalid object returned from to_sequence_set"
367
481
  end
@@ -376,23 +490,96 @@ module Net
376
490
  end
377
491
 
378
492
  # 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.
383
- def initialize(input = nil) input ? replace(input) : clear end
493
+ # SequenceSet, an IMAP formatted +sequence-set+ string, a non-zero 32 bit
494
+ # unsigned integer, a range, <tt>:*</tt>, a Set of numbers or <tt>*</tt>,
495
+ # an object that responds to +to_sequence_set+ (such as SearchResult) or
496
+ # an Array of these (array inputs may be nested).
497
+ #
498
+ # set = Net::IMAP::SequenceSet.new(1)
499
+ # set.valid_string #=> "1"
500
+ # set = Net::IMAP::SequenceSet.new(1..100)
501
+ # set.valid_string #=> "1:100"
502
+ # set = Net::IMAP::SequenceSet.new(1...100)
503
+ # set.valid_string #=> "1:99"
504
+ # set = Net::IMAP::SequenceSet.new([1, 2, 5..])
505
+ # set.valid_string #=> "1:2,5:*"
506
+ # set = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
507
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
508
+ # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
509
+ # set.valid_string #=> "1:10,1024,2048"
510
+ #
511
+ # With no arguments (or +nil+) creates an empty sequence set. Note that
512
+ # an empty sequence set is invalid in the \IMAP grammar.
513
+ #
514
+ # set = Net::IMAP::SequenceSet.new
515
+ # set.empty? #=> true
516
+ # set.valid? #=> false
517
+ # set.valid_string #!> raises DataFormatError
518
+ # set << 1..10
519
+ # set.empty? #=> false
520
+ # set.valid? #=> true
521
+ # set.valid_string #=> "1:10"
522
+ #
523
+ # When +input+ is a SequenceSet, ::new behaves the same as calling #dup on
524
+ # that other set. The input's #string will be preserved.
525
+ #
526
+ # input = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
527
+ # copy = Net::IMAP::SequenceSet.new(input)
528
+ # input.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
529
+ # copy.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
530
+ # copy2 = input.dup # same as calling new with a SequenceSet input
531
+ # copy == input #=> true, same set membership
532
+ # copy.eql? input #=> true, same string value
533
+ # copy.equal? input #=> false, different objects
534
+ #
535
+ # copy.normalize!
536
+ # copy.valid_string #=> "1:10,1024,2048"
537
+ # copy == input #=> true, same set membership
538
+ # copy.eql? input #=> false, different string value
539
+ #
540
+ # copy << 999
541
+ # copy.valid_string #=> "1:10,999,1024,2048"
542
+ # copy == input #=> false, different set membership
543
+ # copy.eql? input #=> false, different string value
544
+ #
545
+ # === Alternative set creation methods
546
+ #
547
+ # * ::[] returns a frozen validated (non-empty) SequenceSet, without
548
+ # allocating a new object when the input is already a valid frozen
549
+ # SequenceSet.
550
+ # * Net::IMAP::SequenceSet() coerces an input to SequenceSet, without
551
+ # allocating a new object when the input is already a SequenceSet.
552
+ # * ::try_convert calls +to_sequence_set+ on inputs that support it and
553
+ # returns +nil+ for inputs that don't.
554
+ # * ::empty and ::full both return frozen singleton sets which can be
555
+ # combined with set operations (#|, #&, #^, #-, etc) to make new sets.
556
+ #
557
+ # See SequenceSet@Creating+sequence+sets.
558
+ def initialize(input = nil)
559
+ @set_data = new_set_data
560
+ @string = nil
561
+ replace(input) unless input.nil?
562
+ end
384
563
 
385
564
  # Removes all elements and returns self.
386
- def clear; @tuples, @string = [], nil; self end
565
+ def clear
566
+ modifying! # redundant check (normalizes the error message for JRuby)
567
+ set_data.clear
568
+ @string = nil
569
+ self
570
+ end
387
571
 
388
572
  # Replace the contents of the set with the contents of +other+ and returns
389
573
  # +self+.
390
574
  #
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.
575
+ # +other+ may be another SequenceSet or any other object that would be
576
+ # accepted by ::new.
393
577
  def replace(other)
394
578
  case other
395
- when SequenceSet then initialize_dup(other)
579
+ when SequenceSet then
580
+ modifying! # short circuit before doing any work
581
+ @set_data = other.dup_set_data
582
+ @string = other.instance_variable_get(:@string)
396
583
  when String then self.string = other
397
584
  else clear; merge other
398
585
  end
@@ -421,39 +608,51 @@ module Net
421
608
  # If the set was created from a single string, it is not normalized. If
422
609
  # the set is updated the string will be normalized.
423
610
  #
424
- # Related: #valid_string, #normalized_string, #to_s
425
- def string; @string ||= normalized_string if valid? end
611
+ # Related: #valid_string, #normalized_string, #to_s, #inspect
612
+ def string; @string || normalized_string if valid? end
613
+
614
+ # Returns an array with #normalized_string when valid and an empty array
615
+ # otherwise.
616
+ def deconstruct; valid? ? [normalized_string] : [] end
426
617
 
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.
618
+ # Assigns a new string to #string and resets #elements to match.
619
+ # Assigning +nil+ or an empty string are equivalent to calling #clear.
620
+ #
621
+ # Non-empty strings are validated but not normalized.
430
622
  #
431
- # Use #add or #merge to add a string to an existing set.
623
+ # Use #add, #merge, or #append to add a string to an existing set.
432
624
  #
433
625
  # Related: #replace, #clear
434
- def string=(str)
435
- if str.nil?
626
+ def string=(input)
627
+ if input.nil?
436
628
  clear
629
+ elsif (str = String.try_convert(input))
630
+ modifying! # short-circuit before parsing the string
631
+ entries = each_parsed_entry(str).to_a
632
+ clear
633
+ if normalized_entries?(entries)
634
+ replace_minmaxes entries.map!(&:minmax)
635
+ else
636
+ add_minmaxes entries.map!(&:minmax)
637
+ @string = -str
638
+ end
437
639
  else
438
- str = String.try_convert(str) or raise ArgumentError, "not a string"
439
- tuples = str_to_tuples str
440
- @tuples, @string = [], -str
441
- tuples_add tuples
640
+ raise ArgumentError, "expected a string or nil, got #{input.class}"
442
641
  end
642
+ input
443
643
  end
444
644
 
445
645
  # Returns the \IMAP +sequence-set+ string representation, or an empty
446
646
  # string when the set is empty. Note that an empty set is invalid in the
447
647
  # \IMAP syntax.
448
648
  #
449
- # Related: #valid_string, #normalized_string, #to_s
649
+ # Related: #string, #valid_string, #normalized_string, #inspect
450
650
  def to_s; string || "" end
451
651
 
452
652
  # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
453
653
  def freeze
454
654
  return self if frozen?
455
- string
456
- @tuples.each(&:freeze).freeze
655
+ freeze_set_data
457
656
  super
458
657
  end
459
658
 
@@ -475,7 +674,7 @@ module Net
475
674
  # Related: #eql?, #normalize
476
675
  def ==(other)
477
676
  self.class == other.class &&
478
- (to_s == other.to_s || tuples == other.tuples)
677
+ (to_s == other.to_s || set_data == other.set_data)
479
678
  end
480
679
 
481
680
  # :call-seq: eql?(other) -> true or false
@@ -499,8 +698,9 @@ module Net
499
698
 
500
699
  # :call-seq: self === other -> true | false | nil
501
700
  #
502
- # Returns whether +other+ is contained within the set. Returns +nil+ if a
503
- # StandardError is raised while converting +other+ to a comparable type.
701
+ # Returns whether +other+ is contained within the set. +other+ may be any
702
+ # object that would be accepted by ::new. Returns +nil+ if StandardError
703
+ # is raised while converting +other+ to a comparable type.
504
704
  #
505
705
  # Related: #cover?, #include?, #include_star?
506
706
  def ===(other)
@@ -514,12 +714,12 @@ module Net
514
714
  # Returns whether +other+ is contained within the set. +other+ may be any
515
715
  # object that would be accepted by ::new.
516
716
  #
517
- # Related: #===, #include?, #include_star?
518
- def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
717
+ # Related: #===, #include?, #include_star?, #intersect?
718
+ def cover?(other) import_runs(other).none? { !include_run?(_1) } end
519
719
 
520
720
  # 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>.
721
+ # otherwise. Returns +nil+ when +number+ isn't a valid SequenceSet
722
+ # element (Integer, Range, <tt>*</tt>, +sequence-set+ string).
523
723
  #
524
724
  # set = Net::IMAP::SequenceSet["5:10,100,111:115"]
525
725
  # set.include? 1 #=> false
@@ -527,8 +727,8 @@ module Net
527
727
  # set.include? 11..20 #=> false
528
728
  # set.include? 100 #=> true
529
729
  # 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
730
+ # set.include? 6..9 #=> true, covered by "5:10"
731
+ # set.include? "6:9" #=> true, strings are parsed
532
732
  # set.include? 4..9 #=> false, intersection is not sufficient
533
733
  # set.include? "*" #=> false, use #limit to re-interpret "*"
534
734
  # set.include? -1 #=> false, -1 is interpreted as "*"
@@ -537,16 +737,19 @@ module Net
537
737
  # set.include? :* #=> true
538
738
  # set.include? "*" #=> true
539
739
  # set.include? -1 #=> true
540
- # set.include? 200.. #=> true
541
- # set.include? 100.. #=> false
740
+ # set.include?(200..) #=> true
741
+ # set.include?(100..) #=> false
542
742
  #
543
- # Related: #include_star?, #cover?, #===
544
- def include?(element) include_tuple? input_to_tuple element end
743
+ # Related: #include_star?, #cover?, #===, #intersect?
744
+ def include?(element)
745
+ run = import_run element rescue nil
746
+ !!include_run?(run) if run
747
+ end
545
748
 
546
749
  alias member? include?
547
750
 
548
751
  # Returns +true+ when the set contains <tt>*</tt>.
549
- def include_star?; @tuples.last&.last == STAR_INT end
752
+ def include_star?; max_num == STAR_INT end
550
753
 
551
754
  # Returns +true+ if the set and a given object have any common elements,
552
755
  # +false+ otherwise.
@@ -554,9 +757,9 @@ module Net
554
757
  # Net::IMAP::SequenceSet["5:10"].intersect? "7,9,11" #=> true
555
758
  # Net::IMAP::SequenceSet["5:10"].intersect? "11:33" #=> false
556
759
  #
557
- # Related: #intersection, #disjoint?
760
+ # Related: #intersection, #disjoint?, #cover?, #include?
558
761
  def intersect?(other)
559
- valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
762
+ valid? && import_runs(other).any? { intersect_run? _1 }
560
763
  end
561
764
  alias overlap? intersect?
562
765
 
@@ -568,39 +771,70 @@ module Net
568
771
  #
569
772
  # Related: #intersection, #intersect?
570
773
  def disjoint?(other)
571
- empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
774
+ empty? || import_runs(other).none? { intersect_run? _1 }
572
775
  end
573
776
 
574
- # :call-seq: max(star: :*) => integer or star or nil
777
+ # :call-seq:
778
+ # max(star: :*) => integer or star or nil
779
+ # max(count) => SequenceSet
575
780
  #
576
781
  # Returns the maximum value in +self+, +star+ when the set includes
577
782
  # <tt>*</tt>, or +nil+ when the set is empty.
578
- def max(star: :*)
579
- (val = @tuples.last&.last) && val == STAR_INT ? star : val
783
+ #
784
+ # When +count+ is given, a new SequenceSet is returned, containing only
785
+ # the last +count+ numbers. An empty SequenceSet is returned when +self+
786
+ # is empty. (+star+ is ignored when +count+ is given.)
787
+ #
788
+ # Related: #min, #minmax, #slice
789
+ def max(count = nil, star: :*)
790
+ if count
791
+ if cardinality <= count
792
+ frozen? ? self : dup
793
+ else
794
+ slice(-count..) || remain_frozen_empty
795
+ end
796
+ elsif (val = max_num)
797
+ val == STAR_INT ? star : val
798
+ end
580
799
  end
581
800
 
582
- # :call-seq: min(star: :*) => integer or star or nil
801
+ # :call-seq:
802
+ # min(star: :*) => integer or star or nil
803
+ # min(count) => SequenceSet
583
804
  #
584
805
  # Returns the minimum value in +self+, +star+ when the only value in the
585
806
  # 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
807
+ #
808
+ # When +count+ is given, a new SequenceSet is returned, containing only
809
+ # the first +count+ numbers. An empty SequenceSet is returned when +self+
810
+ # is empty. (+star+ is ignored when +count+ is given.)
811
+ #
812
+ # Related: #max, #minmax, #slice
813
+ def min(count = nil, star: :*)
814
+ if count
815
+ slice(0...count) || remain_frozen_empty
816
+ elsif (val = min_num)
817
+ val != STAR_INT ? val : star
818
+ end
588
819
  end
589
820
 
590
- # :call-seq: minmax(star: :*) => nil or [integer, integer or star]
821
+ # :call-seq: minmax(star: :*) => [min, max] or nil
591
822
  #
592
823
  # Returns a 2-element array containing the minimum and maximum numbers in
593
- # +self+, or +nil+ when the set is empty.
824
+ # +self+, or +nil+ when the set is empty. +star+ is handled the same way
825
+ # as by #min and #max.
826
+ #
827
+ # Related: #min, #max
594
828
  def minmax(star: :*); [min(star: star), max(star: star)] unless empty? end
595
829
 
596
830
  # Returns false when the set is empty.
597
831
  def valid?; !empty? end
598
832
 
599
833
  # Returns true if the set contains no elements
600
- def empty?; @tuples.empty? end
834
+ def empty?; runs.empty? end
601
835
 
602
836
  # Returns true if the set contains every possible element.
603
- def full?; @tuples == [[1, STAR_INT]] end
837
+ def full?; set_data == FULL_SET_DATA end
604
838
 
605
839
  # :call-seq:
606
840
  # self + other -> sequence set
@@ -610,14 +844,19 @@ module Net
610
844
  # Returns a new sequence set that has every number in the +other+ object
611
845
  # added.
612
846
  #
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.
847
+ # +other+ may be any object that would be accepted by ::new.
616
848
  #
617
849
  # Net::IMAP::SequenceSet["1:5"] | 2 | [4..6, 99]
618
850
  # #=> Net::IMAP::SequenceSet["1:6,99"]
619
851
  #
620
- # Related: #add, #merge
852
+ # Related: #add, #merge, #&, #-, #^, #~
853
+ #
854
+ # ==== Set identities
855
+ #
856
+ # <tt>lhs | rhs</tt> is equivalent to:
857
+ # * <tt>rhs | lhs</tt> (commutative)
858
+ # * <tt>~(~lhs & ~rhs)</tt> (De Morgan's Law)
859
+ # * <tt>(lhs & rhs) ^ (lhs ^ rhs)</tt>
621
860
  def |(other) remain_frozen dup.merge other end
622
861
  alias :+ :|
623
862
  alias union :|
@@ -629,14 +868,22 @@ module Net
629
868
  # Returns a new sequence set built by duplicating this set and removing
630
869
  # every number that appears in +other+.
631
870
  #
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.
871
+ # +other+ may be any object that would be accepted by ::new.
635
872
  #
636
873
  # Net::IMAP::SequenceSet[1..5] - 2 - 4 - 6
637
874
  # #=> Net::IMAP::SequenceSet["1,3,5"]
638
875
  #
639
- # Related: #subtract
876
+ # Related: #subtract, #|, #&, #^, #~
877
+ #
878
+ # ==== Set identities
879
+ #
880
+ # <tt>lhs - rhs</tt> is equivalent to:
881
+ # * <tt>~rhs - ~lhs</tt>
882
+ # * <tt>lhs & ~rhs</tt>
883
+ # * <tt>~(~lhs | rhs)</tt>
884
+ # * <tt>lhs & (lhs ^ rhs)</tt>
885
+ # * <tt>lhs ^ (lhs & rhs)</tt>
886
+ # * <tt>rhs ^ (lhs | rhs)</tt>
640
887
  def -(other) remain_frozen dup.subtract other end
641
888
  alias difference :-
642
889
 
@@ -647,17 +894,23 @@ module Net
647
894
  # Returns a new sequence set containing only the numbers common to this
648
895
  # set and +other+.
649
896
  #
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.
897
+ # +other+ may be any object that would be accepted by ::new.
653
898
  #
654
899
  # Net::IMAP::SequenceSet[1..5] & [2, 4, 6]
655
900
  # #=> Net::IMAP::SequenceSet["2,4"]
656
901
  #
657
- # <tt>(seqset & other)</tt> is equivalent to <tt>(seqset - ~other)</tt>.
658
- def &(other)
659
- remain_frozen dup.subtract SequenceSet.new(other).complement!
660
- end
902
+ # Related: #intersect?, #|, #-, #^, #~
903
+ #
904
+ # ==== Set identities
905
+ #
906
+ # <tt>lhs & rhs</tt> is equivalent to:
907
+ # * <tt>rhs & lhs</tt> (commutative)
908
+ # * <tt>~(~lhs | ~rhs)</tt> (De Morgan's Law)
909
+ # * <tt>lhs - ~rhs</tt>
910
+ # * <tt>lhs - (lhs - rhs)</tt>
911
+ # * <tt>lhs - (lhs ^ rhs)</tt>
912
+ # * <tt>lhs ^ (lhs - rhs)</tt>
913
+ def &(other) remain_frozen dup.intersect! other end
661
914
  alias intersection :&
662
915
 
663
916
  # :call-seq:
@@ -667,16 +920,22 @@ module Net
667
920
  # Returns a new sequence set containing numbers that are exclusive between
668
921
  # this set and +other+.
669
922
  #
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.
923
+ # +other+ may be any object that would be accepted by ::new.
673
924
  #
674
925
  # Net::IMAP::SequenceSet[1..5] ^ [2, 4, 6]
675
926
  # #=> Net::IMAP::SequenceSet["1,3,5:6"]
676
927
  #
677
- # <tt>(seqset ^ other)</tt> is equivalent to <tt>((seqset | other) -
678
- # (seqset & other))</tt>.
679
- def ^(other) remain_frozen (dup | other).subtract(self & other) end
928
+ # Related: #|, #&, #-, #~
929
+ #
930
+ # ==== Set identities
931
+ #
932
+ # <tt>lhs ^ rhs</tt> is equivalent to:
933
+ # * <tt>rhs ^ lhs</tt> (commutative)
934
+ # * <tt>~lhs ^ ~rhs</tt>
935
+ # * <tt>(lhs | rhs) - (lhs & rhs)</tt>
936
+ # * <tt>(lhs - rhs) | (rhs - lhs)</tt>
937
+ # * <tt>(lhs ^ other) ^ (other ^ rhs)</tt>
938
+ def ^(other) remain_frozen dup.xor! other end
680
939
  alias xor :^
681
940
 
682
941
  # :call-seq:
@@ -693,7 +952,12 @@ module Net
693
952
  # ~Net::IMAP::SequenceSet["6:99,223:*"]
694
953
  # #=> Net::IMAP::SequenceSet["1:5,100:222"]
695
954
  #
696
- # Related: #complement!
955
+ # Related: #complement!, #|, #&, #-, #^
956
+ #
957
+ # ==== Set identities
958
+ #
959
+ # <tt>~set</tt> is equivalent to:
960
+ # * <tt>full - set</tt>, where "full" is Net::IMAP::SequenceSet.full
697
961
  def ~; remain_frozen dup.complement! end
698
962
  alias complement :~
699
963
 
@@ -705,9 +969,13 @@ module Net
705
969
  #
706
970
  # #string will be regenerated. Use #merge to add many elements at once.
707
971
  #
708
- # Related: #add?, #merge, #union
972
+ # Use #append to append new elements to #string. See
973
+ # SequenceSet@Ordered+and+Normalized+sets.
974
+ #
975
+ # Related: #add?, #merge, #union, #append
709
976
  def add(element)
710
- tuple_add input_to_tuple element
977
+ modifying! # short-circuit before import_run
978
+ add_run import_run element
711
979
  normalize!
712
980
  end
713
981
  alias << add
@@ -716,13 +984,56 @@ module Net
716
984
  #
717
985
  # Unlike #add, #merge, or #union, the new value is appended to #string.
718
986
  # This may result in a #string which has duplicates or is out-of-order.
987
+ #
988
+ # set = Net::IMAP::SequenceSet.new
989
+ # set.append(1..2) # => Net::IMAP::SequenceSet("1:2")
990
+ # set.append(5) # => Net::IMAP::SequenceSet("1:2,5")
991
+ # set.append(4) # => Net::IMAP::SequenceSet("1:2,5,4")
992
+ # set.append(3) # => Net::IMAP::SequenceSet("1:2,5,4,3")
993
+ # set.append(2) # => Net::IMAP::SequenceSet("1:2,5,4,3,2")
994
+ #
995
+ # If +entry+ is a string, it will be converted into normal form.
996
+ #
997
+ # set = Net::IMAP::SequenceSet("4:5,1:2")
998
+ # set.append("6:6") # => Net::IMAP::SequenceSet("4:5,1:2,6")
999
+ # set.append("9:8") # => Net::IMAP::SequenceSet("4:5,1:2,6,8:9")
1000
+ #
1001
+ # If +entry+ adjacently follows the last entry, they will coalesced:
1002
+ # set = Net::IMAP::SequenceSet.new("2,1,9:10")
1003
+ # set.append(11..12) # => Net::IMAP::SequenceSet("2,1,9:12")
1004
+ #
1005
+ # Non-normalized sets store the string <em>in addition to</em> an internal
1006
+ # normalized uint32 set representation. This can more than double memory
1007
+ # usage, so large sets should avoid using #append unless preserving order
1008
+ # is required. See SequenceSet@Ordered+and+Normalized+sets.
1009
+ #
1010
+ # Related: #add, #merge, #union
719
1011
  def append(entry)
720
- modifying!
721
- tuple = input_to_tuple entry
722
- entry = tuple_to_str tuple
723
- string unless empty? # write @string before tuple_add
724
- tuple_add tuple
725
- @string = -(@string ? "#{@string},#{entry}" : entry)
1012
+ modifying! # short-circuit before import_minmax
1013
+ minmax = import_minmax entry
1014
+ adj = minmax.first - 1
1015
+ if @string.nil? && (runs.empty? || max_num <= adj)
1016
+ # append to elements or coalesce with last element
1017
+ add_minmax minmax
1018
+ return self
1019
+ elsif @string.nil?
1020
+ # generate string for out-of-order append
1021
+ head, comma = normalized_string, ","
1022
+ else
1023
+ # @string already exists... maybe coalesce with last entry
1024
+ head, comma, last_entry = @string.rpartition(",")
1025
+ last_min, last_max = import_minmax last_entry
1026
+ if last_max == adj
1027
+ # coalesce with last entry
1028
+ minmax[0] = last_min
1029
+ else
1030
+ # append to existing string
1031
+ head, comma = @string, ","
1032
+ end
1033
+ end
1034
+ entry = export_minmax minmax
1035
+ add_minmax minmax
1036
+ @string = -"#{head}#{comma}#{entry}"
726
1037
  self
727
1038
  end
728
1039
 
@@ -735,6 +1046,7 @@ module Net
735
1046
  #
736
1047
  # Related: #add, #merge, #union, #include?
737
1048
  def add?(element)
1049
+ modifying! # short-circuit before include?
738
1050
  add element unless include? element
739
1051
  end
740
1052
 
@@ -747,7 +1059,8 @@ module Net
747
1059
  #
748
1060
  # Related: #delete?, #delete_at, #subtract, #difference
749
1061
  def delete(element)
750
- tuple_subtract input_to_tuple element
1062
+ modifying! # short-circuit before import_run
1063
+ subtract_run import_run element
751
1064
  normalize!
752
1065
  end
753
1066
 
@@ -784,15 +1097,17 @@ module Net
784
1097
  #
785
1098
  # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
786
1099
  def delete?(element)
787
- tuple = input_to_tuple element
788
- if tuple.first == tuple.last
789
- return unless include_tuple? tuple
790
- tuple_subtract tuple
1100
+ modifying! # short-circuit before import_minmax
1101
+ element = input_try_convert(element)
1102
+ minmax = import_minmax element
1103
+ if number_input?(element)
1104
+ return unless include_minmax? minmax
1105
+ subtract_minmax minmax
791
1106
  normalize!
792
- from_tuple_int tuple.first
1107
+ export_num minmax.first
793
1108
  else
794
1109
  copy = dup
795
- tuple_subtract tuple
1110
+ subtract_minmax minmax
796
1111
  normalize!
797
1112
  copy if copy.subtract(self).valid?
798
1113
  end
@@ -824,35 +1139,34 @@ module Net
824
1139
  #
825
1140
  # Related: #slice, #delete_at, #delete, #delete?, #subtract, #difference
826
1141
  def slice!(index, length = nil)
1142
+ modifying! # short-circuit before slice
827
1143
  deleted = slice(index, length) and subtract deleted
828
1144
  deleted
829
1145
  end
830
1146
 
831
- # Merges all of the elements that appear in any of the +sets+ into the
832
- # set, and returns +self+.
1147
+ # In-place set #union. Merges all of the elements that appear in any of
1148
+ # the +sets+ into this set, and returns +self+.
833
1149
  #
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.
1150
+ # The +sets+ may be any objects that would be accepted by ::new.
837
1151
  #
838
1152
  # #string will be regenerated after all sets have been merged.
839
1153
  #
840
1154
  # Related: #add, #add?, #union
841
1155
  def merge(*sets)
842
- tuples_add input_to_tuples sets
1156
+ modifying! # short-circuit before import_runs
1157
+ add_runs import_runs sets
843
1158
  normalize!
844
1159
  end
845
1160
 
846
- # Removes all of the elements that appear in any of the given +sets+ from
847
- # the set, and returns +self+.
1161
+ # In-place set #difference. Removes all of the elements that appear in
1162
+ # any of the given +sets+ from this set, and returns +self+.
848
1163
  #
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.
1164
+ # The +sets+ may be any objects that would be accepted by ::new.
852
1165
  #
853
1166
  # Related: #difference
854
1167
  def subtract(*sets)
855
- tuples_subtract input_to_tuples sets
1168
+ modifying! # short-circuit before import_runs
1169
+ subtract_runs import_runs sets
856
1170
  normalize!
857
1171
  end
858
1172
 
@@ -864,21 +1178,21 @@ module Net
864
1178
  # This is useful when the given order is significant, for example in a
865
1179
  # ESEARCH response to IMAP#sort.
866
1180
  #
1181
+ # See SequenceSet@Ordered+and+Normalized+sets.
1182
+ #
867
1183
  # Related: #each_entry, #elements
868
1184
  def entries; each_entry.to_a end
869
1185
 
870
1186
  # Returns an array of ranges and integers and <tt>:*</tt>.
871
1187
  #
872
1188
  # The returned elements are sorted and coalesced, even when the input
873
- # #string is not. <tt>*</tt> will sort last. See #normalize.
1189
+ # #string is not. <tt>*</tt> will sort last. See #normalize,
1190
+ # SequenceSet@Ordered+and+Normalized+sets.
874
1191
  #
875
1192
  # By itself, <tt>*</tt> translates to <tt>:*</tt>. A range containing
876
1193
  # <tt>*</tt> translates to an endless range. Use #limit to translate both
877
1194
  # cases to a maximum value.
878
1195
  #
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
1196
  # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].elements
883
1197
  # #=> [2, 5..9, 11..12, :*]
884
1198
  #
@@ -889,15 +1203,13 @@ module Net
889
1203
  # Returns an array of ranges
890
1204
  #
891
1205
  # The returned elements are sorted and coalesced, even when the input
892
- # #string is not. <tt>*</tt> will sort last. See #normalize.
1206
+ # #string is not. <tt>*</tt> will sort last. See #normalize,
1207
+ # SequenceSet@Ordered+and+Normalized+sets.
893
1208
  #
894
1209
  # <tt>*</tt> translates to an endless range. By itself, <tt>*</tt>
895
1210
  # translates to <tt>:*..</tt>. Use #limit to set <tt>*</tt> to a maximum
896
1211
  # value.
897
1212
  #
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
1213
  # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
902
1214
  # #=> [2..2, 5..9, 11..12, :*..]
903
1215
  # Net::IMAP::SequenceSet["123,999:*,456:789"].ranges
@@ -909,7 +1221,7 @@ module Net
909
1221
  # Returns a sorted array of all of the number values in the sequence set.
910
1222
  #
911
1223
  # The returned numbers are sorted and de-duplicated, even when the input
912
- # #string is not. See #normalize.
1224
+ # #string is not. See #normalize, SequenceSet@Ordered+and+Normalized+sets.
913
1225
  #
914
1226
  # Net::IMAP::SequenceSet["2,5:9,6,12:11"].numbers
915
1227
  # #=> [2, 5, 6, 7, 8, 9, 11, 12]
@@ -941,54 +1253,34 @@ module Net
941
1253
  # no sorting, deduplication, or coalescing. When #string is in its
942
1254
  # normalized form, this will yield the same values as #each_element.
943
1255
  #
1256
+ # See SequenceSet@Ordered+and+Normalized+sets.
1257
+ #
944
1258
  # Related: #entries, #each_element
945
1259
  def each_entry(&block) # :yields: integer or range or :*
946
1260
  return to_enum(__method__) unless block_given?
947
- each_entry_tuple do yield tuple_to_entry _1 end
1261
+ each_entry_run do yield export_run_entry _1 end
948
1262
  end
949
1263
 
950
1264
  # Yields each number or range (or <tt>:*</tt>) in #elements to the block
951
1265
  # and returns self. Returns an enumerator when called without a block.
952
1266
  #
953
1267
  # The returned numbers are sorted and de-duplicated, even when the input
954
- # #string is not. See #normalize.
1268
+ # #string is not. See #normalize, SequenceSet@Ordered+and+Normalized+sets.
955
1269
  #
956
1270
  # Related: #elements, #each_entry
957
1271
  def each_element # :yields: integer or range or :*
958
1272
  return to_enum(__method__) unless block_given?
959
- @tuples.each do yield tuple_to_entry _1 end
960
- self
961
- end
962
-
963
- private
964
-
965
- def each_entry_tuple(&block)
966
- return to_enum(__method__) unless block_given?
967
- if @string
968
- @string.split(",") do block.call str_to_tuple _1 end
969
- else
970
- @tuples.each(&block)
971
- end
1273
+ runs.each do yield export_run_entry _1 end
972
1274
  self
973
1275
  end
974
1276
 
975
- def tuple_to_entry((min, max))
976
- if min == STAR_INT then :*
977
- elsif max == STAR_INT then min..
978
- elsif min == max then min
979
- else min..max
980
- end
981
- end
982
-
983
- public
984
-
985
1277
  # Yields each range in #ranges to the block and returns self.
986
1278
  # Returns an enumerator when called without a block.
987
1279
  #
988
1280
  # Related: #ranges
989
1281
  def each_range # :yields: range
990
1282
  return to_enum(__method__) unless block_given?
991
- @tuples.each do |min, max|
1283
+ minmaxes.each do |min, max|
992
1284
  if min == STAR_INT then yield :*..
993
1285
  elsif max == STAR_INT then yield min..
994
1286
  else yield min..max
@@ -1007,7 +1299,7 @@ module Net
1007
1299
  def each_number(&block) # :yields: integer
1008
1300
  return to_enum(__method__) unless block_given?
1009
1301
  raise RangeError, '%s contains "*"' % [self.class] if include_star?
1010
- @tuples.each do each_number_in_tuple _1, _2, &block end
1302
+ minmaxes.each do each_number_in_minmax _1, _2, &block end
1011
1303
  self
1012
1304
  end
1013
1305
 
@@ -1021,16 +1313,7 @@ module Net
1021
1313
  def each_ordered_number(&block)
1022
1314
  return to_enum(__method__) unless block_given?
1023
1315
  raise RangeError, '%s contains "*"' % [self.class] if include_star?
1024
- each_entry_tuple do each_number_in_tuple _1, _2, &block end
1025
- end
1026
-
1027
- private def each_number_in_tuple(min, max, &block)
1028
- if min == STAR_INT then yield :*
1029
- elsif min == max then yield min
1030
- elsif max != STAR_INT then (min..max).each(&block)
1031
- else
1032
- raise RangeError, "#{SequenceSet} cannot enumerate range with '*'"
1033
- end
1316
+ each_entry_minmax do each_number_in_minmax _1, _2, &block end
1034
1317
  end
1035
1318
 
1036
1319
  # Returns a Set with all of the #numbers in the sequence set.
@@ -1042,35 +1325,103 @@ module Net
1042
1325
  # Related: #elements, #ranges, #numbers
1043
1326
  def to_set; Set.new(numbers) end
1044
1327
 
1045
- # Returns the count of #numbers in the set.
1328
+ # Returns the number of members in the set.
1329
+ #
1330
+ # Unlike #count, <tt>"*"</tt> is considered to be distinct from
1331
+ # <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1332
+ #
1333
+ # set = Net::IMAP::SequenceSet[1..10]
1334
+ # set.count #=> 10
1335
+ # set.cardinality #=> 10
1046
1336
  #
1047
- # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1048
- # unsigned integer value).
1337
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1338
+ # set.count #=> 1
1339
+ # set.cardinality #=> 2
1049
1340
  #
1050
- # Related: #count_with_duplicates
1341
+ # set = Net::IMAP::SequenceSet[1..]
1342
+ # set.count #=> 4294967295
1343
+ # set.cardinality #=> 4294967296
1344
+ #
1345
+ # Related: #count, #count_with_duplicates
1346
+ def cardinality = minmaxes.sum(runs.count) { _2 - _1 }
1347
+
1348
+ # Returns the count of distinct #numbers in the set.
1349
+ #
1350
+ # Unlike #cardinality, <tt>"*"</tt> is considered to be equal to
1351
+ # <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1352
+ #
1353
+ # set = Net::IMAP::SequenceSet[1..10]
1354
+ # set.count #=> 10
1355
+ # set.cardinality #=> 10
1356
+ #
1357
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1358
+ # set.count #=> 1
1359
+ # set.cardinality #=> 2
1360
+ #
1361
+ # set = Net::IMAP::SequenceSet[1..]
1362
+ # set.count #=> 4294967295
1363
+ # set.cardinality #=> 4294967296
1364
+ #
1365
+ # Related: #cardinality, #count_with_duplicates
1051
1366
  def count
1052
- @tuples.sum(@tuples.count) { _2 - _1 } +
1053
- (include_star? && include?(UINT32_MAX) ? -1 : 0)
1367
+ cardinality + (include_star? && include?(UINT32_MAX) ? -1 : 0)
1054
1368
  end
1055
1369
 
1056
- alias size count
1057
-
1058
1370
  # Returns the count of numbers in the ordered #entries, including any
1059
1371
  # repeated numbers.
1060
1372
  #
1061
- # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1062
- # unsigned integer value).
1063
- #
1064
- # When #string is normalized, this behaves the same as #count.
1065
- #
1066
- # Related: #entries, #count_duplicates, #has_duplicates?
1373
+ # When #string is normalized, this returns the same as #count. Like
1374
+ # #count, <tt>"*"</tt> is considered to be equal to <tt>2³² - 1</tt> (the
1375
+ # maximum 32-bit unsigned integer value).
1376
+ #
1377
+ # In a range, <tt>"*"</tt> is _not_ considered a duplicate:
1378
+ # set = Net::IMAP::SequenceSet["4294967295:*"]
1379
+ # set.count_with_duplicates #=> 1
1380
+ # set.size #=> 2
1381
+ # set.count #=> 1
1382
+ # set.cardinality #=> 2
1383
+ #
1384
+ # In a separate entry, <tt>"*"</tt> _is_ considered a duplicate:
1385
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1386
+ # set.count_with_duplicates #=> 2
1387
+ # set.size #=> 2
1388
+ # set.count #=> 1
1389
+ # set.cardinality #=> 2
1390
+ #
1391
+ # Related: #count, #cardinality, #size, #count_duplicates,
1392
+ # #has_duplicates?, #entries
1067
1393
  def count_with_duplicates
1068
1394
  return count unless @string
1069
- each_entry_tuple.sum {|min, max|
1395
+ each_entry_minmax.sum {|min, max|
1070
1396
  max - min + ((max == STAR_INT && min != STAR_INT) ? 0 : 1)
1071
1397
  }
1072
1398
  end
1073
1399
 
1400
+ # Returns the combined size of the ordered #entries, including any
1401
+ # repeated numbers.
1402
+ #
1403
+ # When #string is normalized, this returns the same as #cardinality.
1404
+ # Like #cardinality, <tt>"*"</tt> is considered to be be distinct from
1405
+ # <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1406
+ #
1407
+ # set = Net::IMAP::SequenceSet["4294967295:*"]
1408
+ # set.size #=> 2
1409
+ # set.count_with_duplicates #=> 1
1410
+ # set.count #=> 1
1411
+ # set.cardinality #=> 2
1412
+ #
1413
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1414
+ # set.size #=> 2
1415
+ # set.count_with_duplicates #=> 2
1416
+ # set.count #=> 1
1417
+ # set.cardinality #=> 2
1418
+ #
1419
+ # Related: #cardinality, #count_with_duplicates, #count, #entries
1420
+ def size
1421
+ return cardinality unless @string
1422
+ each_entry_minmax.sum {|min, max| max - min + 1 }
1423
+ end
1424
+
1074
1425
  # Returns the count of repeated numbers in the ordered #entries, the
1075
1426
  # difference between #count_with_duplicates and #count.
1076
1427
  #
@@ -1088,7 +1439,7 @@ module Net
1088
1439
  #
1089
1440
  # Always returns +false+ when #string is normalized.
1090
1441
  #
1091
- # Related: #entries, #count_with_duplicates, #count_duplicates?
1442
+ # Related: #entries, #count_with_duplicates, #count_duplicates
1092
1443
  def has_duplicates?
1093
1444
  return false unless @string
1094
1445
  count_with_duplicates != count
@@ -1099,10 +1450,10 @@ module Net
1099
1450
  #
1100
1451
  # Related: #[], #at, #find_ordered_index
1101
1452
  def find_index(number)
1102
- number = to_tuple_int number
1103
- each_tuple_with_index(@tuples) do |min, max, idx_min|
1453
+ number = import_num number
1454
+ each_minmax_with_index(minmaxes) do |min, max, idx_min|
1104
1455
  number < min and return nil
1105
- number <= max and return from_tuple_int(idx_min + (number - min))
1456
+ number <= max and return export_num(idx_min + (number - min))
1106
1457
  end
1107
1458
  nil
1108
1459
  end
@@ -1112,38 +1463,15 @@ module Net
1112
1463
  #
1113
1464
  # Related: #find_index
1114
1465
  def find_ordered_index(number)
1115
- number = to_tuple_int number
1116
- each_tuple_with_index(each_entry_tuple) do |min, max, idx_min|
1466
+ number = import_num number
1467
+ each_minmax_with_index(each_entry_minmax) do |min, max, idx_min|
1117
1468
  if min <= number && number <= max
1118
- return from_tuple_int(idx_min + (number - min))
1469
+ return export_num(idx_min + (number - min))
1119
1470
  end
1120
1471
  end
1121
1472
  nil
1122
1473
  end
1123
1474
 
1124
- private
1125
-
1126
- def each_tuple_with_index(tuples)
1127
- idx_min = 0
1128
- tuples.each do |min, max|
1129
- idx_max = idx_min + (max - min)
1130
- yield min, max, idx_min, idx_max
1131
- idx_min = idx_max + 1
1132
- end
1133
- idx_min
1134
- end
1135
-
1136
- def reverse_each_tuple_with_index(tuples)
1137
- idx_max = -1
1138
- tuples.reverse_each do |min, max|
1139
- yield min, max, (idx_min = idx_max - (max - min)), idx_max
1140
- idx_max = idx_min - 1
1141
- end
1142
- idx_max
1143
- end
1144
-
1145
- public
1146
-
1147
1475
  # :call-seq: at(index) -> integer or nil
1148
1476
  #
1149
1477
  # Returns the number at the given +index+ in the sorted set, without
@@ -1154,7 +1482,7 @@ module Net
1154
1482
  #
1155
1483
  # Related: #[], #slice, #ordered_at
1156
1484
  def at(index)
1157
- lookup_number_by_tuple_index(tuples, index)
1485
+ seek_number_in_minmaxes(minmaxes, index)
1158
1486
  end
1159
1487
 
1160
1488
  # :call-seq: ordered_at(index) -> integer or nil
@@ -1167,21 +1495,7 @@ module Net
1167
1495
  #
1168
1496
  # Related: #[], #slice, #ordered_at
1169
1497
  def ordered_at(index)
1170
- lookup_number_by_tuple_index(each_entry_tuple, index)
1171
- end
1172
-
1173
- private def lookup_number_by_tuple_index(tuples, index)
1174
- index = Integer(index.to_int)
1175
- if index.negative?
1176
- reverse_each_tuple_with_index(tuples) do |min, max, idx_min, idx_max|
1177
- idx_min <= index and return from_tuple_int(min + (index - idx_min))
1178
- end
1179
- else
1180
- each_tuple_with_index(tuples) do |min, _, idx_min, idx_max|
1181
- index <= idx_max and return from_tuple_int(min + (index - idx_min))
1182
- end
1183
- end
1184
- nil
1498
+ seek_number_in_minmaxes(each_entry_minmax, index)
1185
1499
  end
1186
1500
 
1187
1501
  # :call-seq:
@@ -1232,37 +1546,58 @@ module Net
1232
1546
 
1233
1547
  alias slice :[]
1234
1548
 
1235
- private
1236
-
1237
- def slice_length(start, length)
1238
- start = Integer(start.to_int)
1239
- length = Integer(length.to_int)
1240
- raise ArgumentError, "length must be positive" unless length.positive?
1241
- last = start + length - 1 unless start.negative? && start.abs <= length
1242
- slice_range(start..last)
1549
+ # Returns a copy of +self+ which only contains the numbers above +num+.
1550
+ #
1551
+ # Net::IMAP::SequenceSet["5,10:22,50"].above(10) # to_s => "11:22,50"
1552
+ # Net::IMAP::SequenceSet["5,10:22,50"].above(20) # to_s => "21:22,50
1553
+ # Net::IMAP::SequenceSet["5,10:22,50"].above(30) # to_s => "50"
1554
+ #
1555
+ # This returns the same result as #intersection with <tt>((num+1)..)</tt>
1556
+ # or #difference with <tt>(..num)</tt>.
1557
+ #
1558
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (11..) # to_s => "11:22,50"
1559
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (..10) # to_s => "11:22,50"
1560
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (21..) # to_s => "21:22,50"
1561
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (..20) # to_s => "21:22,50"
1562
+ #
1563
+ # Related: #above, #-, #&
1564
+ def above(num)
1565
+ NumValidator.valid_nz_number?(num) or
1566
+ raise ArgumentError, "not a valid sequence set number"
1567
+ difference(..num)
1243
1568
  end
1244
1569
 
1245
- def slice_range(range)
1246
- first = range.begin || 0
1247
- last = range.end || -1
1248
- if range.exclude_end?
1249
- return remain_frozen_empty if last.zero?
1250
- last -= 1 if range.end && last != STAR_INT
1251
- end
1252
- if (first * last).positive? && last < first
1253
- remain_frozen_empty
1254
- elsif (min = at(first))
1255
- max = at(last)
1256
- max = :* if max.nil?
1257
- if max == :* then self & (min..)
1258
- elsif min <= max then self & (min..max)
1259
- else remain_frozen_empty
1260
- end
1261
- end
1570
+ # Returns a copy of +self+ which only contains numbers below +num+.
1571
+ #
1572
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(10) # to_s => "5"
1573
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(20) # to_s => "5,10:19"
1574
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(30) # to_s => "5,10:22"
1575
+ #
1576
+ # This returns the same result as #intersection with <tt>(..(num-1))</tt>
1577
+ # or #difference with <tt>(num..)</tt>.
1578
+ #
1579
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (..9) # to_s => "5"
1580
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (10..) # to_s => "5"
1581
+ # Net::IMAP::SequenceSet["5,10:22,50"] & (..19) # to_s => "5,10:19"
1582
+ # Net::IMAP::SequenceSet["5,10:22,50"] - (20..) # to_s => "5,10:19"
1583
+ #
1584
+ # When the set does not contain <tt>*</tt>, #below is identical to #limit
1585
+ # with <tt>max: num - 1</tt>. When the set does contain <tt>*</tt>,
1586
+ # #below always drops it from the result. Use #limit when the IMAP
1587
+ # semantics for <tt>*</tt> must be enforced.
1588
+ #
1589
+ # Net::IMAP::SequenceSet["5,10:22,50"].below(30) # to_s => "5,10:22"
1590
+ # Net::IMAP::SequenceSet["5,10:22,50"].limit(max: 29) # to_s => "5,10:22"
1591
+ # Net::IMAP::SequenceSet["5,10:22,*"].below(30) # to_s => "5,10:22"
1592
+ # Net::IMAP::SequenceSet["5,10:22,*"].limit(max: 29) # to_s => "5,10:22,29"
1593
+ #
1594
+ # Related: #above, #-, #&, #limit
1595
+ def below(num)
1596
+ NumValidator.valid_nz_number?(num) or
1597
+ raise ArgumentError, "not a valid sequence set number"
1598
+ difference(num..)
1262
1599
  end
1263
1600
 
1264
- public
1265
-
1266
1601
  # Returns a frozen SequenceSet with <tt>*</tt> converted to +max+, numbers
1267
1602
  # and ranges over +max+ removed, and ranges containing +max+ converted to
1268
1603
  # end at +max+.
@@ -1280,8 +1615,9 @@ module Net
1280
1615
  # Net::IMAP::SequenceSet["500:*"].limit(max: 37)
1281
1616
  # #=> Net::IMAP::SequenceSet["37"]
1282
1617
  #
1618
+ # Related: #limit!
1283
1619
  def limit(max:)
1284
- max = to_tuple_int(max)
1620
+ max = import_num(max)
1285
1621
  if empty? then self.class.empty
1286
1622
  elsif !include_star? && max < min then self.class.empty
1287
1623
  elsif max(star: STAR_INT) <= max then frozen? ? self : dup.freeze
@@ -1294,76 +1630,206 @@ module Net
1294
1630
  #
1295
1631
  # Related: #limit
1296
1632
  def limit!(max:)
1633
+ modifying! # short-circuit before querying
1297
1634
  star = include_star?
1298
- max = to_tuple_int(max)
1299
- tuple_subtract [max + 1, STAR_INT]
1300
- tuple_add [max, max ] if star
1635
+ max = import_num(max)
1636
+ subtract_minmax [max + 1, STAR_INT]
1637
+ add_minmax [max, max ] if star
1301
1638
  normalize!
1302
1639
  end
1303
1640
 
1304
1641
  # :call-seq: complement! -> self
1305
1642
  #
1306
- # Converts the SequenceSet to its own #complement. It will contain all
1307
- # possible values _except_ for those currently in the set.
1643
+ # In-place set #complement. Replaces the contents of this set with its
1644
+ # own #complement. It will contain all possible values _except_ for those
1645
+ # currently in the set.
1308
1646
  #
1309
1647
  # Related: #complement
1310
1648
  def complement!
1649
+ modifying! # short-circuit before querying
1311
1650
  return replace(self.class.full) if empty?
1312
1651
  return clear if full?
1313
- flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
1652
+ flat = minmaxes.flat_map { [_1 - 1, _2 + 1] }
1314
1653
  if flat.first < 1 then flat.shift else flat.unshift 1 end
1315
1654
  if STAR_INT < flat.last then flat.pop else flat.push STAR_INT end
1316
- @tuples = flat.each_slice(2).to_a
1655
+ replace_minmaxes flat.each_slice(2).to_a
1317
1656
  normalize!
1318
1657
  end
1319
1658
 
1320
- # Returns a new SequenceSet with a normalized string representation.
1659
+ # In-place set #intersection. Removes any elements that are missing from
1660
+ # +other+ from this set, keeping only the #intersection, and returns
1661
+ # +self+.
1662
+ #
1663
+ # +other+ can be any object that would be accepted by ::new.
1321
1664
  #
1322
- # The returned set's #string is sorted and deduplicated. Adjacent or
1323
- # overlapping elements will be merged into a single larger range.
1665
+ # set = Net::IMAP::SequenceSet.new(1..5)
1666
+ # set.intersect! [2, 4, 6]
1667
+ # set #=> Net::IMAP::SequenceSet("2,4")
1668
+ #
1669
+ # Related: #intersection, #intersect?
1670
+ def intersect!(other)
1671
+ modifying! # short-circuit before processing input
1672
+ subtract SequenceSet.new(other).complement!
1673
+ end
1674
+
1675
+ # In-place set #xor. Adds any numbers in +other+ that are missing from
1676
+ # this set, removes any numbers in +other+ that are already in this set,
1677
+ # and returns +self+.
1678
+ #
1679
+ # +other+ can be any object that would be accepted by ::new.
1680
+ #
1681
+ # set = Net::IMAP::SequenceSet.new(1..5)
1682
+ # set.xor! [2, 4, 6]
1683
+ # set #=> Net::IMAP::SequenceSet["1,3,5:6"]
1684
+ #
1685
+ # Related: #xor, #merge, #subtract
1686
+ def xor!(other)
1687
+ modifying! # short-circuit before processing input
1688
+ other = SequenceSet.new(other)
1689
+ copy = dup
1690
+ merge(other).subtract(other.subtract(copy.complement!))
1691
+ end
1692
+
1693
+ # Returns whether #string is fully normalized: entries have been sorted,
1694
+ # deduplicated, and coalesced, and all entries are in normal form. See
1695
+ # SequenceSet@Ordered+and+Normalized+sets.
1696
+ #
1697
+ # Net::IMAP::SequenceSet["1,3,5"].normalized? #=> true
1698
+ # Net::IMAP::SequenceSet["20:30"].normalized? #=> true
1699
+ #
1700
+ # Net::IMAP::SequenceSet["3,5,1"].normalized? #=> false, not sorted
1701
+ # Net::IMAP::SequenceSet["1,2,3"].normalized? #=> false, not coalesced
1702
+ # Net::IMAP::SequenceSet["1:5,2"].normalized? #=> false, repeated number
1703
+ #
1704
+ # Net::IMAP::SequenceSet["1:1"].normalized? #=> false, number as range
1705
+ # Net::IMAP::SequenceSet["5:1"].normalized? #=> false, backwards range
1706
+ #
1707
+ # Returns +true+ if (and only if) #string is equal to #normalized_string:
1708
+ # seqset = Net::IMAP::SequenceSet["1:3,5"]
1709
+ # seqset.string #=> "1:3,5"
1710
+ # seqset.normalized_string #=> "1:3,5"
1711
+ # seqset.entries #=> [1..3, 5]
1712
+ # seqset.elements #=> [1..3, 5]
1713
+ # seqset.normalized? #=> true
1714
+ #
1715
+ # seqset = Net::IMAP::SequenceSet["3,1,2"]
1716
+ # seqset.string #=> "3,1,2"
1717
+ # seqset.normalized_string #=> "1:3"
1718
+ # seqset.entries #=> [3, 1, 2]
1719
+ # seqset.elements #=> [1..3]
1720
+ # seqset.normalized? #=> false
1721
+ #
1722
+ # Can return +false+ even when #entries and #elements are the same:
1723
+ # seqset = Net::IMAP::SequenceSet["5:1"]
1724
+ # seqset.string #=> "5:1"
1725
+ # seqset.normalized_string #=> "1:5"
1726
+ # seqset.entries #=> [1..5]
1727
+ # seqset.elements #=> [1..5]
1728
+ # seqset.normalized? #=> false
1729
+ #
1730
+ # Note that empty sets are normalized, even though they are not #valid?:
1731
+ # seqset = Net::IMAP::SequenceSet.empty
1732
+ # seqset.normalized? #=> true
1733
+ # seqset.valid? #=> false
1734
+ #
1735
+ # Related: #normalize, #normalize!, #normalized_string
1736
+ def normalized?
1737
+ @string.nil? || normal_string?(@string)
1738
+ end
1739
+
1740
+ # Returns a SequenceSet with a normalized string representation: entries
1741
+ # have been sorted, deduplicated, and coalesced, and all entries
1742
+ # are in normal form. Returns +self+ for frozen normalized sets, and a
1743
+ # normalized duplicate otherwise.
1744
+ #
1745
+ # See SequenceSet@Ordered+and+Normalized+sets.
1324
1746
  #
1325
1747
  # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalize
1326
1748
  # #=> Net::IMAP::SequenceSet["1:7,9:11"]
1327
1749
  #
1328
- # Related: #normalize!, #normalized_string
1750
+ # Related: #normalize!, #normalized_string, #normalized?
1329
1751
  def normalize
1330
- str = normalized_string
1331
- return self if frozen? && str == string
1332
- remain_frozen dup.instance_exec { @string = str&.-@; self }
1752
+ frozen? && normalized? ? self : remain_frozen(dup.normalize!)
1333
1753
  end
1334
1754
 
1335
1755
  # Resets #string to be sorted, deduplicated, and coalesced. Returns
1336
- # +self+.
1756
+ # +self+. See SequenceSet@Ordered+and+Normalized+sets.
1337
1757
  #
1338
- # Related: #normalize, #normalized_string
1758
+ # Related: #normalize, #normalized_string, #normalized?
1339
1759
  def normalize!
1760
+ modifying! # redundant check (normalizes the error message for JRuby)
1340
1761
  @string = nil
1341
1762
  self
1342
1763
  end
1343
1764
 
1344
1765
  # Returns a normalized +sequence-set+ string representation, sorted
1345
1766
  # and deduplicated. Adjacent or overlapping elements will be merged into
1346
- # a single larger range. Returns +nil+ when the set is empty.
1767
+ # a single larger range. See SequenceSet@Ordered+and+Normalized+sets.
1347
1768
  #
1348
1769
  # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalized_string
1349
1770
  # #=> "1:7,9:11"
1350
1771
  #
1351
- # Related: #normalize!, #normalize
1772
+ # Returns +nil+ when the set is empty.
1773
+ #
1774
+ # Related: #normalize!, #normalize, #string, #to_s, #normalized?
1352
1775
  def normalized_string
1353
- @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
1776
+ export_runs(runs) unless runs.empty?
1354
1777
  end
1355
1778
 
1779
+ # Returns an inspection string for the SequenceSet.
1780
+ #
1781
+ # Net::IMAP::SequenceSet.new.inspect
1782
+ # #=> "Net::IMAP::SequenceSet()"
1783
+ #
1784
+ # Net::IMAP::SequenceSet(1..5, 1024, 15, 2000).inspect
1785
+ # #=> 'Net::IMAP::SequenceSet("1:5,15,1024,2000")'
1786
+ #
1787
+ # Frozen sets have slightly different output:
1788
+ #
1789
+ # Net::IMAP::SequenceSet.empty.inspect
1790
+ # #=> "Net::IMAP::SequenceSet.empty"
1791
+ #
1792
+ # Net::IMAP::SequenceSet[1..5, 1024, 15, 2000].inspect
1793
+ # #=> 'Net::IMAP::SequenceSet["1:5,15,1024,2000"]'
1794
+ #
1795
+ # Large sets (by number of #entries) have abridged output, with only the
1796
+ # first and last entries:
1797
+ #
1798
+ # Net::IMAP::SequenceSet(((1..5000) % 2).to_a).inspect
1799
+ # #=> #<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">
1800
+ #
1801
+ # Related: #to_s, #string
1356
1802
  def inspect
1357
- if empty?
1358
- (frozen? ? "%s.empty" : "#<%s empty>") % [self.class]
1359
- elsif frozen?
1360
- "%s[%p]" % [self.class, to_s]
1803
+ case (count = count_entries)
1804
+ when 0
1805
+ (frozen? ? "%s.empty" : "%s()") % [self.class]
1806
+ when ..INSPECT_MAX_LEN
1807
+ (frozen? ? "%s[%p]" : "%s(%p)") % [self.class, to_s]
1361
1808
  else
1362
- "#<%s %p>" % [self.class, to_s]
1809
+ if @string
1810
+ head = @string[INSPECT_ABRIDGED_HEAD_RE]
1811
+ tail = @string[INSPECT_ABRIDGED_TAIL_RE]
1812
+ else
1813
+ head = export_runs(runs.first(INSPECT_TRUNCATE_LEN)) + ","
1814
+ tail = "," + export_runs(runs.last(INSPECT_TRUNCATE_LEN))
1815
+ end
1816
+ '#<%s %d entries "%s...(%d entries omitted)...%s"%s>' % [
1817
+ self.class, count,
1818
+ head, count - INSPECT_TRUNCATE_LEN * 2, tail,
1819
+ frozen? ? " (frozen)" : "",
1820
+ ]
1363
1821
  end
1364
1822
  end
1365
1823
 
1366
- # Returns self
1824
+ ##
1825
+ # :method: to_sequence_set
1826
+ # :call-seq: to_sequence_set -> self
1827
+ #
1828
+ # Returns +self+
1829
+ #
1830
+ # Related: ::try_convert
1831
+
1832
+ # :nodoc: (work around rdoc bug)
1367
1833
  alias to_sequence_set itself
1368
1834
 
1369
1835
  # Unstable API: currently for internal use only (Net::IMAP#validate_data)
@@ -1377,9 +1843,25 @@ module Net
1377
1843
  imap.__send__(:put_string, valid_string)
1378
1844
  end
1379
1845
 
1846
+ # For YAML serialization
1847
+ def encode_with(coder) # :nodoc:
1848
+ # we can perfectly reconstruct from the string
1849
+ coder['string'] = to_s
1850
+ end
1851
+
1852
+ # For YAML deserialization
1853
+ def init_with(coder) # :nodoc:
1854
+ @set_data = new_set_data
1855
+ self.string = coder['string']
1856
+ end
1857
+
1858
+ # :stopdoc:
1380
1859
  protected
1381
1860
 
1382
- attr_reader :tuples # :nodoc:
1861
+ attr_reader :set_data
1862
+
1863
+ alias runs set_data
1864
+ alias minmaxes runs
1383
1865
 
1384
1866
  private
1385
1867
 
@@ -1388,38 +1870,42 @@ module Net
1388
1870
 
1389
1871
  # frozen clones are shallow copied
1390
1872
  def initialize_clone(other)
1391
- other.frozen? ? super : initialize_dup(other)
1873
+ @set_data = other.dup_set_data unless other.frozen?
1874
+ super
1392
1875
  end
1393
1876
 
1394
1877
  def initialize_dup(other)
1395
- @tuples = other.tuples.map(&:dup)
1396
- @string = other.string&.-@
1878
+ @set_data = other.dup_set_data
1397
1879
  super
1398
1880
  end
1399
1881
 
1400
- def input_to_tuple(entry)
1401
- entry = input_try_convert entry
1882
+ ######################################################################{{{2
1883
+ # Import methods
1884
+
1885
+ def import_minmax(input)
1886
+ entry = input_try_convert input
1402
1887
  case entry
1403
- when *STARS, Integer then [int = to_tuple_int(entry), int]
1404
- when Range then range_to_tuple(entry)
1405
- when String then str_to_tuple(entry)
1888
+ when *STARS, Integer then [int = import_num(entry), int]
1889
+ when Range then import_range_minmax(entry)
1890
+ when String then parse_minmax(entry)
1406
1891
  else
1407
- raise DataFormatError, "expected number or range, got %p" % [entry]
1892
+ raise DataFormatError, "expected number or range, got %p" % [input]
1408
1893
  end
1409
1894
  end
1895
+ alias import_run import_minmax
1410
1896
 
1411
- def input_to_tuples(set)
1412
- set = input_try_convert set
1897
+ def import_runs(input)
1898
+ set = input_try_convert input
1413
1899
  case set
1414
- when *STARS, Integer, Range then [input_to_tuple(set)]
1415
- when String then str_to_tuples set
1416
- when SequenceSet then set.tuples
1417
- when ENUMABLE then set.flat_map { input_to_tuples _1 }
1900
+ when *STARS, Integer, Range then [import_run(set)]
1901
+ when String then parse_runs set
1902
+ when SequenceSet then set.runs
1903
+ when Set then set.map { [import_num(_1)] * 2 }
1904
+ when Array then set.flat_map { import_runs _1 }
1418
1905
  when nil then []
1419
1906
  else
1420
- raise DataFormatError,
1421
- "expected nz-number, range, string, or enumerable; " \
1422
- "got %p" % [set]
1907
+ raise DataFormatError, "expected nz-number, range, '*', Set, Array; " \
1908
+ "got %p" % [input]
1423
1909
  end
1424
1910
  end
1425
1911
 
@@ -1427,15 +1913,22 @@ module Net
1427
1913
  # String, Set, Array, or... any type of object.
1428
1914
  def input_try_convert(input)
1429
1915
  SequenceSet.try_convert(input) ||
1430
- # Integer.try_convert(input) || # ruby 3.1+
1431
- input.respond_to?(:to_int) && Integer(input.to_int) ||
1916
+ Integer.try_convert(input) ||
1432
1917
  String.try_convert(input) ||
1433
1918
  input
1434
1919
  end
1435
1920
 
1436
- def range_to_tuple(range)
1437
- first = to_tuple_int(range.begin || 1)
1438
- last = to_tuple_int(range.end || :*)
1921
+ # NOTE: input_try_convert must be called on input first
1922
+ def number_input?(input)
1923
+ case input
1924
+ when *STARS, Integer then true
1925
+ when String then !input.include?(/[:,]/)
1926
+ end
1927
+ end
1928
+
1929
+ def import_range_minmax(range)
1930
+ first = import_num(range.begin || 1)
1931
+ last = import_num(range.end || :*)
1439
1932
  last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1440
1933
  unless first <= last
1441
1934
  raise DataFormatError, "invalid range for sequence-set: %p" % [range]
@@ -1443,67 +1936,260 @@ module Net
1443
1936
  [first, last]
1444
1937
  end
1445
1938
 
1446
- def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1447
- def from_tuple_int(num) num == STAR_INT ? :* : num end
1939
+ def import_num(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1940
+ def nz_number(num) = NumValidator.coerce_nz_number(num)
1941
+
1942
+ ######################################################################{{{2
1943
+ # Export methods
1944
+
1945
+ def export_num(num) num == STAR_INT ? :* : num end
1448
1946
 
1449
- def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
1450
- def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
1451
- def str_to_tuple(str)
1947
+ def export_minmaxes(minmaxes)
1948
+ -minmaxes.map { export_minmax _1 }.join(",")
1949
+ end
1950
+
1951
+ def export_minmax(minmax) minmax.uniq.map { export_num _1 }.join(":") end
1952
+
1953
+ alias export_runs export_minmaxes
1954
+ alias export_run export_minmax
1955
+
1956
+ def export_minmax_entry((min, max))
1957
+ if min == STAR_INT then :*
1958
+ elsif max == STAR_INT then min..
1959
+ elsif min == max then min
1960
+ else min..max
1961
+ end
1962
+ end
1963
+ alias export_run_entry export_minmax_entry
1964
+
1965
+ def each_number_in_minmax(min, max, &block)
1966
+ if min == STAR_INT then yield :*
1967
+ elsif min == max then yield min
1968
+ elsif max != STAR_INT then (min..max).each(&block)
1969
+ else
1970
+ raise RangeError, "#{SequenceSet} cannot enumerate range with '*'"
1971
+ end
1972
+ end
1973
+
1974
+ ######################################################################{{{2
1975
+ # Parse methods
1976
+
1977
+ def parse_runs(str) str.split(",", -1).map! { parse_run _1 } end
1978
+ def parse_minmax(str) parse_entry(str).minmax end
1979
+ alias parse_run parse_minmax
1980
+
1981
+ def parse_entry(str)
1452
1982
  raise DataFormatError, "invalid sequence set string" if str.empty?
1453
- str.split(":", 2).map! { to_tuple_int _1 }.minmax
1983
+ str.split(":", 2).map! { import_num _1 }
1984
+ end
1985
+
1986
+ # yields validated but unsorted [num] or [num, num]
1987
+ def each_parsed_entry(str)
1988
+ return to_enum(__method__, str) unless block_given?
1989
+ str&.split(",", -1) do |entry| yield parse_entry(entry) end
1454
1990
  end
1455
1991
 
1456
- def include_tuple?((min, max)) range_gte_to(min)&.cover?(min..max) end
1992
+ def normal_string?(str) normalized_entries? each_parsed_entry str end
1457
1993
 
1458
- def intersect_tuple?((min, max))
1459
- range = range_gte_to(min) and
1994
+ def normalized_entries?(entries)
1995
+ max = nil
1996
+ entries.each do |first, last|
1997
+ return false if last && last <= first # 1:1 or 2:1
1998
+ return false if max && first <= max + 1 # 2,1 or 1,1 or 1,2
1999
+ max = last || first
2000
+ end
2001
+ true
2002
+ end
2003
+
2004
+ ######################################################################{{{2
2005
+ # Ordered entry methods
2006
+
2007
+ def count_entries
2008
+ @string ? @string.count(",") + 1 : runs.count
2009
+ end
2010
+
2011
+ def each_entry_minmax(&block)
2012
+ return to_enum(__method__) unless block_given?
2013
+ if @string
2014
+ @string.split(",") do block.call parse_minmax _1 end
2015
+ else
2016
+ minmaxes.each(&block)
2017
+ end
2018
+ self
2019
+ end
2020
+ alias each_entry_run each_entry_minmax
2021
+
2022
+ ######################################################################{{{2
2023
+ # Search methods
2024
+
2025
+ def include_minmax?((min, max)) bsearch_range(min)&.cover?(min..max) end
2026
+
2027
+ def intersect_minmax?((min, max))
2028
+ range = bsearch_range(min) and
1460
2029
  range.include?(min) || range.include?(max) || (min..max).cover?(range)
1461
2030
  end
1462
2031
 
2032
+ alias include_run? include_minmax?
2033
+ alias intersect_run? intersect_minmax?
2034
+
2035
+ def bsearch_index(num) = minmaxes.bsearch_index { _2 >= num }
2036
+ def bsearch_minmax(num) = minmaxes.bsearch { _2 >= num }
2037
+ def bsearch_range(num) = (min, max = bsearch_minmax(num)) && (min..max)
2038
+
2039
+ ######################################################################{{{2
2040
+ # Number indexing methods
2041
+
2042
+ def seek_number_in_minmaxes(minmaxes, index)
2043
+ index = Integer(index.to_int)
2044
+ if index.negative?
2045
+ reverse_each_minmax_with_index(minmaxes) do |min, max, idx_min, idx_max|
2046
+ idx_min <= index and return export_num(min + (index - idx_min))
2047
+ end
2048
+ else
2049
+ each_minmax_with_index(minmaxes) do |min, _, idx_min, idx_max|
2050
+ index <= idx_max and return export_num(min + (index - idx_min))
2051
+ end
2052
+ end
2053
+ nil
2054
+ end
2055
+
2056
+ def each_minmax_with_index(minmaxes)
2057
+ idx_min = 0
2058
+ minmaxes.each do |min, max|
2059
+ idx_max = idx_min + (max - min)
2060
+ yield min, max, idx_min, idx_max
2061
+ idx_min = idx_max + 1
2062
+ end
2063
+ idx_min
2064
+ end
2065
+
2066
+ def reverse_each_minmax_with_index(minmaxes)
2067
+ idx_max = -1
2068
+ minmaxes.reverse_each do |min, max|
2069
+ yield min, max, (idx_min = idx_max - (max - min)), idx_max
2070
+ idx_max = idx_min - 1
2071
+ end
2072
+ idx_max
2073
+ end
2074
+
2075
+ def slice_length(start, length)
2076
+ start = Integer(start.to_int)
2077
+ length = Integer(length.to_int)
2078
+ raise ArgumentError, "length must be positive" unless length.positive?
2079
+ last = start + length - 1 unless start.negative? && start.abs <= length
2080
+ slice_range(start..last)
2081
+ end
2082
+
2083
+ def slice_range(range)
2084
+ first = range.begin || 0
2085
+ last = range.end || -1
2086
+ if range.exclude_end?
2087
+ return remain_frozen_empty if last.zero?
2088
+ last -= 1 if range.end && last != STAR_INT
2089
+ end
2090
+ if (first * last).positive? && last < first
2091
+ remain_frozen_empty
2092
+ elsif (min = at(first))
2093
+ max = at(last)
2094
+ max = :* if max.nil?
2095
+ if max == :* then self & (min..)
2096
+ elsif min <= max then self & (min..max)
2097
+ else remain_frozen_empty
2098
+ end
2099
+ end
2100
+ end
2101
+
2102
+ ######################################################################{{{2
2103
+ # Core set data create/freeze/dup primitives
2104
+
2105
+ def new_set_data = []
2106
+ def freeze_set_data = set_data.each(&:freeze).freeze
2107
+ def dup_set_data = set_data.map { _1.dup }
2108
+ protected :dup_set_data
2109
+
2110
+ ######################################################################{{{2
2111
+ # Core set data query/enumeration primitives
2112
+
2113
+ def min_num = minmaxes.first&.first
2114
+ def max_num = minmaxes.last&.last
2115
+
2116
+ def min_at(idx) = minmaxes[idx][0]
2117
+ def max_at(idx) = minmaxes[idx][1]
2118
+
2119
+ ######################################################################{{{2
2120
+ # Core set data modification primitives
2121
+
2122
+ def set_min_at(idx, min) = minmaxes[idx][0] = min
2123
+ def set_max_at(idx, max) = minmaxes[idx][1] = max
2124
+ def replace_minmaxes(other) = minmaxes.replace(other)
2125
+ def append_minmax(min, max) = minmaxes << [min, max]
2126
+ def insert_minmax(idx, min, max) = minmaxes.insert idx, [min, max]
2127
+ def delete_run_at(idx) = runs.delete_at(idx)
2128
+ def slice_runs!(...) = runs.slice!(...)
2129
+ def truncate_runs!(idx) = runs.slice!(idx..)
2130
+
2131
+ ######################################################################{{{2
2132
+ # Update methods
2133
+
1463
2134
  def modifying!
1464
2135
  if frozen?
1465
2136
  raise FrozenError, "can't modify frozen #{self.class}: %p" % [self]
1466
2137
  end
1467
2138
  end
1468
2139
 
1469
- def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
1470
- def tuples_subtract(tuples) tuples.each do tuple_subtract _1 end; self end
2140
+ def add_minmaxes(minmaxes)
2141
+ minmaxes.each do |minmax|
2142
+ add_minmax minmax
2143
+ end
2144
+ self
2145
+ end
2146
+
2147
+ def subtract_minmaxes(minmaxes)
2148
+ minmaxes.each do |minmax|
2149
+ subtract_minmax minmax
2150
+ end
2151
+ self
2152
+ end
1471
2153
 
1472
2154
  #
1473
- # --|=====| |=====new tuple=====| append
1474
- # ?????????-|=====new tuple=====|-|===lower===|-- insert
2155
+ # --|=====| |=====new run=======| append
2156
+ # ?????????-|=====new run=======|-|===lower===|-- insert
1475
2157
  #
1476
- # |=====new tuple=====|
2158
+ # |=====new run=======|
1477
2159
  # ---------??=======lower=======??--------------- noop
1478
2160
  #
1479
2161
  # ---------??===lower==|--|==| join remaining
1480
2162
  # ---------??===lower==|--|==|----|===upper===|-- join until upper
1481
2163
  # ---------??===lower==|--|==|--|=====upper===|-- join to upper
1482
- def tuple_add(tuple)
2164
+ def add_minmax(minmax)
1483
2165
  modifying!
1484
- min, max = tuple
1485
- lower, lower_idx = tuple_gte_with_index(min - 1)
1486
- if lower.nil? then tuples << [min, max]
1487
- elsif (max + 1) < lower.first then tuples.insert(lower_idx, [min, max])
1488
- else tuple_coalesce(lower, lower_idx, min, max)
2166
+ min, max = minmax
2167
+ lower_idx = bsearch_index(min - 1)
2168
+ lmin, lmax = min_at(lower_idx), max_at(lower_idx) if lower_idx
2169
+ if lmin.nil? then append_minmax min, max
2170
+ elsif (max + 1) < lmin then insert_minmax lower_idx, min, max
2171
+ else add_coalesced_minmax(lower_idx, lmin, lmax, min, max)
1489
2172
  end
1490
2173
  end
1491
2174
 
1492
- def tuple_coalesce(lower, lower_idx, min, max)
1493
- return if lower.first <= min && max <= lower.last
1494
- lower[0] = [min, lower.first].min
1495
- lower[1] = [max, lower.last].max
1496
- lower_idx += 1
1497
- return if lower_idx == tuples.count
1498
- tmax_adj = lower.last + 1
1499
- upper, upper_idx = tuple_gte_with_index(tmax_adj)
1500
- if upper
1501
- tmax_adj < upper.first ? (upper_idx -= 1) : (lower[1] = upper.last)
2175
+ def add_coalesced_minmax(lower_idx, lmin, lmax, min, max)
2176
+ return if lmin <= min && max <= lmax
2177
+ set_min_at lower_idx, (lmin = min) if min < lmin
2178
+ set_max_at lower_idx, (lmax = max) if lmax < max
2179
+ next_idx = lower_idx + 1
2180
+ return if next_idx == runs.count
2181
+ tmax_adj = lmax + 1
2182
+ if (upper_idx = bsearch_index(tmax_adj))
2183
+ if tmax_adj < min_at(upper_idx)
2184
+ upper_idx -= 1
2185
+ else
2186
+ set_max_at lower_idx, max_at(upper_idx)
2187
+ end
1502
2188
  end
1503
- tuples.slice!(lower_idx..upper_idx)
2189
+ slice_runs! next_idx..upper_idx
1504
2190
  end
1505
2191
 
1506
- # |====tuple================|
2192
+ # |====subtracted run=======|
1507
2193
  # --|====| no more 1. noop
1508
2194
  # --|====|---------------------------|====lower====|-- 2. noop
1509
2195
  # -------|======lower================|---------------- 3. split
@@ -1516,62 +2202,59 @@ module Net
1516
2202
  # -------??=====lower====|--|====| no more 6. delete rest
1517
2203
  # -------??=====lower====|--|====|---|====upper====|-- 7. delete until
1518
2204
  # -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
1519
- def tuple_subtract(tuple)
2205
+ def subtract_minmax(minmax)
1520
2206
  modifying!
1521
- min, max = tuple
1522
- lower, idx = tuple_gte_with_index(min)
1523
- if lower.nil? then nil # case 1.
1524
- elsif max < lower.first then nil # case 2.
1525
- elsif max < lower.last then tuple_trim_or_split lower, idx, min, max
1526
- else tuples_trim_or_delete lower, idx, min, max
2207
+ min, max = minmax
2208
+ idx = bsearch_index(min)
2209
+ lmin, lmax = min_at(idx), max_at(idx) if idx
2210
+ if lmin.nil? then nil # case 1.
2211
+ elsif max < lmin then nil # case 2.
2212
+ elsif max < lmax then trim_or_split_minmax idx, lmin, min, max
2213
+ else trim_or_delete_minmax idx, lmin, lmax, min, max
1527
2214
  end
1528
2215
  end
1529
2216
 
1530
- def tuple_trim_or_split(lower, idx, tmin, tmax)
1531
- if lower.first < tmin # split
1532
- tuples.insert(idx, [lower.first, tmin - 1])
2217
+ def trim_or_split_minmax(idx, lmin, tmin, tmax)
2218
+ set_min_at idx, tmax + 1
2219
+ if lmin < tmin # split
2220
+ insert_minmax idx, lmin, tmin - 1
1533
2221
  end
1534
- lower[0] = tmax + 1
1535
2222
  end
1536
2223
 
1537
- def tuples_trim_or_delete(lower, lower_idx, tmin, tmax)
1538
- if lower.first < tmin # trim lower
1539
- lower[1] = tmin - 1
2224
+ def trim_or_delete_minmax(lower_idx, lmin, lmax, tmin, tmax)
2225
+ if lmin < tmin # trim lower
2226
+ lmax = set_max_at lower_idx, tmin - 1
1540
2227
  lower_idx += 1
1541
2228
  end
1542
- if tmax == lower.last # case 5
1543
- upper_idx = lower_idx
1544
- elsif (upper, upper_idx = tuple_gte_with_index(tmax + 1))
1545
- upper_idx -= 1 # cases 7 and 8
1546
- upper[0] = tmax + 1 if upper.first <= tmax # case 8 (else case 7)
2229
+ if tmax == lmax # case 5
2230
+ delete_run_at lower_idx
2231
+ elsif (upper_idx = bsearch_index(tmax + 1))
2232
+ if min_at(upper_idx) <= tmax # case 8
2233
+ set_min_at upper_idx, tmax + 1
2234
+ end
2235
+ slice_runs! lower_idx..upper_idx - 1 # cases 7 and 8
2236
+ else # case 6
2237
+ truncate_runs! lower_idx
1547
2238
  end
1548
- tuples.slice!(lower_idx..upper_idx)
1549
2239
  end
1550
2240
 
1551
- def tuple_gte_with_index(num)
1552
- idx = tuples.bsearch_index { _2 >= num } and [tuples[idx], idx]
1553
- end
1554
-
1555
- def range_gte_to(num)
1556
- first, last = tuples.bsearch { _2 >= num }
1557
- first..last if first
1558
- end
1559
-
1560
- 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
1567
- end
2241
+ alias add_runs add_minmaxes
2242
+ alias add_run add_minmax
2243
+ alias subtract_runs subtract_minmaxes
2244
+ alias subtract_run subtract_minmax
1568
2245
 
2246
+ ######################################################################{{{2
1569
2247
  # intentionally defined after the class implementation
1570
2248
 
2249
+ FULL_SET_DATA = [[1, STAR_INT].freeze].freeze
2250
+ private_constant :FULL_SET_DATA
2251
+
1571
2252
  EMPTY = new.freeze
1572
2253
  FULL = self["1:*"]
1573
2254
  private_constant :EMPTY, :FULL
1574
2255
 
2256
+ # }}}
2257
+ # vim:foldmethod=marker
1575
2258
  end
1576
2259
  end
1577
2260
  end