diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb index 9ce7f80e..3cc80087 100644 --- a/lib/net/imap/sequence_set.rb +++ b/lib/net/imap/sequence_set.rb @@ -183,7 +183,7 @@ class IMAP # # When a set includes *, some methods may have surprising behavior. # - # For example, #complement treats * as its own number. This way, + # For example, #complement treats * as its own member. This way, # the #intersection of a set and its #complement will always be empty. And # * is sorted as greater than any other number in the set. This is # not how an \IMAP server interprets the set: it will convert * to @@ -203,7 +203,7 @@ class IMAP # (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4] # # When counting the number of numbers in a set, * will be counted - # _except_ when UINT32_MAX is also in the set: + # as if it were equal to UINT32_MAX: # UINT32_MAX = 2**32 - 1 # Net::IMAP::SequenceSet["*"].count => 1 # Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX @@ -212,6 +212,12 @@ class IMAP # Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1 # Net::IMAP::SequenceSet[UINT32_MAX..].count => 1 # + # Use #cardinality to count the set members wxth * counted as a + # distinct member: + # Net::IMAP::SequenceSet[1..].cardinality #=> UINT32_MAX + 1 + # Net::IMAP::SequenceSet[UINT32_MAX, :*].cardinality #=> 2 + # Net::IMAP::SequenceSet[UINT32_MAX..].cardinality #=> 2 + # # == What's here? # # SequenceSet provides methods for: @@ -275,6 +281,8 @@ class IMAP # occurrence in entries. # # Set cardinality: + # - #cardinality: Returns the number of distinct members in the set. + # * is counted as its own member, distinct from UINT32_MAX. # - #count (aliased as #size): Returns the count of numbers in the set. # Duplicated numbers are not counted. # - #empty?: Returns whether the set has no members. \IMAP syntax does not @@ -1336,15 +1344,46 @@ def each_ordered_number(&block) # Related: #elements, #ranges, #numbers def to_set; Set.new(numbers) end + # Returns the number of members in the set. + # + # Unlike #count, "*" is considered to be distinct from + # 2³² - 1 (the maximum 32-bit unsigned integer value). + # + # set = Net::IMAP::SequenceSet[1..10] + # set.count #=> 10 + # set.cardinality #=> 10 + # + # set = Net::IMAP::SequenceSet["4294967295,*"] + # set.count #=> 1 + # set.cardinality #=> 2 + # + # set = Net::IMAP::SequenceSet[1..] + # set.count #=> 4294967295 + # set.cardinality #=> 4294967296 + # + # Related: #count, #count_with_duplicates + def cardinality = minmaxes.sum(@set_data.count) { _2 - _1 } + # Returns the count of #numbers in the set. # - # * will be counted as 2**32 - 1 (the maximum 32-bit - # unsigned integer value). + # Unlike #cardinality, "*" is considered to be equal to + # 2³² - 1 (the maximum 32-bit unsigned integer value). + # + # set = Net::IMAP::SequenceSet[1..10] + # set.count #=> 10 + # set.cardinality #=> 10 + # + # set = Net::IMAP::SequenceSet["4294967295,*"] + # set.count #=> 1 + # set.cardinality #=> 2 # - # Related: #count_with_duplicates + # set = Net::IMAP::SequenceSet[1..] + # set.count #=> 4294967295 + # set.cardinality #=> 4294967296 + # + # Related: #cardinality, #count_with_duplicates def count - minmaxes.sum(minmaxes.count) { _2 - _1 } + - (include_star? && include?(UINT32_MAX) ? -1 : 0) + cardinality + (include_star? && include?(UINT32_MAX) ? -1 : 0) end alias size count @@ -1352,12 +1391,24 @@ def count # Returns the count of numbers in the ordered #entries, including any # repeated numbers. # - # * will be counted as 2**32 - 1 (the maximum 32-bit - # unsigned integer value). + # When #string is normalized, this returns the same as #count. + # Like #count, "*" is be considered to be equal to + # 2³² - 1 (the maximum 32-bit unsigned integer value). + # + # In a range, "*" is _not_ considered a duplicate: + # set = Net::IMAP::SequenceSet["4294967295:*"] + # set.count_with_duplicates #=> 1 + # set.count #=> 1 + # set.cardinality #=> 2 # - # When #string is normalized, this behaves the same as #count. + # In a separate entry, "*" _is_ considered a duplicate: + # set = Net::IMAP::SequenceSet["4294967295,*"] + # set.count_with_duplicates #=> 2 + # set.count #=> 1 + # set.cardinality #=> 2 # - # Related: #entries, #count_duplicates, #has_duplicates? + # Related: #count, #cardinality, #count_duplicates, #has_duplicates?, + # #entries def count_with_duplicates return count unless @string each_entry_minmax.sum {|min, max| @@ -1382,7 +1433,7 @@ def count_duplicates # # Always returns +false+ when #string is normalized. # - # Related: #entries, #count_with_duplicates, #count_duplicates? + # Related: #entries, #count_with_duplicates, #count_duplicates def has_duplicates? return false unless @string count_with_duplicates != count diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index 9cfac1da..a015afe5 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -441,6 +441,7 @@ def obj.to_sequence_set; 192_168.001_255 end test "#[start, length]" do assert_equal SequenceSet[10..99], SequenceSet.full[9, 90] assert_equal 90, SequenceSet.full[9, 90].count + assert_equal 90, SequenceSet.full[9, 90].cardinality assert_equal SequenceSet[1000..1099], SequenceSet[1..100, 1000..1111][100, 100] assert_equal SequenceSet[11, 21, 31, 41], @@ -1065,6 +1066,7 @@ def test_inspect((expected, input, freeze)) to_s: "4294967000:*", normalize: "4294967000:*", count: 2**32 - 4_294_967_000, + cardinality: 2**32 - 4_294_967_000 + 1, complement: "1:4294966999", }, keep: true @@ -1143,6 +1145,7 @@ def test_inspect((expected, input, freeze)) normalize: "2:*", count: 2**32 - 2, count_dups: 2**32 - 2, + cardinality: 2**32 - 1, complement: "1", }, keep: true @@ -1182,6 +1185,34 @@ def test_inspect((expected, input, freeze)) complement: "6:8,12:98,100:#{2**32 - 1}", }, keep: true + data "UINT32_MAX,*", { + input: "#{2**32-1},*", + elements: [2**32 - 1..], + entries: [2**32 - 1, :*], + ranges: [2**32 - 1..], + numbers: RangeError, + to_s: "#{2**32 - 1},*", + normalize: "#{2**32 - 1}:*", + count: 1, + cardinality: 2, + count_dups: 1, + complement: "1:#{2**32 - 2}", + }, keep: true + + data "UINT32_MAX:*", { + input: "#{2**32-1}:*", + elements: [2**32 - 1..], + entries: [2**32 - 1..], + ranges: [2**32 - 1..], + numbers: RangeError, + to_s: "#{2**32 - 1}:*", + normalize: "#{2**32 - 1}:*", + count: 1, + cardinality: 2, + count_dups: 0, + complement: "1:#{2**32 - 2}", + }, keep: true + data "empty", { input: nil, elements: [], @@ -1329,6 +1360,15 @@ def assert_seqset_enum(expected, seqset, enum) assert_equal data[:count], SequenceSet.new(data[:input]).count end + test "#size" do |data| + assert_equal data[:count], SequenceSet.new(data[:input]).size + end + + test "#cardinality" do |data| + expected = data[:cardinality] || data[:count] + assert_equal expected, SequenceSet.new(data[:input]).cardinality + end + test "#count_with_duplicates" do |data| dups = data[:count_dups] || 0 count = data[:count] + dups