Skip to content

Commit

Permalink
🔀 Merge pull request #384 from ruby/sequence-set-ordered-duplicates-m…
Browse files Browse the repository at this point in the history
…ethods

✨ Add SequenceSet methods for querying about duplicates
  • Loading branch information
nevans authored Jan 24, 2025
2 parents 4cabe29 + dab3492 commit a9e1ce9
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 11 deletions.
88 changes: 77 additions & 11 deletions lib/net/imap/sequence_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,21 @@ class IMAP
# - #full?: Returns whether the set contains every possible value, including
# <tt>*</tt>.
#
# <i>Denormalized properties:</i>
# - #has_duplicates?: Returns whether the ordered entries repeat any
# numbers.
# - #count_duplicates: Returns the count of repeated numbers in the ordered
# entries.
# - #count_with_duplicates: Returns the count of numbers in the ordered
# entries, including any repeated numbers.
#
# === Methods for Iterating
#
# <i>Normalized (sorted and coalesced):</i>
# - #each_element: Yields each number and range in the set, sorted and
# coalesced, and returns +self+.
# - #elements (aliased as #to_a): Returns an Array of every number and range
# in the set, sorted and coalesced.
# - #each_entry: Yields each number and range in the set, unsorted and
# without deduplicating numbers or coalescing ranges, and returns +self+.
# - #entries: Returns an Array of every number and range in the set,
# unsorted and without deduplicating numbers or coalescing ranges.
# - #each_range:
# Yields each element in the set as a Range and returns +self+.
# - #ranges: Returns an Array of every element in the set, converting
Expand All @@ -217,6 +222,12 @@ class IMAP
# ranges into all of their contained numbers.
# - #to_set: Returns a Set containing all of the #numbers in the set.
#
# <i>Order preserving:</i>
# - #each_entry: Yields each number and range in the set, unsorted and
# without deduplicating numbers or coalescing ranges, and returns +self+.
# - #entries: Returns an Array of every number and range in the set,
# unsorted and without deduplicating numbers or coalescing ranges.
#
# === Methods for \Set Operations
# These methods do not modify +self+.
#
Expand All @@ -236,19 +247,29 @@ class IMAP
# === Methods for Assigning
# These methods add or replace elements in +self+.
#
# <i>Normalized (sorted and coalesced):</i>
#
# These methods always update #string to be fully sorted and coalesced.
#
# - #add (aliased as #<<): Adds a given object to the set; returns +self+.
# - #add?: If the given object is not an element in the set, adds it and
# returns +self+; otherwise, returns +nil+.
# - #merge: Merges multiple elements into the set; returns +self+.
# - #complement!: Replaces the contents of the set with its own #complement.
#
# <i>Order preserving:</i>
#
# These methods _may_ cause #string to not be sorted or coalesced.
#
# - #append: Adds a given object to the set, appending it to the existing
# string, and returns +self+.
# - #string=: Assigns a new #string value and replaces #elements to match.
# - #replace: Replaces the contents of the set with the contents
# of a given object.
# - #complement!: Replaces the contents of the set with its own #complement.
#
# === Methods for Deleting
# These methods remove elements from +self+.
# These methods remove elements from +self+, and update #string to be fully
# sorted and coalesced.
#
# - #clear: Removes all elements in the set; returns +self+.
# - #delete: Removes a given object from the set; returns +self+.
Expand Down Expand Up @@ -910,9 +931,7 @@ def numbers; each_number.to_a end
# Related: #entries, #each_element
def each_entry(&block) # :yields: integer or range or :*
return to_enum(__method__) unless block_given?
return each_element(&block) unless @string
@string.split(",").each do yield tuple_to_entry str_to_tuple _1 end
self
each_entry_tuple do yield tuple_to_entry _1 end
end

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

private

def each_entry_tuple(&block)
return to_enum(__method__) unless block_given?
if @string
@string.split(",") do block.call str_to_tuple _1 end
else
@tuples.each(&block)
end
self
end

def tuple_to_entry((min, max))
if min == STAR_INT then :*
elsif max == STAR_INT then min..
Expand Down Expand Up @@ -988,12 +1017,49 @@ def to_set; Set.new(numbers) end
# If <tt>*</tt> and <tt>2**32 - 1</tt> (the maximum 32-bit unsigned
# integer value) are both in the set, they will only be counted once.
def count
@tuples.sum(@tuples.count) { _2 - _1 } +
(include_star? && include?(UINT32_MAX) ? -1 : 0)
count_numbers_in_tuples(@tuples)
end

alias size count

# Returns the count of numbers in the ordered #entries, including any
# repeated numbers.
#
# When #string is normalized, this behaves the same as #count.
#
# Related: #entries, #count_duplicates, #has_duplicates?
def count_with_duplicates
return count unless @string
count_numbers_in_tuples(each_entry_tuple)
end

# Returns the count of repeated numbers in the ordered #entries.
#
# When #string is normalized, this is zero.
#
# Related: #entries, #count_with_duplicates, #has_duplicates?
def count_duplicates
return 0 unless @string
count_with_duplicates - count
end

# :call-seq: has_duplicates? -> true | false
#
# Returns whether or not the ordered #entries repeat any numbers.
#
# Always returns +false+ when #string is normalized.
#
# Related: #entries, #count_with_duplicates, #count_duplicates?
def has_duplicates?
return false unless @string
count_with_duplicates != count
end

private def count_numbers_in_tuples(tuples)
tuples.sum(tuples.count) { _2 - _1 } +
(include_star? && include?(UINT32_MAX) ? -1 : 0)
end

# Returns the index of +number+ in the set, or +nil+ if +number+ isn't in
# the set.
#
Expand Down
21 changes: 21 additions & 0 deletions test/net/imap/test_sequence_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ def test_inspect((expected, input, freeze))
to_s: "1:5,3:7,10:9,10:11",
normalize: "1:7,9:11",
count: 10,
count_dups: 4,
complement: "8,12:*",
}, keep: true

Expand All @@ -722,6 +723,7 @@ def test_inspect((expected, input, freeze))
to_s: "1:5,3:4,9:11,10",
normalize: "1:5,9:11",
count: 8,
count_dups: 3,
complement: "6:8,12:*",
}, keep: true

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

test "#count_with_duplicates" do |data|
dups = data[:count_dups] || 0
count = data[:count] + dups
seqset = SequenceSet.new(data[:input])
assert_equal count, seqset.count_with_duplicates
end

test "#count_duplicates" do |data|
dups = data[:count_dups] || 0
seqset = SequenceSet.new(data[:input])
assert_equal dups, seqset.count_duplicates
end

test "#has_duplicates?" do |data|
has_dups = !(data[:count_dups] || 0).zero?
seqset = SequenceSet.new(data[:input])
assert_equal has_dups, seqset.has_duplicates?
end

test "#valid_string" do |data|
if (expected = data[:to_s]).empty?
assert_raise DataFormatError do
Expand Down

0 comments on commit a9e1ce9

Please sign in to comment.