From ec863f585391b13efa86d305f0766972e9d4d0e8 Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Wed, 29 Nov 2023 06:14:47 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20seq-set=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/sequence_set.rb | 189 +++++++++++++---------------- test/net/imap/test_sequence_set.rb | 26 +++- 2 files changed, 108 insertions(+), 107 deletions(-) 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: [],