diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb
index 5bb01e2e..dee68a79 100644
--- a/lib/net/imap/sequence_set.rb
+++ b/lib/net/imap/sequence_set.rb
@@ -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
@@ -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,
@@ -267,14 +269,14 @@ def include?(number)
end
# Returns +true+ when the set contains *.
- 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
# *, 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
@@ -282,7 +284,7 @@ def max(star: :*)
# Returns the minimum value in +self+, +star+ when the only value in the
# set is *, or +nil+ when the set is empty.
def min(star: :*)
- (val = @tuples.first&.first) && val == STAR ? star : val
+ (val = @tuples.first&.first) && val == STAR_INT ? star : val
end
# :call-seq: minmax(star: :*) => nil or [integer, integer or star]
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -495,7 +497,7 @@ def each_number(&block)
#
# If the set contains a *, 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
@@ -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"
#
# * is always interpreted as the maximum value. When the set
- # contains star, it will be set equal to the limit.
+ # contains *, it will be set equal to the limit.
#
# Net::IMAP::SequenceSet["*"].limit(max: 37)
# # => Net::IMAP::SequenceSet["37"]
@@ -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 * is a
+ # 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?
- # 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
@@ -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
#
@@ -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
@@ -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
@@ -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,
@@ -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)
@@ -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
@@ -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]
diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb
index 19680d62..95d38d91 100644
--- a/test/net/imap/test_sequence_set.rb
+++ b/test/net/imap/test_sequence_set.rb
@@ -119,8 +119,8 @@ class IMAPSequenceSetTest < Test::Unit::TestCase
end
test "#limit with empty result" do
- assert_equal nil, SequenceSet["1234567890"].limit(max: 37)
- assert_equal nil, SequenceSet["99:195,458"].limit(max: 37)
+ 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
@@ -407,6 +407,28 @@ def test_inspect((expected, input, freeze))
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: [],