# frozen_string_literal: true module Net class IMAP ## # 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" # # == Normalized form # # When a sequence set is created with a single String value, that #string # representation is preserved. SequenceSet's internal representation # implicitly sorts all entries, de-duplicates numbers, and coalesces # adjacent or overlapping ranges. Most enumeration methods and offset-based # methods use this normalized representation. Most modification methods # will convert #string to its normalized form. # # In some cases the order of the string representation is significant, such # as the +ESORT+, CONTEXT=SORT, and +UIDPLUS+ extensions. Use # #entries or #each_entry to enumerate the set in its original order. To # preserve #string order while modifying a set, use #append, #string=, or # #replace. # # == Using * # # \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? (aliased as #overlap?): # Returns whether +self+ and a given object have any common elements. # - #disjoint?: # Returns whether +self+ and a given object have no common elements. # # === 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, sorted and # coalesced, and returns +self+. # - #elements (aliased as #to_a): Returns an Array of every number and range # in the set, sorted and coalesced. # - #each_entry: Yields each number and range in the set, unsorted and # without deduplicating numbers or coalescing ranges, and returns +self+. # - #entries: Returns an Array of every number and range in the set, # unsorted and without deduplicating numbers or coalescing ranges. # - #each_range: # Yields each element in the set as a Range and returns +self+. # - #ranges: Returns an Array of every element in the set, converting # 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+. # - #append: Adds a given object to the set, appending it to the existing # string, and returns +self+. # - #string=: Assigns a new #string value and replaces #elements to match. # - #replace: Replaces the contents of the set with the contents # of a given object. # - #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 # 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 # 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 +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 # Freezes and returns the set. A frozen SequenceSet is Ractor-safe. def freeze return self if frozen? string @tuples.each(&:freeze).freeze 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. # # 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 # :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 # 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, 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 alias overlap? intersect? # 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 # Adds a range or number to the set and returns +self+. # # Unlike #add, #merge, or #union, the new value is appended to #string. # This may result in a #string which has duplicates or is out-of-order. def append(object) tuple = input_to_tuple object entry = tuple_to_str tuple tuple_add tuple @string = -(string ? "#{@string},#{entry}" : entry) self end # :call-seq: add?(object) -> self or nil # # Adds a range or number to the set and returns +self+. Returns +nil+ # 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 and :*. # # The entries are in the same order they appear in #string, with no # sorting, deduplication, or coalescing. When #string is in its # normalized form, this will return the same result as #elements. # This is useful when the given order is significant, for example in a # ESEARCH response to IMAP#sort. # # Related: #each_entry, #elements def entries; each_entry.to_a end # Returns an array of ranges and integers and :*. # # 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 #string to the block and returns +self+. # Returns an enumerator when called without a block. # # The entries are yielded in the same order they appear in #string, with # no sorting, deduplication, or coalescing. When #string is in its # normalized form, this will yield the same values as #each_element. # # Related: #entries, #each_element def each_entry(&block) # :yields: integer or range or :* return to_enum(__method__) unless block_given? return each_element(&block) unless @string @string.split(",").each do yield tuple_to_entry str_to_tuple _1 end self end # Yields each number or range (or :*) in #elements to the block # and returns self. Returns an enumerator when called without a block. # # The returned numbers are sorted and de-duplicated, even when the input # #string is not. See #normalize. # # Related: #elements, #each_entry def each_element # :yields: integer or range or :* return to_enum(__method__) unless block_given? @tuples.each do yield tuple_to_entry _1 end self end private def tuple_to_entry((min, max)) if min == STAR_INT then :* elsif max == STAR_INT then min.. elsif min == max then min else min..max end end public # 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 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 public # :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 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 public # 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 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 # Returns self alias to_sequence_set itself # Unstable API: currently for internal use only (Net::IMAP#validate_data) def validate # :nodoc: empty? and raise DataFormatError, "empty sequence-set is invalid" self end # Unstable API: for internal use only (Net::IMAP#send_data) def send_data(imap, tag) # :nodoc: 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