Skip to content
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
75 changes: 63 additions & 12 deletions lib/net/imap/sequence_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class IMAP
#
# When a set includes <tt>*</tt>, some methods may have surprising behavior.
#
# For example, #complement treats <tt>*</tt> as its own number. This way,
# For example, #complement treats <tt>*</tt> as its own member. This way,
# the #intersection of a set and its #complement will always be empty. And
# <tt>*</tt> is sorted as greater than any other number in the set. This is
# not how an \IMAP server interprets the set: it will convert <tt>*</tt> to
Expand All @@ -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, <tt>*</tt> 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
Expand All @@ -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 <tt>*</tt> 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:
Expand Down Expand Up @@ -275,6 +281,8 @@ class IMAP
# occurrence in entries.
#
# <i>Set cardinality:</i>
# - #cardinality: Returns the number of distinct members in the set.
# <tt>*</tt> 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
Expand Down Expand Up @@ -1336,28 +1344,71 @@ 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, <tt>"*"</tt> is considered to be distinct from
# <tt>2³² - 1</tt> (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.
#
# <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
# unsigned integer value).
# Unlike #cardinality, <tt>"*"</tt> is considered to be equal to
# <tt>2³² - 1</tt> (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

# Returns the count of numbers in the ordered #entries, including any
# repeated numbers.
#
# <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
# unsigned integer value).
# When #string is normalized, this returns the same as #count.
# Like #count, <tt>"*"</tt> is be considered to be equal to
# <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
#
# In a range, <tt>"*"</tt> 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, <tt>"*"</tt> _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|
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions test/net/imap/test_sequence_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -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
Expand Down