Skip to content

Commit c4a243b

Browse files
committed
✨ Add SequenceSet#cardinality method
Unlike `#count`, `#cardinality` handles `*` in a more conventional way, as its own distinct member.
1 parent 5015fbe commit c4a243b

2 files changed

Lines changed: 103 additions & 12 deletions

File tree

lib/net/imap/sequence_set.rb

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class IMAP
183183
#
184184
# When a set includes <tt>*</tt>, some methods may have surprising behavior.
185185
#
186-
# For example, #complement treats <tt>*</tt> as its own number. This way,
186+
# For example, #complement treats <tt>*</tt> as its own member. This way,
187187
# the #intersection of a set and its #complement will always be empty. And
188188
# <tt>*</tt> is sorted as greater than any other number in the set. This is
189189
# not how an \IMAP server interprets the set: it will convert <tt>*</tt> to
@@ -203,7 +203,7 @@ class IMAP
203203
# (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4]
204204
#
205205
# When counting the number of numbers in a set, <tt>*</tt> will be counted
206-
# _except_ when UINT32_MAX is also in the set:
206+
# as if it were equal to UINT32_MAX:
207207
# UINT32_MAX = 2**32 - 1
208208
# Net::IMAP::SequenceSet["*"].count => 1
209209
# Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX
@@ -212,6 +212,12 @@ class IMAP
212212
# Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
213213
# Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
214214
#
215+
# Use #cardinality to count the set members wxth <tt>*</tt> counted as a
216+
# distinct member:
217+
# Net::IMAP::SequenceSet[1..].cardinality #=> UINT32_MAX + 1
218+
# Net::IMAP::SequenceSet[UINT32_MAX, :*].cardinality #=> 2
219+
# Net::IMAP::SequenceSet[UINT32_MAX..].cardinality #=> 2
220+
#
215221
# == What's here?
216222
#
217223
# SequenceSet provides methods for:
@@ -275,6 +281,8 @@ class IMAP
275281
# occurrence in entries.
276282
#
277283
# <i>Set cardinality:</i>
284+
# - #cardinality: Returns the number of distinct members in the set.
285+
# <tt>*</tt> is counted as its own member, distinct from UINT32_MAX.
278286
# - #count (aliased as #size): Returns the count of numbers in the set.
279287
# Duplicated numbers are not counted.
280288
# - #empty?: Returns whether the set has no members. \IMAP syntax does not
@@ -1336,28 +1344,71 @@ def each_ordered_number(&block)
13361344
# Related: #elements, #ranges, #numbers
13371345
def to_set; Set.new(numbers) end
13381346

1347+
# Returns the number of members in the set.
1348+
#
1349+
# Unlike #count, <tt>"*"</tt> is considered to be distinct from
1350+
# <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1351+
#
1352+
# set = Net::IMAP::SequenceSet[1..10]
1353+
# set.count #=> 10
1354+
# set.cardinality #=> 10
1355+
#
1356+
# set = Net::IMAP::SequenceSet["4294967295,*"]
1357+
# set.count #=> 1
1358+
# set.cardinality #=> 2
1359+
#
1360+
# set = Net::IMAP::SequenceSet[1..]
1361+
# set.count #=> 4294967295
1362+
# set.cardinality #=> 4294967296
1363+
#
1364+
# Related: #count, #count_with_duplicates
1365+
def cardinality = minmaxes.sum(@set_data.count) { _2 - _1 }
1366+
13391367
# Returns the count of #numbers in the set.
13401368
#
1341-
# <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1342-
# unsigned integer value).
1369+
# Unlike #cardinality, <tt>"*"</tt> is considered to be equal to
1370+
# <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1371+
#
1372+
# set = Net::IMAP::SequenceSet[1..10]
1373+
# set.count #=> 10
1374+
# set.cardinality #=> 10
1375+
#
1376+
# set = Net::IMAP::SequenceSet["4294967295,*"]
1377+
# set.count #=> 1
1378+
# set.cardinality #=> 2
13431379
#
1344-
# Related: #count_with_duplicates
1380+
# set = Net::IMAP::SequenceSet[1..]
1381+
# set.count #=> 4294967295
1382+
# set.cardinality #=> 4294967296
1383+
#
1384+
# Related: #cardinality, #count_with_duplicates
13451385
def count
1346-
minmaxes.sum(minmaxes.count) { _2 - _1 } +
1347-
(include_star? && include?(UINT32_MAX) ? -1 : 0)
1386+
cardinality + (include_star? && include?(UINT32_MAX) ? -1 : 0)
13481387
end
13491388

13501389
alias size count
13511390

13521391
# Returns the count of numbers in the ordered #entries, including any
13531392
# repeated numbers.
13541393
#
1355-
# <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1356-
# unsigned integer value).
1394+
# When #string is normalized, this returns the same as #count.
1395+
# Like #count, <tt>"*"</tt> is be considered to be equal to
1396+
# <tt>2³² - 1</tt> (the maximum 32-bit unsigned integer value).
1397+
#
1398+
# In a range, <tt>"*"</tt> is _not_ considered a duplicate:
1399+
# set = Net::IMAP::SequenceSet["4294967295:*"]
1400+
# set.count_with_duplicates #=> 1
1401+
# set.count #=> 1
1402+
# set.cardinality #=> 2
13571403
#
1358-
# When #string is normalized, this behaves the same as #count.
1404+
# In a separate entry, <tt>"*"</tt> _is_ considered a duplicate:
1405+
# set = Net::IMAP::SequenceSet["4294967295,*"]
1406+
# set.count_with_duplicates #=> 2
1407+
# set.count #=> 1
1408+
# set.cardinality #=> 2
13591409
#
1360-
# Related: #entries, #count_duplicates, #has_duplicates?
1410+
# Related: #count, #cardinality, #count_duplicates, #has_duplicates?,
1411+
# #entries
13611412
def count_with_duplicates
13621413
return count unless @string
13631414
each_entry_minmax.sum {|min, max|
@@ -1382,7 +1433,7 @@ def count_duplicates
13821433
#
13831434
# Always returns +false+ when #string is normalized.
13841435
#
1385-
# Related: #entries, #count_with_duplicates, #count_duplicates?
1436+
# Related: #entries, #count_with_duplicates, #count_duplicates
13861437
def has_duplicates?
13871438
return false unless @string
13881439
count_with_duplicates != count

test/net/imap/test_sequence_set.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ def obj.to_sequence_set; 192_168.001_255 end
441441
test "#[start, length]" do
442442
assert_equal SequenceSet[10..99], SequenceSet.full[9, 90]
443443
assert_equal 90, SequenceSet.full[9, 90].count
444+
assert_equal 90, SequenceSet.full[9, 90].cardinality
444445
assert_equal SequenceSet[1000..1099],
445446
SequenceSet[1..100, 1000..1111][100, 100]
446447
assert_equal SequenceSet[11, 21, 31, 41],
@@ -1065,6 +1066,7 @@ def test_inspect((expected, input, freeze))
10651066
to_s: "4294967000:*",
10661067
normalize: "4294967000:*",
10671068
count: 2**32 - 4_294_967_000,
1069+
cardinality: 2**32 - 4_294_967_000 + 1,
10681070
complement: "1:4294966999",
10691071
}, keep: true
10701072

@@ -1143,6 +1145,7 @@ def test_inspect((expected, input, freeze))
11431145
normalize: "2:*",
11441146
count: 2**32 - 2,
11451147
count_dups: 2**32 - 2,
1148+
cardinality: 2**32 - 1,
11461149
complement: "1",
11471150
}, keep: true
11481151

@@ -1182,6 +1185,34 @@ def test_inspect((expected, input, freeze))
11821185
complement: "6:8,12:98,100:#{2**32 - 1}",
11831186
}, keep: true
11841187

1188+
data "UINT32_MAX,*", {
1189+
input: "#{2**32-1},*",
1190+
elements: [2**32 - 1..],
1191+
entries: [2**32 - 1, :*],
1192+
ranges: [2**32 - 1..],
1193+
numbers: RangeError,
1194+
to_s: "#{2**32 - 1},*",
1195+
normalize: "#{2**32 - 1}:*",
1196+
count: 1,
1197+
cardinality: 2,
1198+
count_dups: 1,
1199+
complement: "1:#{2**32 - 2}",
1200+
}, keep: true
1201+
1202+
data "UINT32_MAX:*", {
1203+
input: "#{2**32-1}:*",
1204+
elements: [2**32 - 1..],
1205+
entries: [2**32 - 1..],
1206+
ranges: [2**32 - 1..],
1207+
numbers: RangeError,
1208+
to_s: "#{2**32 - 1}:*",
1209+
normalize: "#{2**32 - 1}:*",
1210+
count: 1,
1211+
cardinality: 2,
1212+
count_dups: 0,
1213+
complement: "1:#{2**32 - 2}",
1214+
}, keep: true
1215+
11851216
data "empty", {
11861217
input: nil,
11871218
elements: [],
@@ -1329,6 +1360,15 @@ def assert_seqset_enum(expected, seqset, enum)
13291360
assert_equal data[:count], SequenceSet.new(data[:input]).count
13301361
end
13311362

1363+
test "#size" do |data|
1364+
assert_equal data[:count], SequenceSet.new(data[:input]).size
1365+
end
1366+
1367+
test "#cardinality" do |data|
1368+
expected = data[:cardinality] || data[:count]
1369+
assert_equal expected, SequenceSet.new(data[:input]).cardinality
1370+
end
1371+
13321372
test "#count_with_duplicates" do |data|
13331373
dups = data[:count_dups] || 0
13341374
count = data[:count] + dups

0 commit comments

Comments
 (0)