Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Preserving sequence set order #254

Merged
merged 4 commits into from
Dec 24, 2023
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
95 changes: 80 additions & 15 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 @@ -151,7 +166,7 @@ class IMAP
# +nil+ if the object cannot be converted to a compatible type.
# - #cover? (aliased as #===):
# Returns whether a given object is fully contained within +self+.
# - #intersect?:
# - #intersect? (aliased as #overlap?):
# Returns whether +self+ and a given object have any common elements.
# - #disjoint?:
# Returns whether +self+ and a given object have no common elements.
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 @@ -502,6 +523,7 @@ def include_star?; @tuples.last&.last == STAR_INT end
def intersect?(other)
valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
end
alias overlap? intersect?

# Returns +true+ if the set and a given object have no common elements,
# +false+ otherwise.
Expand Down Expand Up @@ -655,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 @@ -787,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 @@ -854,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