diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb
index 614288d2..daeb0957 100644
--- a/lib/net/imap/response_data.rb
+++ b/lib/net/imap/response_data.rb
@@ -763,6 +763,19 @@ class ThreadMember < Struct.new(:seqno, :children)
#
# An array of Net::IMAP::ThreadMember objects for mail items that are
# children of this in the thread.
+
+ # Returns a SequenceSet containing #seqno and all #children's seqno,
+ # recursively.
+ def to_sequence_set
+ SequenceSet.new all_seqnos
+ end
+
+ protected
+
+ def all_seqnos(node = self)
+ [node.seqno].concat node.children.flat_map { _1.all_seqnos }
+ end
+
end
# Net::IMAP::BodyStructure is included by all of the structs that can be
diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb
index 23bec7ce..25baa087 100644
--- a/lib/net/imap/response_parser.rb
+++ b/lib/net/imap/response_parser.rb
@@ -464,7 +464,7 @@ def unescape_quoted(quoted)
def sequence_set
str = combine_adjacent(*SEQUENCE_SET_TOKENS)
if Patterns::SEQUENCE_SET_STR.match?(str)
- SequenceSet.new(str)
+ SequenceSet[str]
else
parse_error("unexpected atom %p, expected sequence-set", str)
end
diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb
index 9b533633..48b7eaf1 100644
--- a/lib/net/imap/sequence_set.rb
+++ b/lib/net/imap/sequence_set.rb
@@ -4,64 +4,1346 @@ module Net
class IMAP
##
- # An IMAP {sequence
- # set}[https://www.rfc-editor.org/rfc/rfc9051.html#section-4.1.1],
- # is a set of message sequence numbers or unique identifier numbers
- # ("UIDs"). It contains numbers and ranges of numbers. The numbers are all
- # non-zero unsigned 32-bit integers and one special value, *, that
- # represents the largest value in the mailbox.
- #
- # *NOTE:* This SequenceSet class is currently a placeholder for unhandled
- # extension data. All it does now is validate. It will be expanded to a
- # full API in a future release.
+ # An \IMAP sequence set is a set of message sequence numbers or unique
+ # identifier numbers ("UIDs"). It contains numbers and ranges of numbers.
+ # The numbers are all non-zero unsigned 32-bit integers and one special
+ # value ("*") that represents the largest value in the mailbox.
+ #
+ # Certain types of \IMAP responses will contain a SequenceSet, for example
+ # the data for a "MODIFIED" ResponseCode. Some \IMAP commands may
+ # receive a SequenceSet as an argument, for example IMAP#search, IMAP#fetch,
+ # and IMAP#store.
+ #
+ # == EXPERIMENTAL API
+ #
+ # SequenceSet is currently experimental. Only two methods, ::[] and
+ # #valid_string, are considered stable. Although the API isn't expected to
+ # change much, any other methods may be removed or changed without
+ # deprecation.
+ #
+ # == Creating sequence sets
+ #
+ # SequenceSet.new with no arguments creates an empty sequence set. Note
+ # that an empty sequence set is invalid in the \IMAP grammar.
+ #
+ # set = Net::IMAP::SequenceSet.new
+ # set.empty? #=> true
+ # set.valid? #=> false
+ # set.valid_string #!> raises DataFormatError
+ # set << 1..10
+ # set.empty? #=> false
+ # set.valid? #=> true
+ # set.valid_string #=> "1:10"
+ #
+ # SequenceSet.new may receive a single optional argument: a non-zero 32 bit
+ # unsigned integer, a range, a sequence-set formatted string,
+ # another sequence set, or an enumerable containing any of these.
+ #
+ # set = Net::IMAP::SequenceSet.new(1)
+ # set.valid_string #=> "1"
+ # set = Net::IMAP::SequenceSet.new(1..100)
+ # set.valid_string #=> "1:100"
+ # set = Net::IMAP::SequenceSet.new(1...100)
+ # set.valid_string #=> "1:99"
+ # set = Net::IMAP::SequenceSet.new([1, 2, 5..])
+ # set.valid_string #=> "1:2,5:*"
+ # set = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
+ # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
+ # set.valid_string #=> "1:10,55,1024:2048"
+ #
+ # Use ::[] with one or more arguments to create a frozen SequenceSet. An
+ # invalid (empty) set cannot be created with ::[].
+ #
+ # set = Net::IMAP::SequenceSet["1,2,3:7,5,6:10,2048,1024"]
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
+ # set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
+ # set.valid_string #=> "1:10,55,1024:2048"
+ #
+ # == Using *
+ #
+ # \IMAP sequence sets may contain a special value "*", which
+ # represents the largest number in use. From +seq-number+ in
+ # {RFC9051 §9}[https://www.rfc-editor.org/rfc/rfc9051.html#section-9-5]:
+ # >>>
+ # In the case of message sequence numbers, it is the number of messages
+ # in a non-empty mailbox. In the case of unique identifiers, it is the
+ # unique identifier of the last message in the mailbox or, if the
+ # mailbox is empty, the mailbox's current UIDNEXT value.
+ #
+ # When creating a SequenceSet, * may be input as -1,
+ # "*", :*, an endless range, or a range ending in
+ # -1. When converting to #elements, #ranges, or #numbers, it will
+ # output as either :* or an endless range. For example:
+ #
+ # Net::IMAP::SequenceSet["1,3,*"].to_a #=> [1, 3, :*]
+ # Net::IMAP::SequenceSet["1,234:*"].to_a #=> [1, 234..]
+ # Net::IMAP::SequenceSet[1234..-1].to_a #=> [1234..]
+ # Net::IMAP::SequenceSet[1234..].to_a #=> [1234..]
+ #
+ # Net::IMAP::SequenceSet[1234..].to_s #=> "1234:*"
+ # Net::IMAP::SequenceSet[1234..-1].to_s #=> "1234:*"
+ #
+ # Use #limit to convert "*" to a maximum value. When a range
+ # includes "*", the maximum value will always be matched:
+ #
+ # Net::IMAP::SequenceSet["9999:*"].limit(max: 25)
+ # #=> Net::IMAP::SequenceSet["25"]
+ #
+ # === Surprising * behavior
+ #
+ # When a set includes *, some methods may have surprising behavior.
+ #
+ # For example, #complement treats * as its own number. This way,
+ # the #intersection of a set and its #complement will always be empty.
+ # This is not how an \IMAP server interprets the set: it will convert
+ # * to either the number of messages in the mailbox or +UIDNEXT+,
+ # as appropriate. And there _will_ be overlap between a set and its
+ # complement after #limit is applied to each:
+ #
+ # ~Net::IMAP::SequenceSet["*"] == Net::IMAP::SequenceSet[1..(2**32-1)]
+ # ~Net::IMAP::SequenceSet[1..5] == Net::IMAP::SequenceSet["6:*"]
+ #
+ # set = Net::IMAP::SequenceSet[1..5]
+ # (set & ~set).empty? => true
+ #
+ # (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4]
+ #
+ # When counting the number of numbers in a set, * will be counted
+ # _except_ when UINT32_MAX is also in the set:
+ # UINT32_MAX = 2**32 - 1
+ # Net::IMAP::SequenceSet["*"].count => 1
+ # Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX
+ #
+ # Net::IMAP::SequenceSet["1:*"].count => UINT32_MAX
+ # Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
+ # Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
+ #
+ # == What's here?
+ #
+ # SequenceSet provides methods for:
+ # * {Creating a SequenceSet}[rdoc-ref:SequenceSet@Methods+for+Creating+a+SequenceSet]
+ # * {Comparing}[rdoc-ref:SequenceSet@Methods+for+Comparing]
+ # * {Querying}[rdoc-ref:SequenceSet@Methods+for+Querying]
+ # * {Iterating}[rdoc-ref:SequenceSet@Methods+for+Iterating]
+ # * {Set Operations}[rdoc-ref:SequenceSet@Methods+for+Set+Operations]
+ # * {Assigning}[rdoc-ref:SequenceSet@Methods+for+Assigning]
+ # * {Deleting}[rdoc-ref:SequenceSet@Methods+for+Deleting]
+ # * {IMAP String Formatting}[rdoc-ref:SequenceSet@Methods+for+IMAP+String+Formatting]
+ #
+ # === Methods for Creating a \SequenceSet
+ # * ::[]: Creates a validated frozen sequence set from one or more inputs.
+ # * ::new: Creates a new mutable sequence set, which may be empty (invalid).
+ # * ::try_convert: Calls +to_sequence_set+ on an object and verifies that
+ # the result is a SequenceSet.
+ # * ::empty: Returns a frozen empty (invalid) SequenceSet.
+ # * ::full: Returns a frozen SequenceSet containing every possible number.
+ #
+ # === Methods for Comparing
+ #
+ # Comparison to another \SequenceSet:
+ # - #==: Returns whether a given set contains the same numbers as +self+.
+ # - #eql?: Returns whether a given set uses the same #string as +self+.
+ #
+ # Comparison to objects which are convertible to \SequenceSet:
+ # - #===:
+ # Returns whether a given object is fully contained within +self+, or
+ # +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?:
+ # Returns whether +self+ and a given object have any common elements.
+ # - #disjoint?:
+ # Returns whether +self+ and a given object have no common elements.
+ #
+ # === Methods for Querying
+ # These methods do not modify +self+.
+ #
+ # Set membership:
+ # - #include? (aliased as #member?):
+ # Returns whether a given object (nz-number, range, or *) is
+ # contained by the set.
+ # - #include_star?: Returns whether the set contains *.
+ #
+ # Minimum and maximum value elements:
+ # - #min: Returns the minimum number in the set.
+ # - #max: Returns the maximum number in the set.
+ # - #minmax: Returns the minimum and maximum numbers in the set.
+ #
+ # Accessing value by offset:
+ # - #[] (aliased as #slice): Returns the number or consecutive subset at a
+ # given offset or range of offsets.
+ # - #at: Returns the number at a given offset.
+ # - #find_index: Returns the given number's offset in the set
+ #
+ # Set cardinality:
+ # - #count (aliased as #size): Returns the count of numbers in the set.
+ # - #empty?: Returns whether the set has no members. \IMAP syntax does not
+ # allow empty sequence sets.
+ # - #valid?: Returns whether the set has any members.
+ # - #full?: Returns whether the set contains every possible value, including
+ # *.
+ #
+ # === 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_range:
+ # Yields each element in the set as a Range and returns +self+.
+ # - #ranges: Returns an Array of every element in the set, converting
+ # numbers into ranges of a single value.
+ # - #each_number: Yields each number in the set and returns +self+.
+ # - #numbers: Returns an Array with every number in the set, expanding
+ # ranges into all of their contained numbers.
+ # - #to_set: Returns a Set containing all of the #numbers in the set.
+ #
+ # === Methods for \Set Operations
+ # These methods do not modify +self+.
+ #
+ # - #| (aliased as #union and #+): Returns a new set combining all members
+ # from +self+ with all members from the other object.
+ # - #& (aliased as #intersection): Returns a new set containing all members
+ # common to +self+ and the other object.
+ # - #- (aliased as #difference): Returns a copy of +self+ with all members
+ # in the other object removed.
+ # - #^ (aliased as #xor): Returns a new set containing all members from
+ # +self+ and the other object except those common to both.
+ # - #~ (aliased as #complement): Returns a new set containing all members
+ # that are not in +self+
+ # - #limit: Returns a copy of +self+ which has replaced * with a
+ # given maximum value and removed all members over that maximum.
+ #
+ # === Methods for Assigning
+ # These methods add or replace elements in +self+.
+ #
+ # - #add (aliased as #<<): Adds a given object to the set; returns +self+.
+ # - #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+.
+ # - #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.
+ # - #complement!: Replaces the contents of the set with its own #complement.
+ #
+ # === Methods for Deleting
+ # These methods remove elements from +self+.
+ #
+ # - #clear: Removes all elements in the set; returns +self+.
+ # - #delete: Removes a given object from the set; returns +self+.
+ # - #delete?: If the given object is an element in the set, removes it and
+ # returns it; otherwise, returns +nil+.
+ # - #delete_at: Removes the number at a given offset.
+ # - #slice!: Removes the number or consecutive numbers at a given offset or
+ # range of offsets.
+ # - #subtract: Removes each given object from the set; returns +self+.
+ # - #limit!: Replaces * with a given maximum value and removes all
+ # members over that maximum; returns +self+.
+ #
+ # === Methods for \IMAP String Formatting
+ #
+ # - #to_s: Returns the +sequence-set+ string, or an empty string when the
+ # set is empty.
+ # - #string: Returns the +sequence-set+ string, or nil when empty.
+ # - #valid_string: Returns the +sequence-set+ string, or raises
+ # DataFormatError when the set is empty.
+ # - #normalized_string: Returns a sequence-set string with its
+ # elements sorted and coalesced, or nil when the set is empty.
+ # - #normalize: Returns a new set with this set's normalized +sequence-set+
+ # representation.
+ # - #normalize!: Updates #string to its normalized +sequence-set+
+ # representation and returns +self+.
+ #
class SequenceSet
+ # The largest possible non-zero unsigned 32-bit integer
+ UINT32_MAX = 2**32 - 1
+
+ # represents "*" internally, to simplify sorting (etc)
+ STAR_INT = UINT32_MAX + 1
+ private_constant :STAR_INT
+
+ # valid inputs for "*"
+ STARS = [:*, ?*, -1].freeze
+ private_constant :STAR_INT, :STARS
+
+ COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
+ ENUMABLE = ->{ _1.respond_to?(:each) && _1.respond_to?(:empty?) }
+ private_constant :COERCIBLE, :ENUMABLE
+
+ 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 [](first, *rest)
+ if rest.empty?
+ if first.is_a?(SequenceSet) && set.frozen? && set.valid?
+ first
+ else
+ new(first).validate.freeze
+ end
+ else
+ new(first).merge(*rest).validate.freeze
+ end
+ 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+.
+ #
+ # If +obj.to_sequence_set+ doesn't return a SequenceSet, an exception is
+ # raised.
+ def try_convert(obj)
+ return obj if obj.is_a?(SequenceSet)
+ 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"
+ end
+
+ # Returns a frozen empty set singleton. Note that valid \IMAP sequence
+ # sets cannot be empty, so this set is _invalid_.
+ def empty; EMPTY end
+
+ # Returns a frozen full set singleton: "1:*"
+ def full; FULL end
+
+ end
+
+ # 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, @string = [], nil; self end
- def self.[](str) new(str).freeze end
+ # Replace the contents of the set with the contents of +other+ and returns
+ # +self+.
+ #
+ # +other+ may be another SequenceSet, or it may be an IMAP +sequence-set+
+ # string, a number, a range, *, or an enumerable of these.
+ def replace(other)
+ case other
+ when SequenceSet then initialize_dup(other)
+ when String then self.string = other
+ else clear; merge other
+ end
+ self
+ end
+
+ # Returns the \IMAP +sequence-set+ string representation, or raises a
+ # DataFormatError when the set is empty.
+ #
+ # Use #string to return +nil+ or #to_s to return an empty string without
+ # error.
+ #
+ # Related: #string, #normalized_string, #to_s
+ def valid_string
+ raise DataFormatError, "empty sequence-set" if empty?
+ string
+ end
+
+ # Returns the \IMAP +sequence-set+ string representation, or +nil+ when
+ # the set is empty. Note that an empty set is invalid in the \IMAP
+ # syntax.
+ #
+ # Use #valid_string to raise an exception when the set is empty, or #to_s
+ # to return an empty string.
+ #
+ # If the set was created from a single string, it is not normalized. If
+ # the set is updated the string will be normalized.
+ #
+ # Related: #valid_string, #normalized_string, #to_s
+ def string; @string ||= normalized_string if valid? end
- def initialize(input)
- @atom = -String.try_convert(input)
- validate
+ # Assigns a new string to #string and resets #elements to match. It
+ # cannot be set to an empty string—assign +nil+ or use #clear instead.
+ # The string is validated but not normalized.
+ #
+ # Use #add or #merge to add a string to an existing set.
+ #
+ # Related: #replace, #clear
+ def string=(str)
+ if str.nil?
+ clear
+ else
+ str = String.try_convert(str) or raise ArgumentError, "not a string"
+ tuples = str_to_tuples str
+ @tuples, @string = [], -str
+ tuples_add tuples
+ end
end
- # Returns the IMAP string representation. In the IMAP grammar,
- # +sequence-set+ is a subset of +atom+ which is a subset of +astring+.
- attr_accessor :atom
+ # Returns the \IMAP +sequence-set+ string representation, or an empty
+ # string when the set is empty. Note that an empty set is invalid in the
+ # \IMAP syntax.
+ #
+ # Related: #valid_string, #normalized_string, #to_s
+ def to_s; string || "" end
- # Returns #atom. In the IMAP grammar, +atom+ is a subset of +astring+.
- alias astring atom
+ # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
+ def freeze
+ return self if frozen?
+ string
+ @tuples.each(&:freeze).freeze
+ super
+ end
- # Returns the value of #atom
- alias to_s atom
+ # :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.
+ #
+ # Net::IMAP::SequenceSet["1:3"] == Net::IMAP::SequenceSet["1:3"]
+ # #=> true
+ # Net::IMAP::SequenceSet["1,2,3"] == Net::IMAP::SequenceSet["1:3"]
+ # #=> true
+ # Net::IMAP::SequenceSet["1,3"] == Net::IMAP::SequenceSet["3,1"]
+ # #=> true
+ # Net::IMAP::SequenceSet["9,1:*"] == Net::IMAP::SequenceSet["1:*"]
+ # #=> true
+ #
+ # Related: #eql?, #normalize
+ 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
#
- # 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
- # Net::IMAP::SequenceSet["1,3"] .eql? Net::IMAP::SequenceSet["3,1"] # => false
- # Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"] # => false
+ # Hash equality requires the same encoded #string representation.
#
- def eql?(other) self.class == other.class && atom == other.atom end
- alias == eql?
+ # 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
+ # Net::IMAP::SequenceSet["1,3"] .eql? Net::IMAP::SequenceSet["3,1"]
+ # #=> false
+ # Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"]
+ # #=> false
+ #
+ # Related: #==, #normalize
+ 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 | false | nil
+ #
+ # Returns whether +other+ is contained within the set. Returns +nil+ if a
+ # StandardError is raised while converting +other+ to a comparable type.
+ #
+ # Related: #cover?, #include?, #include_star?
+ def ===(other)
+ cover?(other)
+ rescue
+ nil
+ end
+
+ # :call-seq: cover?(other) -> true | false | nil
+ #
+ # Returns whether +other+ is contained within the set. +other+ may be any
+ # object that would be accepted by ::new.
+ #
+ # Related: #===, #include?, #include_star?
+ def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
+
+ # Returns +true+ when a given number or range is in +self+, and +false+
+ # otherwise. Returns +false+ unless +number+ is an Integer, Range, or
+ # *.
+ #
+ # set = Net::IMAP::SequenceSet["5:10,100,111:115"]
+ # set.include? 1 #=> false
+ # set.include? 5..10 #=> true
+ # set.include? 11..20 #=> false
+ # set.include? 100 #=> true
+ # set.include? 6 #=> true, covered by "5:10"
+ # set.include? 4..9 #=> true, covered by "5:10"
+ # set.include? "4:9" #=> true, strings are parsed
+ # set.include? 4..9 #=> false, intersection is not sufficient
+ # set.include? "*" #=> false, use #limit to re-interpret "*"
+ # set.include? -1 #=> false, -1 is interpreted as "*"
+ #
+ # set = Net::IMAP::SequenceSet["5:10,100,111:*"]
+ # set.include? :* #=> true
+ # set.include? "*" #=> true
+ # set.include? -1 #=> true
+ # set.include? 200.. #=> true
+ # set.include? 100.. #=> false
+ #
+ # Related: #include_star?, #cover?, #===
+ def include?(element) include_tuple? input_to_tuple element end
+
+ alias member? include?
+
+ # Returns +true+ when the set contains *.
+ def include_star?; @tuples.last&.last == STAR_INT end
+
+ # Returns +true+ if the set and a given object have any common elements,
+ # +false+ otherwise.
+ #
+ # Net::IMAP::SequenceSet["5:10"].intersect? "7,9,11" #=> true
+ # Net::IMAP::SequenceSet["5:10"].intersect? "11:33" #=> false
+ #
+ # Related: #intersection, #disjoint?
+ def intersect?(other)
+ valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
+ end
+
+ # Returns +true+ if the set and a given object have no common elements,
+ # +false+ otherwise.
+ #
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "7,9,11" #=> false
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "11:33" #=> true
+ #
+ # Related: #intersection, #intersect?
+ def disjoint?(other)
+ empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
+ end
+
+ # :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.
+ def max(star: :*)
+ (val = @tuples.last&.last) && val == STAR_INT ? star : val
+ end
+
+ # :call-seq: min(star: :*) => integer or star or nil
+ #
+ # 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_INT ? star : val
+ end
+
+ # :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
+
+ # 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_INT]] end
+
+ # :call-seq:
+ # self + other -> sequence set
+ # self | other -> sequence set
+ # union(other) -> sequence set
+ #
+ # Returns a new sequence set that has every number in the +other+ object
+ # added.
+ #
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
+ # bit unsigned integer, range, sequence-set formatted string,
+ # another sequence set, or an enumerable containing any of these.
+ #
+ # Net::IMAP::SequenceSet["1:5"] | 2 | [4..6, 99]
+ # #=> Net::IMAP::SequenceSet["1:6,99"]
+ #
+ # Related: #add, #merge
+ def |(other) remain_frozen dup.merge 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 +other+.
+ #
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
+ # bit unsigned integer, range, sequence-set formatted string,
+ # another sequence set, or an enumerable containing any of these.
+ #
+ # Net::IMAP::SequenceSet[1..5] - 2 - 4 - 6
+ # #=> Net::IMAP::SequenceSet["1,3,5"]
+ #
+ # Related: #subtract
+ def -(other) remain_frozen dup.subtract other end
+ alias difference :-
+
+ # :call-seq:
+ # self & other -> sequence set
+ # intersection(other) -> sequence set
+ #
+ # Returns a new sequence set containing only the numbers common to this
+ # set and +other+.
+ #
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
+ # bit unsigned integer, range, sequence-set formatted string,
+ # another sequence set, or an enumerable containing any of these.
+ #
+ # Net::IMAP::SequenceSet[1..5] & [2, 4, 6]
+ # #=> Net::IMAP::SequenceSet["2,4"]
+ #
+ # (seqset & other) is equivalent to (seqset - ~other).
+ def &(other)
+ remain_frozen dup.subtract SequenceSet.new(other).complement!
+ end
+ alias intersection :&
+
+ # :call-seq:
+ # self ^ other -> sequence set
+ # xor(other) -> sequence set
+ #
+ # Returns a new sequence set containing numbers that are exclusive between
+ # this set and +other+.
+ #
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
+ # bit unsigned integer, range, sequence-set formatted string,
+ # another sequence set, or an enumerable containing any of these.
+ #
+ # Net::IMAP::SequenceSet[1..5] ^ [2, 4, 6]
+ # #=> Net::IMAP::SequenceSet["1,3,5:6"]
+ #
+ # (seqset ^ other) is equivalent to ((seqset | other) -
+ # (seqset & other)).
+ def ^(other) remain_frozen (self | other).subtract(self & other) end
+ alias xor :^
+
+ # :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.
+ #
+ # ~Net::IMAP::SequenceSet.full #=> Net::IMAP::SequenceSet.empty
+ # ~Net::IMAP::SequenceSet.empty #=> Net::IMAP::SequenceSet.full
+ # ~Net::IMAP::SequenceSet["1:5,100:222"]
+ # #=> Net::IMAP::SequenceSet["6:99,223:*"]
+ # ~Net::IMAP::SequenceSet["6:99,223:*"]
+ # #=> Net::IMAP::SequenceSet["1:5,100:222"]
+ #
+ # Related: #complement!
+ def ~; remain_frozen dup.complement! end
+ alias complement :~
+
+ # :call-seq:
+ # add(object) -> self
+ # self << other -> self
+ #
+ # Adds a range or number to the set and returns +self+.
+ #
+ # #string will be regenerated. Use #merge to add many elements at once.
+ #
+ # Related: #add?, #merge, #union
+ def add(object)
+ tuple_add input_to_tuple object
+ normalize!
+ end
+ alias << add
+
+ # :call-seq: add?(object) -> self or nil
+ #
+ # Adds a range or number to the set and returns +self+. Returns +nil+
+ # when the object is already included in the set.
+ #
+ # #string will be regenerated. Use #merge to add many elements at once.
+ #
+ # Related: #add, #merge, #union, #include?
+ def add?(object)
+ add object unless include? object
+ end
+
+ # :call-seq: delete(object) -> self
+ #
+ # Deletes the given range or number from the set and returns +self+.
+ #
+ # #string will be regenerated after deletion. Use #subtract to remove
+ # many elements at once.
+ #
+ # Related: #delete?, #delete_at, #subtract, #difference
+ def delete(object)
+ tuple_subtract input_to_tuple object
+ normalize!
+ end
+
+ # :call-seq:
+ # delete?(number) -> integer or nil
+ # delete?(star) -> :* or nil
+ # delete?(range) -> sequence set or nil
+ #
+ # Removes a specified value from the set, and returns the removed value.
+ # Returns +nil+ if nothing was removed.
+ #
+ # Returns an integer when the specified +number+ argument was removed:
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
+ # set.delete?(7) #=> 7
+ # set #=> #
+ # set.delete?("20") #=> 20
+ # set #=> #
+ # set.delete?(30) #=> nil
+ #
+ # Returns :* when * or -1 is specified and
+ # removed:
+ # set = Net::IMAP::SequenceSet.new "5:9,20,35,*"
+ # set.delete?(-1) #=> :*
+ # set #=> #
+ #
+ # And returns a new SequenceSet when a range is specified:
+ #
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
+ # set.delete?(9..) #=> #
+ # set #=> #
+ # set.delete?(21..) #=> nil
+ #
+ # #string will be regenerated after deletion.
+ #
+ # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
+ def delete?(object)
+ tuple = input_to_tuple object
+ if tuple.first == tuple.last
+ return unless include_tuple? tuple
+ tuple_subtract tuple
+ normalize!
+ from_tuple_int tuple.first
+ else
+ copy = dup
+ tuple_subtract tuple
+ normalize!
+ copy if copy.subtract(self).valid?
+ end
+ end
+
+ # :call-seq: delete_at(index) -> number or :* or nil
+ #
+ # Deletes a number the set, indicated by the given +index+. Returns the
+ # number that was removed, or +nil+ if nothing was removed.
+ #
+ # #string will be regenerated after deletion.
+ #
+ # Related: #delete, #delete?, #slice!, #subtract, #difference
+ def delete_at(index)
+ slice! Integer(index.to_int)
+ end
+
+ # :call-seq:
+ # slice!(index) -> integer or :* or nil
+ # slice!(start, length) -> sequence set or nil
+ # slice!(range) -> sequence set or nil
+ #
+ # Deletes a number or consecutive numbers from the set, indicated by the
+ # given +index+, +start+ and +length+, or +range+ of offsets. Returns the
+ # number or sequence set that was removed, or +nil+ if nothing was
+ # removed. Arguments are interpreted the same as for #slice or #[].
+ #
+ # #string will be regenerated after deletion.
+ #
+ # Related: #slice, #delete_at, #delete, #delete?, #subtract, #difference
+ def slice!(index, length = nil)
+ deleted = slice(index, length) and subtract deleted
+ deleted
+ end
+
+ # Merges all of the elements that appear in any of the +inputs+ into the
+ # set, and returns +self+.
+ #
+ # The +inputs+ may be any objects that would be accepted by ::new:
+ # non-zero 32 bit unsigned integers, ranges, sequence-set
+ # formatted strings, other sequence sets, or enumerables containing any of
+ # these.
+ #
+ # #string will be regenerated after all inputs have been merged.
+ #
+ # Related: #add, #add?, #union
+ def merge(*inputs)
+ tuples_add input_to_tuples inputs
+ normalize!
+ end
+
+ # Removes all of the elements that appear in any of the given +objects+
+ # from the set, and returns +self+.
+ #
+ # The +objects+ may be any objects that would be accepted by ::new:
+ # non-zero 32 bit unsigned integers, ranges, sequence-set
+ # formatted strings, other sequence sets, or enumerables containing any of
+ # these.
+ #
+ # Related: #difference
+ def subtract(*objects)
+ tuples_subtract input_to_tuples objects
+ normalize!
+ end
+
+ # Returns an array of ranges and integers.
+ #
+ # The returned elements are sorted and coalesced, even when the input
+ # #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
+ # cases to a maximum value.
+ #
+ # If the original input was unordered or contains overlapping ranges, the
+ # returned ranges will be ordered and coalesced.
+ #
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].elements
+ # #=> [2, 5..9, 11..12, :*]
+ #
+ # Related: #each_element, #ranges, #numbers
+ def elements; each_element.to_a end
+ alias to_a elements
+
+ # Returns an array of ranges
+ #
+ # The returned elements are sorted and coalesced, even when the input
+ # #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
+ # #string is not. * will sort last. See #normalize.
+ #
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
+ # #=> [2..2, 5..9, 11..12, :*..]
+ # Net::IMAP::SequenceSet["123,999:*,456:789"].ranges
+ # #=> [123..123, 456..789, 999..]
+ #
+ # Related: #each_range, #elements, #numbers, #to_set
+ def ranges; each_range.to_a end
+
+ # Returns a sorted array of all of the number values in the sequence set.
+ #
+ # The returned numbers are sorted and de-duplicated, even when the input
+ # #string is not. See #normalize.
+ #
+ # Net::IMAP::SequenceSet["2,5:9,6,12:11"].numbers
+ # #=> [2, 5, 6, 7, 8, 9, 11, 12]
+ #
+ # If the set contains a *, RangeError is raised. See #limit.
+ #
+ # Net::IMAP::SequenceSet["10000:*"].numbers
+ # #!> RangeError
+ #
+ # *WARNING:* Even excluding sets with *, an enormous result can
+ # easily be created. An array with over 4 billion integers could be
+ # returned, requiring up to 32GiB of memory on a 64-bit architecture.
+ #
+ # Net::IMAP::SequenceSet[10000..2**32-1].numbers
+ # # ...probably freezes the process for a while...
+ # #!> NoMemoryError (probably)
+ #
+ # For safety, consider using #limit or #intersection to set an upper
+ # bound. Alternatively, use #each_element, #each_range, or even
+ # #each_number to avoid allocation of a result array.
+ #
+ # 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.
+ # Returns an enumerator when called without a block.
+ #
+ # Related: #elements
+ 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
+ self
+ end
+
+ # Yields each range in #ranges to the block and returns self.
+ # Returns an enumerator when called without a block.
+ #
+ # Related: #ranges
+ def each_range # :yields: range
+ 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..
+ else yield min..max
+ end
+ end
+ self
+ end
+
+ # Yields each number in #numbers to the block and returns self.
+ # If the set contains a *, RangeError will be raised.
+ #
+ # Returns an enumerator when called without a block (even if the set
+ # contains *).
+ #
+ # Related: #numbers
+ def each_number(&block) # :yields: integer
+ return to_enum(__method__) unless block_given?
+ raise RangeError, '%s contains "*"' % [self.class] if include_star?
+ each_element do |elem|
+ case elem
+ when Range then elem.each(&block)
+ when Integer then block.(elem)
+ end
+ end
+ self
+ end
+
+ # Returns a Set with all of the #numbers in the sequence set.
+ #
+ # If the set contains a *, RangeError will be raised.
+ #
+ # See #numbers for the warning about very large sets.
+ #
+ # Related: #elements, #ranges, #numbers
+ def to_set; Set.new(numbers) end
+
+ # Returns the count of #numbers in the set.
+ #
+ # If * and 2**32 - 1 (the maximum 32-bit unsigned
+ # integer value) are both in the set, they will only be counted once.
+ def count
+ @tuples.sum(@tuples.count) { _2 - _1 } +
+ (include_star? && include?(UINT32_MAX) ? -1 : 0)
+ end
+
+ alias size count
+
+ # Returns the index of +number+ in the set, or +nil+ if +number+ isn't in
+ # the set.
+ #
+ # Related: #[]
+ def find_index(number)
+ number = to_tuple_int number
+ each_tuple_with_index do |min, max, idx_min|
+ number < min and return nil
+ number <= max and return from_tuple_int(idx_min + (number - min))
+ end
+ nil
+ end
+
+ private def each_tuple_with_index
+ idx_min = 0
+ @tuples.each do |min, max|
+ yield min, max, idx_min, (idx_max = idx_min + (max - min))
+ idx_min = idx_max + 1
+ end
+ idx_min
+ end
+
+ private def reverse_each_tuple_with_index
+ idx_max = -1
+ @tuples.reverse_each do |min, max|
+ yield min, max, (idx_min = idx_max - (max - min)), idx_max
+ idx_max = idx_min - 1
+ end
+ idx_max
+ end
+
+ # :call-seq: at(index) -> integer or nil
+ #
+ # Returns a number from +self+, without modifying the set. Behaves the
+ # same as #[], except that #at only allows a single integer argument.
+ #
+ # Related: #[], #slice
+ def at(index)
+ index = Integer(index.to_int)
+ if index.negative?
+ reverse_each_tuple_with_index do |min, max, idx_min, idx_max|
+ idx_min <= index and return from_tuple_int(min + (index - idx_min))
+ end
+ else
+ each_tuple_with_index do |min, _, idx_min, idx_max|
+ index <= idx_max and return from_tuple_int(min + (index - idx_min))
+ end
+ end
+ nil
+ end
+
+ # :call-seq:
+ # seqset[index] -> integer or :* or nil
+ # slice(index) -> integer or :* or nil
+ # seqset[start, length] -> sequence set or nil
+ # slice(start, length) -> sequence set or nil
+ # seqset[range] -> sequence set or nil
+ # slice(range) -> sequence set or nil
+ #
+ # Returns a number or a subset from +self+, without modifying the set.
+ #
+ # When an Integer argument +index+ is given, the number at offset +index+
+ # is returned:
+ #
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
+ # set[0] #=> 10
+ # set[5] #=> 15
+ # set[10] #=> 26
+ #
+ # If +index+ is negative, it counts relative to the end of +self+:
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
+ # set[-1] #=> 26
+ # set[-3] #=> 22
+ # set[-6] #=> 15
+ #
+ # If +index+ is out of range, +nil+ is returned.
+ #
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
+ # set[11] #=> nil
+ # set[-12] #=> nil
+ #
+ # The result is based on the normalized set—sorted and de-duplicated—not
+ # on the assigned value of #string.
+ #
+ # set = Net::IMAP::SequenceSet["12,20:23,11:16,21"]
+ # set[0] #=> 11
+ # set[-1] #=> 23
+ #
+ def [](index, length = nil)
+ if length then slice_length(index, length)
+ elsif index.is_a?(Range) then slice_range(index)
+ else at(index)
+ end
+ end
+
+ alias slice :[]
+
+ private def slice_length(start, length)
+ start = Integer(start.to_int)
+ length = Integer(length.to_int)
+ raise ArgumentError, "length must be positive" unless length.positive?
+ last = start + length - 1 unless start.negative? && start.abs <= length
+ slice_range(start..last)
+ end
+
+ private def slice_range(range)
+ first = range.begin || 0
+ last = range.end || -1
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
+ if (first * last).positive? && last < first
+ SequenceSet.empty
+ elsif (min = at(first))
+ max = at(last)
+ if max == :* then self & (min..)
+ elsif min <= max then self & (min..max)
+ else SequenceSet.empty
+ end
+ end
+ end
+
+ # Returns a frozen SequenceSet with * converted to +max+, numbers
+ # and ranges over +max+ removed, and ranges containing +max+ converted to
+ # end at +max+.
+ #
+ # Net::IMAP::SequenceSet["5,10:22,50"].limit(max: 20).to_s
+ # #=> "5,10:20"
+ #
+ # * is always interpreted as the maximum value. When the set
+ # contains *, it will be set equal to the limit.
+ #
+ # Net::IMAP::SequenceSet["*"].limit(max: 37)
+ # #=> Net::IMAP::SequenceSet["37"]
+ # Net::IMAP::SequenceSet["5:*"].limit(max: 37)
+ # #=> Net::IMAP::SequenceSet["5:37"]
+ # Net::IMAP::SequenceSet["500:*"].limit(max: 37)
+ # #=> Net::IMAP::SequenceSet["37"]
+ #
+ def limit(max:)
+ max = to_tuple_int(max)
+ if empty? then self.class.empty
+ elsif !include_star? && max < min then self.class.empty
+ elsif max(star: STAR_INT) <= max then frozen? ? self : dup.freeze
+ else dup.limit!(max: max).freeze
+ end
+ end
+
+ # Removes all members over +max+ and returns self. If * is a
+ # member, it will be converted to +max+.
+ #
+ # Related: #limit
+ def limit!(max:)
+ star = include_star?
+ max = to_tuple_int(max)
+ tuple_subtract [max + 1, STAR_INT]
+ tuple_add [max, max ] if star
+ normalize!
+ end
+
+ # :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(self.class.full) 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_INT < flat.last then flat.pop else flat.push STAR_INT end
+ @tuples = flat.each_slice(2).to_a
+ normalize!
+ end
+
+ # Returns a new SequenceSet with a normalized string representation.
+ #
+ # 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"].normalize
+ # #=> Net::IMAP::SequenceSet["1:7,9:11"]
+ #
+ # Related: #normalize!, #normalized_string
+ def normalize
+ str = normalized_string
+ return self if frozen? && str == string
+ remain_frozen dup.instance_exec { @string = str&.-@; self }
+ end
+
+ # Resets #string to be sorted, deduplicated, and coalesced. Returns
+ # +self+.
+ #
+ # Related: #normalize, #normalized_string
+ def normalize!
+ @string = nil
+ self
+ end
+
+ # Returns a normalized +sequence-set+ string representation, sorted
+ # and deduplicated. Adjacent or overlapping elements will be merged into
+ # a single larger range. Returns +nil+ when the set is empty.
+ #
+ # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalized_string
+ # #=> "1:7,9:11"
+ #
+ # Related: #normalize!, #normalize
+ def normalized_string
+ @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
+ end
def inspect
- (frozen? ? "%s[%p]" : "#<%s %p>") % [self.class, to_s]
+ if empty?
+ (frozen? ? "%s.empty" : "#<%s empty>") % [self.class]
+ elsif frozen?
+ "%s[%p]" % [self.class, to_s]
+ else
+ "#<%s %p>" % [self.class, to_s]
+ end
end
- # Unstable API, for internal use only (Net::IMAP#validate_data)
+ # Returns self
+ alias to_sequence_set itself
+
+ # Unstable API: currently for internal use only (Net::IMAP#validate_data)
def validate # :nodoc:
- ResponseParser::Patterns::SEQUENCE_SET_STR.match?(@atom) or
- raise ArgumentError, "invalid sequence-set: %p" % [input]
- true
+ empty? and raise DataFormatError, "empty sequence-set is invalid"
+ self
end
- # Unstable API, for internal use only (Net::IMAP#send_data)
+ # 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, valid_string)
+ end
+
+ protected
+
+ attr_reader :tuples # :nodoc:
+
+ private
+
+ def remain_frozen(set) frozen? ? set.freeze : set end
+
+ # frozen clones are shallow copied
+ def initialize_clone(other)
+ other.frozen? ? super : initialize_dup(other)
end
+ def initialize_dup(other)
+ @tuples = other.tuples.map(&:dup)
+ @string = other.string&.-@
+ super
+ end
+
+ def input_to_tuple(obj)
+ obj = input_try_convert obj
+ case obj
+ when *STARS, Integer then [int = to_tuple_int(obj), int]
+ when Range then range_to_tuple(obj)
+ when String then str_to_tuple(obj)
+ else
+ raise DataFormatError, "expected number or range, got %p" % [obj]
+ end
+ end
+
+ def input_to_tuples(obj)
+ obj = input_try_convert obj
+ case obj
+ when *STARS, Integer, Range then [input_to_tuple(obj)]
+ when String then str_to_tuples obj
+ when SequenceSet then obj.tuples
+ when ENUMABLE then obj.flat_map { input_to_tuples _1 }
+ when nil then []
+ else
+ raise DataFormatError,
+ "expected nz-number, range, string, or enumerable; " \
+ "got %p" % [obj]
+ end
+ end
+
+ # unlike SequenceSet#try_convert, this returns an Integer, Range,
+ # String, Set, Array, or... any type of object.
+ def input_try_convert(input)
+ SequenceSet.try_convert(input) ||
+ # Integer.try_convert(input) || # ruby 3.1+
+ input.respond_to?(:to_int) && Integer(input.to_int) ||
+ String.try_convert(input) ||
+ input
+ end
+
+ def range_to_tuple(range)
+ first = to_tuple_int(range.begin || 1)
+ last = to_tuple_int(range.end || :*)
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
+ unless first <= last
+ raise DataFormatError, "invalid range for sequence-set: %p" % [range]
+ end
+ [first, last]
+ end
+
+ def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
+ def from_tuple_int(num) num == STAR_INT ? :* : num end
+
+ def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
+ def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
+ def str_to_tuple(str)
+ raise DataFormatError, "invalid sequence set string" if str.empty?
+ str.split(":", 2).map! { to_tuple_int _1 }.minmax
+ end
+
+ def include_tuple?((min, max)) range_gte_to(min)&.cover?(min..max) end
+
+ def intersect_tuple?((min, max))
+ range = range_gte_to(min) and
+ range.include?(min) || range.include?(max) || (min..max).cover?(range)
+ 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; self end
+
+ #
+ # --|=====| |=====new tuple=====| append
+ # ?????????-|=====new tuple=====|-|===lower===|-- insert
+ #
+ # |=====new tuple=====|
+ # ---------??=======lower=======??--------------- noop
+ #
+ # ---------??===lower==|--|==| join remaining
+ # ---------??===lower==|--|==|----|===upper===|-- join until upper
+ # ---------??===lower==|--|==|--|=====upper===|-- join to upper
+ def tuple_add(tuple)
+ min, max = tuple
+ lower, lower_idx = tuple_gte_with_index(min - 1)
+ if lower.nil? then tuples << tuple
+ elsif (max + 1) < lower.first then tuples.insert(lower_idx, tuple)
+ else tuple_coalesce(lower, lower_idx, min, max)
+ end
+ end
+
+ def tuple_coalesce(lower, lower_idx, min, max)
+ return if lower.first <= min && max <= lower.last
+ lower[0] = [min, lower.first].min
+ lower[1] = [max, lower.last].max
+ lower_idx += 1
+ return if lower_idx == tuples.count
+ tmax_adj = lower.last + 1
+ upper, upper_idx = tuple_gte_with_index(tmax_adj)
+ if upper
+ tmax_adj < upper.first ? (upper_idx -= 1) : (lower[1] = upper.last)
+ end
+ tuples.slice!(lower_idx..upper_idx)
+ end
+
+ # |====tuple================|
+ # --|====| no more 1. noop
+ # --|====|---------------------------|====lower====|-- 2. noop
+ # -------|======lower================|---------------- 3. split
+ # --------|=====lower================|---------------- 4. trim beginning
+ #
+ # -------|======lower====????????????----------------- trim lower
+ # --------|=====lower====????????????----------------- delete lower
+ #
+ # -------??=====lower===============|----------------- 5. trim/delete one
+ # -------??=====lower====|--|====| no more 6. delete rest
+ # -------??=====lower====|--|====|---|====upper====|-- 7. delete until
+ # -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
+ def tuple_subtract(tuple)
+ min, max = tuple
+ lower, idx = tuple_gte_with_index(min)
+ if lower.nil? then nil # case 1.
+ elsif max < lower.first then nil # case 2.
+ elsif max < lower.last then tuple_trim_or_split lower, idx, min, max
+ else tuples_trim_or_delete lower, idx, min, max
+ end
+ end
+
+ def tuple_trim_or_split(lower, idx, tmin, tmax)
+ if lower.first < tmin # split
+ tuples.insert(idx, [lower.first, tmin - 1])
+ end
+ lower[0] = tmax + 1
+ end
+
+ def tuples_trim_or_delete(lower, lower_idx, tmin, tmax)
+ if lower.first < tmin # trim lower
+ lower[1] = tmin - 1
+ lower_idx += 1
+ end
+ if tmax == lower.last # case 5
+ upper_idx = lower_idx
+ elsif (upper, upper_idx = tuple_gte_with_index(tmax + 1))
+ upper_idx -= 1 # cases 7 and 8
+ upper[0] = tmax + 1 if upper.first <= tmax # case 8 (else case 7)
+ end
+ tuples.slice!(lower_idx..upper_idx)
+ end
+
+ def tuple_gte_with_index(num)
+ idx = tuples.bsearch_index { _2 >= num } and [tuples[idx], idx]
+ end
+
+ def range_gte_to(num)
+ first, last = tuples.bsearch { _2 >= num }
+ first..last if first
+ end
+
+ def nz_number(num)
+ case num
+ when Integer, /\A[1-9]\d*\z/ then num = Integer(num)
+ else raise DataFormatError, "%p is not a valid nz-number" % [num]
+ end
+ NumValidator.ensure_nz_number(num)
+ num
+ end
+
+ # intentionally defined after the class implementation
+
+ EMPTY = new.freeze
+ FULL = self["1:*"]
+ private_constant :EMPTY, :FULL
+
end
end
end
diff --git a/test/net/imap/fixtures/response_parser/status_responses.yml b/test/net/imap/fixtures/response_parser/status_responses.yml
index a11e83ee..074b56ad 100644
--- a/test/net/imap/fixtures/response_parser/status_responses.yml
+++ b/test/net/imap/fixtures/response_parser/status_responses.yml
@@ -51,7 +51,12 @@
NUM: 1
SEQ: !ruby/struct:Net::IMAP::ExtensionData
data: !ruby/object:Net::IMAP::SequenceSet
- atom: 1234:5,*:789654
+ string: 1234:5,*:789654
+ tuples:
+ - - 5
+ - 1234
+ - - 789654
+ - 4294967296
COMP-EMPTY: !ruby/struct:Net::IMAP::ExtensionData
data: []
COMP-QUOTED: !ruby/struct:Net::IMAP::ExtensionData
diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb
index 6e24aa11..a313939c 100644
--- a/test/net/imap/test_imap.rb
+++ b/test/net/imap/test_imap.rb
@@ -591,32 +591,40 @@ def test_send_invalid_number
sock = server.accept
begin
sock.print("* OK test server\r\n")
- sock.gets
+ sock.gets # Integer: 0
sock.print("RUBY0001 OK TEST completed\r\n")
- sock.gets
+ sock.gets # Integer: 2**32 - 1
sock.print("RUBY0002 OK TEST completed\r\n")
- sock.gets
+ sock.gets # MessageSet: 1
sock.print("RUBY0003 OK TEST completed\r\n")
- sock.gets
+ sock.gets # MessageSet: 2**32 - 1
sock.print("RUBY0004 OK TEST completed\r\n")
- sock.gets
+ sock.gets # SequenceSet: -1 => "*"
+ sock.print("RUBY0005 OK TEST completed\r\n")
+ sock.gets # SequenceSet: 1
+ sock.print("RUBY0006 OK TEST completed\r\n")
+ sock.gets # SequenceSet: 2**32 - 1
+ sock.print("RUBY0007 OK TEST completed\r\n")
+ sock.gets # LOGOUT
sock.print("* BYE terminating connection\r\n")
- sock.print("RUBY0005 OK LOGOUT completed\r\n")
+ sock.print("RUBY0008 OK LOGOUT completed\r\n")
ensure
sock.close
server.close
end
end
begin
+ # regular numbers may be any uint32
imap = Net::IMAP.new(server_addr, :port => port)
assert_raise(Net::IMAP::DataFormatError) do
imap.__send__(:send_command, "TEST", -1)
end
imap.__send__(:send_command, "TEST", 0)
- imap.__send__(:send_command, "TEST", 4294967295)
+ imap.__send__(:send_command, "TEST", 2**32 - 1)
assert_raise(Net::IMAP::DataFormatError) do
- imap.__send__(:send_command, "TEST", 4294967296)
+ imap.__send__(:send_command, "TEST", 2**32)
end
+ # MessageSet numbers may be non-zero uint32
assert_raise(Net::IMAP::DataFormatError) do
imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(-1))
end
@@ -624,9 +632,19 @@ def test_send_invalid_number
imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(0))
end
imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(1))
- imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967295))
+ imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(2**32 - 1))
+ assert_raise(Net::IMAP::DataFormatError) do
+ imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(2**32))
+ end
+ # SequenceSet numbers may be non-zero uint3, and -1 is translated to *
+ imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(-1))
+ assert_raise(Net::IMAP::DataFormatError) do
+ imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(0))
+ end
+ imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(1))
+ imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(2**32-1))
assert_raise(Net::IMAP::DataFormatError) do
- imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967296))
+ imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(2**32))
end
imap.logout
ensure
diff --git a/test/net/imap/test_imap_response_data.rb b/test/net/imap/test_imap_response_data.rb
index d7ab5b81..dc546432 100644
--- a/test/net/imap/test_imap_response_data.rb
+++ b/test/net/imap/test_imap_response_data.rb
@@ -35,4 +35,23 @@ def test_uidplus_copyuid__uid_mapping
)
end
+ def test_thread_member_to_sequence_set
+ # copied from the fourth example in RFC5256: (3 6 (4 23)(44 7 96))
+ thmember = Net::IMAP::ThreadMember.method :new
+ thread = thmember.(3, [
+ thmember.(6, [
+ thmember.(4, [
+ thmember.(23, [])
+ ]),
+ thmember.(44, [
+ thmember.(7, [
+ thmember.(96, [])
+ ])
+ ])
+ ])
+ ])
+ expected = Net::IMAP::SequenceSet.new("3:4,6:7,23,44,96")
+ assert_equal(expected, thread.to_sequence_set)
+ end
+
end
diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb
new file mode 100644
index 00000000..4f607fe2
--- /dev/null
+++ b/test/net/imap/test_sequence_set.rb
@@ -0,0 +1,772 @@
+# frozen_string_literal: true
+
+require "net/imap"
+require "test/unit"
+require "set"
+
+class IMAPSequenceSetTest < Test::Unit::TestCase
+ # alias for convenience
+ SequenceSet = Net::IMAP::SequenceSet
+ DataFormatError = Net::IMAP::DataFormatError
+
+ def compare_to_reference_set(nums, set, seqset)
+ set.merge nums
+ seqset.merge nums
+ assert_equal set, seqset.to_set
+ assert seqset.elements.size <= set.size
+ sorted = set.to_a.sort
+ assert_equal sorted, seqset.numbers
+ Array.new(50) { rand(sorted.count) }.each do |idx|
+ assert_equal sorted.at(idx), seqset.at(idx)
+ assert_equal sorted.at(-idx), seqset.at(-idx)
+ end
+ assert seqset.cover? sorted.sample 100
+ end
+
+ test "compared to reference Set, add many random values" do
+ set = Set.new
+ seqset = SequenceSet.new
+ 10.times do
+ nums = Array.new(1000) { rand(1..10_000) }
+ compare_to_reference_set(nums, set, seqset)
+ end
+ end
+
+ test "compared to reference Set, add many large ranges" do
+ set = Set.new
+ seqset = SequenceSet.new
+ (1..10_000).each_slice(250) do
+ compare_to_reference_set _1, set, seqset
+ assert_equal 1, seqset.elements.size
+ end
+ end
+
+ test "#== equality by value (not by identity or representation)" do
+ assert_equal SequenceSet.new, SequenceSet.new
+ assert_equal SequenceSet.new("1"), SequenceSet[1]
+ assert_equal SequenceSet.new("*"), SequenceSet[:*]
+ assert_equal SequenceSet["2:4"], SequenceSet["4:2"]
+ end
+
+ test "#freeze" do
+ set = SequenceSet.new "2:4,7:11,99,999"
+ assert !set.frozen?
+ set.freeze
+ assert set.frozen?
+ assert Ractor.shareable?(set) if defined?(Ractor)
+ assert_equal set, set.freeze
+ end
+
+ %i[clone dup].each do |method|
+ test "##{method}" do
+ orig = SequenceSet.new "2:4,7:11,99,999"
+ copy = orig.send method
+ assert_equal orig, copy
+ orig << 123
+ copy << 456
+ assert_not_equal orig, copy
+ assert orig.include?(123)
+ assert copy.include?(456)
+ assert !copy.include?(123)
+ assert !orig.include?(456)
+ end
+ end
+
+ if defined?(Ractor)
+ test "#freeze makes ractor sharable (deeply frozen)" do
+ assert Ractor.shareable? SequenceSet.new("1:9,99,999").freeze
+ end
+
+ test ".[] returns ractor sharable (deeply frozen)" do
+ assert Ractor.shareable? SequenceSet["2:8,88,888"]
+ end
+
+ test "#clone preserves ractor sharability (deeply frozen)" do
+ assert Ractor.shareable? SequenceSet["3:7,77,777"].clone
+ end
+ end
+
+ test ".new, input must be valid" do
+ assert_raise DataFormatError do SequenceSet.new [0] end
+ assert_raise DataFormatError do SequenceSet.new "0" end
+ assert_raise DataFormatError do SequenceSet.new [2**32] end
+ assert_raise DataFormatError do SequenceSet.new [2**33] end
+ assert_raise DataFormatError do SequenceSet.new (2**32).to_s end
+ assert_raise DataFormatError do SequenceSet.new (2**33).to_s end
+ assert_raise DataFormatError do SequenceSet.new "0:2" end
+ assert_raise DataFormatError do SequenceSet.new ":2" end
+ assert_raise DataFormatError do SequenceSet.new " 2" end
+ assert_raise DataFormatError do SequenceSet.new "2 " end
+ assert_raise DataFormatError do SequenceSet.new "2," end
+ assert_raise DataFormatError do SequenceSet.new Time.now end
+ end
+
+ test ".new, input may be empty" do
+ assert_empty SequenceSet.new
+ assert_empty SequenceSet.new []
+ assert_empty SequenceSet.new [[]]
+ assert_empty SequenceSet.new nil
+ assert_empty SequenceSet.new ""
+ end
+
+ test ".[] must not be empty" do
+ assert_raise ArgumentError do SequenceSet[] end
+ assert_raise DataFormatError do SequenceSet[[]] end
+ assert_raise DataFormatError do SequenceSet[[[]]] end
+ assert_raise DataFormatError do SequenceSet[nil] end
+ assert_raise DataFormatError do SequenceSet[""] end
+ end
+
+ test "#[non-negative index]" do
+ assert_nil SequenceSet.empty[0]
+ assert_equal 1, SequenceSet[1..][0]
+ assert_equal 1, SequenceSet.full[0]
+ assert_equal 111, SequenceSet.full[110]
+ assert_equal 4, SequenceSet[2,4,6,8][1]
+ assert_equal 8, SequenceSet[2,4,6,8][3]
+ assert_equal 6, SequenceSet[4..6][2]
+ assert_nil SequenceSet[4..6][3]
+ assert_equal 205, SequenceSet["101:110,201:210,301:310"][14]
+ assert_equal 310, SequenceSet["101:110,201:210,301:310"][29]
+ assert_nil SequenceSet["101:110,201:210,301:310"][44]
+ assert_equal :*, SequenceSet["1:10,*"][10]
+ end
+
+ test "#[negative index]" do
+ assert_nil SequenceSet.empty[0]
+ assert_equal :*, SequenceSet[1..][-1]
+ assert_equal 1, SequenceSet.full[-(2**32)]
+ assert_equal 111, SequenceSet[1..111][-1]
+ assert_equal 4, SequenceSet[2,4,6,8][1]
+ assert_equal 8, SequenceSet[2,4,6,8][3]
+ assert_equal 6, SequenceSet[4..6][2]
+ assert_nil SequenceSet[4..6][3]
+ assert_equal 205, SequenceSet["101:110,201:210,301:310"][14]
+ assert_equal 310, SequenceSet["101:110,201:210,301:310"][29]
+ assert_nil SequenceSet["101:110,201:210,301:310"][44]
+ end
+
+ test "#[start, length]" do
+ assert_equal SequenceSet[10..99], SequenceSet.full[9, 90]
+ assert_equal 90, SequenceSet.full[9, 90].count
+ assert_equal SequenceSet[1000..1099],
+ SequenceSet[1..100, 1000..1111][100, 100]
+ assert_equal SequenceSet[11, 21, 31, 41],
+ SequenceSet[((1..10_000) % 10).to_a][1, 4]
+ assert_equal SequenceSet[9981, 9971, 9961, 9951],
+ SequenceSet[((1..10_000) % 10).to_a][-5, 4]
+ assert_nil SequenceSet[111..222, 888..999][2000, 4]
+ assert_nil SequenceSet[111..222, 888..999][-2000, 4]
+ end
+
+ test "#[range]" do
+ assert_equal SequenceSet[10..100], SequenceSet.full[9..99]
+ assert_equal SequenceSet[1000..1100],
+ SequenceSet[1..100, 1000..1111][100..200]
+ assert_equal SequenceSet[1000..1099],
+ SequenceSet[1..100, 1000..1111][100...200]
+ assert_equal SequenceSet[11, 21, 31, 41],
+ SequenceSet[((1..10_000) % 10).to_a][1..4]
+ assert_equal SequenceSet[9981, 9971, 9961, 9951],
+ SequenceSet[((1..10_000) % 10).to_a][-5..-2]
+ assert_equal SequenceSet[((51..9951) % 10).to_a],
+ SequenceSet[((1..10_000) % 10).to_a][5..-5]
+ assert_equal SequenceSet.full, SequenceSet.full[0..]
+ assert_equal SequenceSet[2..], SequenceSet.full[1..]
+ assert_equal SequenceSet[:*], SequenceSet.full[-1..]
+ assert_equal SequenceSet.empty, SequenceSet[1..100][60..50]
+ assert_equal SequenceSet.empty, SequenceSet[1..100][-50..-60]
+ assert_equal SequenceSet.empty, SequenceSet[1..100][-10..10]
+ assert_equal SequenceSet.empty, SequenceSet[1..100][60..-60]
+ assert_nil SequenceSet.empty[2..4]
+ assert_nil SequenceSet[101..200][1000..1060]
+ assert_nil SequenceSet[101..200][-1000..-60]
+ end
+
+ test "#find_index" do
+ assert_equal 9, SequenceSet.full.find_index(10)
+ assert_equal 99, SequenceSet.full.find_index(100)
+ set = SequenceSet[1..100, 1000..1111]
+ assert_equal 100, set.find_index(1000)
+ assert_equal 200, set.find_index(1100)
+ set = SequenceSet[((1..10_000) % 10).to_a]
+ assert_equal 0, set.find_index(1)
+ assert_equal 1, set.find_index(11)
+ assert_equal 5, set.find_index(51)
+ assert_nil SequenceSet.empty.find_index(1)
+ assert_nil SequenceSet[5..9].find_index(4)
+ assert_nil SequenceSet[5..9,12..24].find_index(10)
+ assert_nil SequenceSet[5..9,12..24].find_index(11)
+ assert_equal 1, SequenceSet[1, :*].find_index(-1)
+ assert_equal 2**32 - 1, SequenceSet.full.find_index(:*)
+ 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
+ end
+
+ test "#limit with *" do
+ assert_equal SequenceSet.new("2,4,5,6,7,9,12,13,14,15"),
+ SequenceSet.new("2,4:7,9,12:*").limit(max: 15)
+ assert_equal(SequenceSet["37"],
+ SequenceSet["50,60,99:*"].limit(max: 37))
+ assert_equal(SequenceSet["1:100,300"],
+ SequenceSet["1:100,500:*"].limit(max: 300))
+ assert_equal [15], SequenceSet["3967:*"].limit(max: 15).numbers
+ assert_equal [15], SequenceSet["*:12293456"].limit(max: 15).numbers
+ end
+
+ test "#limit with empty result" do
+ assert_equal SequenceSet.empty, SequenceSet["1234567890"].limit(max: 37)
+ assert_equal SequenceSet.empty, SequenceSet["99:195,458"].limit(max: 37)
+ end
+
+ test "values for '*'" do
+ assert_equal "*", SequenceSet[?*].to_s
+ assert_equal "*", SequenceSet[:*].to_s
+ assert_equal "*", SequenceSet[-1].to_s
+ assert_equal "*", SequenceSet[[?*]].to_s
+ assert_equal "*", SequenceSet[[:*]].to_s
+ assert_equal "*", SequenceSet[[-1]].to_s
+ assert_equal "1:*", SequenceSet[1..].to_s
+ assert_equal "1:*", SequenceSet[1..-1].to_s
+ end
+
+ test "#empty?" do
+ refute SequenceSet.new("1:*").empty?
+ refute SequenceSet.new(:*).empty?
+ assert SequenceSet.new(nil).empty?
+ assert SequenceSet.new.empty?
+ assert SequenceSet.empty.empty?
+ set = SequenceSet.new "1:1111"
+ refute set.empty?
+ set.string = nil
+ assert set.empty?
+ end
+
+ test "#full?" do
+ assert SequenceSet.new("1:*").full?
+ refute SequenceSet.new(1..2**32-1).full?
+ refute SequenceSet.new(nil).full?
+ end
+
+ test "#to_sequence_set" do
+ assert_equal (set = SequenceSet["*"]), set.to_sequence_set
+ assert_equal (set = SequenceSet["15:36,5,99,*,2"]), set.to_sequence_set
+ end
+
+ test "set + other" do
+ seqset = -> { SequenceSet.new _1 }
+ assert_equal seqset["1,5"], seqset["1"] + seqset["5"]
+ assert_equal seqset["1,*"], seqset["*"] + seqset["1"]
+ assert_equal seqset["1:*"], seqset["1:4"] + seqset["5:*"]
+ assert_equal seqset["1:*"], seqset["5:*"] + seqset["1:4"]
+ assert_equal seqset["1:5"], seqset["1,3,5"] + seqset["2,4"]
+ assert_equal seqset["1:3,5,7:9"], seqset["1,3,5,7:8"] + seqset["2,8:9"]
+ assert_equal seqset["1:*"], seqset["1,3,5,7:*"] + seqset["2,4:6"]
+ end
+
+ test "#add" do
+ assert_equal SequenceSet["1,5"], SequenceSet.new("1").add("5")
+ assert_equal SequenceSet["1,*"], SequenceSet.new("*").add(1)
+ assert_equal SequenceSet["1:9"], SequenceSet.new("1:6").add("4:9")
+ assert_equal SequenceSet["1:*"], SequenceSet.new("1:4").add(5..)
+ assert_equal SequenceSet["1:*"], SequenceSet.new("5:*").add(1..4)
+ end
+
+ test "#<<" do
+ assert_equal SequenceSet["1,5"], SequenceSet.new("1") << "5"
+ assert_equal SequenceSet["1,*"], SequenceSet.new("*") << 1
+ assert_equal SequenceSet["1:9"], SequenceSet.new("1:6") << "4:9"
+ assert_equal SequenceSet["1:*"], SequenceSet.new("1:4") << (5..)
+ assert_equal SequenceSet["1:*"], SequenceSet.new("5:*") << (1..4)
+ end
+
+ test "#merge" do
+ seqset = -> { SequenceSet.new _1 }
+ assert_equal seqset["1,5"], seqset["1"].merge("5")
+ assert_equal seqset["1,*"], seqset["*"].merge(1)
+ assert_equal seqset["1:*"], seqset["1:4"].merge(5..)
+ assert_equal seqset["1:3,5,7:9"], seqset["1,3,5,7:8"].merge(seqset["2,8:9"])
+ assert_equal seqset["1:*"], seqset["5:*"].merge(1..4)
+ assert_equal seqset["1:5"], seqset["1,3,5"].merge(seqset["2,4"])
+ end
+
+ test "set - other" do
+ seqset = -> { SequenceSet.new _1 }
+ assert_equal seqset["1,5"], seqset["1,5"] - 9
+ assert_equal seqset["1,5"], seqset["1,5"] - "3"
+ assert_equal seqset["1,5"], seqset["1,3,5"] - [3]
+ assert_equal seqset["1,9"], seqset["1,3:9"] - "2:8"
+ assert_equal seqset["1,9"], seqset["1:7,9"] - (2..8)
+ assert_equal seqset["1,9"], seqset["1:9"] - (2..8).to_a
+ assert_equal seqset["1,5"], seqset["1,5:9,11:99"] - "6:999"
+ assert_equal seqset["1,5,99"], seqset["1,5:9,11:88,99"] - ["6:98"]
+ assert_equal seqset["1,5,99"], seqset["1,5:6,8:9,11:99"] - "6:98"
+ assert_equal seqset["1,5,11:99"], seqset["1,5:6,8:9,11:99"] - "6:9"
+ assert_equal seqset["1:10"], seqset["1:*"] - (11..)
+ assert_equal seqset[nil], seqset["1,5"] - [1..8, 10..]
+ end
+
+ test "#intersection" do
+ seqset = -> { SequenceSet.new _1 }
+ assert_equal seqset[nil], seqset["1,5"] & "9"
+ assert_equal seqset["1,5"], seqset["1:5"].intersection([1, 5..9])
+ assert_equal seqset["1,5"], seqset["1:5"] & [1, 5, 9, 55]
+ assert_equal seqset["*"], seqset["9999:*"] & "1,5,9,*"
+ end
+
+ test "#intersect?" do
+ set = SequenceSet["1:5,11:20"]
+ refute set.intersect? "9"
+ refute set.intersect? 9
+ refute set.intersect? 6..10
+ refute set.intersect? ~set
+ assert set.intersect? 6..11
+ assert set.intersect? "1,5,11,20"
+ assert set.intersect? set
+ end
+
+ test "#disjoint?" do
+ set = SequenceSet["1:5,11:20"]
+ assert set.disjoint? "9"
+ assert set.disjoint? 6..10
+ assert set.disjoint? ~set
+ refute set.disjoint? 6..11
+ refute set.disjoint? "1,5,11,20"
+ refute set.disjoint? set
+ end
+
+ test "#delete" do
+ seqset = -> { SequenceSet.new _1 }
+ assert_equal seqset["1,5"], seqset["1,5"].delete("9")
+ assert_equal seqset["1,5"], seqset["1,5"].delete("3")
+ assert_equal seqset["1,5"], seqset["1,3,5"].delete("3")
+ assert_equal seqset["1,9"], seqset["1,3:9"].delete("2:8")
+ assert_equal seqset["1,9"], seqset["1:7,9"].delete("2:8")
+ assert_equal seqset["1,9"], seqset["1:9"].delete("2:8")
+ assert_equal seqset["1,5"], seqset["1,5:9,11:99"].delete("6:999")
+ assert_equal seqset["1,5,99"], seqset["1,5:9,11:88,99"].delete("6:98")
+ assert_equal seqset["1,5,99"], seqset["1,5:6,8:9,11:99"].delete("6:98")
+ assert_equal seqset["1,5,11:99"], seqset["1,5:6,8:9,11:99"].delete("6:9")
+ end
+
+ test "#subtract" do
+ seqset = -> { SequenceSet.new _1 }
+ assert_equal seqset["1,5"], seqset["1,5"].subtract("9")
+ assert_equal seqset["1,5"], seqset["1,5"].subtract("3")
+ assert_equal seqset["1,5"], seqset["1,3,5"].subtract("3")
+ assert_equal seqset["1,9"], seqset["1,3:9"].subtract("2:8")
+ assert_equal seqset["1,9"], seqset["1:7,9"].subtract("2:8")
+ assert_equal seqset["1,9"], seqset["1:9"].subtract("2:8")
+ assert_equal seqset["1,5"], seqset["1,5:9,11:99"].subtract("6:999")
+ assert_equal seqset["1,5,99"], seqset["1,5:9,11:88,99"].subtract("6:98")
+ assert_equal seqset["1,5,99"], seqset["1,5:6,8:9,11:99"].subtract("6:98")
+ assert_equal seqset["1,5,11:99"], seqset["1,5:6,8:9,11:99"].subtract("6:9")
+ end
+
+ test "#min" do
+ assert_equal 3, SequenceSet.new("34:3").min
+ assert_equal 345, SequenceSet.new("345,678").min
+ assert_nil SequenceSet.new.min
+ end
+
+ test "#max" do
+ assert_equal 34, SequenceSet["34:3"].max
+ assert_equal 678, SequenceSet["345,678"].max
+ assert_equal 678, SequenceSet["345:678"].max(star: "unused")
+ assert_equal :*, SequenceSet["345:*"].max
+ assert_equal nil, SequenceSet["345:*"].max(star: nil)
+ assert_equal "*", SequenceSet["345:*"].max(star: "*")
+ assert_nil SequenceSet.new.max(star: "ignored")
+ end
+
+ test "#minmax" do
+ assert_equal [ 3, 3], SequenceSet["3"].minmax
+ assert_equal [ :*, :*], SequenceSet["*"].minmax
+ assert_equal [ 99, 99], SequenceSet["*"].minmax(star: 99)
+ assert_equal [ 3, 34], SequenceSet["34:3"].minmax
+ assert_equal [345, 678], SequenceSet["345,678"].minmax
+ assert_equal [345, 678], SequenceSet["345:678"].minmax(star: "unused")
+ assert_equal [345, :*], SequenceSet["345:*"].minmax
+ assert_equal [345, nil], SequenceSet["345:*"].minmax(star: nil)
+ assert_equal [345, "*"], SequenceSet["345:*"].minmax(star: "*")
+ assert_nil SequenceSet.new.minmax(star: "ignored")
+ end
+
+ test "#add?" do
+ assert_equal(SequenceSet.new("1:3,5,7:8"),
+ SequenceSet.new("1,3,5,7:8").add?("2"))
+ assert_equal(SequenceSet.new("1,3,5,7:9"),
+ SequenceSet.new("1,3,5,7:8").add?("8:9"))
+ assert_nil SequenceSet.new("1,3,5,7:*").add?("3")
+ assert_nil SequenceSet.new("1,3,5,7:*").add?("9:91")
+ end
+
+ test "#delete?" do
+ set = SequenceSet.new [5..10, 20]
+ assert_nil set.delete?(11)
+ assert_equal SequenceSet[5..10, 20], set
+ assert_equal 6, set.delete?(6)
+ assert_equal SequenceSet[5, 7..10, 20], set
+ assert_equal SequenceSet[9..10, 20], set.delete?(9..)
+ assert_equal SequenceSet[5, 7..8], set
+ assert_nil set.delete?(11..)
+ end
+
+ test "#slice!" do
+ set = SequenceSet.new 1..20
+ assert_equal SequenceSet[1..4], set.slice!(0, 4)
+ assert_equal SequenceSet[5..20], set
+ assert_equal 14, set.slice!(-7)
+ assert_equal SequenceSet[5..13, 15..20], set
+ assert_equal 11, set.slice!(6)
+ assert_equal SequenceSet[5..10, 12..13, 15..20], set
+ assert_equal SequenceSet[12..13, 15..19], set.slice!(6..12)
+ assert_equal SequenceSet[5..10, 20], set
+ assert_nil set.slice!(10)
+ assert_equal SequenceSet[5..10, 20], set
+ assert_equal 6, set.slice!(1)
+ assert_equal SequenceSet[5, 7..10, 20], set
+ assert_equal SequenceSet[9..10, 20], set.slice!(3..)
+ assert_equal SequenceSet[5, 7..8], set
+ assert_nil set.slice!(3)
+ assert_nil set.slice!(3..)
+ end
+
+ test "#delete_at" do
+ set = SequenceSet.new [5..10, 20]
+ assert_nil set.delete_at(20)
+ assert_equal SequenceSet[5..10, 20], set
+ assert_equal 6, set.delete_at(1)
+ assert_equal 9, set.delete_at(3)
+ assert_equal 10, set.delete_at(3)
+ assert_equal 20, set.delete_at(3)
+ assert_equal nil, set.delete_at(3)
+ assert_equal SequenceSet[5, 7..8], set
+ end
+
+ test "#include_star?" do
+ assert SequenceSet["2,*:12"].include_star?
+ assert SequenceSet[-1].include_star?
+ refute SequenceSet["12"].include_star?
+ end
+
+ test "#include?" do
+ assert SequenceSet["2:4"].include?(3)
+ assert SequenceSet["2,*:12"].include? :*
+ assert SequenceSet["2,*:12"].include?(-1)
+ set = SequenceSet.new Array.new(100) { rand(1..1500) }
+ rev = (~set).limit(max: 1_501)
+ set.numbers.each do assert set.include?(_1) end
+ rev.numbers.each do refute set.include?(_1) end
+ end
+
+ test "#cover?" do
+ assert SequenceSet["2:4"].cover?(3)
+ assert SequenceSet["2,4:7,9,12:*"] === 2
+ assert SequenceSet["2,4:7,9,12:*"].cover?(2222)
+ assert SequenceSet["2,*:12"].cover? :*
+ assert SequenceSet["2,*:12"].cover?(-1)
+ assert SequenceSet["2,*:12"].cover?(99..5000)
+ refute SequenceSet["2,*:12"].cover?(10)
+ refute SequenceSet["2,*:12"].cover?(10..13)
+ assert SequenceSet["2:12"].cover?(10..12)
+ refute SequenceSet["2:12"].cover?(10..13)
+ assert SequenceSet["2:12"].cover?(10...13)
+ set = SequenceSet.new Array.new(100) { rand(1..1500) }
+ rev = (~set).limit(max: 1_501)
+ refute set.cover?(rev)
+ set.each_element do assert set.cover?(_1) end
+ rev.each_element do refute set.cover?(_1) end
+ assert SequenceSet["2:4"].cover? []
+ assert SequenceSet["2:4"].cover? SequenceSet.empty
+ assert SequenceSet["2:4"].cover? nil
+ assert SequenceSet["2:4"].cover? ""
+ refute SequenceSet["2:4"].cover? "*"
+ refute SequenceSet["2:4"].cover? SequenceSet.full
+ assert SequenceSet.full .cover? SequenceSet.full
+ assert SequenceSet.full .cover? :*
+ assert SequenceSet.full .cover?(-1)
+ assert SequenceSet.empty .cover? SequenceSet.empty
+ refute SequenceSet.empty .cover? SequenceSet[:*]
+ end
+
+ test "~full == empty" do
+ assert_equal SequenceSet.new("1:*"), ~SequenceSet.new
+ assert_equal SequenceSet.new, ~SequenceSet.new("1:*")
+ assert_equal SequenceSet.new("1:*"), SequenceSet.new .complement
+ assert_equal SequenceSet.new, SequenceSet.new("1:*").complement
+ assert_equal SequenceSet.new("1:*"), SequenceSet.new .complement!
+ assert_equal SequenceSet.new, SequenceSet.new("1:*").complement!
+ end
+
+ data(
+ # desc => [expected, input, freeze]
+ "empty" => ["#", nil],
+ "frozen empty" => ["Net::IMAP::SequenceSet.empty", nil, true],
+ "normalized" => ['#', [2, 1]],
+ "denormalized" => ['#', "2,1"],
+ "star" => ['#', "*"],
+ "frozen" => ['Net::IMAP::SequenceSet["1,3,5:*"]', [1, 3, 5..], true],
+ )
+ def test_inspect((expected, input, freeze))
+ seqset = SequenceSet.new(input)
+ seqset = seqset.freeze if freeze
+ assert_equal expected, seqset.inspect
+ end
+
+ data "single number", {
+ input: "123456",
+ elements: [123_456],
+ ranges: [123_456..123_456],
+ numbers: [123_456],
+ to_s: "123456",
+ normalize: "123456",
+ count: 1,
+ complement: "1:123455,123457:*",
+ }, keep: true
+
+ data "single range", {
+ input: "1:3",
+ elements: [1..3],
+ ranges: [1..3],
+ numbers: [1, 2, 3],
+ to_s: "1:3",
+ normalize: "1:3",
+ count: 3,
+ complement: "4:*",
+ }, keep: true
+
+ data "simple numbers list", {
+ input: "1,3,5",
+ elements: [ 1, 3, 5],
+ ranges: [1..1, 3..3, 5..5],
+ numbers: [ 1, 3, 5],
+ to_s: "1,3,5",
+ normalize: "1,3,5",
+ count: 3,
+ complement: "2,4,6:*",
+ }, keep: true
+
+ data "numbers and ranges list", {
+ input: "1:3,5,7:9,46",
+ elements: [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",
+ normalize: "1:3,5,7:9,46",
+ count: 8,
+ complement: "4,6,10:45,47:*",
+ }, keep: true
+
+ data "just *", {
+ input: "*",
+ elements: [:*],
+ ranges: [:*..],
+ numbers: RangeError,
+ to_s: "*",
+ normalize: "*",
+ count: 1,
+ complement: "1:%d" % [2**32-1]
+ }, keep: true
+
+ data "range with *", {
+ input: "4294967000:*",
+ elements: [4_294_967_000..],
+ ranges: [4_294_967_000..],
+ numbers: RangeError,
+ to_s: "4294967000:*",
+ normalize: "4294967000:*",
+ count: 2**32 - 4_294_967_000,
+ complement: "1:4294966999",
+ }, keep: true
+
+ data "* sorts last", {
+ input: "5,*,7",
+ elements: [5, 7, :*],
+ ranges: [5..5, 7..7, :*..],
+ numbers: RangeError,
+ to_s: "5,*,7",
+ normalize: "5,7,*",
+ complement: "1:4,6,8:%d" % [2**32-1],
+ count: 3,
+ }, keep: true
+
+ data "out of order", {
+ input: "46,7:6,15,3:1",
+ elements: [1..3, 6..7, 15, 46],
+ 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",
+ normalize: "1:3,6:7,15,46",
+ count: 7,
+ complement: "4:5,8:14,16:45,47:*",
+ }, keep: true
+
+ data "adjacent", {
+ input: "1,2,3,5,7:9,10:11",
+ elements: [1..3, 5, 7..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",
+ normalize: "1:3,5,7:11",
+ count: 9,
+ complement: "4,6,12:*",
+ }, keep: true
+
+ data "overlapping", {
+ input: "1:5,3:7,10:9,10:11",
+ elements: [1..7, 9..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",
+ normalize: "1:7,9:11",
+ count: 10,
+ complement: "8,12:*",
+ }, keep: true
+
+ data "contained", {
+ input: "1:5,3:4,9:11,10",
+ elements: [1..5, 9..11],
+ ranges: [1..5, 9..11],
+ numbers: [1, 2, 3, 4, 5, 9, 10, 11],
+ to_s: "1:5,3:4,9:11,10",
+ normalize: "1:5,9:11",
+ count: 8,
+ complement: "6:8,12:*",
+ }, keep: true
+
+ data "array", {
+ input: ["1:5,3:4", 9..11, "10", 99, :*],
+ elements: [1..5, 9..11, 99, :*],
+ ranges: [1..5, 9..11, 99..99, :*..],
+ numbers: RangeError,
+ to_s: "1:5,9:11,99,*",
+ normalize: "1:5,9:11,99,*",
+ count: 10,
+ complement: "6:8,12:98,100:#{2**32 - 1}",
+ }, keep: true
+
+ data "nested array", {
+ input: [["1:5", [3..4], [[[9..11, "10"], 99], :*]]],
+ elements: [1..5, 9..11, 99, :*],
+ ranges: [1..5, 9..11, 99..99, :*..],
+ numbers: RangeError,
+ to_s: "1:5,9:11,99,*",
+ normalize: "1:5,9:11,99,*",
+ count: 10,
+ complement: "6:8,12:98,100:#{2**32 - 1}",
+ }, keep: true
+
+ data "empty", {
+ input: nil,
+ elements: [],
+ ranges: [],
+ numbers: [],
+ to_s: "",
+ normalize: nil,
+ count: 0,
+ complement: "1:*",
+ }, keep: true
+
+ test "#elements" do |data|
+ assert_equal data[:elements], SequenceSet.new(data[:input]).elements
+ end
+
+ test "#ranges" do |data|
+ assert_equal data[:ranges], SequenceSet.new(data[:input]).ranges
+ end
+
+ test "#string" do |data|
+ set = SequenceSet.new(data[:input])
+ str = data[:to_s]
+ str = nil if str.empty?
+ assert_equal str, set.string
+ end
+
+ test "#normalized_string" do |data|
+ set = SequenceSet.new(data[:input])
+ assert_equal data[:normalize], set.normalized_string
+ end
+
+ test "#normalize" do |data|
+ set = SequenceSet.new(data[:input])
+ assert_equal data[:normalize], set.normalize.string
+ if data[:input]
+ end
+ end
+
+ test "#normalize!" do |data|
+ set = SequenceSet.new(data[:input])
+ set.normalize!
+ assert_equal data[:normalize], set.string
+ end
+
+ test "#to_s" do |data|
+ assert_equal data[:to_s], SequenceSet.new(data[:input]).to_s
+ end
+
+ test "#count" do |data|
+ assert_equal data[:count], SequenceSet.new(data[:input]).count
+ end
+
+ test "#valid_string" do |data|
+ if (expected = data[:to_s]).empty?
+ assert_raise DataFormatError do
+ SequenceSet.new(data[:input]).valid_string
+ end
+ else
+ assert_equal data[:to_s], SequenceSet.new(data[:input]).valid_string
+ end
+ end
+
+ test "#~ and #complement" do |data|
+ set = SequenceSet.new(data[:input])
+ assert_equal(data[:complement], set.complement.to_s)
+ assert_equal(data[:complement], (~set).to_s)
+ end
+
+ test "#numbers" do |data|
+ expected = data[:numbers]
+ if expected.is_a?(Class) && expected < Exception
+ assert_raise expected do SequenceSet.new(data[:input]).numbers end
+ else
+ assert_equal expected, SequenceSet.new(data[:input]).numbers
+ end
+ end
+
+ test "SequenceSet[input]" do |input|
+ case (input = data[:input])
+ when nil
+ assert_raise DataFormatError do SequenceSet[input] end
+ when String
+ seqset = SequenceSet[input]
+ assert_equal data[:input], seqset.to_s
+ assert_equal data[:normalize], seqset.normalized_string
+ assert seqset.frozen?
+ else
+ seqset = SequenceSet[input]
+ assert_equal data[:normalize], seqset.to_s
+ assert seqset.frozen?
+ end
+ end
+
+ test "set == ~~set" do |data|
+ set = SequenceSet.new(data[:input])
+ assert_equal set, set.complement.complement
+ assert_equal set, ~~set
+ end
+
+ test "set | ~set == full" do |data|
+ set = SequenceSet.new(data[:input])
+ assert_equal SequenceSet.new("1:*"), set + set.complement
+ end
+
+end