diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb
index c1a488e6..9bf1814c 100644
--- a/lib/net/imap/sequence_set.rb
+++ b/lib/net/imap/sequence_set.rb
@@ -62,17 +62,27 @@ class SequenceSet
class << self
+ # :call-seq:
+ # SequenceSet[*values] -> valid frozen sequence set
+ #
# Returns a frozen SequenceSet, constructed from +values+.
#
# An empty SequenceSet is invalid and will raise a DataFormatError.
#
# Use ::new to create a mutable or empty SequenceSet.
- def [](*values)
- seqset = new.merge(*values)
+ def [](first, *rest)
+ if rest.empty? && first.is_a?(SequenceSet) && first.frozen?
+ seqset = first
+ else
+ seqset = new.merge(first, *rest)
+ end
seqset.validate
seqset.freeze
end
+ # :call-seq:
+ # SequenceSet.try_convert(obj) -> sequence set or nil
+ #
# If +obj+ is a SequenceSet, returns +obj+. If +obj+ responds_to
# +to_sequence_set+, calls +obj.to_sequence_set+ and returns the result.
# Otherwise returns +nil+.
@@ -81,7 +91,7 @@ def [](*values)
# raised.
def try_convert(obj)
return obj if obj.is_a?(SequenceSet)
- return unless respond_to?(:to_sequence_set)
+ return nil unless respond_to?(:to_sequence_set)
obj = obj.to_sequence_set
return obj if obj.is_a?(SequenceSet)
raise DataFormatError, "invalid object returned from to_sequence_set"
@@ -89,15 +99,15 @@ def try_convert(obj)
end
- # Create a new SequenceSet object from +input, which may be another
- # SequenceSet, or it may be an IMAP +sequence-set+ string, a number, a
- # range, *, or an enumerable of these.
+ # Create a new SequenceSet object from +input+, which may be another
+ # SequenceSet, an IMAP formatted +sequence-set+ string, a number, a
+ # range, :*, or an enumerable of these.
#
# Use ::[] to create a frozen (non-empty) SequenceSet.
def initialize(input = nil) input ? replace(input) : clear end
# Removes all elements and returns self.
- def clear; @tuples, @str = [], ""; self end
+ def clear; @tuples, @string = [], -""; self end
# Replace the contents of the set with the contents of +other+ and returns
# self.
@@ -109,38 +119,39 @@ def clear; @tuples, @str = [], ""; self end
def replace(other)
case other = object_try_convert(other)
when SequenceSet then initialize_dup(other)
- when String then self.atom = other
+ when String then self.string = other
else clear << other
end
self
end
- # Returns the IMAP string representation. In the IMAP grammar,
- # +sequence-set+ is a subset of +atom+ which is a subset of +astring+.
+ # Returns the IMAP string representation.
#
# Raises a DataFormatError when the set is empty. Use #to_s to return an
# empty string without error.
#
# If the set was created from a single string, that string is returned
# without calling #normalize. When a new value is added to the set, the
- # atom string is automatically #normalized.
- def atom
+ # string is automatically #normalized.
+ def imap_string
raise DataFormatError, "empty sequence-set" if empty?
- @str.clone
+ string
end
- # Returns #atom. In the IMAP grammar, +atom+ is a subset of +astring+.
- alias astring atom
-
- # Returns the value of #atom, or an empty string when the set is empty.
- def to_s; @str.clone end
+ # Returns the IMAP string representation, or an empty string when the set
+ # is empty. Note that an empty set is invalid in the IMAP grammar.
+ #
+ # Related: #imap_string
+ def string; @string.clone end
+ alias to_s string
- # Assigns a new string to #atom and resets #elements to match.
+ # Assigns a new string to #string and resets #elements to match.
#
# Use #add or #merge to add a string to the existing set.
- def atom=(str)
+ def string=(str)
+ str = String.try_convert(str) or raise ArgumentError, "not a string"
tuples = str_to_tuples str
- @tuples, @str = [], -str.to_str
+ @tuples, @string = [], -str
tuples_add tuples
end
@@ -148,10 +159,12 @@ def atom=(str)
def freeze
return if frozen?
@tuples.each(&:freeze).freeze
- @str = -@str
+ @string = -@string
super
end
+ # :call-seq: self == other -> true or false
+ #
# Returns true when the other SequenceSet represents the same message
# identifiers. Encoding difference—such as order, overlaps, or
# duplicates—are ignored.
@@ -162,11 +175,14 @@ def freeze
# Net::IMAP::SequenceSet["9,1:*"] == Net::IMAP::SequenceSet["1:*"] # => true
#
# Related: #eql?, #normalize
- def ==(rhs)
- self.class == rhs.class && (to_s == rhs.to_s || tuples == rhs.tuples)
+ def ==(other)
+ self.class == other.class &&
+ (to_s == other.to_s || tuples == other.tuples)
end
- # Hash equality requires the same encoded #atom representation.
+ # :call-seq: eql?(other) -> true or false
+ #
+ # Hash equality requires the same encoded #string representation.
#
# Net::IMAP::SequenceSet["1:3"] .eql? Net::IMAP::SequenceSet["1:3"] # => true
# Net::IMAP::SequenceSet["1,2,3"].eql? Net::IMAP::SequenceSet["1:3"] # => false
@@ -174,15 +190,17 @@ def ==(rhs)
# Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"] # => false
#
# Related: #==, #normalize
- def eql?(other) self.class == other.class && atom == other.atom end
+ def eql?(other) self.class == other.class && string == other.string end
# See #eql?
- def hash; [self.class, atom].hash end
+ def hash; [self.class, string].hash end
+ # :call-seq: self === other -> true or false or nil
+ #
# Returns the result of #cover? Returns +nil+ if #cover? raises a
# StandardError exception.
- def ===(rhs)
- cover?(rhs)
+ def ===(other)
+ cover?(other)
rescue
nil
end
@@ -195,11 +213,10 @@ def ===(rhs)
def cover?(obj)
obj = object_try_convert(obj)
case obj
- when VALID then include? obj
+ when STAR, VALID then include? obj
when Range then range_cover? obj
when SequenceSet then seqset_cover? obj
- when COERCIBLE then seqset_cover? SequenceSet.try_convert obj
- when Set, Array, String then seqset_cover? SequenceSet.new obj
+ when Set, Array, String then seqset_cover? SequenceSet.new obj
end
end
@@ -216,7 +233,7 @@ def include?(number)
# Returns +true+ when the set contains *.
def include_star?; @tuples.last&.last == STAR end
- # :call-seq: max(star: :*) => Integer | star | nil
+ # :call-seq: max(star: :*) => integer or star or nil
#
# Returns the maximum value in +self+, +star+ when the set includes
# *, or +nil+ when the set is empty.
@@ -224,41 +241,65 @@ def max(star: :*)
(val = @tuples.last&.last) && val == STAR ? star : val
end
- # :call-seq: min => Integer | nil
+ # :call-seq: min(star: :*) => integer or star or nil
#
- # Returns the minimum value in +self+, or +nil+ if empty.
+ # Returns the minimum value in +self+, +star+ when the only value in the
+ # set is *, or +nil+ when the set is empty.
def min(star: :*)
(val = @tuples.first&.first) && val == STAR ? star : val
end
- # :call-seq: minmax(star: :*) => [Integer, Integer | star] | nil
+ # :call-seq: minmax(star: :*) => nil or [integer, integer or star]
#
# Returns a 2-element array containing the minimum and maximum numbers in
# +self+, or +nil+ when the set is empty.
def minmax(star: :*); [min(star: star), max(star: star)] unless empty? end
+ # :call-seq:
+ # self + other -> sequence set
+ # self | other -> sequence set
+ # union(other) -> sequence set
+ #
# Returns a new sequence set that is the union of both sequence sets.
#
# Related: #add, #merge
- def +(rhs) dup.add rhs end
+ def +(other) dup.add other end
alias :| :+
alias union :+
+ # :call-seq:
+ # self - other -> sequence set
+ # difference(other) -> sequence set
+ #
# Returns a new sequence set built by duplicating this set and removing
- # every number that appears in the +rhs+ object.
+ # every number that appears in the +other+ object.
#
# Related: #subtract
- def -(rhs) dup.subtract rhs end
+ def -(other) dup.subtract other end
alias difference :-
- def &(rhs) self - SequenceSet.new(rhs).complement! end
+ # :call-seq:
+ # self & other -> sequence set
+ # intersection(other) -> sequence set
+ #
+ # Returns a new sequence set from the intersection of both sets.
+ def &(other) self - SequenceSet.new(other).complement! end
alias intersection :&
- def ^(rhs) (self | rhs).subtract(self & rhs) end
+ # :call-seq:
+ # self ^ other -> sequence set
+ # xor(other) -> sequence set
+ #
+ # Returns a new sequence set from the XOR (exclusive or) of both sets.
+ def ^(other) (self | other).subtract(self & other) end
alias xor :^
- # Adds a range, number, or string to the set and returns self. The #atom
- # will be regenerated. Use #merge to add many elements at once.
+ # :call-seq:
+ # add(object) -> self
+ # self << other -> self
+ #
+ # Adds a range, number, or string to the set and returns self. The
+ # #string will be regenerated. Use #merge to add many elements at once.
def add(object)
tuples_add input_to_tuples object
normalize!
@@ -271,7 +312,7 @@ def add(object)
def add?(obj) add(obj) unless cover?(obj) end
# Merges the elements in each object to the set and returns self. The
- # #atom will be regenerated after all inputs have been merged.
+ # #string will be regenerated after all inputs have been merged.
def merge(*inputs)
tuples_add inputs.flat_map { input_to_tuples _1 }
normalize!
@@ -280,7 +321,7 @@ def merge(*inputs)
# Deletes every number that appears in +object+ and returns self. +object
# can be a range, a number, or an enumerable of ranges and numbers. The
- # #atom will be regenerated.
+ # #string will be regenerated.
def subtract(object)
tuples_subtract input_to_tuples object
normalize!
@@ -290,7 +331,7 @@ def subtract(object)
# Returns an array of ranges and integers.
#
# The returned elements are sorted and deduplicated, even when the input
- # #atom is not. * will sort last. See #normalize.
+ # #string is not. * will sort last. See #normalize.
#
# By itself, * translates to :*. A range containing
# * translates to an endless range. Use #limit to translate both
@@ -317,14 +358,14 @@ def each_element
# Returns an array of ranges
#
# The returned elements are sorted and deduplicated, even when the input
- # #atom is not. * will sort last. See #normalize.
+ # #string is not. * will sort last. See #normalize.
#
# * translates to an endless range. By itself, *
# translates to :*... Use #limit to set * to a maximum
# value.
#
# The returned ranges will be ordered and coalesced, even when the input
- # #atom is not. * will sort last. See #normalize.
+ # #string is not. * will sort last. See #normalize.
#
# Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
# # => [2..2, 5..9, 11..12, :*..]
@@ -346,7 +387,7 @@ def each_range
# Returns a sorted array of all of the number values in the sequence set.
#
# The returned numbers are sorted and deduplicated, even when the input
- # #atom is not. * will sort last. See #normalize.
+ # #string is not. * will sort last. See #normalize.
#
# Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].numbers
# # => [2, 5, 6, 7, 8, 9, 11, 12, :*]
@@ -432,22 +473,35 @@ def limit!(max:)
self
end
+ # Returns false when the set is empty.
+ def valid?; !empty? end
+
# Returns true if the set contains no elements
def empty?; @tuples.empty? end
# Returns true if the set contains every possible element.
def full?; @tuples == [[1, STAR]] end
+ # :call-seq:
+ # ~ self -> sequence set
+ # complement -> sequence set
+ #
# Returns the complement of self, a SequenceSet which contains all numbers
# _except_ for those in this set.
+ #
+ # Related: #complement!
def complement; dup.complement! end
- alias ~ complement
+ alias :~ complement
+ # :call-seq: complement! -> self
+ #
# Converts the SequenceSet to its own #complement. It will contain all
# possible values _except_ for those currently in the set.
+ #
+ # Related: #complement
def complement!
- return replace(VALID) if empty?
- return clear if full?
+ return replace(VALID) << STAR if empty?
+ return clear if full?
flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
if flat.first < 1 then flat.shift else flat.unshift 1 end
if STAR < flat.last then flat.pop else flat.push STAR end
@@ -458,15 +512,15 @@ def complement!
# Returns a new SequenceSet with a normalized string representation.
#
- # The returned set's #atom string are sorted and deduplicated. Adjacent
- # or overlapping elements will be merged into a single larger range.
+ # The returned set's #string is sorted and deduplicated. Adjacent or
+ # overlapping elements will be merged into a single larger range.
#
# Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"]
# # => Net::IMAP::SequenceSet["1:7,9:11"]
def normalize; dup.normalize! end
- # Sorts, deduplicates, and merges the #atom string, as appropriate
- def normalize!; @str = @tuples.map { tuple_to_str _1 }.join(",") end
+ # Sorts, deduplicates, and merges the #string, as appropriate
+ def normalize!; @string = -@tuples.map { tuple_to_str _1 }.join(",") end
def inspect
if frozen?
@@ -479,21 +533,20 @@ def inspect
# Returns self
alias to_sequence_set itself
- # Unstable API, for internal use only (Net::IMAP#validate_data)
+ # Unstable API, currently for internal use only (Net::IMAP#validate_data)
def validate # :nodoc:
empty? and raise DataFormatError, "empty sequence-set is invalid"
- validate_tuples # validated during input; only raises when there's a bug
true
end
# Unstable API, for internal use only (Net::IMAP#send_data)
def send_data(imap, tag) # :nodoc:
- imap.__send__(:put_string, atom)
+ imap.__send__(:put_string, string)
end
protected
- attr_reader :tuples
+ attr_reader :tuples # :nodoc:
private
@@ -504,7 +557,7 @@ def initialize_clone(other)
def initialize_dup(other)
@tuples = other.tuples.map(&:dup)
- @str = -other.to_s
+ @string = -other.to_s
super
end
@@ -525,6 +578,8 @@ def enum_to_tuples(enum)
enum.flat_map {|obj| object_to_tuples!(obj) }
end
+ # unlike SequenceSet#trykconvert, this can return an Integer, Range,
+ # String, Set, Array, or... any type of object.
def object_try_convert(input)
SequenceSet.try_convert(input) ||
STARS.include?(input) && STAR ||
@@ -537,7 +592,7 @@ def object_try_convert(input)
def object_to_tuples(obj)
obj = object_try_convert obj
case obj
- when VALID then [[obj, obj]]
+ when STAR, VALID then [[obj, obj]]
when Range then [range_to_tuple(obj)]
when String then str_to_tuples obj
when SequenceSet then obj.tuples
@@ -554,8 +609,8 @@ def raise_invalid(obj)
def el_to_nums(elem) elem.is_a?(Range) ? elem.to_a : elem end
def valid_int(obj)
- if VALID.cover?(obj) then obj
- elsif STARS.include?(obj) then STAR
+ if STARS.include?(obj) then STAR
+ elsif VALID.cover?(obj) then obj
else nz_number(obj)
end
end
@@ -609,7 +664,7 @@ def tuple_to_str(tuple)
tuple.uniq.map{ _1 == STAR ? "*" : _1 }.join(":")
end
- def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
+ def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
def tuples_subtract(tuples) tuples.each do tuple_subtract _1 end end
#
@@ -704,22 +759,6 @@ def range_gte_to(num)
first..last if first
end
- def validate_tuples
- tuples.each do validate_tuple _1 end
- if (a, b = tuples.each_cons(2).find {|a, b| b.first <= (a.last + 1) })
- raise DataFormatError, "sequence-set failed to merge %p and %p" % [
- a, b,
- ]
- end
- end
-
- def validate_tuple(tuple)
- min, max = tuple
- unless VALID.cover?(min) && VALID.cover?(max) && min <= max
- raise DataFormatError, "invalid sequence-set range: %p" % [tuple]
- end
- end
-
def nz_number(num)
/\A[1-9]\d*\z/.match?(num) or
raise DataFormatError, "%p is not a valid nz-number" % [num]
diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb
index 8ba03c6f..21205dd3 100644
--- a/test/net/imap/test_sequence_set.rb
+++ b/test/net/imap/test_sequence_set.rb
@@ -91,18 +91,19 @@ class IMAPSequenceSetTest < Test::Unit::TestCase
end
test ".[] must not be empty" do
- assert_raise DataFormatError do SequenceSet[] end
+ assert_raise ArgumentError do SequenceSet[] end
assert_raise DataFormatError do SequenceSet[nil] end
+ assert_raise DataFormatError do SequenceSet[""] end
end
test "#limit" do
set = SequenceSet["1:100,500"]
- assert_equal [1..99], set.limit(max: 99).ranges
- assert_equal (1..15).to_a, set.limit(max: 15).numbers
- assert_equal SequenceSet["1:100"], set.limit(max: 101)
- assert_equal SequenceSet["1:97"], set.limit(max: 97)
- assert_equal [1..99], set.limit(max: 99).ranges
- assert_equal (1..15).to_a, set.limit(max: 15).numbers
+ assert_equal [1..99], set.limit(max: 99).ranges
+ assert_equal (1..15).to_a, set.limit(max: 15).numbers
+ assert_equal SequenceSet["1:100"], set.limit(max: 101)
+ assert_equal SequenceSet["1:97"], set.limit(max: 97)
+ assert_equal [1..99], set.limit(max: 99).ranges
+ assert_equal (1..15).to_a, set.limit(max: 15).numbers
end
test "#limit with *" do
@@ -250,6 +251,10 @@ def test_include
assert SequenceSet["2:4"].include?(3)
assert SequenceSet["2,4:7,9,12:*"] === 2
assert SequenceSet["2,4:7,9,12:*"].cover?(2222)
+ assert SequenceSet["2,*:12"].include_star?
+ assert SequenceSet["2,*:12"].include? :*
+ assert SequenceSet["2,*:12"].include?(-1)
+ refute SequenceSet["12"].include_star?
set = SequenceSet.new Array.new(1_000) { rand(1..1500) }
set.numbers
.each do assert set.include?(_1) end
@@ -426,15 +431,13 @@ def test_count(data)
assert_equal data[:count], SequenceSet.new(data[:input]).count
end
- %i[atom astring].each do |method|
- define_method :"test_#{method}" do |data|
- if (expected = data[:to_s]).empty?
- assert_raise DataFormatError do
- SequenceSet.new(data[:input]).send(method)
- end
- else
- assert_equal data[:to_s], SequenceSet.new(data[:input]).send(method)
+ test "#imap_string" do |data|
+ if (expected = data[:to_s]).empty?
+ assert_raise DataFormatError do
+ SequenceSet.new(data[:input]).imap_string
end
+ else
+ assert_equal data[:to_s], SequenceSet.new(data[:input]).imap_string
end
end