Skip to content

Commit 6de22fd

Browse files
committed
♻️ Coalesce entries in SequenceSet#append
You can build an ordered set from an enumarable input as simply as `enum.inject(Net::IMAP::SequenceSet.new, &:append)`. But, prior to this change, that could easily give you very inefficient output, e.g: it might return `9,1,2,3,4,5,6,7,8`. With this change, the same input will return `9,1:8`. This also short-circuits whenever `@string` is `nil`, avoiding string generation. This significantly improves performance when the set remains monotonically sorted.
1 parent 70218a0 commit 6de22fd

File tree

2 files changed

+52
-4
lines changed

2 files changed

+52
-4
lines changed

lib/net/imap/sequence_set.rb

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -954,16 +954,52 @@ def add(element)
954954
# Unlike #add, #merge, or #union, the new value is appended to #string.
955955
# This may result in a #string which has duplicates or is out-of-order.
956956
#
957+
# set = Net::IMAP::SequenceSet.new
958+
# set.append(1..2) # => Net::IMAP::SequenceSet("1:2")
959+
# set.append(5) # => Net::IMAP::SequenceSet("1:2,5")
960+
# set.append(4) # => Net::IMAP::SequenceSet("1:2,5,4")
961+
# set.append(3) # => Net::IMAP::SequenceSet("1:2,5,4,3")
962+
# set.append(2) # => Net::IMAP::SequenceSet("1:2,5,4,3,2")
963+
#
964+
# If +entry+ is a string, it will be converted into normal form.
965+
#
966+
# set = Net::IMAP::SequenceSet("4:5,1:2")
967+
# set.append("6:6") # => Net::IMAP::SequenceSet("4:5,1:2,6")
968+
# set.append("9:8") # => Net::IMAP::SequenceSet("4:5,1:2,6,8:9")
969+
#
970+
# If +entry+ adjacently follows the last entry, they will coalesced:
971+
# set = Net::IMAP::SequenceSet.new("2,1,9:10")
972+
# set.append(11..12) # => Net::IMAP::SequenceSet("2,1,9:12")
973+
#
957974
# See SequenceSet@Ordered+and+Normalized+sets.
958975
#
959976
# Related: #add, #merge, #union
960977
def append(entry)
961978
modifying! # short-circuit before input_to_tuple
962979
tuple = input_to_tuple entry
980+
adj = tuple.first - 1
981+
if @string.nil? && (@tuples.empty? || tuples.last.last <= adj)
982+
# append to elements or coalesce with last element
983+
tuple_add tuple
984+
return self
985+
elsif @string.nil?
986+
# generate string for out-of-order append
987+
head, comma = normalized_string, ","
988+
else
989+
# @string already exists... maybe coalesce with last entry
990+
head, comma, last_entry = @string.rpartition(",")
991+
last_min, last_max = input_to_tuple last_entry
992+
if last_max == adj
993+
# coalesce with last entry
994+
tuple[0] = last_min
995+
else
996+
# append to existing string
997+
head, comma = @string, ","
998+
end
999+
end
9631000
entry = tuple_to_str tuple
964-
string unless empty? # write @string before tuple_add
9651001
tuple_add tuple
966-
@string = -(@string ? "#{@string},#{entry}" : entry)
1002+
@string = -"#{head}#{comma}#{entry}"
9671003
self
9681004
end
9691005

test/net/imap/test_sequence_set.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,15 +661,27 @@ def obj.to_sequence_set; 192_168.001_255 end
661661
assert_equal "1,5", SequenceSet.new("1").append("5").string
662662
assert_equal "*,1", SequenceSet.new("*").append(1).string
663663
assert_equal "1:6,4:9", SequenceSet.new("1:6").append("4:9").string
664-
assert_equal "1:4,5:*", SequenceSet.new("1:4").append(5..).string
665664
assert_equal "5:*,1:4", SequenceSet.new("5:*").append(1..4).string
665+
# also works when previous string was not normal
666+
assert_equal "3:1,8:9", SequenceSet.new("3:1").append("9:8").string
667+
# coalesces adjacent entries (when previously normalized)
668+
assert_equal "1:9", SequenceSet.new("1:3").append("4:9").string
669+
assert_equal "1:9", SequenceSet.new("1:3").append("9:4").string
670+
assert_equal "1:*", SequenceSet.new("1:4").append(5..).string
671+
# coalesces adjacent entries (when _not_ previously normalized)
672+
assert_equal "1:9", SequenceSet.new("3:1").append("4:9").string
673+
assert_equal "4,1:*", SequenceSet.new("4,1").append(2..).string
674+
# assert_equal "3:1,4:9", SequenceSet.new("3:1").append("4:9").string
675+
# a non-normal appended string will be normalized
676+
assert_equal "1:6,4:9", SequenceSet.new("1:6").append("9:4").string
677+
assert_equal "1:6,999", SequenceSet.new("1:6").append("999:999").string
666678
# also works from empty
667679
assert_equal "5,1", SequenceSet.new.append(5).append(1).string
668680
# also works when *previously* input was non-strings
669681
assert_equal "*,1", SequenceSet.new(:*).append(1).string
670682
assert_equal "1,5", SequenceSet.new(1).append("5").string
671683
assert_equal "1:6,4:9", SequenceSet.new(1..6).append(4..9).string
672-
assert_equal "1:4,5:*", SequenceSet.new(1..4).append(5..).string
684+
assert_equal "1:*", SequenceSet.new(1..4).append(5..).string
673685
assert_equal "5:*,1:4", SequenceSet.new(5..).append(1..4).string
674686
end
675687

0 commit comments

Comments
 (0)