Skip to content

Commit 1a26054

Browse files
authored
🔀 Merge pull request #254 from nevans/preserving-sequence-set-order
✨ Preserving sequence set order
2 parents 2746d34 + 3e9ee5a commit 1a26054

File tree

2 files changed

+120
-14
lines changed

2 files changed

+120
-14
lines changed

‎lib/net/imap/sequence_set.rb

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ class IMAP
6060
# set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
6161
# set.valid_string #=> "1:10,55,1024:2048"
6262
#
63+
# == Normalized form
64+
#
65+
# When a sequence set is created with a single String value, that #string
66+
# representation is preserved. SequenceSet's internal representation
67+
# implicitly sorts all entries, de-duplicates numbers, and coalesces
68+
# adjacent or overlapping ranges. Most enumeration methods and offset-based
69+
# methods use this normalized representation. Most modification methods
70+
# will convert #string to its normalized form.
71+
#
72+
# In some cases the order of the string representation is significant, such
73+
# as the +ESORT+, <tt>CONTEXT=SORT</tt>, and +UIDPLUS+ extensions. Use
74+
# #entries or #each_entry to enumerate the set in its original order. To
75+
# preserve #string order while modifying a set, use #append, #string=, or
76+
# #replace.
77+
#
6378
# == Using <tt>*</tt>
6479
#
6580
# \IMAP sequence sets may contain a special value <tt>"*"</tt>, which
@@ -186,10 +201,14 @@ class IMAP
186201
#
187202
# === Methods for Iterating
188203
#
189-
# - #each_element: Yields each number and range in the set and returns
190-
# +self+.
191-
# - #elements (aliased as #to_a):
192-
# Returns an Array of every number and range in the set.
204+
# - #each_element: Yields each number and range in the set, sorted and
205+
# coalesced, and returns +self+.
206+
# - #elements (aliased as #to_a): Returns an Array of every number and range
207+
# in the set, sorted and coalesced.
208+
# - #each_entry: Yields each number and range in the set, unsorted and
209+
# without deduplicating numbers or coalescing ranges, and returns +self+.
210+
# - #entries: Returns an Array of every number and range in the set,
211+
# unsorted and without deduplicating numbers or coalescing ranges.
193212
# - #each_range:
194213
# Yields each element in the set as a Range and returns +self+.
195214
# - #ranges: Returns an Array of every element in the set, converting
@@ -222,6 +241,8 @@ class IMAP
222241
# - #add?: If the given object is not an element in the set, adds it and
223242
# returns +self+; otherwise, returns +nil+.
224243
# - #merge: Merges multiple elements into the set; returns +self+.
244+
# - #append: Adds a given object to the set, appending it to the existing
245+
# string, and returns +self+.
225246
# - #string=: Assigns a new #string value and replaces #elements to match.
226247
# - #replace: Replaces the contents of the set with the contents
227248
# of a given object.
@@ -656,6 +677,18 @@ def add(object)
656677
end
657678
alias << add
658679

680+
# Adds a range or number to the set and returns +self+.
681+
#
682+
# Unlike #add, #merge, or #union, the new value is appended to #string.
683+
# This may result in a #string which has duplicates or is out-of-order.
684+
def append(object)
685+
tuple = input_to_tuple object
686+
entry = tuple_to_str tuple
687+
tuple_add tuple
688+
@string = -(string ? "#{@string},#{entry}" : entry)
689+
self
690+
end
691+
659692
# :call-seq: add?(object) -> self or nil
660693
#
661694
# Adds a range or number to the set and returns +self+. Returns +nil+
@@ -788,7 +821,18 @@ def subtract(*objects)
788821
normalize!
789822
end
790823

791-
# Returns an array of ranges and integers.
824+
# Returns an array of ranges and integers and <tt>:*</tt>.
825+
#
826+
# The entries are in the same order they appear in #string, with no
827+
# sorting, deduplication, or coalescing. When #string is in its
828+
# normalized form, this will return the same result as #elements.
829+
# This is useful when the given order is significant, for example in a
830+
# ESEARCH response to IMAP#sort.
831+
#
832+
# Related: #each_entry, #elements
833+
def entries; each_entry.to_a end
834+
835+
# Returns an array of ranges and integers and <tt>:*</tt>.
792836
#
793837
# The returned elements are sorted and coalesced, even when the input
794838
# #string is not. <tt>*</tt> will sort last. See #normalize.
@@ -855,22 +899,42 @@ def ranges; each_range.to_a end
855899
# Related: #elements, #ranges, #to_set
856900
def numbers; each_number.to_a end
857901

858-
# Yields each number or range in #elements to the block and returns self.
902+
# Yields each number or range in #string to the block and returns +self+.
859903
# Returns an enumerator when called without a block.
860904
#
861-
# Related: #elements
905+
# The entries are yielded in the same order they appear in #tring, with no
906+
# sorting, deduplication, or coalescing. When #string is in its
907+
# normalized form, this will yield the same values as #each_element.
908+
#
909+
# Related: #entries, #each_element
910+
def each_entry(&block)
911+
return to_enum(__method__) unless block_given?
912+
return each_element(&block) unless @string
913+
@string.split(",").each do yield tuple_to_entry str_to_tuple _1 end
914+
self
915+
end
916+
917+
# Yields each number or range (or <tt>:*</tt>) in #elements to the block
918+
# and returns self. Returns an enumerator when called without a block.
919+
#
920+
# The returned numbers are sorted and de-duplicated, even when the input
921+
# #string is not. See #normalize.
922+
#
923+
# Related: #elements, #each_entry
862924
def each_element # :yields: integer or range or :*
863925
return to_enum(__method__) unless block_given?
864-
@tuples.each do |min, max|
865-
if min == STAR_INT then yield :*
866-
elsif max == STAR_INT then yield min..
867-
elsif min == max then yield min
868-
else yield min..max
869-
end
870-
end
926+
@tuples.each do yield tuple_to_entry _1 end
871927
self
872928
end
873929

930+
private def tuple_to_entry((min, max))
931+
if min == STAR_INT then :*
932+
elsif max == STAR_INT then min..
933+
elsif min == max then min
934+
else min..max
935+
end
936+
end
937+
874938
# Yields each range in #ranges to the block and returns self.
875939
# Returns an enumerator when called without a block.
876940
#

‎test/net/imap/test_sequence_set.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ def compare_to_reference_set(nums, set, seqset)
288288
assert_equal SequenceSet["1:*"], SequenceSet.new("5:*") << (1..4)
289289
end
290290

291+
test "#append" do
292+
assert_equal "1,5", SequenceSet.new("1").append("5").string
293+
assert_equal "*,1", SequenceSet.new("*").append(1).string
294+
assert_equal "1:6,4:9", SequenceSet.new("1:6").append("4:9").string
295+
assert_equal "1:4,5:*", SequenceSet.new("1:4").append(5..).string
296+
assert_equal "5:*,1:4", SequenceSet.new("5:*").append(1..4).string
297+
end
298+
291299
test "#merge" do
292300
seqset = -> { SequenceSet.new _1 }
293301
assert_equal seqset["1,5"], seqset["1"].merge("5")
@@ -525,6 +533,7 @@ def test_inspect((expected, input, freeze))
525533
data "single number", {
526534
input: "123456",
527535
elements: [123_456],
536+
entries: [123_456],
528537
ranges: [123_456..123_456],
529538
numbers: [123_456],
530539
to_s: "123456",
@@ -536,6 +545,7 @@ def test_inspect((expected, input, freeze))
536545
data "single range", {
537546
input: "1:3",
538547
elements: [1..3],
548+
entries: [1..3],
539549
ranges: [1..3],
540550
numbers: [1, 2, 3],
541551
to_s: "1:3",
@@ -547,6 +557,7 @@ def test_inspect((expected, input, freeze))
547557
data "simple numbers list", {
548558
input: "1,3,5",
549559
elements: [ 1, 3, 5],
560+
entries: [ 1, 3, 5],
550561
ranges: [1..1, 3..3, 5..5],
551562
numbers: [ 1, 3, 5],
552563
to_s: "1,3,5",
@@ -558,6 +569,7 @@ def test_inspect((expected, input, freeze))
558569
data "numbers and ranges list", {
559570
input: "1:3,5,7:9,46",
560571
elements: [1..3, 5, 7..9, 46],
572+
entries: [1..3, 5, 7..9, 46],
561573
ranges: [1..3, 5..5, 7..9, 46..46],
562574
numbers: [1, 2, 3, 5, 7, 8, 9, 46],
563575
to_s: "1:3,5,7:9,46",
@@ -569,6 +581,7 @@ def test_inspect((expected, input, freeze))
569581
data "just *", {
570582
input: "*",
571583
elements: [:*],
584+
entries: [:*],
572585
ranges: [:*..],
573586
numbers: RangeError,
574587
to_s: "*",
@@ -580,6 +593,7 @@ def test_inspect((expected, input, freeze))
580593
data "range with *", {
581594
input: "4294967000:*",
582595
elements: [4_294_967_000..],
596+
entries: [4_294_967_000..],
583597
ranges: [4_294_967_000..],
584598
numbers: RangeError,
585599
to_s: "4294967000:*",
@@ -591,6 +605,7 @@ def test_inspect((expected, input, freeze))
591605
data "* sorts last", {
592606
input: "5,*,7",
593607
elements: [5, 7, :*],
608+
entries: [5, :*, 7],
594609
ranges: [5..5, 7..7, :*..],
595610
numbers: RangeError,
596611
to_s: "5,*,7",
@@ -602,6 +617,7 @@ def test_inspect((expected, input, freeze))
602617
data "out of order", {
603618
input: "46,7:6,15,3:1",
604619
elements: [1..3, 6..7, 15, 46],
620+
entries: [46, 6..7, 15, 1..3],
605621
ranges: [1..3, 6..7, 15..15, 46..46],
606622
numbers: [1, 2, 3, 6, 7, 15, 46],
607623
to_s: "46,7:6,15,3:1",
@@ -613,6 +629,7 @@ def test_inspect((expected, input, freeze))
613629
data "adjacent", {
614630
input: "1,2,3,5,7:9,10:11",
615631
elements: [1..3, 5, 7..11],
632+
entries: [1, 2, 3, 5, 7..9, 10..11],
616633
ranges: [1..3, 5..5, 7..11],
617634
numbers: [1, 2, 3, 5, 7, 8, 9, 10, 11],
618635
to_s: "1,2,3,5,7:9,10:11",
@@ -624,6 +641,7 @@ def test_inspect((expected, input, freeze))
624641
data "overlapping", {
625642
input: "1:5,3:7,10:9,10:11",
626643
elements: [1..7, 9..11],
644+
entries: [1..5, 3..7, 9..10, 10..11],
627645
ranges: [1..7, 9..11],
628646
numbers: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11],
629647
to_s: "1:5,3:7,10:9,10:11",
@@ -635,6 +653,7 @@ def test_inspect((expected, input, freeze))
635653
data "contained", {
636654
input: "1:5,3:4,9:11,10",
637655
elements: [1..5, 9..11],
656+
entries: [1..5, 3..4, 9..11, 10],
638657
ranges: [1..5, 9..11],
639658
numbers: [1, 2, 3, 4, 5, 9, 10, 11],
640659
to_s: "1:5,3:4,9:11,10",
@@ -646,6 +665,7 @@ def test_inspect((expected, input, freeze))
646665
data "array", {
647666
input: ["1:5,3:4", 9..11, "10", 99, :*],
648667
elements: [1..5, 9..11, 99, :*],
668+
entries: [1..5, 9..11, 99, :*],
649669
ranges: [1..5, 9..11, 99..99, :*..],
650670
numbers: RangeError,
651671
to_s: "1:5,9:11,99,*",
@@ -657,6 +677,7 @@ def test_inspect((expected, input, freeze))
657677
data "nested array", {
658678
input: [["1:5", [3..4], [[[9..11, "10"], 99], :*]]],
659679
elements: [1..5, 9..11, 99, :*],
680+
entries: [1..5, 9..11, 99, :*],
660681
ranges: [1..5, 9..11, 99..99, :*..],
661682
numbers: RangeError,
662683
to_s: "1:5,9:11,99,*",
@@ -668,6 +689,7 @@ def test_inspect((expected, input, freeze))
668689
data "empty", {
669690
input: nil,
670691
elements: [],
692+
entries: [],
671693
ranges: [],
672694
numbers: [],
673695
to_s: "",
@@ -680,6 +702,26 @@ def test_inspect((expected, input, freeze))
680702
assert_equal data[:elements], SequenceSet.new(data[:input]).elements
681703
end
682704

705+
test "#each_element" do |data|
706+
seqset = SequenceSet.new(data[:input])
707+
array = []
708+
assert_equal seqset, seqset.each_element { array << _1 }
709+
assert_equal data[:elements], array
710+
assert_equal data[:elements], seqset.each_element.to_a
711+
end
712+
713+
test "#entries" do |data|
714+
assert_equal data[:entries], SequenceSet.new(data[:input]).entries
715+
end
716+
717+
test "#each_entry" do |data|
718+
seqset = SequenceSet.new(data[:input])
719+
array = []
720+
assert_equal seqset, seqset.each_entry { array << _1 }
721+
assert_equal data[:entries], array
722+
assert_equal data[:entries], seqset.each_entry.to_a
723+
end
724+
683725
test "#ranges" do |data|
684726
assert_equal data[:ranges], SequenceSet.new(data[:input]).ranges
685727
end

0 commit comments

Comments
 (0)