Skip to content

✨ Add SequenceSet methods for querying about duplicates #384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading