Skip to content

Commit

Permalink
🔀 Merge pull request #254 from nevans/preserving-sequence-set-order
Browse files Browse the repository at this point in the history
✨ Preserving sequence set order
  • Loading branch information
nevans authored Dec 24, 2023
2 parents 2746d34 + 3e9ee5a commit 1a26054
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 14 deletions.
92 changes: 78 additions & 14 deletions lib/net/imap/sequence_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ class IMAP
# set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
# set.valid_string #=> "1:10,55,1024:2048"
#
# == Normalized form
#
# When a sequence set is created with a single String value, that #string
# representation is preserved. SequenceSet's internal representation
# implicitly sorts all entries, de-duplicates numbers, and coalesces
# adjacent or overlapping ranges. Most enumeration methods and offset-based
# methods use this normalized representation. Most modification methods
# will convert #string to its normalized form.
#
# In some cases the order of the string representation is significant, such
# as the +ESORT+, <tt>CONTEXT=SORT</tt>, and +UIDPLUS+ extensions. Use
# #entries or #each_entry to enumerate the set in its original order. To
# preserve #string order while modifying a set, use #append, #string=, or
# #replace.
#
# == Using <tt>*</tt>
#
# \IMAP sequence sets may contain a special value <tt>"*"</tt>, which
Expand Down Expand Up @@ -186,10 +201,14 @@ class IMAP
#
# === Methods for Iterating
#
# - #each_element: Yields each number and range in the set and returns
# +self+.
# - #elements (aliased as #to_a):
# Returns an Array of every number and range in the set.
# - #each_element: Yields each number and range in the set, sorted and
# coalesced, and returns +self+.
# - #elements (aliased as #to_a): Returns an Array of every number and range
# in the set, sorted and coalesced.
# - #each_entry: Yields each number and range in the set, unsorted and
# without deduplicating numbers or coalescing ranges, and returns +self+.
# - #entries: Returns an Array of every number and range in the set,
# unsorted and without deduplicating numbers or coalescing ranges.
# - #each_range:
# Yields each element in the set as a Range and returns +self+.
# - #ranges: Returns an Array of every element in the set, converting
Expand Down Expand Up @@ -222,6 +241,8 @@ class IMAP
# - #add?: If the given object is not an element in the set, adds it and
# returns +self+; otherwise, returns +nil+.
# - #merge: Merges multiple elements into the set; returns +self+.
# - #append: Adds a given object to the set, appending it to the existing
# string, and returns +self+.
# - #string=: Assigns a new #string value and replaces #elements to match.
# - #replace: Replaces the contents of the set with the contents
# of a given object.
Expand Down Expand Up @@ -656,6 +677,18 @@ def add(object)
end
alias << add

# Adds a range or number to the set and returns +self+.
#
# Unlike #add, #merge, or #union, the new value is appended to #string.
# This may result in a #string which has duplicates or is out-of-order.
def append(object)
tuple = input_to_tuple object
entry = tuple_to_str tuple
tuple_add tuple
@string = -(string ? "#{@string},#{entry}" : entry)
self
end

# :call-seq: add?(object) -> self or nil
#
# Adds a range or number to the set and returns +self+. Returns +nil+
Expand Down Expand Up @@ -788,7 +821,18 @@ def subtract(*objects)
normalize!
end

# Returns an array of ranges and integers.
# Returns an array of ranges and integers and <tt>:*</tt>.
#
# The entries are in the same order they appear in #string, with no
# sorting, deduplication, or coalescing. When #string is in its
# normalized form, this will return the same result as #elements.
# This is useful when the given order is significant, for example in a
# ESEARCH response to IMAP#sort.
#
# Related: #each_entry, #elements
def entries; each_entry.to_a end

# Returns an array of ranges and integers and <tt>:*</tt>.
#
# The returned elements are sorted and coalesced, even when the input
# #string is not. <tt>*</tt> will sort last. See #normalize.
Expand Down Expand Up @@ -855,22 +899,42 @@ def ranges; each_range.to_a end
# Related: #elements, #ranges, #to_set
def numbers; each_number.to_a end

# Yields each number or range in #elements to the block and returns self.
# Yields each number or range in #string to the block and returns +self+.
# Returns an enumerator when called without a block.
#
# Related: #elements
# The entries are yielded in the same order they appear in #tring, with no
# sorting, deduplication, or coalescing. When #string is in its
# normalized form, this will yield the same values as #each_element.
#
# Related: #entries, #each_element
def each_entry(&block)
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
end

# Yields each number or range (or <tt>:*</tt>) in #elements to the block
# and returns self. Returns an enumerator when called without a block.
#
# The returned numbers are sorted and de-duplicated, even when the input
# #string is not. See #normalize.
#
# Related: #elements, #each_entry
def each_element # :yields: integer or range or :*
return to_enum(__method__) unless block_given?
@tuples.each do |min, max|
if min == STAR_INT then yield :*
elsif max == STAR_INT then yield min..
elsif min == max then yield min
else yield min..max
end
end
@tuples.each do yield tuple_to_entry _1 end
self
end

private def tuple_to_entry((min, max))
if min == STAR_INT then :*
elsif max == STAR_INT then min..
elsif min == max then min
else min..max
end
end

# Yields each range in #ranges to the block and returns self.
# Returns an enumerator when called without a block.
#
Expand Down
42 changes: 42 additions & 0 deletions test/net/imap/test_sequence_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ def compare_to_reference_set(nums, set, seqset)
assert_equal SequenceSet["1:*"], SequenceSet.new("5:*") << (1..4)
end

test "#append" do
assert_equal "1,5", SequenceSet.new("1").append("5").string
assert_equal "*,1", SequenceSet.new("*").append(1).string
assert_equal "1:6,4:9", SequenceSet.new("1:6").append("4:9").string
assert_equal "1:4,5:*", SequenceSet.new("1:4").append(5..).string
assert_equal "5:*,1:4", SequenceSet.new("5:*").append(1..4).string
end

test "#merge" do
seqset = -> { SequenceSet.new _1 }
assert_equal seqset["1,5"], seqset["1"].merge("5")
Expand Down Expand Up @@ -525,6 +533,7 @@ def test_inspect((expected, input, freeze))
data "single number", {
input: "123456",
elements: [123_456],
entries: [123_456],
ranges: [123_456..123_456],
numbers: [123_456],
to_s: "123456",
Expand All @@ -536,6 +545,7 @@ def test_inspect((expected, input, freeze))
data "single range", {
input: "1:3",
elements: [1..3],
entries: [1..3],
ranges: [1..3],
numbers: [1, 2, 3],
to_s: "1:3",
Expand All @@ -547,6 +557,7 @@ def test_inspect((expected, input, freeze))
data "simple numbers list", {
input: "1,3,5",
elements: [ 1, 3, 5],
entries: [ 1, 3, 5],
ranges: [1..1, 3..3, 5..5],
numbers: [ 1, 3, 5],
to_s: "1,3,5",
Expand All @@ -558,6 +569,7 @@ def test_inspect((expected, input, freeze))
data "numbers and ranges list", {
input: "1:3,5,7:9,46",
elements: [1..3, 5, 7..9, 46],
entries: [1..3, 5, 7..9, 46],
ranges: [1..3, 5..5, 7..9, 46..46],
numbers: [1, 2, 3, 5, 7, 8, 9, 46],
to_s: "1:3,5,7:9,46",
Expand All @@ -569,6 +581,7 @@ def test_inspect((expected, input, freeze))
data "just *", {
input: "*",
elements: [:*],
entries: [:*],
ranges: [:*..],
numbers: RangeError,
to_s: "*",
Expand All @@ -580,6 +593,7 @@ def test_inspect((expected, input, freeze))
data "range with *", {
input: "4294967000:*",
elements: [4_294_967_000..],
entries: [4_294_967_000..],
ranges: [4_294_967_000..],
numbers: RangeError,
to_s: "4294967000:*",
Expand All @@ -591,6 +605,7 @@ def test_inspect((expected, input, freeze))
data "* sorts last", {
input: "5,*,7",
elements: [5, 7, :*],
entries: [5, :*, 7],
ranges: [5..5, 7..7, :*..],
numbers: RangeError,
to_s: "5,*,7",
Expand All @@ -602,6 +617,7 @@ def test_inspect((expected, input, freeze))
data "out of order", {
input: "46,7:6,15,3:1",
elements: [1..3, 6..7, 15, 46],
entries: [46, 6..7, 15, 1..3],
ranges: [1..3, 6..7, 15..15, 46..46],
numbers: [1, 2, 3, 6, 7, 15, 46],
to_s: "46,7:6,15,3:1",
Expand All @@ -613,6 +629,7 @@ def test_inspect((expected, input, freeze))
data "adjacent", {
input: "1,2,3,5,7:9,10:11",
elements: [1..3, 5, 7..11],
entries: [1, 2, 3, 5, 7..9, 10..11],
ranges: [1..3, 5..5, 7..11],
numbers: [1, 2, 3, 5, 7, 8, 9, 10, 11],
to_s: "1,2,3,5,7:9,10:11",
Expand All @@ -624,6 +641,7 @@ def test_inspect((expected, input, freeze))
data "overlapping", {
input: "1:5,3:7,10:9,10:11",
elements: [1..7, 9..11],
entries: [1..5, 3..7, 9..10, 10..11],
ranges: [1..7, 9..11],
numbers: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11],
to_s: "1:5,3:7,10:9,10:11",
Expand All @@ -635,6 +653,7 @@ def test_inspect((expected, input, freeze))
data "contained", {
input: "1:5,3:4,9:11,10",
elements: [1..5, 9..11],
entries: [1..5, 3..4, 9..11, 10],
ranges: [1..5, 9..11],
numbers: [1, 2, 3, 4, 5, 9, 10, 11],
to_s: "1:5,3:4,9:11,10",
Expand All @@ -646,6 +665,7 @@ def test_inspect((expected, input, freeze))
data "array", {
input: ["1:5,3:4", 9..11, "10", 99, :*],
elements: [1..5, 9..11, 99, :*],
entries: [1..5, 9..11, 99, :*],
ranges: [1..5, 9..11, 99..99, :*..],
numbers: RangeError,
to_s: "1:5,9:11,99,*",
Expand All @@ -657,6 +677,7 @@ def test_inspect((expected, input, freeze))
data "nested array", {
input: [["1:5", [3..4], [[[9..11, "10"], 99], :*]]],
elements: [1..5, 9..11, 99, :*],
entries: [1..5, 9..11, 99, :*],
ranges: [1..5, 9..11, 99..99, :*..],
numbers: RangeError,
to_s: "1:5,9:11,99,*",
Expand All @@ -668,6 +689,7 @@ def test_inspect((expected, input, freeze))
data "empty", {
input: nil,
elements: [],
entries: [],
ranges: [],
numbers: [],
to_s: "",
Expand All @@ -680,6 +702,26 @@ def test_inspect((expected, input, freeze))
assert_equal data[:elements], SequenceSet.new(data[:input]).elements
end

test "#each_element" do |data|
seqset = SequenceSet.new(data[:input])
array = []
assert_equal seqset, seqset.each_element { array << _1 }
assert_equal data[:elements], array
assert_equal data[:elements], seqset.each_element.to_a
end

test "#entries" do |data|
assert_equal data[:entries], SequenceSet.new(data[:input]).entries
end

test "#each_entry" do |data|
seqset = SequenceSet.new(data[:input])
array = []
assert_equal seqset, seqset.each_entry { array << _1 }
assert_equal data[:entries], array
assert_equal data[:entries], seqset.each_entry.to_a
end

test "#ranges" do |data|
assert_equal data[:ranges], SequenceSet.new(data[:input]).ranges
end
Expand Down

0 comments on commit 1a26054

Please sign in to comment.