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