lib/sarah.rb in sarah-0.0.4 vs lib/sarah.rb in sarah-2.0.0

- old
+ new

@@ -1,393 +1,1431 @@ -# Sarah - Combination sequential array/random-access hash +# Sarah is a hybrid sequential array, sparse array, and random-access hash +# implementing alternate semantics for Ruby arrays. # -# Sequential values beginning at key (index) 0 are stored in an array. -# Values with sparse or non-numeric keys are stored in a hash. Values -# may migrate between the two if holes in the sequential key sequence -# are created or removed. +# By default, negative indexes are relative to the end of the array (just +# like a regular Ruby Array), but they can be configured to be distinct, +# actual indexes instead. # -# @author Brian Katzung <briank@kappacs.com>, Kappa Computer Solutions, LLC -# @copyright 2013 Brian Katzung and Kappa Computer Solutions, LLC +# Sarahs can also be used to implement (pure Ruby) sparse matrices, +# especially when used in conjunction with the Sarah::XK module (gem +# sarah-xk), an XKeys extension for Sarah. +# +# = Background +# +# Standard Ruby lets you create an array literal like: +# +# a = [ 1, 2, 5 => :five, :six => 'hello' ] +# +# which is a short-cut for an array with an embedded hash: +# +# a = [ 1, 2, { 5 => :five, :six => 'hello' } } +# +# Initially: After v = a.shift: +# +# a[0] = a[-3] = 1 v = 1 +# a[1] = a[-2] = 2 a[0] = a[-2] = 2 +# a[2] = a[-1] = a hash a[1] = a[-1] = a hash +# a[3][5] = :five a[2][5] = :five +# a[3][:six] = 'hello' a[2][:six] = 'hello' +# +# In contrast, using a Sarah looks like this: +# +# s = Sarah[ 1, 2, 5 => :five, :six => 'hello' ] +# +# Initially: After v = s.shift: +# +# s[0] = s[-6] = 1 v = 1 +# s[1] = s[-5] = 2 s[0] = s[-5] = 2 +# s[5] = s[-1] = :five s[4] = s[-1] = :five +# s[:six] = 'hello' s[:six] = 'hello' +# +# = Internal Structure +# +# As of version 2.0.0, there are three major data structures internally: +# * "seq" - an array of sequentially-indexed values beginning at index 0 +# (except when negative actual keys are present; see {#negative_mode=} +# :actual) +# * "spr" - a hash representing a sparse array of all other +# numerically-indexed values +# * "rnd" - a "random access" hash of all non-numerically keyed values +# +# The sequential and sparse parts are collectively referred to as "ary". +# All three parts together are collectively referred to as "all". +# +# Some methods allow you to direct their action to all or specific parts +# of the structure by specifying a corrsponding symbol, :seq, :spr, :ary, +# :rnd, or :all. +# +# @author Brian Katzung (briank@kappacs.com), Kappa Computer Solutions, LLC +# @copyright 2013-2014 Brian Katzung and Kappa Computer Solutions, LLC # @license MIT License +# @version 2.0.0 class Sarah + VERSION = "2.0.0" + + # Private attributes: + # seq [Array] An array of (zero-origin) sequential values. + # spr [Hash] A hash of (typically sparse) integer-keyed values. If + # there are any negatively-keyed values (in :actual negative mode), + # this hash contains all of the numerically-indexed values. + # rnd [Hash] A hash of non-numerically-keyed values. + # ary_first [Integer] The first integer array index. + # ary_next [Integer] One more than the highest integer array index. + # @!attribute default - # The default value returned for non-existent keys. + # The default value to return for missing keys and for pop or shift + # on an empty array. attr_accessor :default # @!attribute default_proc - # @return [Proc] - # The default proc to call for non-existent keys. This takes precedence - # over the default value. It is passed the Sarah and the referenced key - # (or nil) as parameters. - attr_accessor :default_proc + # A proc to call to return a default value for missing keys and for + # pop or shift on an empty array. The proc is passed the Sarah + # and the missing key (or nil for pop and shift). + # @return [Proc|nil] + attr :default_proc + # @!attribute [r] negative_mode + # @return [:actual|:error|:ignore] + # How negative indexes/keys are handled. Possible values are + # :actual, :error, and :ignore. See {#negative_mode=}. + attr_reader :negative_mode + + # ##### Class Methods ##### + + # Instantiate a "Sarah literal". + # + # s = Sarah[pos1, ..., posN, key1 => val1, ..., keyN => valN] + # + # s = Sarah['sequen', 'tial', 5 => 'sparse', :key => 'random'] + # + # @return [Sarah] + # @since 2.0.0 + def self.[] (*args); new.set *args; end + + # Try to convert the passed object to a Sarah. + # + # In the current implementation, the object must respond to #to_sarah, + # #each_index, or #each_pair. + # + # @param source [Object] The object to try to convert + # @return [Sarah|nil] + # @since 2.0.0 + def self.try_convert (source) + if source.respond_to? :to_sarah then source.to_sarah + elsif source.respond_to?(:each_index) || source.respond_to?(:each_pair) + new :from => source + else nil + end + end + # Initialize a new instance. # + # An initialization array may optionally be passed as the first + # parameter. (Since 2.0.0) + # # If passed a block, the block is called to provide default values # instead of using the :default option value. The block is passed the # hash and the requested key (or nil for a shift or pop on an empty # sequential array). # - # @param opts [Hash] Setup options. - # @option opts :default The default value to return for a non-existent key. - # @option opts [Proc] :default_proc The default proc to call for a - # non-existent key. - # @option opts [Array, Hash] :array An array (or hash!) to use for - # initialization (first). - # @option opts [Hash, Array] :hash A hash (or array!) to use for - # initialization (second). - # @option opts [Array, Hash] :from An array or hash to use for - # initialization (third). - def initialize (opts = {}, &block) + # s = Sarah.new([pos1, ..., posN, key1 => val1, ..., keyN => valN], + # :default => default, :default_proc => Proc.new { |sarah, key| block }, + # :array => source, :hash => source, :from => source, + # :negative_mode => negative_mode) + def initialize (*args, &block) + opts = (!args.empty? && args[-1].is_a?(Hash)) ? args.pop : {} clear + @negative_mode = :error + self.negative_mode = opts[:negative_mode] @default = opts[:default] @default_proc = block || opts[:default_proc] + if !args.empty? + case args[0] + when Array then set *args[0] + when Sarah + self.default = args[0].default + self.default_proc = args[0].default_proc + self.negative_mode = args[0].negative_mode + merge! args[0] + end + end merge! opts[:array], opts[:hash], opts[:from] end - # Clear all sequential array and random-access hash values. + # ##### Instance Methods ##### + + # Compute the intersection with another object. # + # The intersection of array values is taken without regard to indexes. + # Random-access hash values must match by key and value. + # # @return [Sarah] - def clear - @seq = [] - @rnd = {} + # @since 2.0.0 + def & (other) + case other + when Array then new_similar :array => (values(:ary) & other) + when Hash, Sarah + new_similar(:array => (values(:ary) & _hash_filter(other, :array). + values), :hash => other.select { |k, v| @rnd[k] == v }) + else raise TypeError.new('Unsupported type in Sarah#&') + end + end + + # Compute the union with another object. + # + # Changed random-access hash values for a given key overwrite the + # original value. + # + # @return [Sarah] + # @since 2.0.0 + def | (other) + case other + when Array then new_similar(:hash => @rnd).merge!(values(:ary) | other) + when Hash, Sarah + new_similar.merge!(@rnd, _hash_filter(other, :other), + values(:ary) | _hash_filter(other, :array).values) + else raise TypeError.new('Unsupported type in Sarah#|') + end + end + + # #* (replicate or join) is not implemented. + + # Return a new Sarah that is the concatenation of this one and + # another object. + # + # (#+ alias since 2.0.0) + def + (other) + new_similar(:from => self).append!(other) + end + + alias_method :concat, :+ + + # Compute the array difference with another object. + # + # Random-access hash values are only removed when both keys and values + # match. + # + # @return [Sarah] + # @since 2.0.0 + def - (other) + case other + when Array then new_similar(:array => (values(:ary) - other), + :hash => @rnd) + when Hash, Sarah + new_similar(:array => (values(:ary) - + other.select { |k, v| k.is_a? Integer}.values), + :hash => @rnd.select { |k, v| other[k] != v }) + else raise TypeError.new('Unsupported type in Sarah#-') + end + end + + # Push (append) sequential values. + # + # @param vlist [Array] A list of values to push (append). + # @return [Sarah] + def << (*vlist) + if !@spr.empty? + # Sparse push + vlist.each do |value| + self[@ary_next] = value + @ary_next += 1 + end + else + # Sequential push + @seq.push *vlist + @ary_next = @seq.size + end self end - # Test key existence. + alias_method :push, :<< + + # Array comparison (#<=>) - not supported + + # Check for equality with another object. # - # @param key The key to check for existence. - # @return [Boolean] - def has_key? (key) - @rnd.has_key?(key) or (key.is_a? Integer and - key >= -@seq.size and key < @seq.size) + # Compares to arrays via {#to_a} and to hashes or Sarahs via {#to_h}. + # + # @since 2.0.0 + def == (other) + case other + when Array then to_a == other + when Hash then to_h == other + when Sarah then to_h == other.to_h + else false + end end + alias_method :eql?, :== + # Get a value by sequential or random-access key. If the key does not # exist, the default value or initial block value is returned. If # called with a range or an additional length parameter, a slice is # returned instead. # # @param key The key for the value to be returned, or a range. # @param len [Integer] An optional slice length. - # @return [Object, Array] + # @return [Object|Array] def [] (key, len = nil) - if len - slice(key, len) - elsif key.is_a? Range - slice(key) - elsif @rnd.has_key? key - @rnd[key] - elsif key.is_a? Integer and key >= -@seq.size and key < @seq.size - @seq[key] + # Slice cases... + return slice(key, len) if len + return slice(key) if key.is_a? Range + + if key.is_a? Integer + # A key in the sequential array or sparse array hash? + catch :index_error do + key = _adjust_key key + return @seq[key] if key >= 0 && key < @seq.size + return @spr[key] if @spr.has_key? key + end else - @default_proc ? @default_proc.call(self, key) : @default + # A key in the random-access hash? + return @rnd[key] if @rnd.has_key? key end + + # Return the default value + @default_proc ? @default_proc.call(self, key) : @default end - # Get a value by sequential or random-access key. If the key does not - # exist, the local default or block value is returned. If no local or - # block value is supplied, a KeyError exception is raised instead. + alias_method :at, :[] + + # Set a value by sequential or random-access key. # - # If a local block is supplied, it is passed the key as a parameter. - # - # @param key The key for the value to be returned. - # @param default The value to return if the key does not exist. - def fetch (key, *default) - if @rnd.has_key? key - @rnd[key] - elsif key.is_a? Integer and key >= -@seq.size and key < @seq.size - @seq[key] - elsif default.size > 0 - default[0] - elsif block_given? - yield key + # @param key The key for which the value should be set. + # @param value The value to set for the key. + # @return Returns the value. + def []= (key, value) + if key.is_a? Integer + # A key in the sequential array or sparse array hash... + catch :index_error do + key = _adjust_key key + if key >= 0 && key <= @seq.size && @ary_first >= 0 + # It's in sequence + @seq[key] = value + _spr_to_seq if @spr.has_key? @seq.size + else + # It's a sparse key + if (@seq.empty? && @spr.empty?) || key < @ary_first + @ary_first = key + end + _seq_to_spr if key < 0 && !@seq.empty? + @spr[key] = value + end + @ary_next = key + 1 if key >= @ary_next + end else - raise KeyError.new("key not found: #{key}") + # A key in the random-access hash... + @rnd[key] = value end + + value end - # Shift and return the first sequential array value. + alias_method :store, :[]= + + # Append arrays or Sarahs and merge hashes. # - # @return Object - def shift - if @seq.size > 0 - @seq.shift - elsif @default_proc - @default_proc.call self, nil - else - @default + # @param ahlist [Array<Array, Hash, Sarah>] The structures to append. + # @return [Sarah] + def append! (*ahlist) + ahlist.each do |ah| + if ah.is_a? Sarah + push *ah.values(:ary) + merge! ah.to_h(:rnd) + elsif ah.respond_to? :each_pair + merge! ah + elsif ah.respond_to? :each_index + push *ah + end end + self end - # Pop and return the last sequential array value - def pop - if @seq.size > 0 - @seq.pop - elsif @default_proc - @default_proc.call self, nil - else - @default + # Return the first associated array. See Array#assoc and Hash#assoc. + # The result is always in Array#assoc format. + # + # @param which [:all|:ary|:seq|:spr|:rnd] Which elements to search. + # @return [Array|nil] + # @since 2.0.0 + def assoc (other, which = :all) + res = nil + case which when :all, :ary, :seq then res ||= @seq.assoc(other) end + case which when :all, :ary, :spr + res ||= @spr.values_at(*@spr.keys.sort).assoc(other) end + case which when :all, :rnd + if res.nil? + res = @rnd.assoc(other) + res.flatten!(1) unless res.nil? + end + end + res end - # Iterate a block (required) over each key and value. + # #bsearch (binary search) is not implemented. + + # Clear sequential+sparse array and/or random-access hash values. # + # @param which [:all|:ary|:rnd] (since 2.0.0) # @return [Sarah] - def each - @seq.each_index { |i| yield(i, @seq[i]) } - @rnd.each { |kv| yield(*kv) } + def clear (which = :all) + case which when :all, :ary + @seq, @spr, @ary_first, @ary_next = [], {}, 0, 0 + end + case which when :all, :rnd then @rnd = {} end self end - alias_method :each_pair, :each + # #collect and #collect! are not implemented, but see #ary_collect!. - # Iterate a block (required) over each key. + # Collect (map) in-place. The block (required) is passed the current + # value and the index/key (in that order). The return value of the + # block becomes the new value. # + # s.collect!(which) { |value, key| block } + # + # @param which [:all|:ary|:rnd|:seq|:spr] Which data structures + # are mapped. # @return [Sarah] - def each_index - @seq.each_index { |i| yield(i) } - @rnd.keys.each { |k| yield(k) } + # @since 2.0.0 + def collect! (which = :all) + case which when :all, :ary, :seq + @seq.each_index { |index| @seq[index] = yield @seq[index], index } + end + case which when :all, :ary, :spr + @spr.each_pair { |key, value| @spr[key] = yield value, key } + end + case which when :all, :rnd + @rnd.each_pair { |key, value| @rnd[key] = yield value, key } + end self end - # Set a value by sequential or random-access key. + alias_method :map!, :collect! + + # #combination is not implemented. + + # Remove nil values in place. In the case of the sequential and sparse + # arrays, the remaining values are reindexed sequentially from 0. # - # @param key The key for which the value should be set. - # @param value The value to set for the key. - # @return Returns the value. - def []= (key, value) - if key.is_a? Integer and key.abs <= @seq.size - key += @seq.size if key < 0 - @seq[key] = value - @rnd.delete key - else - @rnd[key] = value + # @param which [:all|:ary|:rnd] Which data structures are compacted. + # @return [Sarah] + def compact! (which = :all) + case which when :all, :ary + @seq = values(:ary).compact + @spr, @ary_first, @ary_next = {}, 0, @seq.size end + case which when :all, :rnd + @rnd.delete_if { |key, value| value.nil? } + end + self + end - # Move adjacent random-access keys to the sequential array - key = @seq.size - while @rnd.has_key? key - @seq[key] = @rnd.delete key - key += 1 + # #compare_by_identity and #compare_by_identity? are not implemented. + + # Return a count of values or matching objects + # + # s.count(which, value) + # s.count(which) { |item| block } + # + # @param which [:all|:ary|:rnd] Where to count + # @return [Integer] + def count (which = :all, *args) + if !args.empty? then values(which).count args[0] + elsif block_given? then values(which).count { |item| yield item } + else size which end + end - value + # #cycle is not implemented. + + # Set the default_proc block for generating default values. + # + # @param proc [Proc|nil] The proc block for default values. + def default_proc= (proc) + if proc.nil? || proc.is_a?(Proc) then @default_proc = proc + else raise TypeError.new('Default_proc must be a Proc or nil') + end + proc end - # Set values and/or key/value pairs. + # #delete is not implemented. Use #delete_at, #delete_value, + # #unset_at, or #unset_value instead. + + # Delete a specific index or key. # - # <tt>set([val1, ..., valN,] [key1 => kval1, ..., keyN => kvalN])</tt> + # (#delete_at alias since 2.0.0) + def delete_at (key) + return unset_at(key) if @negative_mode == :actual + if key.is_a? Integer + res = nil + catch :index_error do + key = _adjust_key key + if key >= 0 && key < @seq.size + res = @seq.delete_at key + _scan_spr -1 + else + res = @spr.delete key + _scan_spr -1, key + end + end + res + else + @rnd.delete key + end + end + + alias_method :delete_key, :delete_at + + # Deletes each value for which the required block returns true. # - # @param list [Array] A list of sequential values or random-access - # key/value pairs to set. + # Subsequent values are re-indexed except when {#negative_mode=} + # :actual. See also {#unset_if}. + # + # The block is passed the current value and nil for the sequential + # and sparse arrays or the current value and key (in that order) + # for the random-access hash. + # + # s.delete_if(which) { |value, key| block } + # + # @param which [:all|:ary|:rnd] The data structures in which to delete. # @return [Sarah] - def set (*list) - hash = (list.size > 0 and list[-1].is_a? Hash) ? list.pop : nil - merge! list - merge! hash if hash + # @since 2.0.0 + def delete_if (which = :all) + if @negative_mode == :actual + return unset_if(which) { |value, key| yield value, key } + end + case which when :all, :ary + num_del = @seq.size + @seq.delete_if { |value| yield value, nil } + num_del -= @seq.size + new_spr = {} + @spr.keys.sort.each do |key| + if yield @spr[key], nil then num_del += 1 + else new_spr[key - num_del] = @spr[key] + end + end + @spr = new_spr + _scan_spr + end + case which when :all, :rnd + @rnd.delete_if { |key, value| yield value, key } + end self end - # Set key/value pairs. + # Delete by value. # - # <tt>set_kv(key1, val1, ..., keyN, valN)</tt> + # Subsequent values are re-indexed except when {#negative_mode=} :actual. + # See also {#unset_value}. # - # @param kvlist [Array] The list of key/value pairs to set. + # @param what [Object] The value to be deleted + # @param which [:all|:ary|:rnd] The data structures in which to delete. # @return [Sarah] - def set_pairs (*kvlist) - kvlist.each_slice(2) { |kv| self.[]=(*kv) } - self + def delete_value (what, which = :all) + if @negative_mode == :actual + unset_if(which) { |value, key| value == what } + else + delete_if(which) { |value, key| value == what } + end end - alias_method :set_kv, :set_pairs + # #drop and #drop_while are not implemented. - # Append arrays/Sarahs (or merge hashes) of values. + # Iterate a block (required) over each key and value like Hash#each. # - # @param ahlist [Array<Array, Hash, Sarah>] The structures to append. + # s.each(which) { |key, value| block } + # + # (#each_key alias since 2.0.0) + # + # @param which [:all|:ary|:rnd|:seq|:spr] The data structures over + # which to iterate. (Since 2.0.0) # @return [Sarah] - def append! (*ahlist) - ahlist.each do |ah| - if ah.respond_to? :seq_values and ah.respond_to? :rnd - push *ah.seq_values - merge! ah.rnd - elsif ah.respond_to? :each_pair - merge! ah - elsif ah.respond_to? :each_index - push *ah + def each (which = :all) + case which when :all, :ary, :seq + @seq.each_index { |i| yield i, @seq[i] } + end + case which when :all, :ary, :spr + @spr.keys.sort.each { |i| yield i, @spr[i] } + end + case which when :all, :rnd + @rnd.each { |key, value| yield key, value } + end + self + end + + alias_method :each_index, :each + alias_method :each_key, :each + alias_method :each_pair, :each + + # #each_value not implemented. + + # Is the array (or are parts of it) empty? + # + # @param which [:all|:ary|:rnd|:seq|:spr] When data structures to check + # @return [Boolean] + # @since 2.0.0 + def empty? (which = :all) + case which + when :all then @seq.empty? && @spr.empty? && @rnd.empty? + when :ary then @seq.empty? && @spr.empty? + when :rnd then @rnd.empty? + when :seq then @seq.empty? + when :spr then @spr.empty? + else true + end + end + + # Get a value by sequential or random-access key. If the key does not + # exist, the local default or block value is returned. If no local or + # block value is supplied, a KeyError exception is raised instead. + # + # If a local block is supplied, it is passed the key as a parameter. + # + # fetch(key) + # fetch(key, default) + # fetch(key) { |key| block } + # + # @param key The key for the value to be returned. + # @param default The value to return if the key does not exist. + def fetch (key, *default) + if key.is_a? Integer + # A key in the sequential array or sparse array hash? + key += @ary_next if key < 0 && @negative_mode != :actual + return @seq[key] if key >= 0 && key < @seq.size + return @spr[key] if @spr.has_key? key + else + # A key in the random-access hash? + return @rnd[key] if @rnd.has_key? key + end + + if !default.empty? then default[0] + elsif block_given? then yield key + else raise KeyError.new('Key not found') + end + end + + # #fill is not implemented. + + # Find the first index within the sequential or sparse array for + # the specified value or yielding true from the supplied block. + # + # find_index(value) + # find_index { |value| block } + # + # @return [Integer|nil] + # @since 2.0.0 + def find_index (*args) + if block_given? + index = @seq.index { |value| yield value } + return index unless index.nil? + @spr.keys.sort.each do |index| + return index if yield @spr[index] end + else + value = args.pop + index = @seq.index value + return index unless index.nil? + @spr.keys.sort.each { |index| return index if @spr[index] == value } end + nil + end + + alias_method :index, :find_index + + # Return the first array value or first n array values. + # + # obj = s.first + # list = s.first(n) + # + # @return [Object|Array] + def first (*args); values(:ary).first(*args); end + + # Flatten sequential and sparse array values in place. + # + # @since 2.0.0 + def flatten! (*levels) + if levels.empty? then @seq = values(:ary).flatten + else @seq = values(:ary).flatten(levels[0]) + end + @ary_first, @ary_next, @spr = 0, @seq.size, {} self end - # Insert arrays/Sarahs (or merge hashes) of values. + # #frozen? is not implemented. + + # #hash is not implemented. + + # #initialize_copy is not implemented. + + # Return a string representation of this object. # + # @since 2.0.0 + def inspect; self.class.name + (@seq + [@spr.merge(@rnd)]).to_s; end + + alias_method :to_s, :inspect + + # #invert is not implemented. + + # Return the sequential and sparse array values as a string, + # separated by the supplied separator (or $, or the empty string). + # + # @param separator [String] + # @return [String] + # @since 2.0.0 + def join (separator = nil); values(:ary).join(separator || $, || ''); end + + # #keep_if is not implemented. + + # Test key/index existence. + # + # (#key? and #member? aliases since 2.0.0) + # + # @param key The key to check for existence. + # @return [Boolean] + def has_key? (key) + if key.is_a? Integer + key += @ary_next if key < 0 && @negative_mode != :actual + (key >= 0 && key < @seq.size) || @spr.has_key?(key) + else + @rnd.has_key? key + end + end + + alias_method :key?, :has_key? + alias_method :member?, :has_key? + + # Test value presence. + # + # @param value The value for which to check presence. + # @param which [:all|:ary|:rnd|:seq|:spr] Where to check for presence. + # @return [Boolean] + # @since 2.0.0 + def has_value? (value, which = :all) + case which when :all, :ary, :seq + return true if @seq.include? value + end + case which when :all, :ary, :spr + return true if @spr.has_value? value + end + case which when :all, :rnd + return true if @rnd.has_value? value + end + false + end + + alias_method :include?, :has_value? + alias_method :value?, :has_value? + + # Insert arrays or Sarahs and merge hashes. + # # @param ahlist [Array<Array, Hash, Sarah>] The structures to insert. # @return [Sarah] def insert! (*ahlist) ahlist.reverse_each do |ah| - if ah.respond_to? :seq_values and ah.respond_to? :rnd - unshift *ah.seq_values - merge! ah.rnd + if ah.is_a? Sarah + unshift *ah.values(:ary) + merge! ah.to_h(:rnd) elsif ah.respond_to? :each_pair merge! ah elsif ah.respond_to? :each_index unshift *ah end end self end - # Load/merge from a hash and/or array/Sarah (beginning at key 0). + # Return the (first) key having the specified value. # + # @param value The value for which the key is desired. + # @param which [:all|:ary|:rnd] Where to search for the value. + def key (value, which = :all) + case which when :all, :ary + pos = index value + return pos unless pos.nil? + end + case which when :all, :rnd then return @rnd.key(value) end + nil + end + + # Return the random-access hash keys. + # + # @return [Array] + # @deprecated Please use {#keys} instead. + def rnd_keys; @rnd.keys; end + + # Return the sequential array keys (indexes). + # + # @return [Array<Integer>] + # @deprecated Please use {#keys} instead. + def seq_keys; 0...@seq.size; end + + # Return an array of indexes and keys. + # + # @param which [:all|:ary|:rnd|:seq|:spr] Which indexes and keys + # to return. (Since 2.0.0) + # @return [Array] + def keys (which = :all) + case which + when :all then keys(:seq) + keys(:spr) + @rnd.keys + when :ary then keys(:seq) + keys(:spr) + when :rnd then @rnd.keys + when :seq then (0...@seq.size).to_a + when :spr then @spr.keys.sort + else [] + end + end + + # Return the last array value or last n array values. + # + # obj = s.last + # list = s.last(n) + # + # @return [Object|Array] + def last (*args); values(:ary).last(*args); end + + # Return the random-access hash size. + # + # @deprecated Please use {#size} instead. + # @return [Integer] + def rnd_length; @rnd.size; end + + alias_method :rnd_size, :rnd_length + + # Return the sequential array size. + # + # @deprecated Please use {#size} instead. + # @return [Integer] + def seq_length; @seq.size; end + + alias_method :seq_size, :seq_length + + # Return the number of stored values (AKA size or length). + # + # @param which [:all|:ary|:rnd|:seq|:spr] The data structures + # for which the (combined) size is to be returned. (Since 2.0.0) + # @return [Integer] + def length (which = :all) + size = 0 + case which when :all, :ary, :seq then size += @seq.size end + case which when :all, :ary, :spr then size += @spr.size end + case which when :all, :rnd then size += @rnd.size end + size + end + + alias_method :size, :length + + # Load/merge from a hash, array, or Sarah (beginning at key 0). + # # @param ahlist [Array<Array, Hash, Sarah>] The structures to load/merge. # @return [Sarah] def merge! (*ahlist) ahlist.each do |ah| if ah.respond_to? :each_pair - ah.each_pair { |kv| self.[]=(*kv) } + ah.each_pair { |key, value| self[key] = value } elsif ah.respond_to? :each_index ah.each_index { |i| self[i] = ah[i] } end end self end alias_method :update, :merge! - # Unshift (insert) sequential values beginning at key 0. + # Sets the negative mode, the manner in which negative integer + # index/key values are handled. # - # @param vlist [Array] A list of values to unshift (insert). + # :actual - Negative keys represent themselves and are not treated + # specially (although delete works like unset in this mode--values + # are not reindexed) + # :error (default) - Negative keys are interpreted relative to the + # end of the array; keys < -@ary_next raise an IndexError + # :ignore - Like :error, but keys < -@ary_next are treated as + # non-existent on fetch and silently ignored on set + def negative_mode= (mode) + case mode + when :actual then @negative_mode = :actual + when :error, :ignore + # These modes are only possible if there aren't currently + # any negative keys. + @negative_mode = mode if @ary_first >= 0 + end + @negative_mode + end + + # Return a new instance configured similarly to this one. + # # @return [Sarah] - def unshift (*vlist) - (@seq.size...@seq.size+vlist.size).each { |k| @rnd.delete k } - @seq.unshift *vlist + # @since 2.0.0 + def new_similar (*args) + opts = { :default => @default, :default_proc => @default_proc, + :negative_mode => @negative_mode } + opts.merge! args.pop if !args.empty? && args[-1].is_a?(Hash) + self.class.new(*args, opts) + end + + # #pack is not implemented. + + # Return a hash of key/value pairs corresponding to the list of + # keys/indexes. + # + # @return [Hash] + # @since 2.0.0 + def pairs_at (*list) + pairs = {} + list.each { |key| pairs[key] = self[key] } + pairs + end + + # #permutation is not implemented. + + # Pop one or more values off the end of the sequential and sparse + # arrays. + # + # @param count [Integer|nil] The number of items to pop (1 if nil). + # (Since 2.0.0) + def pop (count = nil) + if !count.nil? + max = size :ary + count = max if max < count + res = [] + count.times { res << pop } + res.reverse + elsif !@seq.empty? || !@spr.empty? then delete_at(@ary_next - 1) + else @default_proc ? @default_proc.call(self, nil) : @default + end + end + + # #product is not implemented. + + # #rassoc is not implemented. + + # #reject is not implemented. + + # Rehash keys + # + # @return [Sarah] + # @since 2.0.0 + def rehash; @spr.rehash; @rnd.rehash; self; end + + # #repeated_combination is not implemented. + + # #repeated_permutation is not implemented. + + # Replace contents with the contents of another array, hash, or Sarah. + # + # @param other [Sarah|hash] + # @return [Sarah] + def replace (other) + clear + negative_mode = other.negative_mode if other.is_a? Sarah + merge! other self end - # Push (append) sequential values. + # Reverse sequential and sparse array values into a sequential list. # - # @param vlist [Array] A list of values to push (append). # @return [Sarah] - def push (*vlist) - (@seq.size...@seq.size+vlist.size).each { |k| @rnd.delete k } - @seq.push *vlist + # @since 2.0.0 + def reverse! + @seq = values(:ary).reverse + @ary_first, @ary_next, @spr = 0, @seq.size, {} self end - alias_method :<<, :push + # Iterate a block (required) over each key and value in reverse order + # like Hash#each (first the random-access hash, then array values in + # reverse key order). + # + # s.reverse_each(which) { |key, value| block } + # + # @param which [:all|:ary|:seq|:spr] The data structures over + # which to iterate. The random-access hash is only included for :all. + # @return [Sarah] + # @since 2.0.0 + def reverse_each (which = :ary) + case which when :all + @rnd.each { |key, value| yield key, value } + end + case which when :all, :ary, :spr + @spr.keys.sort.reverse_each { |i| yield i, @spr[i] } + end + case which when :all, :ary, :seq + (@seq.size - 1).downto(0) { |i| yield i, @seq[i] } + end + self + end - # Delete by sequential or random-access key, returning any existing value - # (or the default or block value, otherwise). + # Find the last index within the sequential or sparse array for + # the specified value or yielding true from the supplied block. # - # @param key The key to be deleted. - # @return The value of the deleted key. - def delete_key (key) - return @rnd.delete key if @rnd.has_key? key - if key.is_a? Integer - key += @seq.size if key < 0 - if key >= 0 and key < @seq.size - result = @seq[key] + # rindex(value) + # rindex { |value| block } + # + # @return [Integer|nil] + # @since 2.0.0 + def rindex (*args) + if block_given? + @spr.keys.sort.reverse_each do |index| + return index if yield @spr[index] + end + return @seq.rindex { |value| yield value } + else + value = args.pop + @spr.keys.sort.reverse_each do |index| + return index if @spr[index] == value + end + return @seq.rindex(value) + end + end - # Move any following keys to the random-access hash - (key+1...@seq.size).each { |i| @rnd[i] = @seq[i] } + # Return the sparse array and random-access hash (for + # backward-compatibility only). + # + # @return [Hash] + # @deprecated Please use {#to_h} instead. + def rnd; @rnd; end - # Truncate the sequential array - @seq = @seq[0...key] + # Return the sparse array and random-access hash values (for + # backward-compatibility only). + # + # @return [Array] + # @deprecated Please use {#values} instead. + def rnd_values; @spr.values + @rnd.values; end - return result - end - end - @default_proc ? @default_proc.call(self, nil) : @default + # Rotate sequential and sparse array values into a sequential list. + # + # @param count [Integer] The amount to rotate. See Array#rotate!. + # @return [Sarah] + # @since 2.0.0 + def rotate! (count = 1) + @seq = values(:ary).rotate(count) + @ary_first, @ary_next, @spr = 0, @seq.size, {} + self end - # Return the sequential array size. + # Return a random sample from the sequential and sparse arrays + # like Array#sample. # - # @return [Integer] - def seq_size; @seq.size; end + # @since 2.0.0 + def sample (*args); values(:ary).sample(*args); end - # Return the random-access hash size. + # Select a subset of values as indicated by the supplied block and + # return as a hash like Hash#select. # - # @return [Integer] - def rnd_size; @rnd.size; end + # s.select(which) { |key, value| block } + # + # @param which [:all|:ary|:rnd|:seq|:spr] Which values to select. + def select (which = :all) + to_h(which).select { |key, value| yield key, value } + end - # Return the total size. + # Return a copy of the sequential array values. # - # @return [Integer] - def size; @seq.size + @rnd.size; end + # @return [Array] + # @deprecated Please use {#values} instead. + def seq; Array.new(@seq); end - alias_method :seq_length, :seq_size - alias_method :rnd_length, :rnd_size - alias_method :length, :size + alias_method :seq_values, :seq - # Return the sequential-access array. + # Set values and/or key/value pairs (in standard Ruby calling syntax). # + # set(seq1, ..., seqN, key1 => val1, ..., keyN => valN) + # + # @param list [Array] A list of sequential values or random-access + # key/value pairs to set. + # @return [Sarah] + def set (*list) + hash = (list.size > 0 and list[-1].is_a? Hash) ? list.pop : nil + merge! list + merge! hash if hash + self + end + + # Set from a list of key/value pairs. + # + # set_kv(key1, val1, ..., keyN, valN) + # + # @param kvlist [Array] The list of key/value pairs to set. + # @return [Sarah] + def set_kv (*kvlist) + kvlist.each_slice(2) { |kv| self.[]=(*kv) } + self + end + + alias_method :set_pairs, :set_kv + + # Shift one or more values off the beginning of the sequential and + # sparse arrays. + # + # Subsequent values are re-indexed unless {#negative_mode=} :actual. + # + # @param count [Integer|nil] The number of items to shift (1 if nil). + # (Since 2.0.0) + def shift (count = nil) + if !count.nil? + max = size :ary + count = max if max < count + res = [] + count.times { res << shift } + res + elsif !@seq.empty? || !@spr.empty? then delete_at @ary_first + else @default_proc ? @default_proc.call(self, nil) : @default + end + end + + # Return the sequential and sparse array values in a shuffled + # sequence like Array#shuffle. + # + # shuffle + # shuffle(random: rng) + # # @return [Array] - def seq; @seq; end + # @since 2.0.0 + def shuffle (*args); values(:ary).shuffle(*args); end - # Return the random-access hash. + # Replaces the sequential and sparse array values with shuffled + # sequential values. # - # @return [Hash] - def rnd; @rnd; end + # shuffle! + # shuffle!(random: rng) + # + # @return [Sarah] + # @since 2.0.0 + def shuffle! (*args) + @seq = values(:ary).shuffle + @ary_first, @ary_next, @spr = 0, @seq.size, {} + self + end - # Return the sequential array keys (indexes). + # Return an element or return a slice of elements from the sequential + + # sparse array as a new Sarah. # - # @return [Array<Integer>] - def seq_keys; 0...@seq.size; end + # The original array is unmodified. + # + # NOTES: This implementation is new since 2.0.0 and incompatible with + # prior versions. Alias #seq_slice is deprecated since 2.0.0. + # + # s.slice(key) # a single element (or nil) + # s.slice(start, length) # up to length elements at index >= start + # s.slice(range) # elements with indexes within the range + # + # @return [Object|Sarah] + # @since 2.0.0 + def slice (*args) + case args.size + when 1 + if args[0].is_a? Range + range = args[0] + new_similar(:hash => pairs_at(*keys(:ary).select do |key| + range.include? key + end)) + else fetch args[0], nil + end + when 2 + in_range = [] + catch :index_error do + start = _adjust_key args[0] + in_range = keys(:ary).select { |key| key >= start } + end + new_similar(:hash => pairs_at(*in_range.slice(0, args[1]))) + else nil + end + end - # Return the random-access hash keys. + alias_method :seq_slice, :slice + + # Extract (delete) and return an element, or a slice of elements from + # the sequential + sparse array as a new Sarah. Elements are unset + # rather than deleted when {#negative_mode=} :actual. # + # NOTES: This implementation is new since 2.0.0 and incompatible with + # prior versions. Alias #seq_slice! is deprecated since 2.0.0. + # + # s.slice!(key) # a single element (or nil) + # s.slice!(start, length) # up to length elements at index >= start + # s.slice!(range) # elements with indexes within the range + # + # @return [Object|Sarah] + # @since 2.0.0 + def slice! (*args) + case args.size + when 1 + if args[0].is_a? Range then res = slice *args + else return has_key?(args[0]) ? delete_key(args[0]) : nil + end + when 2 then res = slice *args + else return nil + end + res.reverse_each(:all) { |key, value| delete_key key } + end + + alias_method :seq_slice!, :slice! + + # Return a sorted list of sequential and sparse array values. + # Accepts an optional block to compare pairs of values. + # + # s.sort + # s.sort { |a, b| block } + # # @return [Array] - def rnd_keys; @rnd.keys; end + # @since 2.0.0 + def sort + if block_given? then values(:ary).sort { |a, b| yield a, b } + else values(:ary).sort + end + end - # Return all the keys. + # Replace the sequential and sparse array values by a sorted + # sequential list of values. Accepts an optional block to compare + # pairs of values. # + # s.sort! + # s.sort! { |a, b| block } + # + # @return [Sarah] + # @since 2.0.0 + def sort! + if block_given? then @seq = values(:ary).sort { |a, b| yield a, b } + else @seq = values(:ary).sort + end + @ary_first, @ary_next, @spr = 0, @seq.size, {} + self + end + + # #sort_by is not implemented. + + # #take is not implemented. + + # #take_while is not implemented. + + # Return all or part of the structure in array representation. + # + # @param which [:all|:ary|:rnd|:seq|:spr] The parts to represent. + # @since 2.0.0 + def to_a (which = :all) + ary, hsh = [], {} + case which when :all, :ary, :seq then ary = @seq end + case which when :all, :ary, :spr then hsh.merge! @spr end + case which when :all, :rnd then hsh.merge! @rnd end + ary + [hsh] + end + + # Return all or part of the structure in hash representation. + # + # @param which [:all|:ary|:rnd|:seq|:spr] The parts to represent. + # @since 2.0.0 + def to_h (which = :all) + hsh = {} + case which when :all, :ary, :seq + @seq.each_index { |i| hsh[i] = @seq[i] } + end + case which when :all, :ary, :spr then hsh.merge! @spr end + case which when :all, :rnd then hsh.merge! @rnd end + hsh + end + + # #transpose is not implemented. + + # Return unique sequential and sparse values as a sequential list. + # + # s.uniq + # s.uniq { |item| block } + # # @return [Array] - def keys; seq_keys.to_a + rnd_keys; end + # @since 2.0.0 + def uniq + if block_given? then values(:ary).uniq { |item| yield item } + else values(:ary).uniq + end + end - # Return the hash values. - def rnd_values; @rnd.values; end + # Replace sequential and sparse values with sequential unique values. + # + # s.uniq! + # s.uniq! { |item| block } + # + # @return [Sarah] + # @since 2.0.0 + def uniq! + if block_given? then @seq = values(:ary).uniq { |item| yield item } + else @seq = values(:ary).uniq + end + @ary_first, @ary_next, @spr = 0, @seq.size, {} + self + end - # Return all the values. - def values; @seq + @rnd.values; end + # Unset a specific index or key (without reindexing other values). + # + # @since 2.0.0 + def unset_at (key) + if key.is_a? Integer + res = nil + catch :index_error do + key = _adjust_key key + if key >= 0 && key < @seq.size + _seq_to_spr key + 1 + res = @seq.pop + else + res = @spr.delete key + end + _scan_spr + end + res + else + @rnd.delete key + end + end - alias_method :seq_values, :seq + alias_method :unset_key, :unset_at - # Slice all. + # Unsets each value for which the required block returns true. # - # @return [Sarah, Object] - def slice (*params) - res = @seq.slice *params - (res.is_a? Array) ? (self.class.new :array => res, :hash => @rnd, - :default_proc => @default_proc, :default => @default) : res + # Subsequent values are never re-indexed. See also {#delete_if}. + # + # The block is passed the current value and index for the sequential + # and sparse arrays or the current value and key (in that order) + # for the random-access hash. + # + # s.unset_if { |value, key| block } + # + # @param which [:all|:ary|:rnd] The data structures in which to unset. + # @since 2.0.0 + # @return [Sarah] + def unset_if (which = :all) + case which when :all, :ary + @seq.each_index do |index| + if yield @seq[index], index + unset_at index + break # Any other sequentials are now sparse + end + end + @spr.keys.sort.each do |key| + @spr.delete(key) if yield @spr[key], key + end + _scan_spr + end + case which when :all, :rnd + @rnd.delete_if { |key, value| yield value, key } + end + self end - # Slice all in place. + # Unset by value. # - # @return [Sarah, Object] - def slice! (*params) - res = @seq.slice! *params - (res.is_a? Array) ? (self.class.new :array => res, :hash => @rnd, - :default_proc => @default_proc, :default => @default) : res + # Subsequent values are never re-indexed. See also {#delete_value}. + # + # @param what [Object] The value to be deleted + # @param which [:all|:ary|:rnd] The data structures in which to unset. + # @return [Sarah] + # @since 2.0.0 + def unset_value (what, which = :all) + unset_if(which) { |value, key| value == what } end - # Slice sequential. + # Unshift (insert) sequential values onto the beginning of the + # sequential or sparse array. # - # @return [Sarah, Object] - def seq_slice (*params) - res = @seq.slice *params - (res.is_a? Array) ? (self.class.new :array => res, - :default_proc => @default_proc, :default => @default) : res + # Subsequent values are re-indexed except when {#negative_mode=} :actual. + # + # @param vlist [Array] A list of values to unshift (insert). + # @return [Sarah] + def unshift (*vlist) + if @negative_mode == :actual + vlist.reverse_each do |value| + @ary_first -= 1 + self[@ary_first] = value + end + else + _scan_spr vlist.size + @seq.unshift *vlist + end + self end - # Slice sequential in place. + # Return an array of values. # - # @return [Sarah, Object] - def seq_slice! (*params) - res = @seq.slice! *params - (res.is_a? Array) ? (self.class.new :array => res, - :default_proc => @default_proc, :default => @default) : res + # @param which [:all|:ary|:rnd|:seq|:spr] Which values to return. + # (Since 2.0.0) + # @return [Array] + def values (which = :all) + case which + when :all then @seq + values(:spr) + @rnd.values + when :ary then @seq + values(:spr) + when :rnd then @rnd.values + when :seq then Array.new @seq + when :spr then @spr.values_at(*@spr.keys.sort) + else [] + end end + # Return the values corresponding to the list of keys/indexes. + # + # @return [Array] + # @since 2.0.0 + def values_at (*list); list.collect { |key| self[key] }; end + + # Zip sequential and sparse array values with other arrays. + # See Array#zip. + # + # @return [Array] + # @since 2.0.0 + def zip (*args); values(:ary).zip(*args); end + + ##### Protected methods ##### + + protected + + def _adjust_key (key) + case @negative_mode + when :error + raise IndexError.new('Index is too small') if key < -@ary_next + when :ignore + throw :index_error, nil if key < -@ary_next + end + if key < 0 && @negative_mode != :actual then key + @ary_next + else key + end + end + + # Extract integer-keyed or other parts from a hash + def _hash_filter (h, type) + case type + when :array then h.select { |k, v| k.is_a?(Integer) && k >= 0 } + when :other then h.select { |k, v| !k.is_a?(Integer) || k < 0 } + end + end + + # Scan the sparse array for min and max, possibly adjusting keys + # as we go. + def _scan_spr (adjustment = 0, from = 0) + @ary_first = @seq.empty? ? nil : 0 + @ary_next = @seq.size + + adj = {} # adjusted sparse array + @spr.each do |key, value| + if adjustment != 0 + key += adjustment if key >= from + adj[key] = value + end + @ary_first = key if !@ary_first || key < @ary_first + @ary_next = key + 1 if key >= @ary_next + end + @ary_first ||= 0 + @spr = adj unless adj.empty? + end + + # Migrate selected sequential values to the sparse hash. + def _seq_to_spr (index = 0) + (@seq.size - 1).downto(index) { |i| @spr[i] = @seq.pop } + end + + # Migrate newly adjacent sparse values to the sequential array. + def _spr_to_seq + @seq.push @spr.delete(@seq.size) while @spr.has_key? @seq.size + end + end + +# END