Skip to content

Commit a9e1ce9

Browse files
authored
🔀 Merge pull request #384 from ruby/sequence-set-ordered-duplicates-methods
✨ Add SequenceSet methods for querying about duplicates
2 parents 4cabe29 + dab3492 commit a9e1ce9

File tree

2 files changed

+98
-11
lines changed

2 files changed

+98
-11
lines changed

‎lib/net/imap/sequence_set.rb

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,21 @@ class IMAP
198198
# - #full?: Returns whether the set contains every possible value, including
199199
# <tt>*</tt>.
200200
#
201+
# <i>Denormalized properties:</i>
202+
# - #has_duplicates?: Returns whether the ordered entries repeat any
203+
# numbers.
204+
# - #count_duplicates: Returns the count of repeated numbers in the ordered
205+
# entries.
206+
# - #count_with_duplicates: Returns the count of numbers in the ordered
207+
# entries, including any repeated numbers.
208+
#
201209
# === Methods for Iterating
202210
#
211+
# <i>Normalized (sorted and coalesced):</i>
203212
# - #each_element: Yields each number and range in the set, sorted and
204213
# coalesced, and returns +self+.
205214
# - #elements (aliased as #to_a): Returns an Array of every number and range
206215
# in the set, sorted and coalesced.
207-
# - #each_entry: Yields each number and range in the set, unsorted and
208-
# without deduplicating numbers or coalescing ranges, and returns +self+.
209-
# - #entries: Returns an Array of every number and range in the set,
210-
# unsorted and without deduplicating numbers or coalescing ranges.
211216
# - #each_range:
212217
# Yields each element in the set as a Range and returns +self+.
213218
# - #ranges: Returns an Array of every element in the set, converting
@@ -217,6 +222,12 @@ class IMAP
217222
# ranges into all of their contained numbers.
218223
# - #to_set: Returns a Set containing all of the #numbers in the set.
219224
#
225+
# <i>Order preserving:</i>
226+
# - #each_entry: Yields each number and range in the set, unsorted and
227+
# without deduplicating numbers or coalescing ranges, and returns +self+.
228+
# - #entries: Returns an Array of every number and range in the set,
229+
# unsorted and without deduplicating numbers or coalescing ranges.
230+
#
220231
# === Methods for \Set Operations
221232
# These methods do not modify +self+.
222233
#
@@ -236,19 +247,29 @@ class IMAP
236247
# === Methods for Assigning
237248
# These methods add or replace elements in +self+.
238249
#
250+
# <i>Normalized (sorted and coalesced):</i>
251+
#
252+
# These methods always update #string to be fully sorted and coalesced.
253+
#
239254
# - #add (aliased as #<<): Adds a given object to the set; returns +self+.
240255
# - #add?: If the given object is not an element in the set, adds it and
241256
# returns +self+; otherwise, returns +nil+.
242257
# - #merge: Merges multiple elements into the set; returns +self+.
258+
# - #complement!: Replaces the contents of the set with its own #complement.
259+
#
260+
# <i>Order preserving:</i>
261+
#
262+
# These methods _may_ cause #string to not be sorted or coalesced.
263+
#
243264
# - #append: Adds a given object to the set, appending it to the existing
244265
# string, and returns +self+.
245266
# - #string=: Assigns a new #string value and replaces #elements to match.
246267
# - #replace: Replaces the contents of the set with the contents
247268
# of a given object.
248-
# - #complement!: Replaces the contents of the set with its own #complement.
249269
#
250270
# === Methods for Deleting
251-
# These methods remove elements from +self+.
271+
# These methods remove elements from +self+, and update #string to be fully
272+
# sorted and coalesced.
252273
#
253274
# - #clear: Removes all elements in the set; returns +self+.
254275
# - #delete: Removes a given object from the set; returns +self+.
@@ -910,9 +931,7 @@ def numbers; each_number.to_a end
910931
# Related: #entries, #each_element
911932
def each_entry(&block) # :yields: integer or range or :*
912933
return to_enum(__method__) unless block_given?
913-
return each_element(&block) unless @string
914-
@string.split(",").each do yield tuple_to_entry str_to_tuple _1 end
915-
self
934+
each_entry_tuple do yield tuple_to_entry _1 end
916935
end
917936

918937
# Yields each number or range (or <tt>:*</tt>) in #elements to the block
@@ -930,6 +949,16 @@ def each_element # :yields: integer or range or :*
930949

931950
private
932951

952+
def each_entry_tuple(&block)
953+
return to_enum(__method__) unless block_given?
954+
if @string
955+
@string.split(",") do block.call str_to_tuple _1 end
956+
else
957+
@tuples.each(&block)
958+
end
959+
self
960+
end
961+
933962
def tuple_to_entry((min, max))
934963
if min == STAR_INT then :*
935964
elsif max == STAR_INT then min..
@@ -988,12 +1017,49 @@ def to_set; Set.new(numbers) end
9881017
# If <tt>*</tt> and <tt>2**32 - 1</tt> (the maximum 32-bit unsigned
9891018
# integer value) are both in the set, they will only be counted once.
9901019
def count
991-
@tuples.sum(@tuples.count) { _2 - _1 } +
992-
(include_star? && include?(UINT32_MAX) ? -1 : 0)
1020+
count_numbers_in_tuples(@tuples)
9931021
end
9941022

9951023
alias size count
9961024

1025+
# Returns the count of numbers in the ordered #entries, including any
1026+
# repeated numbers.
1027+
#
1028+
# When #string is normalized, this behaves the same as #count.
1029+
#
1030+
# Related: #entries, #count_duplicates, #has_duplicates?
1031+
def count_with_duplicates
1032+
return count unless @string
1033+
count_numbers_in_tuples(each_entry_tuple)
1034+
end
1035+
1036+
# Returns the count of repeated numbers in the ordered #entries.
1037+
#
1038+
# When #string is normalized, this is zero.
1039+
#
1040+
# Related: #entries, #count_with_duplicates, #has_duplicates?
1041+
def count_duplicates
1042+
return 0 unless @string
1043+
count_with_duplicates - count
1044+
end
1045+
1046+
# :call-seq: has_duplicates? -> true | false
1047+
#
1048+
# Returns whether or not the ordered #entries repeat any numbers.
1049+
#
1050+
# Always returns +false+ when #string is normalized.
1051+
#
1052+
# Related: #entries, #count_with_duplicates, #count_duplicates?
1053+
def has_duplicates?
1054+
return false unless @string
1055+
count_with_duplicates != count
1056+
end
1057+
1058+
private def count_numbers_in_tuples(tuples)
1059+
tuples.sum(tuples.count) { _2 - _1 } +
1060+
(include_star? && include?(UINT32_MAX) ? -1 : 0)
1061+
end
1062+
9971063
# Returns the index of +number+ in the set, or +nil+ if +number+ isn't in
9981064
# the set.
9991065
#

‎test/net/imap/test_sequence_set.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ def test_inspect((expected, input, freeze))
710710
to_s: "1:5,3:7,10:9,10:11",
711711
normalize: "1:7,9:11",
712712
count: 10,
713+
count_dups: 4,
713714
complement: "8,12:*",
714715
}, keep: true
715716

@@ -722,6 +723,7 @@ def test_inspect((expected, input, freeze))
722723
to_s: "1:5,3:4,9:11,10",
723724
normalize: "1:5,9:11",
724725
count: 8,
726+
count_dups: 3,
725727
complement: "6:8,12:*",
726728
}, keep: true
727729

@@ -878,6 +880,25 @@ def test_inspect((expected, input, freeze))
878880
assert_equal data[:count], SequenceSet.new(data[:input]).count
879881
end
880882

883+
test "#count_with_duplicates" do |data|
884+
dups = data[:count_dups] || 0
885+
count = data[:count] + dups
886+
seqset = SequenceSet.new(data[:input])
887+
assert_equal count, seqset.count_with_duplicates
888+
end
889+
890+
test "#count_duplicates" do |data|
891+
dups = data[:count_dups] || 0
892+
seqset = SequenceSet.new(data[:input])
893+
assert_equal dups, seqset.count_duplicates
894+
end
895+
896+
test "#has_duplicates?" do |data|
897+
has_dups = !(data[:count_dups] || 0).zero?
898+
seqset = SequenceSet.new(data[:input])
899+
assert_equal has_dups, seqset.has_duplicates?
900+
end
901+
881902
test "#valid_string" do |data|
882903
if (expected = data[:to_s]).empty?
883904
assert_raise DataFormatError do

0 commit comments

Comments
 (0)