Skip to content

Commit

Permalink
🚧 seq-set updates
Browse files Browse the repository at this point in the history
  • Loading branch information
nevans committed Nov 30, 2023
1 parent a000bb5 commit ec863f5
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 107 deletions.
189 changes: 84 additions & 105 deletions lib/net/imap/sequence_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,18 @@ class IMAP
# Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
# Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
class SequenceSet
MAX = 2**32 - 1
STAR = 2**32
VALID = (1..STAR).freeze
STAR_INT = 2**32
STARS = [:*, ?*, -1, 2**32].freeze
private_constant :STAR, :STAR_INT, :STARS

MAX = 2**32 - 1
VALID = (1..STAR_INT).freeze
private_constant :MAX, :VALID

COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
private_constant :MAX, :STAR, :VALID, :STARS, :COERCIBLE
ENUMABLE = ->{ _1.respond_to?(:each) && _1.respond_to?(:empty?) }
private_constant :COERCIBLE, :ENUMABLE

class << self

Expand Down Expand Up @@ -235,13 +241,9 @@ def hash; [self.class, string].hash end
#
# Returns the result of #cover? Returns +nil+ if #cover? raises a
# StandardError exception.
def ===(other)
cover?(other)
rescue
nil
end
def ===(other) cover?(other) rescue nil end

# Returns +true+ when +obj+ is in found within set, and +false+
# Returns +true+ when +obj+ is contained within the set, and +false+
# otherwise.
#
# Returns +false+ unless +obj+ is an Integer, Range, Set,
Expand All @@ -267,22 +269,22 @@ def include?(number)
end

# Returns +true+ when the set contains <tt>*</tt>.
def include_star?; @tuples.last&.last == STAR end
def include_star?; @tuples.last&.last == STAR_INT end

# :call-seq: max(star: :*) => integer or star or nil
#
# Returns the maximum value in +self+, +star+ when the set includes
# <tt>*</tt>, or +nil+ when the set is empty.
def max(star: :*)
(val = @tuples.last&.last) && val == STAR ? star : val
(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 <tt>*</tt>, or +nil+ when the set is empty.
def min(star: :*)
(val = @tuples.first&.first) && val == STAR ? star : val
(val = @tuples.first&.first) && val == STAR_INT ? star : val
end

# :call-seq: minmax(star: :*) => nil or [integer, integer or star]
Expand Down Expand Up @@ -348,7 +350,7 @@ def complement; remain_frozen dup.complement! end
# Adds a range, number, or string to the set and returns self. The
# #string will be regenerated. Use #merge to add many elements at once.
def add(object)
tuples_add input_to_tuples object
tuples_add object_to_tuples! object
normalize!
self
end
Expand All @@ -361,7 +363,7 @@ def add?(obj) add(obj) unless cover?(obj) end
# Merges the elements in each object to the set and returns self. The
# #string will be regenerated after all inputs have been merged.
def merge(*inputs)
tuples_add inputs.flat_map { input_to_tuples _1 }
tuples_add inputs.flat_map { object_to_tuples! _1 }
normalize!
self
end
Expand All @@ -370,7 +372,7 @@ def merge(*inputs)
# can be a range, a number, or an enumerable of ranges and numbers. The
# #string will be regenerated.
def subtract(object)
tuples_subtract input_to_tuples object
tuples_subtract object_to_tuples! object
normalize!
self
end
Expand Down Expand Up @@ -448,10 +450,10 @@ def numbers; each_number.to_a end
def each_element
return to_enum(__method__) unless block_given?
@tuples.each do |min, max|
if min == STAR then yield :*
elsif max == STAR then yield min..
elsif min == max then yield min
else yield 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
Expand All @@ -464,9 +466,9 @@ def each_element
def each_range
return to_enum(__method__) unless block_given?
@tuples.each do |min, max|
if min == STAR then yield :*..
elsif max == STAR then yield min..
else yield min..max
if min == STAR_INT then yield :*..
elsif max == STAR_INT then yield min..
else yield min..max
end
end
self
Expand All @@ -484,8 +486,8 @@ def each_number(&block)
raise RangeError, '%s contains "*"' % [self.class] if include_star?
each_element do |elem|
case elem
in Range => range then range.each(&block)
in Integer => number then block.(number)
when Range then elem.each(&block)
when Integer then block.(elem)
end
end
self
Expand All @@ -495,7 +497,7 @@ def each_number(&block)
#
# If the set contains a <tt>*</tt>, RangeError will be raised.
#
# See #numbers of the warning about very large sets.
# See #numbers for the warning about very large sets.
#
# Related: #elements, #ranges, #numbers
def to_set; Set.new(numbers) end
Expand All @@ -513,14 +515,11 @@ def count
# and ranges over +max+ removed, and ranges containing +max+ converted to
# end at +max+.
#
# Use #limit to set the largest number in use before enumerating. See the
# warning on #numbers.
#
# Net::IMAP::SequenceSet["5,10:500,999"].limit(max: 37)
# # => Net::IMAP::SequenceSet["5,10:37"]
# Net::IMAP::SequenceSet["5,10:22,50"].limit(max: 20).to_s
# # => "5,10:20"
#
# <tt>*</tt> is always interpreted as the maximum value. When the set
# contains star, it will be set equal to the limit.
# contains <tt>*</tt>, it will be set equal to the limit.
#
# Net::IMAP::SequenceSet["*"].limit(max: 37)
# # => Net::IMAP::SequenceSet["37"]
Expand All @@ -529,36 +528,25 @@ def count
# Net::IMAP::SequenceSet["500:*"].limit(max: 37)
# # => Net::IMAP::SequenceSet["37"]
#
# Returns +nil+ when all members are excluded, not an empty SequenceSet.
#
# Net::IMAP::SequenceSet["500:999"].limit(max: 37) # => nil
#
# When the set is frozen and the result would be unchanged, +self+ is
# returned.
def limit(max:)
max = valid_int(max)
if empty? then nil
elsif !include_star? && max < min then nil
elsif max(star: STAR) <= max then frozen? ? self : dup.freeze
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+ an returns self. If <tt>*</tt> is a
# Removes all members over +max+ and returns self. If <tt>*</tt> is a
# member, it will be converted to +max+.
#
# Related: #limit
def limit!(max:)
star = include_star?
# TODO: subtract(max..)
if (over_range, idx = tuple_gte_with_index(max + 1))
if over_range.first <= max
over_range[1] = max
idx += 1
end
tuples.slice!(idx..)
end
star and add max
max = to_tuple_int(max)
tuple_subtract [max + 1, STAR_INT]
tuple_add [max, max ] if star
normalize!
self
end

Expand All @@ -569,7 +557,7 @@ def valid?; !empty? end
def empty?; @tuples.empty? end

# Returns true if the set contains every possible element.
def full?; @tuples == [[1, STAR]] end
def full?; @tuples == [[1, STAR_INT]] end

# :call-seq: complement! -> self
#
Expand All @@ -581,8 +569,8 @@ def complement!
return replace(VALID) << STAR if empty?
return clear if full?
flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
if flat.first < 1 then flat.shift else flat.unshift 1 end
if STAR < flat.last then flat.pop else flat.push STAR end
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!
self
Expand All @@ -600,17 +588,19 @@ def normalize; dup.normalize! end
# Updates #string to be sorted, deduplicated, and coalesced. Returns
# self.
def normalize!
@string = -@tuples.map { tuple_to_str _1 }.join(",")
@string = -@tuples.map {|tuple|
tuple.uniq.map{ _1 == STAR_INT ? "*" : _1 }.join(":")
}.join(",")
self
end

def inspect
if !frozen?
"#<%s %s>" % [self.class, empty? ? "empty" : to_s.inspect]
elsif empty?
"%s.empty" % [self.class]
else
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

Expand Down Expand Up @@ -647,21 +637,23 @@ def initialize_dup(other)
super
end

def merging(normalize: true)
yield
normalize! if normalize
self
end

def input_to_tuples(obj)
object_to_tuples(obj) ||
obj.respond_to?(:each) && enum_to_tuples(obj) or
raise_invalid(obj)
def object_to_tuples!(obj)
object_to_tuples(obj) or
raise DataFormatError,
"expected nz-number, range, string, or enumerable, " \
"got %p" % [obj]
end

def enum_to_tuples(enum)
raise DataFormatError, "invalid empty enum" if enum.empty?
enum.flat_map {|obj| object_to_tuples!(obj) }
def object_to_tuples(obj)
obj = object_try_convert obj
case obj
when STARS then [[STAR_INT, STAR_INT]]
when VALID then [[obj, obj]]
when Range then [range_to_tuple(obj)]
when SequenceSet then obj.tuples
when String then str_to_tuples obj
when ENUMABLE then enum_to_tuples obj
end
end

# unlike SequenceSet#trykconvert, this can return an Integer, Range,
Expand All @@ -675,28 +667,9 @@ def object_try_convert(input)
input
end

def object_to_tuples(obj)
obj = object_try_convert obj
case obj
when STAR, VALID then [[obj, obj]]
when Range then [range_to_tuple(obj)]
when String then str_to_tuples obj
when SequenceSet then obj.tuples
end
end

def object_to_tuples!(obj) object_to_tuples(obj) or raise_invalid(obj) end

def raise_invalid(obj)
raise DataFormatError,
"expected %p to be nz-number, range, or string" % [obj]
end

def valid_int(obj)
if STARS.include?(obj) then STAR
elsif VALID.cover?(obj) then obj
else nz_number(obj)
end
def enum_to_tuples(enum)
raise DataFormatError, "invalid empty enum" if enum.empty?
enum.flat_map {|obj| object_to_tuples!(obj) }
end

def range_cover?(rng)
Expand All @@ -706,31 +679,30 @@ def range_cover?(rng)
end

def range_to_tuple(range)
first, last = [range.begin || 1, range.end || STAR]
.map! { valid_int _1 }
last -= 1 if range.exclude_end?
first = to_tuple_int(range.begin || 1)
last = to_tuple_int(range.end || STAR_INT)
last -= 1 if range.exclude_end? && range.end
unless first <= last
raise DataFormatError, "invalid range for sequence-set: %p" % [range]
end
[first, last]
end

def seqset_cover?(seqset)
(min..max(star: nil)).cover?(seqset.min..seqset.max(star: nil)) &&
seqset.elements.all? { cover? _1 }
def seqset_cover?(other)
range_self = min..max(star: nil)
range_other = other.min..other.max(star: nil)
range_self.cover?(range_other) && other.each_element.all? { cover? _1 }
end

def str_to_num(str) str == "*" ? STAR : nz_number(str) end

def str_to_tuples(string)
string.to_str
.split(",")
.tap { _1.empty? and raise DataFormatError, "invalid empty string" }
.map! {|str| str.split(":", 2).compact.map! { str_to_num _1 }.minmax }
.map! {|str| str.split(":", 2).compact.map! { to_tuple_int _1 }.minmax }
end

def tuple_to_str(tuple)
tuple.uniq.map{ _1 == STAR ? "*" : _1 }.join(":")
tuple.uniq.map{ _1 == STAR_INT ? "*" : _1 }.join(":")
end

def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
Expand Down Expand Up @@ -828,6 +800,13 @@ def range_gte_to(num)
first..last if first
end

def to_tuple_int(obj)
if STARS.include?(obj) then STAR_INT
elsif VALID.cover?(obj) then obj
else nz_number(obj)
end
end

def nz_number(num)
/\A[1-9]\d*\z/.match?(num) or
raise DataFormatError, "%p is not a valid nz-number" % [num]
Expand Down
Loading

0 comments on commit ec863f5

Please sign in to comment.