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