diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb
index ba3af4e3..b83c9f84 100644
--- a/lib/net/imap/sequence_set.rb
+++ b/lib/net/imap/sequence_set.rb
@@ -198,6 +198,14 @@ class IMAP
# - #full?: Returns whether the set contains every possible value, including
# *.
#
+ # Denormalized properties:
+ # - #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
#
# Normalized (sorted and coalesced):
@@ -923,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 :*) in #elements to the block
@@ -943,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..
@@ -1001,12 +1017,49 @@ def to_set; Set.new(numbers) end
# If * and 2**32 - 1 (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.
#
diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb
index 33e7388b..971a81fd 100644
--- a/test/net/imap/test_sequence_set.rb
+++ b/test/net/imap/test_sequence_set.rb
@@ -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
@@ -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
@@ -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