net-imap 0.5.13 → 0.6.0

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.
@@ -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
  #
@@ -531,12 +555,17 @@ module Net
531
555
  # combined with set operations (#|, #&, #^, #-, etc) to make new sets.
532
556
  #
533
557
  # See SequenceSet@Creating+sequence+sets.
534
- 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
535
563
 
536
564
  # Removes all elements and returns self.
537
565
  def clear
538
- modifying! # redundant check, to normalize the error message for JRuby
539
- @tuples, @string = [], nil
566
+ modifying! # redundant check (normalizes the error message for JRuby)
567
+ set_data.clear
568
+ @string = nil
540
569
  self
541
570
  end
542
571
 
@@ -549,7 +578,7 @@ module Net
549
578
  case other
550
579
  when SequenceSet then
551
580
  modifying! # short circuit before doing any work
552
- @tuples = other.deep_copy_tuples
581
+ @set_data = other.dup_set_data
553
582
  @string = other.instance_variable_get(:@string)
554
583
  when String then self.string = other
555
584
  else clear; merge other
@@ -580,7 +609,7 @@ module Net
580
609
  # the set is updated the string will be normalized.
581
610
  #
582
611
  # Related: #valid_string, #normalized_string, #to_s, #inspect
583
- def string; @string ||= normalized_string if valid? end
612
+ def string; @string || normalized_string if valid? end
584
613
 
585
614
  # Returns an array with #normalized_string when valid and an empty array
586
615
  # otherwise.
@@ -599,13 +628,18 @@ module Net
599
628
  clear
600
629
  elsif (str = String.try_convert(input))
601
630
  modifying! # short-circuit before parsing the string
602
- tuples = str_to_tuples str
603
- @tuples, @string = [], -str
604
- tuples_add tuples
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
605
639
  else
606
640
  raise ArgumentError, "expected a string or nil, got #{input.class}"
607
641
  end
608
- str
642
+ input
609
643
  end
610
644
 
611
645
  # Returns the \IMAP +sequence-set+ string representation, or an empty
@@ -618,8 +652,7 @@ module Net
618
652
  # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
619
653
  def freeze
620
654
  return self if frozen?
621
- string
622
- @tuples.each(&:freeze).freeze
655
+ freeze_set_data
623
656
  super
624
657
  end
625
658
 
@@ -641,7 +674,7 @@ module Net
641
674
  # Related: #eql?, #normalize
642
675
  def ==(other)
643
676
  self.class == other.class &&
644
- (to_s == other.to_s || tuples == other.tuples)
677
+ (to_s == other.to_s || set_data == other.set_data)
645
678
  end
646
679
 
647
680
  # :call-seq: eql?(other) -> true or false
@@ -682,7 +715,7 @@ module Net
682
715
  # object that would be accepted by ::new.
683
716
  #
684
717
  # Related: #===, #include?, #include_star?, #intersect?
685
- def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
718
+ def cover?(other) import_runs(other).none? { !include_run?(_1) } end
686
719
 
687
720
  # Returns +true+ when a given number or range is in +self+, and +false+
688
721
  # otherwise. Returns +nil+ when +number+ isn't a valid SequenceSet
@@ -709,14 +742,14 @@ module Net
709
742
  #
710
743
  # Related: #include_star?, #cover?, #===, #intersect?
711
744
  def include?(element)
712
- tuple = input_to_tuple element rescue nil
713
- !!include_tuple?(tuple) if tuple
745
+ run = import_run element rescue nil
746
+ !!include_run?(run) if run
714
747
  end
715
748
 
716
749
  alias member? include?
717
750
 
718
751
  # Returns +true+ when the set contains <tt>*</tt>.
719
- def include_star?; @tuples.last&.last == STAR_INT end
752
+ def include_star?; max_num == STAR_INT end
720
753
 
721
754
  # Returns +true+ if the set and a given object have any common elements,
722
755
  # +false+ otherwise.
@@ -726,7 +759,7 @@ module Net
726
759
  #
727
760
  # Related: #intersection, #disjoint?, #cover?, #include?
728
761
  def intersect?(other)
729
- valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
762
+ valid? && import_runs(other).any? { intersect_run? _1 }
730
763
  end
731
764
  alias overlap? intersect?
732
765
 
@@ -738,7 +771,7 @@ module Net
738
771
  #
739
772
  # Related: #intersection, #intersect?
740
773
  def disjoint?(other)
741
- empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
774
+ empty? || import_runs(other).none? { intersect_run? _1 }
742
775
  end
743
776
 
744
777
  # :call-seq:
@@ -755,14 +788,8 @@ module Net
755
788
  # Related: #min, #minmax, #slice
756
789
  def max(count = nil, star: :*)
757
790
  if count
758
- # n.b: #cardinality has not been backported to 0.5
759
- cardinality = @tuples.sum(@tuples.count) { _2 - _1 }
760
- if cardinality <= count
761
- frozen? ? self : dup
762
- else
763
- slice(-count..) || remain_frozen_empty
764
- end
765
- elsif (val = @tuples.last&.last)
791
+ slice(-[count, size].min..) || remain_frozen_empty
792
+ elsif (val = max_num)
766
793
  val == STAR_INT ? star : val
767
794
  end
768
795
  end
@@ -782,7 +809,7 @@ module Net
782
809
  def min(count = nil, star: :*)
783
810
  if count
784
811
  slice(0...count) || remain_frozen_empty
785
- elsif (val = @tuples.first&.first)
812
+ elsif (val = min_num)
786
813
  val != STAR_INT ? val : star
787
814
  end
788
815
  end
@@ -800,10 +827,10 @@ module Net
800
827
  def valid?; !empty? end
801
828
 
802
829
  # Returns true if the set contains no elements
803
- def empty?; @tuples.empty? end
830
+ def empty?; runs.empty? end
804
831
 
805
832
  # Returns true if the set contains every possible element.
806
- def full?; @tuples == [[1, STAR_INT]] end
833
+ def full?; set_data == FULL_SET_DATA end
807
834
 
808
835
  # :call-seq:
809
836
  # self + other -> sequence set
@@ -879,9 +906,7 @@ module Net
879
906
  # * <tt>lhs - (lhs - rhs)</tt>
880
907
  # * <tt>lhs - (lhs ^ rhs)</tt>
881
908
  # * <tt>lhs ^ (lhs - rhs)</tt>
882
- def &(other)
883
- remain_frozen dup.subtract SequenceSet.new(other).complement!
884
- end
909
+ def &(other) remain_frozen dup.intersect! other end
885
910
  alias intersection :&
886
911
 
887
912
  # :call-seq:
@@ -906,7 +931,7 @@ module Net
906
931
  # * <tt>(lhs | rhs) - (lhs & rhs)</tt>
907
932
  # * <tt>(lhs - rhs) | (rhs - lhs)</tt>
908
933
  # * <tt>(lhs ^ other) ^ (other ^ rhs)</tt>
909
- def ^(other) remain_frozen (dup | other).subtract(self & other) end
934
+ def ^(other) remain_frozen dup.xor! other end
910
935
  alias xor :^
911
936
 
912
937
  # :call-seq:
@@ -945,8 +970,8 @@ module Net
945
970
  #
946
971
  # Related: #add?, #merge, #union, #append
947
972
  def add(element)
948
- modifying! # short-circuit before input_to_tuple
949
- tuple_add input_to_tuple element
973
+ modifying! # short-circuit before import_run
974
+ add_run import_run element
950
975
  normalize!
951
976
  end
952
977
  alias << add
@@ -956,16 +981,55 @@ module Net
956
981
  # Unlike #add, #merge, or #union, the new value is appended to #string.
957
982
  # This may result in a #string which has duplicates or is out-of-order.
958
983
  #
959
- # See SequenceSet@Ordered+and+Normalized+sets.
984
+ # set = Net::IMAP::SequenceSet.new
985
+ # set.append(1..2) # => Net::IMAP::SequenceSet("1:2")
986
+ # set.append(5) # => Net::IMAP::SequenceSet("1:2,5")
987
+ # set.append(4) # => Net::IMAP::SequenceSet("1:2,5,4")
988
+ # set.append(3) # => Net::IMAP::SequenceSet("1:2,5,4,3")
989
+ # set.append(2) # => Net::IMAP::SequenceSet("1:2,5,4,3,2")
990
+ #
991
+ # If +entry+ is a string, it will be converted into normal form.
992
+ #
993
+ # set = Net::IMAP::SequenceSet("4:5,1:2")
994
+ # set.append("6:6") # => Net::IMAP::SequenceSet("4:5,1:2,6")
995
+ # set.append("9:8") # => Net::IMAP::SequenceSet("4:5,1:2,6,8:9")
996
+ #
997
+ # If +entry+ adjacently follows the last entry, they will coalesced:
998
+ # set = Net::IMAP::SequenceSet.new("2,1,9:10")
999
+ # set.append(11..12) # => Net::IMAP::SequenceSet("2,1,9:12")
1000
+ #
1001
+ # Non-normalized sets store the string <em>in addition to</em> an internal
1002
+ # normalized uint32 set representation. This can more than double memory
1003
+ # usage, so large sets should avoid using #append unless preserving order
1004
+ # is required. See SequenceSet@Ordered+and+Normalized+sets.
960
1005
  #
961
1006
  # Related: #add, #merge, #union
962
1007
  def append(entry)
963
- modifying! # short-circuit before input_to_tuple
964
- tuple = input_to_tuple entry
965
- entry = tuple_to_str tuple
966
- string unless empty? # write @string before tuple_add
967
- tuple_add tuple
968
- @string = -(@string ? "#{@string},#{entry}" : entry)
1008
+ modifying! # short-circuit before import_minmax
1009
+ minmax = import_minmax entry
1010
+ adj = minmax.first - 1
1011
+ if @string.nil? && (runs.empty? || max_num <= adj)
1012
+ # append to elements or coalesce with last element
1013
+ add_minmax minmax
1014
+ return self
1015
+ elsif @string.nil?
1016
+ # generate string for out-of-order append
1017
+ head, comma = normalized_string, ","
1018
+ else
1019
+ # @string already exists... maybe coalesce with last entry
1020
+ head, comma, last_entry = @string.rpartition(",")
1021
+ last_min, last_max = import_minmax last_entry
1022
+ if last_max == adj
1023
+ # coalesce with last entry
1024
+ minmax[0] = last_min
1025
+ else
1026
+ # append to existing string
1027
+ head, comma = @string, ","
1028
+ end
1029
+ end
1030
+ entry = export_minmax minmax
1031
+ add_minmax minmax
1032
+ @string = -"#{head}#{comma}#{entry}"
969
1033
  self
970
1034
  end
971
1035
 
@@ -991,8 +1055,8 @@ module Net
991
1055
  #
992
1056
  # Related: #delete?, #delete_at, #subtract, #difference
993
1057
  def delete(element)
994
- modifying! # short-circuit before input_to_tuple
995
- tuple_subtract input_to_tuple element
1058
+ modifying! # short-circuit before import_run
1059
+ subtract_run import_run element
996
1060
  normalize!
997
1061
  end
998
1062
 
@@ -1029,17 +1093,16 @@ module Net
1029
1093
  #
1030
1094
  # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
1031
1095
  def delete?(element)
1032
- modifying! # short-circuit before input_to_tuple
1033
- element = input_try_convert(element)
1034
- tuple = input_to_tuple element
1035
- if number_input?(element)
1036
- return unless include_tuple? tuple
1037
- tuple_subtract tuple
1096
+ modifying! # short-circuit before import_minmax
1097
+ minmax = import_minmax element
1098
+ if minmax.first == minmax.last
1099
+ return unless include_minmax? minmax
1100
+ subtract_minmax minmax
1038
1101
  normalize!
1039
- from_tuple_int tuple.first
1102
+ export_num minmax.first
1040
1103
  else
1041
1104
  copy = dup
1042
- tuple_subtract tuple
1105
+ subtract_minmax minmax
1043
1106
  normalize!
1044
1107
  copy if copy.subtract(self).valid?
1045
1108
  end
@@ -1076,8 +1139,8 @@ module Net
1076
1139
  deleted
1077
1140
  end
1078
1141
 
1079
- # Merges all of the elements that appear in any of the +sets+ into the
1080
- # set, and returns +self+.
1142
+ # In-place set #union. Merges all of the elements that appear in any of
1143
+ # the +sets+ into this set, and returns +self+.
1081
1144
  #
1082
1145
  # The +sets+ may be any objects that would be accepted by ::new.
1083
1146
  #
@@ -1085,19 +1148,20 @@ module Net
1085
1148
  #
1086
1149
  # Related: #add, #add?, #union
1087
1150
  def merge(*sets)
1088
- modifying! # short-circuit before input_to_tuples
1089
- tuples_add input_to_tuples sets
1151
+ modifying! # short-circuit before import_runs
1152
+ add_runs import_runs sets
1090
1153
  normalize!
1091
1154
  end
1092
1155
 
1093
- # Removes all of the elements that appear in any of the given +sets+ from
1094
- # the set, and returns +self+.
1156
+ # In-place set #difference. Removes all of the elements that appear in
1157
+ # any of the given +sets+ from this set, and returns +self+.
1095
1158
  #
1096
1159
  # The +sets+ may be any objects that would be accepted by ::new.
1097
1160
  #
1098
1161
  # Related: #difference
1099
1162
  def subtract(*sets)
1100
- tuples_subtract input_to_tuples sets
1163
+ modifying! # short-circuit before import_runs
1164
+ subtract_runs import_runs sets
1101
1165
  normalize!
1102
1166
  end
1103
1167
 
@@ -1189,7 +1253,7 @@ module Net
1189
1253
  # Related: #entries, #each_element
1190
1254
  def each_entry(&block) # :yields: integer or range or :*
1191
1255
  return to_enum(__method__) unless block_given?
1192
- each_entry_tuple do yield tuple_to_entry _1 end
1256
+ each_entry_run do yield export_run_entry _1 end
1193
1257
  end
1194
1258
 
1195
1259
  # Yields each number or range (or <tt>:*</tt>) in #elements to the block
@@ -1201,39 +1265,17 @@ module Net
1201
1265
  # Related: #elements, #each_entry
1202
1266
  def each_element # :yields: integer or range or :*
1203
1267
  return to_enum(__method__) unless block_given?
1204
- @tuples.each do yield tuple_to_entry _1 end
1205
- self
1206
- end
1207
-
1208
- private
1209
-
1210
- def each_entry_tuple(&block)
1211
- return to_enum(__method__) unless block_given?
1212
- if @string
1213
- @string.split(",") do block.call str_to_tuple _1 end
1214
- else
1215
- @tuples.each(&block)
1216
- end
1268
+ runs.each do yield export_run_entry _1 end
1217
1269
  self
1218
1270
  end
1219
1271
 
1220
- def tuple_to_entry((min, max))
1221
- if min == STAR_INT then :*
1222
- elsif max == STAR_INT then min..
1223
- elsif min == max then min
1224
- else min..max
1225
- end
1226
- end
1227
-
1228
- public
1229
-
1230
1272
  # Yields each range in #ranges to the block and returns self.
1231
1273
  # Returns an enumerator when called without a block.
1232
1274
  #
1233
1275
  # Related: #ranges
1234
1276
  def each_range # :yields: range
1235
1277
  return to_enum(__method__) unless block_given?
1236
- @tuples.each do |min, max|
1278
+ minmaxes.each do |min, max|
1237
1279
  if min == STAR_INT then yield :*..
1238
1280
  elsif max == STAR_INT then yield min..
1239
1281
  else yield min..max
@@ -1252,7 +1294,7 @@ module Net
1252
1294
  def each_number(&block) # :yields: integer
1253
1295
  return to_enum(__method__) unless block_given?
1254
1296
  raise RangeError, '%s contains "*"' % [self.class] if include_star?
1255
- @tuples.each do each_number_in_tuple _1, _2, &block end
1297
+ minmaxes.each do each_number_in_minmax _1, _2, &block end
1256
1298
  self
1257
1299
  end
1258
1300
 
@@ -1266,16 +1308,7 @@ module Net
1266
1308
  def each_ordered_number(&block)
1267
1309
  return to_enum(__method__) unless block_given?
1268
1310
  raise RangeError, '%s contains "*"' % [self.class] if include_star?
1269
- each_entry_tuple do each_number_in_tuple _1, _2, &block end
1270
- end
1271
-
1272
- private def each_number_in_tuple(min, max, &block)
1273
- if min == STAR_INT then yield :*
1274
- elsif min == max then yield min
1275
- elsif max != STAR_INT then (min..max).each(&block)
1276
- else
1277
- raise RangeError, "#{SequenceSet} cannot enumerate range with '*'"
1278
- end
1311
+ each_entry_minmax do each_number_in_minmax _1, _2, &block end
1279
1312
  end
1280
1313
 
1281
1314
  # Returns a Set with all of the #numbers in the sequence set.
@@ -1287,35 +1320,103 @@ module Net
1287
1320
  # Related: #elements, #ranges, #numbers
1288
1321
  def to_set; Set.new(numbers) end
1289
1322
 
1290
- # Returns the count of #numbers in the set.
1323
+ # Returns the number of members in the set.
1324
+ #
1325
+ # Unlike #count, <tt>"*"</tt> is considered to be distinct from
1326
+ # <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1327
+ #
1328
+ # set = Net::IMAP::SequenceSet[1..10]
1329
+ # set.count #=> 10
1330
+ # set.cardinality #=> 10
1331
+ #
1332
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1333
+ # set.count #=> 1
1334
+ # set.cardinality #=> 2
1335
+ #
1336
+ # set = Net::IMAP::SequenceSet[1..]
1337
+ # set.count #=> 4294967295
1338
+ # set.cardinality #=> 4294967296
1339
+ #
1340
+ # Related: #count, #count_with_duplicates
1341
+ def cardinality = minmaxes.sum(runs.count) { _2 - _1 }
1342
+
1343
+ # Returns the count of distinct #numbers in the set.
1344
+ #
1345
+ # Unlike #cardinality, <tt>"*"</tt> is considered to be equal to
1346
+ # <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1347
+ #
1348
+ # set = Net::IMAP::SequenceSet[1..10]
1349
+ # set.count #=> 10
1350
+ # set.cardinality #=> 10
1351
+ #
1352
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1353
+ # set.count #=> 1
1354
+ # set.cardinality #=> 2
1291
1355
  #
1292
- # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1293
- # unsigned integer value).
1356
+ # set = Net::IMAP::SequenceSet[1..]
1357
+ # set.count #=> 4294967295
1358
+ # set.cardinality #=> 4294967296
1294
1359
  #
1295
- # Related: #count_with_duplicates
1360
+ # Related: #cardinality, #count_with_duplicates
1296
1361
  def count
1297
- @tuples.sum(@tuples.count) { _2 - _1 } +
1298
- (include_star? && include?(UINT32_MAX) ? -1 : 0)
1362
+ cardinality + (include_star? && include?(UINT32_MAX) ? -1 : 0)
1299
1363
  end
1300
1364
 
1301
- alias size count
1302
-
1303
1365
  # Returns the count of numbers in the ordered #entries, including any
1304
1366
  # repeated numbers.
1305
1367
  #
1306
- # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1307
- # unsigned integer value).
1308
- #
1309
- # When #string is normalized, this behaves the same as #count.
1310
- #
1311
- # Related: #entries, #count_duplicates, #has_duplicates?
1368
+ # When #string is normalized, this returns the same as #count. Like
1369
+ # #count, <tt>"*"</tt> is considered to be equal to <tt>2³² - 1</tt> (the
1370
+ # maximum 32-bit unsigned integer value).
1371
+ #
1372
+ # In a range, <tt>"*"</tt> is _not_ considered a duplicate:
1373
+ # set = Net::IMAP::SequenceSet["4294967295:*"]
1374
+ # set.count_with_duplicates #=> 1
1375
+ # set.size #=> 2
1376
+ # set.count #=> 1
1377
+ # set.cardinality #=> 2
1378
+ #
1379
+ # In a separate entry, <tt>"*"</tt> _is_ considered a duplicate:
1380
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1381
+ # set.count_with_duplicates #=> 2
1382
+ # set.size #=> 2
1383
+ # set.count #=> 1
1384
+ # set.cardinality #=> 2
1385
+ #
1386
+ # Related: #count, #cardinality, #size, #count_duplicates,
1387
+ # #has_duplicates?, #entries
1312
1388
  def count_with_duplicates
1313
1389
  return count unless @string
1314
- each_entry_tuple.sum {|min, max|
1390
+ each_entry_minmax.sum {|min, max|
1315
1391
  max - min + ((max == STAR_INT && min != STAR_INT) ? 0 : 1)
1316
1392
  }
1317
1393
  end
1318
1394
 
1395
+ # Returns the combined size of the ordered #entries, including any
1396
+ # repeated numbers.
1397
+ #
1398
+ # When #string is normalized, this returns the same as #cardinality.
1399
+ # Like #cardinality, <tt>"*"</tt> is considered to be be distinct from
1400
+ # <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1401
+ #
1402
+ # set = Net::IMAP::SequenceSet["4294967295:*"]
1403
+ # set.size #=> 2
1404
+ # set.count_with_duplicates #=> 1
1405
+ # set.count #=> 1
1406
+ # set.cardinality #=> 2
1407
+ #
1408
+ # set = Net::IMAP::SequenceSet["4294967295,*"]
1409
+ # set.size #=> 2
1410
+ # set.count_with_duplicates #=> 2
1411
+ # set.count #=> 1
1412
+ # set.cardinality #=> 2
1413
+ #
1414
+ # Related: #cardinality, #count_with_duplicates, #count, #entries
1415
+ def size
1416
+ return cardinality unless @string
1417
+ each_entry_minmax.sum {|min, max| max - min + 1 }
1418
+ end
1419
+
1319
1420
  # Returns the count of repeated numbers in the ordered #entries, the
1320
1421
  # difference between #count_with_duplicates and #count.
1321
1422
  #
@@ -1333,7 +1434,7 @@ module Net
1333
1434
  #
1334
1435
  # Always returns +false+ when #string is normalized.
1335
1436
  #
1336
- # Related: #entries, #count_with_duplicates, #count_duplicates?
1437
+ # Related: #entries, #count_with_duplicates, #count_duplicates
1337
1438
  def has_duplicates?
1338
1439
  return false unless @string
1339
1440
  count_with_duplicates != count
@@ -1344,10 +1445,10 @@ module Net
1344
1445
  #
1345
1446
  # Related: #[], #at, #find_ordered_index
1346
1447
  def find_index(number)
1347
- number = to_tuple_int number
1348
- each_tuple_with_index(@tuples) do |min, max, idx_min|
1448
+ number = import_num number
1449
+ each_minmax_with_index(minmaxes) do |min, max, idx_min|
1349
1450
  number < min and return nil
1350
- number <= max and return from_tuple_int(idx_min + (number - min))
1451
+ number <= max and return export_num(idx_min + (number - min))
1351
1452
  end
1352
1453
  nil
1353
1454
  end
@@ -1357,38 +1458,15 @@ module Net
1357
1458
  #
1358
1459
  # Related: #find_index
1359
1460
  def find_ordered_index(number)
1360
- number = to_tuple_int number
1361
- each_tuple_with_index(each_entry_tuple) do |min, max, idx_min|
1461
+ number = import_num number
1462
+ each_minmax_with_index(each_entry_minmax) do |min, max, idx_min|
1362
1463
  if min <= number && number <= max
1363
- return from_tuple_int(idx_min + (number - min))
1464
+ return export_num(idx_min + (number - min))
1364
1465
  end
1365
1466
  end
1366
1467
  nil
1367
1468
  end
1368
1469
 
1369
- private
1370
-
1371
- def each_tuple_with_index(tuples)
1372
- idx_min = 0
1373
- tuples.each do |min, max|
1374
- idx_max = idx_min + (max - min)
1375
- yield min, max, idx_min, idx_max
1376
- idx_min = idx_max + 1
1377
- end
1378
- idx_min
1379
- end
1380
-
1381
- def reverse_each_tuple_with_index(tuples)
1382
- idx_max = -1
1383
- tuples.reverse_each do |min, max|
1384
- yield min, max, (idx_min = idx_max - (max - min)), idx_max
1385
- idx_max = idx_min - 1
1386
- end
1387
- idx_max
1388
- end
1389
-
1390
- public
1391
-
1392
1470
  # :call-seq: at(index) -> integer or nil
1393
1471
  #
1394
1472
  # Returns the number at the given +index+ in the sorted set, without
@@ -1399,7 +1477,7 @@ module Net
1399
1477
  #
1400
1478
  # Related: #[], #slice, #ordered_at
1401
1479
  def at(index)
1402
- lookup_number_by_tuple_index(tuples, index)
1480
+ seek_number_in_minmaxes(minmaxes, index)
1403
1481
  end
1404
1482
 
1405
1483
  # :call-seq: ordered_at(index) -> integer or nil
@@ -1412,21 +1490,7 @@ module Net
1412
1490
  #
1413
1491
  # Related: #[], #slice, #ordered_at
1414
1492
  def ordered_at(index)
1415
- lookup_number_by_tuple_index(each_entry_tuple, index)
1416
- end
1417
-
1418
- private def lookup_number_by_tuple_index(tuples, index)
1419
- index = Integer(index.to_int)
1420
- if index.negative?
1421
- reverse_each_tuple_with_index(tuples) do |min, max, idx_min, idx_max|
1422
- idx_min <= index and return from_tuple_int(min + (index - idx_min))
1423
- end
1424
- else
1425
- each_tuple_with_index(tuples) do |min, _, idx_min, idx_max|
1426
- index <= idx_max and return from_tuple_int(min + (index - idx_min))
1427
- end
1428
- end
1429
- nil
1493
+ seek_number_in_minmaxes(each_entry_minmax, index)
1430
1494
  end
1431
1495
 
1432
1496
  # :call-seq:
@@ -1477,37 +1541,6 @@ module Net
1477
1541
 
1478
1542
  alias slice :[]
1479
1543
 
1480
- private
1481
-
1482
- def slice_length(start, length)
1483
- start = Integer(start.to_int)
1484
- length = Integer(length.to_int)
1485
- raise ArgumentError, "length must be positive" unless length.positive?
1486
- last = start + length - 1 unless start.negative? && start.abs <= length
1487
- slice_range(start..last)
1488
- end
1489
-
1490
- def slice_range(range)
1491
- first = range.begin || 0
1492
- last = range.end || -1
1493
- if range.exclude_end?
1494
- return remain_frozen_empty if last.zero?
1495
- last -= 1 if range.end && last != STAR_INT
1496
- end
1497
- if (first * last).positive? && last < first
1498
- remain_frozen_empty
1499
- elsif (min = at(first))
1500
- max = at(last)
1501
- max = :* if max.nil?
1502
- if max == :* then self & (min..)
1503
- elsif min <= max then self & (min..max)
1504
- else remain_frozen_empty
1505
- end
1506
- end
1507
- end
1508
-
1509
- public
1510
-
1511
1544
  # Returns a copy of +self+ which only contains the numbers above +num+.
1512
1545
  #
1513
1546
  # Net::IMAP::SequenceSet["5,10:22,50"].above(10) # to_s => "11:22,50"
@@ -1579,7 +1612,7 @@ module Net
1579
1612
  #
1580
1613
  # Related: #limit!
1581
1614
  def limit(max:)
1582
- max = to_tuple_int(max)
1615
+ max = import_num(max)
1583
1616
  if empty? then self.class.empty
1584
1617
  elsif !include_star? && max < min then self.class.empty
1585
1618
  elsif max(star: STAR_INT) <= max then frozen? ? self : dup.freeze
@@ -1592,53 +1625,134 @@ module Net
1592
1625
  #
1593
1626
  # Related: #limit
1594
1627
  def limit!(max:)
1595
- modifying! # short-circuit, and normalize the error message for JRuby
1628
+ modifying! # short-circuit before querying
1596
1629
  star = include_star?
1597
- max = to_tuple_int(max)
1598
- tuple_subtract [max + 1, STAR_INT]
1599
- tuple_add [max, max ] if star
1630
+ max = import_num(max)
1631
+ subtract_minmax [max + 1, STAR_INT]
1632
+ add_minmax [max, max ] if star
1600
1633
  normalize!
1601
1634
  end
1602
1635
 
1603
1636
  # :call-seq: complement! -> self
1604
1637
  #
1605
- # Converts the SequenceSet to its own #complement. It will contain all
1606
- # possible values _except_ for those currently in the set.
1638
+ # In-place set #complement. Replaces the contents of this set with its
1639
+ # own #complement. It will contain all possible values _except_ for those
1640
+ # currently in the set.
1607
1641
  #
1608
1642
  # Related: #complement
1609
1643
  def complement!
1610
- modifying! # short-circuit, and normalize the error message for JRuby
1644
+ modifying! # short-circuit before querying
1611
1645
  return replace(self.class.full) if empty?
1612
1646
  return clear if full?
1613
- flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
1647
+ flat = minmaxes.flat_map { [_1 - 1, _2 + 1] }
1614
1648
  if flat.first < 1 then flat.shift else flat.unshift 1 end
1615
1649
  if STAR_INT < flat.last then flat.pop else flat.push STAR_INT end
1616
- @tuples = flat.each_slice(2).to_a
1650
+ replace_minmaxes flat.each_slice(2).to_a
1617
1651
  normalize!
1618
1652
  end
1619
1653
 
1620
- # Returns a new SequenceSet with a normalized string representation.
1654
+ # In-place set #intersection. Removes any elements that are missing from
1655
+ # +other+ from this set, keeping only the #intersection, and returns
1656
+ # +self+.
1657
+ #
1658
+ # +other+ can be any object that would be accepted by ::new.
1659
+ #
1660
+ # set = Net::IMAP::SequenceSet.new(1..5)
1661
+ # set.intersect! [2, 4, 6]
1662
+ # set #=> Net::IMAP::SequenceSet("2,4")
1663
+ #
1664
+ # Related: #intersection, #intersect?
1665
+ def intersect!(other)
1666
+ modifying! # short-circuit before processing input
1667
+ subtract SequenceSet.new(other).complement!
1668
+ end
1669
+
1670
+ # In-place set #xor. Adds any numbers in +other+ that are missing from
1671
+ # this set, removes any numbers in +other+ that are already in this set,
1672
+ # and returns +self+.
1673
+ #
1674
+ # +other+ can be any object that would be accepted by ::new.
1675
+ #
1676
+ # set = Net::IMAP::SequenceSet.new(1..5)
1677
+ # set.xor! [2, 4, 6]
1678
+ # set #=> Net::IMAP::SequenceSet["1,3,5:6"]
1679
+ #
1680
+ # Related: #xor, #merge, #subtract
1681
+ def xor!(other)
1682
+ modifying! # short-circuit before processing input
1683
+ other = SequenceSet.new(other)
1684
+ copy = dup
1685
+ merge(other).subtract(other.subtract(copy.complement!))
1686
+ end
1687
+
1688
+ # Returns whether #string is fully normalized: entries have been sorted,
1689
+ # deduplicated, and coalesced, and all entries are in normal form. See
1690
+ # SequenceSet@Ordered+and+Normalized+sets.
1691
+ #
1692
+ # Net::IMAP::SequenceSet["1,3,5"].normalized? #=> true
1693
+ # Net::IMAP::SequenceSet["20:30"].normalized? #=> true
1694
+ #
1695
+ # Net::IMAP::SequenceSet["3,5,1"].normalized? #=> false, not sorted
1696
+ # Net::IMAP::SequenceSet["1,2,3"].normalized? #=> false, not coalesced
1697
+ # Net::IMAP::SequenceSet["1:5,2"].normalized? #=> false, repeated number
1698
+ #
1699
+ # Net::IMAP::SequenceSet["1:1"].normalized? #=> false, number as range
1700
+ # Net::IMAP::SequenceSet["5:1"].normalized? #=> false, backwards range
1701
+ #
1702
+ # Returns +true+ if (and only if) #string is equal to #normalized_string:
1703
+ # seqset = Net::IMAP::SequenceSet["1:3,5"]
1704
+ # seqset.string #=> "1:3,5"
1705
+ # seqset.normalized_string #=> "1:3,5"
1706
+ # seqset.entries #=> [1..3, 5]
1707
+ # seqset.elements #=> [1..3, 5]
1708
+ # seqset.normalized? #=> true
1709
+ #
1710
+ # seqset = Net::IMAP::SequenceSet["3,1,2"]
1711
+ # seqset.string #=> "3,1,2"
1712
+ # seqset.normalized_string #=> "1:3"
1713
+ # seqset.entries #=> [3, 1, 2]
1714
+ # seqset.elements #=> [1..3]
1715
+ # seqset.normalized? #=> false
1716
+ #
1717
+ # Can return +false+ even when #entries and #elements are the same:
1718
+ # seqset = Net::IMAP::SequenceSet["5:1"]
1719
+ # seqset.string #=> "5:1"
1720
+ # seqset.normalized_string #=> "1:5"
1721
+ # seqset.entries #=> [1..5]
1722
+ # seqset.elements #=> [1..5]
1723
+ # seqset.normalized? #=> false
1724
+ #
1725
+ # Note that empty sets are normalized, even though they are not #valid?:
1726
+ # seqset = Net::IMAP::SequenceSet.empty
1727
+ # seqset.normalized? #=> true
1728
+ # seqset.valid? #=> false
1729
+ #
1730
+ # Related: #normalize, #normalize!, #normalized_string
1731
+ def normalized?
1732
+ @string.nil? || normal_string?(@string)
1733
+ end
1734
+
1735
+ # Returns a SequenceSet with a normalized string representation: entries
1736
+ # have been sorted, deduplicated, and coalesced, and all entries
1737
+ # are in normal form. Returns +self+ for frozen normalized sets, and a
1738
+ # normalized duplicate otherwise.
1621
1739
  #
1622
- # The returned set's #string is sorted and deduplicated. Adjacent or
1623
- # overlapping elements will be merged into a single larger range.
1624
1740
  # See SequenceSet@Ordered+and+Normalized+sets.
1625
1741
  #
1626
1742
  # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalize
1627
1743
  # #=> Net::IMAP::SequenceSet["1:7,9:11"]
1628
1744
  #
1629
- # Related: #normalize!, #normalized_string
1745
+ # Related: #normalize!, #normalized_string, #normalized?
1630
1746
  def normalize
1631
- str = normalized_string
1632
- return self if frozen? && str == string
1633
- remain_frozen dup.instance_exec { @string = str&.-@; self }
1747
+ frozen? && normalized? ? self : remain_frozen(dup.normalize!)
1634
1748
  end
1635
1749
 
1636
1750
  # Resets #string to be sorted, deduplicated, and coalesced. Returns
1637
1751
  # +self+. See SequenceSet@Ordered+and+Normalized+sets.
1638
1752
  #
1639
- # Related: #normalize, #normalized_string
1753
+ # Related: #normalize, #normalized_string, #normalized?
1640
1754
  def normalize!
1641
- modifying! # redundant check, to normalize the error message for JRuby
1755
+ modifying! # redundant check (normalizes the error message for JRuby)
1642
1756
  @string = nil
1643
1757
  self
1644
1758
  end
@@ -1652,9 +1766,9 @@ module Net
1652
1766
  #
1653
1767
  # Returns +nil+ when the set is empty.
1654
1768
  #
1655
- # Related: #normalize!, #normalize, #string, #to_s
1769
+ # Related: #normalize!, #normalize, #string, #to_s, #normalized?
1656
1770
  def normalized_string
1657
- @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
1771
+ export_runs(runs) unless runs.empty?
1658
1772
  end
1659
1773
 
1660
1774
  # Returns an inspection string for the SequenceSet.
@@ -1691,8 +1805,8 @@ module Net
1691
1805
  head = @string[INSPECT_ABRIDGED_HEAD_RE]
1692
1806
  tail = @string[INSPECT_ABRIDGED_TAIL_RE]
1693
1807
  else
1694
- head = export_string_entries(@tuples.first(INSPECT_TRUNCATE_LEN)) + ","
1695
- tail = "," + export_string_entries(@tuples.last(INSPECT_TRUNCATE_LEN))
1808
+ head = export_runs(runs.first(INSPECT_TRUNCATE_LEN)) + ","
1809
+ tail = "," + export_runs(runs.last(INSPECT_TRUNCATE_LEN))
1696
1810
  end
1697
1811
  '#<%s %d entries "%s...(%d entries omitted)...%s"%s>' % [
1698
1812
  self.class, count,
@@ -1702,10 +1816,6 @@ module Net
1702
1816
  end
1703
1817
  end
1704
1818
 
1705
- private def count_entries
1706
- @string ? @string.count(",") + 1 : @tuples.count
1707
- end
1708
-
1709
1819
  ##
1710
1820
  # :method: to_sequence_set
1711
1821
  # :call-seq: to_sequence_set -> self
@@ -1736,15 +1846,17 @@ module Net
1736
1846
 
1737
1847
  # For YAML deserialization
1738
1848
  def init_with(coder) # :nodoc:
1739
- @tuples = []
1849
+ @set_data = new_set_data
1740
1850
  self.string = coder['string']
1741
1851
  end
1742
1852
 
1853
+ # :stopdoc:
1743
1854
  protected
1744
1855
 
1745
- attr_reader :tuples # :nodoc:
1856
+ attr_reader :set_data
1746
1857
 
1747
- def deep_copy_tuples; @tuples.map { _1.dup } end # :nodoc:
1858
+ alias runs set_data
1859
+ alias minmaxes runs
1748
1860
 
1749
1861
  private
1750
1862
 
@@ -1753,38 +1865,42 @@ module Net
1753
1865
 
1754
1866
  # frozen clones are shallow copied
1755
1867
  def initialize_clone(other)
1756
- @tuples = other.deep_copy_tuples unless other.frozen?
1868
+ @set_data = other.dup_set_data unless other.frozen?
1757
1869
  super
1758
1870
  end
1759
1871
 
1760
1872
  def initialize_dup(other)
1761
- @tuples = other.deep_copy_tuples
1873
+ @set_data = other.dup_set_data
1762
1874
  super
1763
1875
  end
1764
1876
 
1765
- def input_to_tuple(entry)
1766
- entry = input_try_convert entry
1877
+ ######################################################################{{{2
1878
+ # Import methods
1879
+
1880
+ def import_minmax(input)
1881
+ entry = input_try_convert input
1767
1882
  case entry
1768
- when *STARS, Integer then [int = to_tuple_int(entry), int]
1769
- when Range then range_to_tuple(entry)
1770
- when String then str_to_tuple(entry)
1883
+ when *STARS, Integer then [int = import_num(entry), int]
1884
+ when Range then import_range_minmax(entry)
1885
+ when String then parse_minmax(entry)
1771
1886
  else
1772
- raise DataFormatError, "expected number or range, got %p" % [entry]
1887
+ raise DataFormatError, "expected number or range, got %p" % [input]
1773
1888
  end
1774
1889
  end
1890
+ alias import_run import_minmax
1775
1891
 
1776
- def input_to_tuples(set)
1777
- set = input_try_convert set
1892
+ def import_runs(input)
1893
+ set = input_try_convert input
1778
1894
  case set
1779
- when *STARS, Integer, Range then [input_to_tuple(set)]
1780
- when String then str_to_tuples set
1781
- when SequenceSet then set.tuples
1782
- when Set then set.map { [to_tuple_int(_1)] * 2 }
1783
- when Array then set.flat_map { input_to_tuples _1 }
1895
+ when *STARS, Integer, Range then [import_run(set)]
1896
+ when String then parse_runs set
1897
+ when SequenceSet then set.runs
1898
+ when Set then set.map { [import_num(_1)] * 2 }
1899
+ when Array then set.flat_map { import_runs _1 }
1784
1900
  when nil then []
1785
1901
  else
1786
1902
  raise DataFormatError, "expected nz-number, range, '*', Set, Array; " \
1787
- "got %p" % [set]
1903
+ "got %p" % [input]
1788
1904
  end
1789
1905
  end
1790
1906
 
@@ -1797,17 +1913,9 @@ module Net
1797
1913
  input
1798
1914
  end
1799
1915
 
1800
- # NOTE: input_try_convert must be called on input first
1801
- def number_input?(input)
1802
- case input
1803
- when *STARS, Integer then true
1804
- when String then !input.include?(/[:,]/)
1805
- end
1806
- end
1807
-
1808
- def range_to_tuple(range)
1809
- first = to_tuple_int(range.begin || 1)
1810
- last = to_tuple_int(range.end || :*)
1916
+ def import_range_minmax(range)
1917
+ first = import_num(range.begin || 1)
1918
+ last = import_num(range.end || :*)
1811
1919
  last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1812
1920
  unless first <= last
1813
1921
  raise DataFormatError, "invalid range for sequence-set: %p" % [range]
@@ -1815,71 +1923,260 @@ module Net
1815
1923
  [first, last]
1816
1924
  end
1817
1925
 
1818
- def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1819
- def from_tuple_int(num) num == STAR_INT ? :* : num end
1926
+ def import_num(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1927
+ def nz_number(num) = NumValidator.coerce_nz_number(num)
1928
+
1929
+ ######################################################################{{{2
1930
+ # Export methods
1820
1931
 
1821
- def export_string_entries(entries)
1822
- -entries.map { tuple_to_str _1 }.join(",")
1932
+ def export_num(num) num == STAR_INT ? :* : num end
1933
+
1934
+ def export_minmaxes(minmaxes)
1935
+ -minmaxes.map { export_minmax _1 }.join(",")
1936
+ end
1937
+
1938
+ def export_minmax(minmax) minmax.uniq.map { export_num _1 }.join(":") end
1939
+
1940
+ alias export_runs export_minmaxes
1941
+ alias export_run export_minmax
1942
+
1943
+ def export_minmax_entry((min, max))
1944
+ if min == STAR_INT then :*
1945
+ elsif max == STAR_INT then min..
1946
+ elsif min == max then min
1947
+ else min..max
1948
+ end
1949
+ end
1950
+ alias export_run_entry export_minmax_entry
1951
+
1952
+ def each_number_in_minmax(min, max, &block)
1953
+ if min == STAR_INT then yield :*
1954
+ elsif min == max then yield min
1955
+ elsif max != STAR_INT then (min..max).each(&block)
1956
+ else
1957
+ raise RangeError, "#{SequenceSet} cannot enumerate range with '*'"
1958
+ end
1823
1959
  end
1824
1960
 
1825
- def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
1826
- def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
1827
- def str_to_tuple(str)
1961
+ ######################################################################{{{2
1962
+ # Parse methods
1963
+
1964
+ def parse_runs(str) str.split(",", -1).map! { parse_run _1 } end
1965
+ def parse_minmax(str) parse_entry(str).minmax end
1966
+ alias parse_run parse_minmax
1967
+
1968
+ def parse_entry(str)
1828
1969
  raise DataFormatError, "invalid sequence set string" if str.empty?
1829
- str.split(":", 2).map! { to_tuple_int _1 }.minmax
1970
+ str.split(":", 2).map! { import_num _1 }
1971
+ end
1972
+
1973
+ # yields validated but unsorted [num] or [num, num]
1974
+ def each_parsed_entry(str)
1975
+ return to_enum(__method__, str) unless block_given?
1976
+ str&.split(",", -1) do |entry| yield parse_entry(entry) end
1977
+ end
1978
+
1979
+ def normal_string?(str) normalized_entries? each_parsed_entry str end
1980
+
1981
+ def normalized_entries?(entries)
1982
+ max = nil
1983
+ entries.each do |first, last|
1984
+ return false if last && last <= first # 1:1 or 2:1
1985
+ return false if max && first <= max + 1 # 2,1 or 1,1 or 1,2
1986
+ max = last || first
1987
+ end
1988
+ true
1989
+ end
1990
+
1991
+ ######################################################################{{{2
1992
+ # Ordered entry methods
1993
+
1994
+ def count_entries
1995
+ @string ? @string.count(",") + 1 : runs.count
1996
+ end
1997
+
1998
+ def each_entry_minmax(&block)
1999
+ return to_enum(__method__) unless block_given?
2000
+ if @string
2001
+ @string.split(",") do block.call parse_minmax _1 end
2002
+ else
2003
+ minmaxes.each(&block)
2004
+ end
2005
+ self
1830
2006
  end
2007
+ alias each_entry_run each_entry_minmax
1831
2008
 
1832
- def include_tuple?((min, max)) range_gte_to(min)&.cover?(min..max) end
2009
+ ######################################################################{{{2
2010
+ # Search methods
1833
2011
 
1834
- def intersect_tuple?((min, max))
1835
- range = range_gte_to(min) and
2012
+ def include_minmax?((min, max)) bsearch_range(min)&.cover?(min..max) end
2013
+
2014
+ def intersect_minmax?((min, max))
2015
+ range = bsearch_range(min) and
1836
2016
  range.include?(min) || range.include?(max) || (min..max).cover?(range)
1837
2017
  end
1838
2018
 
2019
+ alias include_run? include_minmax?
2020
+ alias intersect_run? intersect_minmax?
2021
+
2022
+ def bsearch_index(num) = minmaxes.bsearch_index { _2 >= num }
2023
+ def bsearch_minmax(num) = minmaxes.bsearch { _2 >= num }
2024
+ def bsearch_range(num) = (min, max = bsearch_minmax(num)) && (min..max)
2025
+
2026
+ ######################################################################{{{2
2027
+ # Number indexing methods
2028
+
2029
+ def seek_number_in_minmaxes(minmaxes, index)
2030
+ index = Integer(index.to_int)
2031
+ if index.negative?
2032
+ reverse_each_minmax_with_index(minmaxes) do |min, max, idx_min, idx_max|
2033
+ idx_min <= index and return export_num(min + (index - idx_min))
2034
+ end
2035
+ else
2036
+ each_minmax_with_index(minmaxes) do |min, _, idx_min, idx_max|
2037
+ index <= idx_max and return export_num(min + (index - idx_min))
2038
+ end
2039
+ end
2040
+ nil
2041
+ end
2042
+
2043
+ def each_minmax_with_index(minmaxes)
2044
+ idx_min = 0
2045
+ minmaxes.each do |min, max|
2046
+ idx_max = idx_min + (max - min)
2047
+ yield min, max, idx_min, idx_max
2048
+ idx_min = idx_max + 1
2049
+ end
2050
+ idx_min
2051
+ end
2052
+
2053
+ def reverse_each_minmax_with_index(minmaxes)
2054
+ idx_max = -1
2055
+ minmaxes.reverse_each do |min, max|
2056
+ yield min, max, (idx_min = idx_max - (max - min)), idx_max
2057
+ idx_max = idx_min - 1
2058
+ end
2059
+ idx_max
2060
+ end
2061
+
2062
+ def slice_length(start, length)
2063
+ start = Integer(start.to_int)
2064
+ length = Integer(length.to_int)
2065
+ raise ArgumentError, "length must be positive" unless length.positive?
2066
+ last = start + length - 1 unless start.negative? && start.abs <= length
2067
+ slice_range(start..last)
2068
+ end
2069
+
2070
+ def slice_range(range)
2071
+ first = range.begin || 0
2072
+ last = range.end || -1
2073
+ if range.exclude_end?
2074
+ return remain_frozen_empty if last.zero?
2075
+ last -= 1 if range.end && last != STAR_INT
2076
+ end
2077
+ if (first * last).positive? && last < first
2078
+ remain_frozen_empty
2079
+ elsif (min = at(first))
2080
+ max = at(last)
2081
+ max = :* if max.nil?
2082
+ if max == :* then self & (min..)
2083
+ elsif min <= max then self & (min..max)
2084
+ else remain_frozen_empty
2085
+ end
2086
+ end
2087
+ end
2088
+
2089
+ ######################################################################{{{2
2090
+ # Core set data create/freeze/dup primitives
2091
+
2092
+ def new_set_data = []
2093
+ def freeze_set_data = set_data.each(&:freeze).freeze
2094
+ def dup_set_data = set_data.map { _1.dup }
2095
+ protected :dup_set_data
2096
+
2097
+ ######################################################################{{{2
2098
+ # Core set data query/enumeration primitives
2099
+
2100
+ def min_num = minmaxes.first&.first
2101
+ def max_num = minmaxes.last&.last
2102
+
2103
+ def min_at(idx) = minmaxes[idx][0]
2104
+ def max_at(idx) = minmaxes[idx][1]
2105
+
2106
+ ######################################################################{{{2
2107
+ # Core set data modification primitives
2108
+
2109
+ def set_min_at(idx, min) = minmaxes[idx][0] = min
2110
+ def set_max_at(idx, max) = minmaxes[idx][1] = max
2111
+ def replace_minmaxes(other) = minmaxes.replace(other)
2112
+ def append_minmax(min, max) = minmaxes << [min, max]
2113
+ def insert_minmax(idx, min, max) = minmaxes.insert idx, [min, max]
2114
+ def delete_run_at(idx) = runs.delete_at(idx)
2115
+ def slice_runs!(...) = runs.slice!(...)
2116
+ def truncate_runs!(idx) = runs.slice!(idx..)
2117
+
2118
+ ######################################################################{{{2
2119
+ # Update methods
2120
+
1839
2121
  def modifying!
1840
2122
  if frozen?
1841
2123
  raise FrozenError, "can't modify frozen #{self.class}: %p" % [self]
1842
2124
  end
1843
2125
  end
1844
2126
 
1845
- def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
1846
- def tuples_subtract(tuples) tuples.each do tuple_subtract _1 end; self end
2127
+ def add_minmaxes(minmaxes)
2128
+ minmaxes.each do |minmax|
2129
+ add_minmax minmax
2130
+ end
2131
+ self
2132
+ end
2133
+
2134
+ def subtract_minmaxes(minmaxes)
2135
+ minmaxes.each do |minmax|
2136
+ subtract_minmax minmax
2137
+ end
2138
+ self
2139
+ end
1847
2140
 
1848
2141
  #
1849
- # --|=====| |=====new tuple=====| append
1850
- # ?????????-|=====new tuple=====|-|===lower===|-- insert
2142
+ # --|=====| |=====new run=======| append
2143
+ # ?????????-|=====new run=======|-|===lower===|-- insert
1851
2144
  #
1852
- # |=====new tuple=====|
2145
+ # |=====new run=======|
1853
2146
  # ---------??=======lower=======??--------------- noop
1854
2147
  #
1855
2148
  # ---------??===lower==|--|==| join remaining
1856
2149
  # ---------??===lower==|--|==|----|===upper===|-- join until upper
1857
2150
  # ---------??===lower==|--|==|--|=====upper===|-- join to upper
1858
- def tuple_add(tuple)
2151
+ def add_minmax(minmax)
1859
2152
  modifying!
1860
- min, max = tuple
1861
- lower, lower_idx = tuple_gte_with_index(min - 1)
1862
- if lower.nil? then tuples << [min, max]
1863
- elsif (max + 1) < lower.first then tuples.insert(lower_idx, [min, max])
1864
- else tuple_coalesce(lower, lower_idx, min, max)
2153
+ min, max = minmax
2154
+ lower_idx = bsearch_index(min - 1)
2155
+ lmin, lmax = min_at(lower_idx), max_at(lower_idx) if lower_idx
2156
+ if lmin.nil? then append_minmax min, max
2157
+ elsif (max + 1) < lmin then insert_minmax lower_idx, min, max
2158
+ else add_coalesced_minmax(lower_idx, lmin, lmax, min, max)
1865
2159
  end
1866
2160
  end
1867
2161
 
1868
- def tuple_coalesce(lower, lower_idx, min, max)
1869
- return if lower.first <= min && max <= lower.last
1870
- lower[0] = [min, lower.first].min
1871
- lower[1] = [max, lower.last].max
1872
- lower_idx += 1
1873
- return if lower_idx == tuples.count
1874
- tmax_adj = lower.last + 1
1875
- upper, upper_idx = tuple_gte_with_index(tmax_adj)
1876
- if upper
1877
- tmax_adj < upper.first ? (upper_idx -= 1) : (lower[1] = upper.last)
2162
+ def add_coalesced_minmax(lower_idx, lmin, lmax, min, max)
2163
+ return if lmin <= min && max <= lmax
2164
+ set_min_at lower_idx, (lmin = min) if min < lmin
2165
+ set_max_at lower_idx, (lmax = max) if lmax < max
2166
+ next_idx = lower_idx + 1
2167
+ return if next_idx == runs.count
2168
+ tmax_adj = lmax + 1
2169
+ if (upper_idx = bsearch_index(tmax_adj))
2170
+ if tmax_adj < min_at(upper_idx)
2171
+ upper_idx -= 1
2172
+ else
2173
+ set_max_at lower_idx, max_at(upper_idx)
2174
+ end
1878
2175
  end
1879
- tuples.slice!(lower_idx..upper_idx)
2176
+ slice_runs! next_idx..upper_idx
1880
2177
  end
1881
2178
 
1882
- # |====tuple================|
2179
+ # |====subtracted run=======|
1883
2180
  # --|====| no more 1. noop
1884
2181
  # --|====|---------------------------|====lower====|-- 2. noop
1885
2182
  # -------|======lower================|---------------- 3. split
@@ -1892,61 +2189,59 @@ module Net
1892
2189
  # -------??=====lower====|--|====| no more 6. delete rest
1893
2190
  # -------??=====lower====|--|====|---|====upper====|-- 7. delete until
1894
2191
  # -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
1895
- def tuple_subtract(tuple)
2192
+ def subtract_minmax(minmax)
1896
2193
  modifying!
1897
- min, max = tuple
1898
- lower, idx = tuple_gte_with_index(min)
1899
- if lower.nil? then nil # case 1.
1900
- elsif max < lower.first then nil # case 2.
1901
- elsif max < lower.last then tuple_trim_or_split lower, idx, min, max
1902
- else tuples_trim_or_delete lower, idx, min, max
2194
+ min, max = minmax
2195
+ idx = bsearch_index(min)
2196
+ lmin, lmax = min_at(idx), max_at(idx) if idx
2197
+ if lmin.nil? then nil # case 1.
2198
+ elsif max < lmin then nil # case 2.
2199
+ elsif max < lmax then trim_or_split_minmax idx, lmin, min, max
2200
+ else trim_or_delete_minmax idx, lmin, lmax, min, max
1903
2201
  end
1904
2202
  end
1905
2203
 
1906
- def tuple_trim_or_split(lower, idx, tmin, tmax)
1907
- if lower.first < tmin # split
1908
- tuples.insert(idx, [lower.first, tmin - 1])
2204
+ def trim_or_split_minmax(idx, lmin, tmin, tmax)
2205
+ set_min_at idx, tmax + 1
2206
+ if lmin < tmin # split
2207
+ insert_minmax idx, lmin, tmin - 1
1909
2208
  end
1910
- lower[0] = tmax + 1
1911
2209
  end
1912
2210
 
1913
- def tuples_trim_or_delete(lower, lower_idx, tmin, tmax)
1914
- if lower.first < tmin # trim lower
1915
- lower[1] = tmin - 1
2211
+ def trim_or_delete_minmax(lower_idx, lmin, lmax, tmin, tmax)
2212
+ if lmin < tmin # trim lower
2213
+ lmax = set_max_at lower_idx, tmin - 1
1916
2214
  lower_idx += 1
1917
2215
  end
1918
- if tmax == lower.last # case 5
1919
- upper_idx = lower_idx
1920
- elsif (upper, upper_idx = tuple_gte_with_index(tmax + 1))
1921
- upper_idx -= 1 # cases 7 and 8
1922
- upper[0] = tmax + 1 if upper.first <= tmax # case 8 (else case 7)
2216
+ if tmax == lmax # case 5
2217
+ delete_run_at lower_idx
2218
+ elsif (upper_idx = bsearch_index(tmax + 1))
2219
+ if min_at(upper_idx) <= tmax # case 8
2220
+ set_min_at upper_idx, tmax + 1
2221
+ end
2222
+ slice_runs! lower_idx..upper_idx - 1 # cases 7 and 8
2223
+ else # case 6
2224
+ truncate_runs! lower_idx
1923
2225
  end
1924
- tuples.slice!(lower_idx..upper_idx)
1925
- end
1926
-
1927
- def tuple_gte_with_index(num)
1928
- idx = tuples.bsearch_index { _2 >= num } and [tuples[idx], idx]
1929
2226
  end
1930
2227
 
1931
- def range_gte_to(num)
1932
- first, last = tuples.bsearch { _2 >= num }
1933
- first..last if first
1934
- end
1935
-
1936
- def nz_number(num)
1937
- String === num && !/\A[1-9]\d*\z/.match?(num) and
1938
- raise DataFormatError, "%p is not a valid nz-number" % [num]
1939
- NumValidator.ensure_nz_number Integer num
1940
- rescue TypeError # To catch errors from Integer()
1941
- raise DataFormatError, $!.message
1942
- end
2228
+ alias add_runs add_minmaxes
2229
+ alias add_run add_minmax
2230
+ alias subtract_runs subtract_minmaxes
2231
+ alias subtract_run subtract_minmax
1943
2232
 
2233
+ ######################################################################{{{2
1944
2234
  # intentionally defined after the class implementation
1945
2235
 
2236
+ FULL_SET_DATA = [[1, STAR_INT].freeze].freeze
2237
+ private_constant :FULL_SET_DATA
2238
+
1946
2239
  EMPTY = new.freeze
1947
2240
  FULL = self["1:*"]
1948
2241
  private_constant :EMPTY, :FULL
1949
2242
 
2243
+ # }}}
2244
+ # vim:foldmethod=marker
1950
2245
  end
1951
2246
  end
1952
2247
  end