net-imap 0.5.9 → 0.6.4

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.
@@ -84,7 +84,7 @@ module Net
84
84
  #
85
85
  # # Other inputs are normalized
86
86
  # set = Net::IMAP::SequenceSet([1, 2, [3..7, 5], 6..10, 2048, 1024])
87
- # set.valid_string #=> "1:10,55,1024:2048"
87
+ # set.valid_string #=> "1:10,1024,2048"
88
88
  # set.frozen? #=> false
89
89
  #
90
90
  # unfrozen = set
@@ -107,7 +107,7 @@ module Net
107
107
  #
108
108
  # # Other inputs are normalized
109
109
  # set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
110
- # set.valid_string #=> "1:10,55,1024:2048"
110
+ # set.valid_string #=> "1:10,1024,2048"
111
111
  # set.frozen? #=> true
112
112
  #
113
113
  # frozen = set
@@ -145,6 +145,10 @@ module Net
145
145
  # #entries and #elements are identical. Use #append to preserve #entries
146
146
  # order while modifying a set.
147
147
  #
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.
151
+ #
148
152
  # == Using <tt>*</tt>
149
153
  #
150
154
  # \IMAP sequence sets may contain a special value <tt>"*"</tt>, which
@@ -179,7 +183,7 @@ module Net
179
183
  #
180
184
  # When a set includes <tt>*</tt>, some methods may have surprising behavior.
181
185
  #
182
- # For example, #complement treats <tt>*</tt> as its own number. This way,
186
+ # For example, #complement treats <tt>*</tt> as its own member. This way,
183
187
  # the #intersection of a set and its #complement will always be empty. And
184
188
  # <tt>*</tt> is sorted as greater than any other number in the set. This is
185
189
  # not how an \IMAP server interprets the set: it will convert <tt>*</tt> to
@@ -199,7 +203,7 @@ module Net
199
203
  # (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4]
200
204
  #
201
205
  # When counting the number of numbers in a set, <tt>*</tt> will be counted
202
- # _except_ when UINT32_MAX is also in the set:
206
+ # as if it were equal to UINT32_MAX:
203
207
  # UINT32_MAX = 2**32 - 1
204
208
  # Net::IMAP::SequenceSet["*"].count => 1
205
209
  # Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX
@@ -208,6 +212,12 @@ module Net
208
212
  # Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
209
213
  # Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
210
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
+ #
211
221
  # == What's here?
212
222
  #
213
223
  # SequenceSet provides methods for:
@@ -271,8 +281,10 @@ module Net
271
281
  # occurrence in entries.
272
282
  #
273
283
  # <i>Set cardinality:</i>
274
- # - #count (aliased as #size): Returns the count of numbers in the set.
275
- # 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.
276
288
  # - #empty?: Returns whether the set has no members. \IMAP syntax does not
277
289
  # allow empty sequence sets.
278
290
  # - #valid?: Returns whether the set has any members.
@@ -280,12 +292,18 @@ module Net
280
292
  # <tt>*</tt>.
281
293
  #
282
294
  # <i>Denormalized properties:</i>
295
+ # - #normalized?: Returns whether #entries are sorted, deduplicated, and
296
+ # coalesced, and all #string entries are in normalized form.
283
297
  # - #has_duplicates?: Returns whether the ordered entries repeat any
284
298
  # numbers.
285
- # - #count_duplicates: Returns the count of repeated numbers in the ordered
286
- # 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.
287
302
  # - #count_with_duplicates: Returns the count of numbers in the ordered
288
- # 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.
289
307
  #
290
308
  # === Methods for Iterating
291
309
  #
@@ -332,7 +350,7 @@ module Net
332
350
  # given maximum value and removed all members over that maximum.
333
351
  #
334
352
  # === Methods for Assigning
335
- # These methods add or replace elements in +self+.
353
+ # These methods add or replace numbers in +self+.
336
354
  #
337
355
  # <i>Normalized (sorted and coalesced):</i>
338
356
  #
@@ -341,8 +359,12 @@ module Net
341
359
  # - #add (aliased as #<<): Adds a given element to the set; returns +self+.
342
360
  # - #add?: If the given element is not fully included the set, adds it and
343
361
  # returns +self+; otherwise, returns +nil+.
344
- # - #merge: Adds all members of the given sets into this set; returns +self+.
345
- # - #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+.
346
368
  #
347
369
  # <i>Order preserving:</i>
348
370
  #
@@ -355,7 +377,7 @@ module Net
355
377
  # of a given object.
356
378
  #
357
379
  # === Methods for Deleting
358
- # 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
359
381
  # sorted and coalesced.
360
382
  #
361
383
  # - #clear: Removes all elements in the set; returns +self+.
@@ -363,10 +385,12 @@ module Net
363
385
  # - #delete?: If the given element is included in the set, removes it and
364
386
  # returns it; otherwise, returns +nil+.
365
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+.
366
390
  # - #slice!: Removes the number or consecutive numbers at a given offset or
367
391
  # range of offsets.
368
- # - #subtract: Removes all members of the given sets from this set; returns
369
- # +self+.
392
+ # - #subtract: In-place set #difference. Removes all members of the given
393
+ # sets from this set; returns +self+.
370
394
  # - #limit!: Replaces <tt>*</tt> with a given maximum value and removes all
371
395
  # members over that maximum; returns +self+.
372
396
  #
@@ -396,6 +420,23 @@ module Net
396
420
  STARS = [:*, ?*, -1].freeze
397
421
  private_constant :STARS
398
422
 
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
439
+
399
440
  class << self
400
441
 
401
442
  # :call-seq:
@@ -427,14 +468,14 @@ module Net
427
468
  # +to_sequence_set+, calls +obj.to_sequence_set+ and returns the result.
428
469
  # Otherwise returns +nil+.
429
470
  #
430
- # If +obj.to_sequence_set+ doesn't return a SequenceSet, an exception is
431
- # raised.
471
+ # If +obj.to_sequence_set+ doesn't return a SequenceSet or +nil+, an
472
+ # exception is raised.
432
473
  #
433
474
  # Related: Net::IMAP::SequenceSet(), ::new, ::[]
434
475
  def try_convert(obj)
435
476
  return obj if obj.is_a?(SequenceSet)
436
477
  return nil unless obj.respond_to?(:to_sequence_set)
437
- obj = obj.to_sequence_set
478
+ return nil unless obj = obj.to_sequence_set
438
479
  return obj if obj.is_a?(SequenceSet)
439
480
  raise DataFormatError, "invalid object returned from to_sequence_set"
440
481
  end
@@ -465,7 +506,7 @@ module Net
465
506
  # set = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
466
507
  # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
467
508
  # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
468
- # set.valid_string #=> "1:10,55,1024:2048"
509
+ # set.valid_string #=> "1:10,1024,2048"
469
510
  #
470
511
  # With no arguments (or +nil+) creates an empty sequence set. Note that
471
512
  # an empty sequence set is invalid in the \IMAP grammar.
@@ -514,12 +555,17 @@ module Net
514
555
  # combined with set operations (#|, #&, #^, #-, etc) to make new sets.
515
556
  #
516
557
  # See SequenceSet@Creating+sequence+sets.
517
- def initialize(input = nil) input ? replace(input) : clear end
558
+ def initialize(input = nil)
559
+ @set_data = new_set_data
560
+ @string = nil
561
+ replace(input) unless input.nil?
562
+ end
518
563
 
519
564
  # Removes all elements and returns self.
520
565
  def clear
521
- modifying! # redundant check, to normalize the error message for JRuby
522
- @tuples, @string = [], nil
566
+ modifying! # redundant check (normalizes the error message for JRuby)
567
+ set_data.clear
568
+ @string = nil
523
569
  self
524
570
  end
525
571
 
@@ -530,7 +576,10 @@ module Net
530
576
  # accepted by ::new.
531
577
  def replace(other)
532
578
  case other
533
- 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)
534
583
  when String then self.string = other
535
584
  else clear; merge other
536
585
  end
@@ -559,45 +608,51 @@ module Net
559
608
  # If the set was created from a single string, it is not normalized. If
560
609
  # the set is updated the string will be normalized.
561
610
  #
562
- # Related: #valid_string, #normalized_string, #to_s
563
- 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
564
613
 
565
614
  # Returns an array with #normalized_string when valid and an empty array
566
615
  # otherwise.
567
616
  def deconstruct; valid? ? [normalized_string] : [] end
568
617
 
569
- # Assigns a new string to #string and resets #elements to match. It
570
- # cannot be set to an empty string—assign +nil+ or use #clear instead.
571
- # 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.
572
620
  #
573
- # Use #add or #merge to add a string to an existing set.
621
+ # Non-empty strings are validated but not normalized.
622
+ #
623
+ # Use #add, #merge, or #append to add a string to an existing set.
574
624
  #
575
625
  # Related: #replace, #clear
576
- def string=(str)
577
- if str.nil?
626
+ def string=(input)
627
+ if input.nil?
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
578
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
579
639
  else
580
- modifying! # redundant check, to normalize the error message for JRuby
581
- str = String.try_convert(str) or raise ArgumentError, "not a string"
582
- tuples = str_to_tuples str
583
- @tuples, @string = [], -str
584
- tuples_add tuples
640
+ raise ArgumentError, "expected a string or nil, got #{input.class}"
585
641
  end
586
- str
642
+ input
587
643
  end
588
644
 
589
645
  # Returns the \IMAP +sequence-set+ string representation, or an empty
590
646
  # string when the set is empty. Note that an empty set is invalid in the
591
647
  # \IMAP syntax.
592
648
  #
593
- # Related: #valid_string, #normalized_string, #to_s
649
+ # Related: #string, #valid_string, #normalized_string, #inspect
594
650
  def to_s; string || "" end
595
651
 
596
652
  # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
597
653
  def freeze
598
654
  return self if frozen?
599
- string
600
- @tuples.each(&:freeze).freeze
655
+ freeze_set_data
601
656
  super
602
657
  end
603
658
 
@@ -619,7 +674,7 @@ module Net
619
674
  # Related: #eql?, #normalize
620
675
  def ==(other)
621
676
  self.class == other.class &&
622
- (to_s == other.to_s || tuples == other.tuples)
677
+ (to_s == other.to_s || set_data == other.set_data)
623
678
  end
624
679
 
625
680
  # :call-seq: eql?(other) -> true or false
@@ -660,7 +715,7 @@ module Net
660
715
  # object that would be accepted by ::new.
661
716
  #
662
717
  # Related: #===, #include?, #include_star?, #intersect?
663
- def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
718
+ def cover?(other) import_runs(other).none? { !include_run?(_1) } end
664
719
 
665
720
  # Returns +true+ when a given number or range is in +self+, and +false+
666
721
  # otherwise. Returns +nil+ when +number+ isn't a valid SequenceSet
@@ -687,14 +742,14 @@ module Net
687
742
  #
688
743
  # Related: #include_star?, #cover?, #===, #intersect?
689
744
  def include?(element)
690
- tuple = input_to_tuple element rescue nil
691
- !!include_tuple?(tuple) if tuple
745
+ run = import_run element rescue nil
746
+ !!include_run?(run) if run
692
747
  end
693
748
 
694
749
  alias member? include?
695
750
 
696
751
  # Returns +true+ when the set contains <tt>*</tt>.
697
- def include_star?; @tuples.last&.last == STAR_INT end
752
+ def include_star?; max_num == STAR_INT end
698
753
 
699
754
  # Returns +true+ if the set and a given object have any common elements,
700
755
  # +false+ otherwise.
@@ -704,7 +759,7 @@ module Net
704
759
  #
705
760
  # Related: #intersection, #disjoint?, #cover?, #include?
706
761
  def intersect?(other)
707
- valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
762
+ valid? && import_runs(other).any? { intersect_run? _1 }
708
763
  end
709
764
  alias overlap? intersect?
710
765
 
@@ -716,7 +771,7 @@ module Net
716
771
  #
717
772
  # Related: #intersection, #intersect?
718
773
  def disjoint?(other)
719
- empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
774
+ empty? || import_runs(other).none? { intersect_run? _1 }
720
775
  end
721
776
 
722
777
  # :call-seq:
@@ -733,8 +788,12 @@ module Net
733
788
  # Related: #min, #minmax, #slice
734
789
  def max(count = nil, star: :*)
735
790
  if count
736
- slice(-[count, size].min..) || remain_frozen_empty
737
- elsif (val = @tuples.last&.last)
791
+ if cardinality <= count
792
+ frozen? ? self : dup
793
+ else
794
+ slice(-count..) || remain_frozen_empty
795
+ end
796
+ elsif (val = max_num)
738
797
  val == STAR_INT ? star : val
739
798
  end
740
799
  end
@@ -754,7 +813,7 @@ module Net
754
813
  def min(count = nil, star: :*)
755
814
  if count
756
815
  slice(0...count) || remain_frozen_empty
757
- elsif (val = @tuples.first&.first)
816
+ elsif (val = min_num)
758
817
  val != STAR_INT ? val : star
759
818
  end
760
819
  end
@@ -772,10 +831,10 @@ module Net
772
831
  def valid?; !empty? end
773
832
 
774
833
  # Returns true if the set contains no elements
775
- def empty?; @tuples.empty? end
834
+ def empty?; runs.empty? end
776
835
 
777
836
  # Returns true if the set contains every possible element.
778
- def full?; @tuples == [[1, STAR_INT]] end
837
+ def full?; set_data == FULL_SET_DATA end
779
838
 
780
839
  # :call-seq:
781
840
  # self + other -> sequence set
@@ -851,9 +910,7 @@ module Net
851
910
  # * <tt>lhs - (lhs - rhs)</tt>
852
911
  # * <tt>lhs - (lhs ^ rhs)</tt>
853
912
  # * <tt>lhs ^ (lhs - rhs)</tt>
854
- def &(other)
855
- remain_frozen dup.subtract SequenceSet.new(other).complement!
856
- end
913
+ def &(other) remain_frozen dup.intersect! other end
857
914
  alias intersection :&
858
915
 
859
916
  # :call-seq:
@@ -878,7 +935,7 @@ module Net
878
935
  # * <tt>(lhs | rhs) - (lhs & rhs)</tt>
879
936
  # * <tt>(lhs - rhs) | (rhs - lhs)</tt>
880
937
  # * <tt>(lhs ^ other) ^ (other ^ rhs)</tt>
881
- def ^(other) remain_frozen (dup | other).subtract(self & other) end
938
+ def ^(other) remain_frozen dup.xor! other end
882
939
  alias xor :^
883
940
 
884
941
  # :call-seq:
@@ -917,8 +974,8 @@ module Net
917
974
  #
918
975
  # Related: #add?, #merge, #union, #append
919
976
  def add(element)
920
- modifying! # short-circuit before input_to_tuple
921
- tuple_add input_to_tuple element
977
+ modifying! # short-circuit before import_run
978
+ add_run import_run element
922
979
  normalize!
923
980
  end
924
981
  alias << add
@@ -928,16 +985,55 @@ module Net
928
985
  # Unlike #add, #merge, or #union, the new value is appended to #string.
929
986
  # This may result in a #string which has duplicates or is out-of-order.
930
987
  #
931
- # See SequenceSet@Ordered+and+Normalized+sets.
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.
932
1009
  #
933
1010
  # Related: #add, #merge, #union
934
1011
  def append(entry)
935
- modifying! # short-circuit before input_to_tuple
936
- tuple = input_to_tuple entry
937
- entry = tuple_to_str tuple
938
- string unless empty? # write @string before tuple_add
939
- tuple_add tuple
940
- @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}"
941
1037
  self
942
1038
  end
943
1039
 
@@ -963,8 +1059,8 @@ module Net
963
1059
  #
964
1060
  # Related: #delete?, #delete_at, #subtract, #difference
965
1061
  def delete(element)
966
- modifying! # short-circuit before input_to_tuple
967
- tuple_subtract input_to_tuple element
1062
+ modifying! # short-circuit before import_run
1063
+ subtract_run import_run element
968
1064
  normalize!
969
1065
  end
970
1066
 
@@ -1001,16 +1097,17 @@ module Net
1001
1097
  #
1002
1098
  # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
1003
1099
  def delete?(element)
1004
- modifying! # short-circuit before input_to_tuple
1005
- tuple = input_to_tuple element
1006
- if tuple.first == tuple.last
1007
- return unless include_tuple? tuple
1008
- 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
1009
1106
  normalize!
1010
- from_tuple_int tuple.first
1107
+ export_num minmax.first
1011
1108
  else
1012
1109
  copy = dup
1013
- tuple_subtract tuple
1110
+ subtract_minmax minmax
1014
1111
  normalize!
1015
1112
  copy if copy.subtract(self).valid?
1016
1113
  end
@@ -1047,8 +1144,8 @@ module Net
1047
1144
  deleted
1048
1145
  end
1049
1146
 
1050
- # Merges all of the elements that appear in any of the +sets+ into the
1051
- # 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+.
1052
1149
  #
1053
1150
  # The +sets+ may be any objects that would be accepted by ::new.
1054
1151
  #
@@ -1056,19 +1153,20 @@ module Net
1056
1153
  #
1057
1154
  # Related: #add, #add?, #union
1058
1155
  def merge(*sets)
1059
- modifying! # short-circuit before input_to_tuples
1060
- tuples_add input_to_tuples sets
1156
+ modifying! # short-circuit before import_runs
1157
+ add_runs import_runs sets
1061
1158
  normalize!
1062
1159
  end
1063
1160
 
1064
- # Removes all of the elements that appear in any of the given +sets+ from
1065
- # 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+.
1066
1163
  #
1067
1164
  # The +sets+ may be any objects that would be accepted by ::new.
1068
1165
  #
1069
1166
  # Related: #difference
1070
1167
  def subtract(*sets)
1071
- tuples_subtract input_to_tuples sets
1168
+ modifying! # short-circuit before import_runs
1169
+ subtract_runs import_runs sets
1072
1170
  normalize!
1073
1171
  end
1074
1172
 
@@ -1160,7 +1258,7 @@ module Net
1160
1258
  # Related: #entries, #each_element
1161
1259
  def each_entry(&block) # :yields: integer or range or :*
1162
1260
  return to_enum(__method__) unless block_given?
1163
- each_entry_tuple do yield tuple_to_entry _1 end
1261
+ each_entry_run do yield export_run_entry _1 end
1164
1262
  end
1165
1263
 
1166
1264
  # Yields each number or range (or <tt>:*</tt>) in #elements to the block
@@ -1172,39 +1270,17 @@ module Net
1172
1270
  # Related: #elements, #each_entry
1173
1271
  def each_element # :yields: integer or range or :*
1174
1272
  return to_enum(__method__) unless block_given?
1175
- @tuples.each do yield tuple_to_entry _1 end
1176
- self
1177
- end
1178
-
1179
- private
1180
-
1181
- def each_entry_tuple(&block)
1182
- return to_enum(__method__) unless block_given?
1183
- if @string
1184
- @string.split(",") do block.call str_to_tuple _1 end
1185
- else
1186
- @tuples.each(&block)
1187
- end
1273
+ runs.each do yield export_run_entry _1 end
1188
1274
  self
1189
1275
  end
1190
1276
 
1191
- def tuple_to_entry((min, max))
1192
- if min == STAR_INT then :*
1193
- elsif max == STAR_INT then min..
1194
- elsif min == max then min
1195
- else min..max
1196
- end
1197
- end
1198
-
1199
- public
1200
-
1201
1277
  # Yields each range in #ranges to the block and returns self.
1202
1278
  # Returns an enumerator when called without a block.
1203
1279
  #
1204
1280
  # Related: #ranges
1205
1281
  def each_range # :yields: range
1206
1282
  return to_enum(__method__) unless block_given?
1207
- @tuples.each do |min, max|
1283
+ minmaxes.each do |min, max|
1208
1284
  if min == STAR_INT then yield :*..
1209
1285
  elsif max == STAR_INT then yield min..
1210
1286
  else yield min..max
@@ -1223,7 +1299,7 @@ module Net
1223
1299
  def each_number(&block) # :yields: integer
1224
1300
  return to_enum(__method__) unless block_given?
1225
1301
  raise RangeError, '%s contains "*"' % [self.class] if include_star?
1226
- @tuples.each do each_number_in_tuple _1, _2, &block end
1302
+ minmaxes.each do each_number_in_minmax _1, _2, &block end
1227
1303
  self
1228
1304
  end
1229
1305
 
@@ -1237,16 +1313,7 @@ module Net
1237
1313
  def each_ordered_number(&block)
1238
1314
  return to_enum(__method__) unless block_given?
1239
1315
  raise RangeError, '%s contains "*"' % [self.class] if include_star?
1240
- each_entry_tuple do each_number_in_tuple _1, _2, &block end
1241
- end
1242
-
1243
- private def each_number_in_tuple(min, max, &block)
1244
- if min == STAR_INT then yield :*
1245
- elsif min == max then yield min
1246
- elsif max != STAR_INT then (min..max).each(&block)
1247
- else
1248
- raise RangeError, "#{SequenceSet} cannot enumerate range with '*'"
1249
- end
1316
+ each_entry_minmax do each_number_in_minmax _1, _2, &block end
1250
1317
  end
1251
1318
 
1252
1319
  # Returns a Set with all of the #numbers in the sequence set.
@@ -1258,35 +1325,103 @@ module Net
1258
1325
  # Related: #elements, #ranges, #numbers
1259
1326
  def to_set; Set.new(numbers) end
1260
1327
 
1261
- # 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
1336
+ #
1337
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1338
+ # set.count #=> 1
1339
+ # set.cardinality #=> 2
1340
+ #
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
1262
1356
  #
1263
- # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1264
- # unsigned integer value).
1357
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1358
+ # set.count #=> 1
1359
+ # set.cardinality #=> 2
1265
1360
  #
1266
- # Related: #count_with_duplicates
1361
+ # set = Net::IMAP::SequenceSet[1..]
1362
+ # set.count #=> 4294967295
1363
+ # set.cardinality #=> 4294967296
1364
+ #
1365
+ # Related: #cardinality, #count_with_duplicates
1267
1366
  def count
1268
- @tuples.sum(@tuples.count) { _2 - _1 } +
1269
- (include_star? && include?(UINT32_MAX) ? -1 : 0)
1367
+ cardinality + (include_star? && include?(UINT32_MAX) ? -1 : 0)
1270
1368
  end
1271
1369
 
1272
- alias size count
1273
-
1274
1370
  # Returns the count of numbers in the ordered #entries, including any
1275
1371
  # repeated numbers.
1276
1372
  #
1277
- # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1278
- # unsigned integer value).
1279
- #
1280
- # When #string is normalized, this behaves the same as #count.
1281
- #
1282
- # 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
1283
1393
  def count_with_duplicates
1284
1394
  return count unless @string
1285
- each_entry_tuple.sum {|min, max|
1395
+ each_entry_minmax.sum {|min, max|
1286
1396
  max - min + ((max == STAR_INT && min != STAR_INT) ? 0 : 1)
1287
1397
  }
1288
1398
  end
1289
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
+
1290
1425
  # Returns the count of repeated numbers in the ordered #entries, the
1291
1426
  # difference between #count_with_duplicates and #count.
1292
1427
  #
@@ -1304,7 +1439,7 @@ module Net
1304
1439
  #
1305
1440
  # Always returns +false+ when #string is normalized.
1306
1441
  #
1307
- # Related: #entries, #count_with_duplicates, #count_duplicates?
1442
+ # Related: #entries, #count_with_duplicates, #count_duplicates
1308
1443
  def has_duplicates?
1309
1444
  return false unless @string
1310
1445
  count_with_duplicates != count
@@ -1315,10 +1450,10 @@ module Net
1315
1450
  #
1316
1451
  # Related: #[], #at, #find_ordered_index
1317
1452
  def find_index(number)
1318
- number = to_tuple_int number
1319
- 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|
1320
1455
  number < min and return nil
1321
- number <= max and return from_tuple_int(idx_min + (number - min))
1456
+ number <= max and return export_num(idx_min + (number - min))
1322
1457
  end
1323
1458
  nil
1324
1459
  end
@@ -1328,38 +1463,15 @@ module Net
1328
1463
  #
1329
1464
  # Related: #find_index
1330
1465
  def find_ordered_index(number)
1331
- number = to_tuple_int number
1332
- 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|
1333
1468
  if min <= number && number <= max
1334
- return from_tuple_int(idx_min + (number - min))
1469
+ return export_num(idx_min + (number - min))
1335
1470
  end
1336
1471
  end
1337
1472
  nil
1338
1473
  end
1339
1474
 
1340
- private
1341
-
1342
- def each_tuple_with_index(tuples)
1343
- idx_min = 0
1344
- tuples.each do |min, max|
1345
- idx_max = idx_min + (max - min)
1346
- yield min, max, idx_min, idx_max
1347
- idx_min = idx_max + 1
1348
- end
1349
- idx_min
1350
- end
1351
-
1352
- def reverse_each_tuple_with_index(tuples)
1353
- idx_max = -1
1354
- tuples.reverse_each do |min, max|
1355
- yield min, max, (idx_min = idx_max - (max - min)), idx_max
1356
- idx_max = idx_min - 1
1357
- end
1358
- idx_max
1359
- end
1360
-
1361
- public
1362
-
1363
1475
  # :call-seq: at(index) -> integer or nil
1364
1476
  #
1365
1477
  # Returns the number at the given +index+ in the sorted set, without
@@ -1370,7 +1482,7 @@ module Net
1370
1482
  #
1371
1483
  # Related: #[], #slice, #ordered_at
1372
1484
  def at(index)
1373
- lookup_number_by_tuple_index(tuples, index)
1485
+ seek_number_in_minmaxes(minmaxes, index)
1374
1486
  end
1375
1487
 
1376
1488
  # :call-seq: ordered_at(index) -> integer or nil
@@ -1383,21 +1495,7 @@ module Net
1383
1495
  #
1384
1496
  # Related: #[], #slice, #ordered_at
1385
1497
  def ordered_at(index)
1386
- lookup_number_by_tuple_index(each_entry_tuple, index)
1387
- end
1388
-
1389
- private def lookup_number_by_tuple_index(tuples, index)
1390
- index = Integer(index.to_int)
1391
- if index.negative?
1392
- reverse_each_tuple_with_index(tuples) do |min, max, idx_min, idx_max|
1393
- idx_min <= index and return from_tuple_int(min + (index - idx_min))
1394
- end
1395
- else
1396
- each_tuple_with_index(tuples) do |min, _, idx_min, idx_max|
1397
- index <= idx_max and return from_tuple_int(min + (index - idx_min))
1398
- end
1399
- end
1400
- nil
1498
+ seek_number_in_minmaxes(each_entry_minmax, index)
1401
1499
  end
1402
1500
 
1403
1501
  # :call-seq:
@@ -1448,37 +1546,6 @@ module Net
1448
1546
 
1449
1547
  alias slice :[]
1450
1548
 
1451
- private
1452
-
1453
- def slice_length(start, length)
1454
- start = Integer(start.to_int)
1455
- length = Integer(length.to_int)
1456
- raise ArgumentError, "length must be positive" unless length.positive?
1457
- last = start + length - 1 unless start.negative? && start.abs <= length
1458
- slice_range(start..last)
1459
- end
1460
-
1461
- def slice_range(range)
1462
- first = range.begin || 0
1463
- last = range.end || -1
1464
- if range.exclude_end?
1465
- return remain_frozen_empty if last.zero?
1466
- last -= 1 if range.end && last != STAR_INT
1467
- end
1468
- if (first * last).positive? && last < first
1469
- remain_frozen_empty
1470
- elsif (min = at(first))
1471
- max = at(last)
1472
- max = :* if max.nil?
1473
- if max == :* then self & (min..)
1474
- elsif min <= max then self & (min..max)
1475
- else remain_frozen_empty
1476
- end
1477
- end
1478
- end
1479
-
1480
- public
1481
-
1482
1549
  # Returns a copy of +self+ which only contains the numbers above +num+.
1483
1550
  #
1484
1551
  # Net::IMAP::SequenceSet["5,10:22,50"].above(10) # to_s => "11:22,50"
@@ -1550,7 +1617,7 @@ module Net
1550
1617
  #
1551
1618
  # Related: #limit!
1552
1619
  def limit(max:)
1553
- max = to_tuple_int(max)
1620
+ max = import_num(max)
1554
1621
  if empty? then self.class.empty
1555
1622
  elsif !include_star? && max < min then self.class.empty
1556
1623
  elsif max(star: STAR_INT) <= max then frozen? ? self : dup.freeze
@@ -1563,53 +1630,134 @@ module Net
1563
1630
  #
1564
1631
  # Related: #limit
1565
1632
  def limit!(max:)
1566
- modifying! # short-circuit, and normalize the error message for JRuby
1633
+ modifying! # short-circuit before querying
1567
1634
  star = include_star?
1568
- max = to_tuple_int(max)
1569
- tuple_subtract [max + 1, STAR_INT]
1570
- 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
1571
1638
  normalize!
1572
1639
  end
1573
1640
 
1574
1641
  # :call-seq: complement! -> self
1575
1642
  #
1576
- # Converts the SequenceSet to its own #complement. It will contain all
1577
- # 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.
1578
1646
  #
1579
1647
  # Related: #complement
1580
1648
  def complement!
1581
- modifying! # short-circuit, and normalize the error message for JRuby
1649
+ modifying! # short-circuit before querying
1582
1650
  return replace(self.class.full) if empty?
1583
1651
  return clear if full?
1584
- flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
1652
+ flat = minmaxes.flat_map { [_1 - 1, _2 + 1] }
1585
1653
  if flat.first < 1 then flat.shift else flat.unshift 1 end
1586
1654
  if STAR_INT < flat.last then flat.pop else flat.push STAR_INT end
1587
- @tuples = flat.each_slice(2).to_a
1655
+ replace_minmaxes flat.each_slice(2).to_a
1588
1656
  normalize!
1589
1657
  end
1590
1658
 
1591
- # 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.
1664
+ #
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.
1592
1744
  #
1593
- # The returned set's #string is sorted and deduplicated. Adjacent or
1594
- # overlapping elements will be merged into a single larger range.
1595
1745
  # See SequenceSet@Ordered+and+Normalized+sets.
1596
1746
  #
1597
1747
  # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalize
1598
1748
  # #=> Net::IMAP::SequenceSet["1:7,9:11"]
1599
1749
  #
1600
- # Related: #normalize!, #normalized_string
1750
+ # Related: #normalize!, #normalized_string, #normalized?
1601
1751
  def normalize
1602
- str = normalized_string
1603
- return self if frozen? && str == string
1604
- remain_frozen dup.instance_exec { @string = str&.-@; self }
1752
+ frozen? && normalized? ? self : remain_frozen(dup.normalize!)
1605
1753
  end
1606
1754
 
1607
1755
  # Resets #string to be sorted, deduplicated, and coalesced. Returns
1608
1756
  # +self+. See SequenceSet@Ordered+and+Normalized+sets.
1609
1757
  #
1610
- # Related: #normalize, #normalized_string
1758
+ # Related: #normalize, #normalized_string, #normalized?
1611
1759
  def normalize!
1612
- modifying! # redundant check, to normalize the error message for JRuby
1760
+ modifying! # redundant check (normalizes the error message for JRuby)
1613
1761
  @string = nil
1614
1762
  self
1615
1763
  end
@@ -1623,18 +1771,53 @@ module Net
1623
1771
  #
1624
1772
  # Returns +nil+ when the set is empty.
1625
1773
  #
1626
- # Related: #normalize!, #normalize
1774
+ # Related: #normalize!, #normalize, #string, #to_s, #normalized?
1627
1775
  def normalized_string
1628
- @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
1776
+ export_runs(runs) unless runs.empty?
1629
1777
  end
1630
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
1631
1802
  def inspect
1632
- if empty?
1633
- (frozen? ? "%s.empty" : "#<%s empty>") % [self.class]
1634
- elsif frozen?
1635
- "%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]
1636
1808
  else
1637
- "#<%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
+ ]
1638
1821
  end
1639
1822
  end
1640
1823
 
@@ -1668,13 +1851,17 @@ module Net
1668
1851
 
1669
1852
  # For YAML deserialization
1670
1853
  def init_with(coder) # :nodoc:
1671
- @tuples = []
1854
+ @set_data = new_set_data
1672
1855
  self.string = coder['string']
1673
1856
  end
1674
1857
 
1858
+ # :stopdoc:
1675
1859
  protected
1676
1860
 
1677
- attr_reader :tuples # :nodoc:
1861
+ attr_reader :set_data
1862
+
1863
+ alias runs set_data
1864
+ alias minmaxes runs
1678
1865
 
1679
1866
  private
1680
1867
 
@@ -1683,39 +1870,42 @@ module Net
1683
1870
 
1684
1871
  # frozen clones are shallow copied
1685
1872
  def initialize_clone(other)
1686
- other.frozen? ? super : initialize_dup(other)
1873
+ @set_data = other.dup_set_data unless other.frozen?
1874
+ super
1687
1875
  end
1688
1876
 
1689
1877
  def initialize_dup(other)
1690
- modifying! # redundant check, to normalize the error message for JRuby
1691
- @tuples = other.tuples.map(&:dup)
1692
- @string = other.string&.-@
1878
+ @set_data = other.dup_set_data
1693
1879
  super
1694
1880
  end
1695
1881
 
1696
- def input_to_tuple(entry)
1697
- entry = input_try_convert entry
1882
+ ######################################################################{{{2
1883
+ # Import methods
1884
+
1885
+ def import_minmax(input)
1886
+ entry = input_try_convert input
1698
1887
  case entry
1699
- when *STARS, Integer then [int = to_tuple_int(entry), int]
1700
- when Range then range_to_tuple(entry)
1701
- 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)
1702
1891
  else
1703
- raise DataFormatError, "expected number or range, got %p" % [entry]
1892
+ raise DataFormatError, "expected number or range, got %p" % [input]
1704
1893
  end
1705
1894
  end
1895
+ alias import_run import_minmax
1706
1896
 
1707
- def input_to_tuples(set)
1708
- set = input_try_convert set
1897
+ def import_runs(input)
1898
+ set = input_try_convert input
1709
1899
  case set
1710
- when *STARS, Integer, Range then [input_to_tuple(set)]
1711
- when String then str_to_tuples set
1712
- when SequenceSet then set.tuples
1713
- when Set then set.map { [to_tuple_int(_1)] * 2 }
1714
- when Array 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 }
1715
1905
  when nil then []
1716
1906
  else
1717
1907
  raise DataFormatError, "expected nz-number, range, '*', Set, Array; " \
1718
- "got %p" % [set]
1908
+ "got %p" % [input]
1719
1909
  end
1720
1910
  end
1721
1911
 
@@ -1728,9 +1918,17 @@ module Net
1728
1918
  input
1729
1919
  end
1730
1920
 
1731
- def range_to_tuple(range)
1732
- first = to_tuple_int(range.begin || 1)
1733
- 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 || :*)
1734
1932
  last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1735
1933
  unless first <= last
1736
1934
  raise DataFormatError, "invalid range for sequence-set: %p" % [range]
@@ -1738,67 +1936,260 @@ module Net
1738
1936
  [first, last]
1739
1937
  end
1740
1938
 
1741
- def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1742
- 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
1946
+
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
1743
1980
 
1744
- def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
1745
- def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
1746
- def str_to_tuple(str)
1981
+ def parse_entry(str)
1747
1982
  raise DataFormatError, "invalid sequence set string" if str.empty?
1748
- str.split(":", 2).map! { to_tuple_int _1 }.minmax
1983
+ str.split(":", 2).map! { import_num _1 }
1749
1984
  end
1750
1985
 
1751
- def include_tuple?((min, max)) range_gte_to(min)&.cover?(min..max) end
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
1990
+ end
1991
+
1992
+ def normal_string?(str) normalized_entries? each_parsed_entry str end
1993
+
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
1752
2003
 
1753
- def intersect_tuple?((min, max))
1754
- range = range_gte_to(min) and
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
1755
2029
  range.include?(min) || range.include?(max) || (min..max).cover?(range)
1756
2030
  end
1757
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
+
1758
2134
  def modifying!
1759
2135
  if frozen?
1760
2136
  raise FrozenError, "can't modify frozen #{self.class}: %p" % [self]
1761
2137
  end
1762
2138
  end
1763
2139
 
1764
- def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
1765
- 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
1766
2153
 
1767
2154
  #
1768
- # --|=====| |=====new tuple=====| append
1769
- # ?????????-|=====new tuple=====|-|===lower===|-- insert
2155
+ # --|=====| |=====new run=======| append
2156
+ # ?????????-|=====new run=======|-|===lower===|-- insert
1770
2157
  #
1771
- # |=====new tuple=====|
2158
+ # |=====new run=======|
1772
2159
  # ---------??=======lower=======??--------------- noop
1773
2160
  #
1774
2161
  # ---------??===lower==|--|==| join remaining
1775
2162
  # ---------??===lower==|--|==|----|===upper===|-- join until upper
1776
2163
  # ---------??===lower==|--|==|--|=====upper===|-- join to upper
1777
- def tuple_add(tuple)
2164
+ def add_minmax(minmax)
1778
2165
  modifying!
1779
- min, max = tuple
1780
- lower, lower_idx = tuple_gte_with_index(min - 1)
1781
- if lower.nil? then tuples << [min, max]
1782
- elsif (max + 1) < lower.first then tuples.insert(lower_idx, [min, max])
1783
- 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)
1784
2172
  end
1785
2173
  end
1786
2174
 
1787
- def tuple_coalesce(lower, lower_idx, min, max)
1788
- return if lower.first <= min && max <= lower.last
1789
- lower[0] = [min, lower.first].min
1790
- lower[1] = [max, lower.last].max
1791
- lower_idx += 1
1792
- return if lower_idx == tuples.count
1793
- tmax_adj = lower.last + 1
1794
- upper, upper_idx = tuple_gte_with_index(tmax_adj)
1795
- if upper
1796
- 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
1797
2188
  end
1798
- tuples.slice!(lower_idx..upper_idx)
2189
+ slice_runs! next_idx..upper_idx
1799
2190
  end
1800
2191
 
1801
- # |====tuple================|
2192
+ # |====subtracted run=======|
1802
2193
  # --|====| no more 1. noop
1803
2194
  # --|====|---------------------------|====lower====|-- 2. noop
1804
2195
  # -------|======lower================|---------------- 3. split
@@ -1811,61 +2202,59 @@ module Net
1811
2202
  # -------??=====lower====|--|====| no more 6. delete rest
1812
2203
  # -------??=====lower====|--|====|---|====upper====|-- 7. delete until
1813
2204
  # -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
1814
- def tuple_subtract(tuple)
2205
+ def subtract_minmax(minmax)
1815
2206
  modifying!
1816
- min, max = tuple
1817
- lower, idx = tuple_gte_with_index(min)
1818
- if lower.nil? then nil # case 1.
1819
- elsif max < lower.first then nil # case 2.
1820
- elsif max < lower.last then tuple_trim_or_split lower, idx, min, max
1821
- 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
1822
2214
  end
1823
2215
  end
1824
2216
 
1825
- def tuple_trim_or_split(lower, idx, tmin, tmax)
1826
- if lower.first < tmin # split
1827
- 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
1828
2221
  end
1829
- lower[0] = tmax + 1
1830
2222
  end
1831
2223
 
1832
- def tuples_trim_or_delete(lower, lower_idx, tmin, tmax)
1833
- if lower.first < tmin # trim lower
1834
- 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
1835
2227
  lower_idx += 1
1836
2228
  end
1837
- if tmax == lower.last # case 5
1838
- upper_idx = lower_idx
1839
- elsif (upper, upper_idx = tuple_gte_with_index(tmax + 1))
1840
- upper_idx -= 1 # cases 7 and 8
1841
- 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
1842
2238
  end
1843
- tuples.slice!(lower_idx..upper_idx)
1844
- end
1845
-
1846
- def tuple_gte_with_index(num)
1847
- idx = tuples.bsearch_index { _2 >= num } and [tuples[idx], idx]
1848
2239
  end
1849
2240
 
1850
- def range_gte_to(num)
1851
- first, last = tuples.bsearch { _2 >= num }
1852
- first..last if first
1853
- end
1854
-
1855
- def nz_number(num)
1856
- String === num && !/\A[1-9]\d*\z/.match?(num) and
1857
- raise DataFormatError, "%p is not a valid nz-number" % [num]
1858
- NumValidator.ensure_nz_number Integer num
1859
- rescue TypeError # To catch errors from Integer()
1860
- raise DataFormatError, $!.message
1861
- 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
1862
2245
 
2246
+ ######################################################################{{{2
1863
2247
  # intentionally defined after the class implementation
1864
2248
 
2249
+ FULL_SET_DATA = [[1, STAR_INT].freeze].freeze
2250
+ private_constant :FULL_SET_DATA
2251
+
1865
2252
  EMPTY = new.freeze
1866
2253
  FULL = self["1:*"]
1867
2254
  private_constant :EMPTY, :FULL
1868
2255
 
2256
+ # }}}
2257
+ # vim:foldmethod=marker
1869
2258
  end
1870
2259
  end
1871
2260
  end