Class: Nodepile::KeyedArrayAccessor

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/nodepile/keyed_array.rb

Overview

Class makes an array of values behave like a hash. Intended to be used for rendering records from a tabular data source.

Defined Under Namespace

Classes: PseudoROHashForDeconstruct

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(keys_array, values_array, extensible: true, source: nil, ref_num: nil, metadata: nil, metadata_key_prefix: '') ⇒ KeyedArrayAccessor

Note that this method will always freeze and retain a reference to the keys_array that is passed in. Note that this method may use a reference to the values_array passed in (allowing later mutation). See the copy parameter for override.

Parameters:

  • keys_array (Array)

    The keys in order (think column header names). Note that these need not be unique (although this non-unique keys will have effect on many methods).

  • values_array (Array, nil)

    Array of values. Should be the same size as the keys array and provides the value of each corresponding key. Note that the newly created object maintains a reference to the array passed in, so if you want to protect from side effects, you should use a copy of your values array. If passed nil, creates an array composed entirely of nil values.

  • extensible (Boolean) (defaults to: true)

    indicates whether adding keys is permitted

  • source (String, nil, Object) (defaults to: nil)

    If provided on creation, will be returned by the #source method. Typically indicates the file or other source of the record.

  • ref_num (Integer, nil) (defaults to: nil)

    If provided on creation, will be returned by the #ref_num method. Typically indicates the relative position of the record within a given source.

  • metadata (#each) (defaults to: nil)

    Read-only data that will be retained with this record. It will be extracted from the object passed in using #each (which works great if the object is a hash or array of two element arrays). NOTE: A metadata key can hide the value of standard keys unless metadata_key_prefix is set to nil. Note that if a key prefix has been provided but does not appear in the keys resulting from the metadata object, then the prefix will be prepended to the key. Except where explicitly noted below, methods do not alter, access, or consider metadata values.

  • metadata_key_prefix (String, nil) (defaults to: '')

    If nil, metadata values cannot be retrieved via the square bracket operators #[]. If non-nil Then keys passed to the square bracket operator will be first tested to see if they have the metadata_key_prefix. See the #[] operator for details about how the prefix is treated.



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/nodepile/keyed_array.rb', line 47

def initialize(keys_array, values_array,  extensible: true,source: nil, ref_num: nil,
               metadata: nil, metadata_key_prefix: ''
              )
    raise "keys must all be of type String or nil" unless keys_array.all?{|k| k.nil? || k.is_a?(String)}
    @keys = keys_array.freeze
    @vals = values_array || Array.new(@keys.length){nil}
    @extensible = extensible
    @source = source
    @ref_num = ref_num
    (,metadata_key_prefix: ) if 
end

Instance Attribute Details

#extensibleObject

Returns the value of attribute extensible.



8
9
10
# File 'lib/nodepile/keyed_array.rb', line 8

def extensible
  @extensible
end

#ref_numObject

Returns the value of attribute ref_num.



101
102
103
# File 'lib/nodepile/keyed_array.rb', line 101

def ref_num
  @ref_num
end

#sourceObject

Returns the value of attribute source.



101
102
103
# File 'lib/nodepile/keyed_array.rb', line 101

def source
  @source
end

Class Method Details

.bulk_overlay(kaa_enumerable) ⇒ KeyedArrayAccessor?

Repeatedly overlays successive KeyedArrayAccessor with the first one being at the bottom and the last one being at the top.

Returns:



343
344
345
# File 'lib/nodepile/keyed_array.rb', line 343

def self.bulk_overlay(kaa_enumerable)
    kaa_enumerable.inject(nil){|accum,kaa|  (accum||kaa.dup).underlay!(kaa)  }
end

Instance Method Details

#==(otr) ⇒ Object

Equality comparison is very tolerant and may not be what you expect.

  • Hashes are deemed equal if they have the same unique keys and the key-value pairs retrieved via #[] are equal.

  • Arrays are deemed equal if the to_a() representation of self matches the other array.

  • Another KeyedArrayAccessor is deemed equal using one of two rules. For #conforms? true objects, the exact value of keys and values is compared. For #conforms? false objects, the set of distinct keys is the same in both arrays and the value associated with each key using #[] is the same.

Another KeyedAccessArray is deemed equal if the key-value pairs have the same number of keys and are equal (which means that only the first of duplicate columns is compared)

Note: Metadata is not considered for purposes of this comparison.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/nodepile/keyed_array.rb', line 205

def ==(otr)
    return true if self.equal?(otr)
    case otr
        in Hash
            return ((@keys + otr.keys)-(@keys&otr.keys)).empty? && 
                                                otr.all?{|k,v| self[k] == v} 
        in Array
            return @vals == otr
        in KeyedArrayAccessor
            if self.conforms?(otr)
                return @vals == otr._internal_vals
            else
                return ((@keys + otr._internal_keys) - (@keys & otr._internal_keys)).empty? &&
                                                                 @keys.all?{|k| self[k] == otr[k]}
            end
        else 
            return false  # currently no other types are supported
        end  #pattern match

        
end

#[](key) ⇒ Object

Provides hash-style access to a value by it’s key (rather than its position). Note that if duplicate keys exist, the leftmost key is returned. Returns the value of the key or quietly returns nil if the key isn’t found

Note, if the object has metadata, and the metadata_key_prefix is not nil, this method will attempt to retrieve metadata matching the key before retrieving the normal key data. If the metadata does not contain the requested key, this will check for a match of the normal data.



247
248
249
250
# File 'lib/nodepile/keyed_array.rb', line 247

def [](key) 
    return @meta[key] if @meta_key_prefix && key.start_with?(@meta_key_prefix) && @meta&.include?(key)
    @keys.index(key)&.tap{|ix| return @vals[ix]}
end

#[]=(key, new_val) ⇒ Object

Uses a hash-style access to update values. Note that becuase this data structure does not enforce uniqueness of keys, this method will only update the leftmost value corresponding to the given key.

Important note. Adding a new key to the object will make it non-conforming with other objects.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/nodepile/keyed_array.rb', line 259

def []=(key,new_val)
    ix = @keys.index(key) 
    if ix
        (@vals[ix] = new_val) if ix  # simple case... update existing value
    else # ix.nil?
        raise <<~ERRMSG if !extensible
            Because the #extensible() attribute is set to false, a new key may not be added [#{key}]
           ERRMSG
        # new keys are appended to the right side
        @keys = (@keys.dup << key)
        @vals << new_val
    end
    return new_val
end

#clear?(*key_names) ⇒ Boolean

Returns true if the value for each provided key is nil

Returns:

  • (Boolean)

    Returns true if the value for each provided key is nil



117
118
119
120
# File 'lib/nodepile/keyed_array.rb', line 117

def clear?(*key_names)
    key_names.flatten!
    return key_names.all?{|k| self[k].nil?}
end

#clear_blanksself

clear blanks will replace all fields where the value is pure whitespace with a nil instead. Note that nils get special treatment in operations like #overlay()

Returns:

  • (self)


111
112
113
114
# File 'lib/nodepile/keyed_array.rb', line 111

def clear_blanks()
    @vals.transform_values{|v| (v.is_a?(String) && /^\s*$/.match?(v)) ? nil : v }
    return self
end

#clearedObject

Return a copy of self where all values have been cleared. Metadata is



105
# File 'lib/nodepile/keyed_array.rb', line 105

def cleared() = self.class.new(@keys,Array.new(@keys.length){nil})

#conforms?(otr) ⇒ Boolean

indicated that the object has exactly the same keys in exactly the same order

Returns:

  • (Boolean)


276
277
278
# File 'lib/nodepile/keyed_array.rb', line 276

def conforms?(otr)
    return otr.is_a?(self.class) && @keys == otr._internal_keys
end

#deconstruct_keys(keys) ⇒ Object

Note that deconstruct_keys will expose metadata values regardless of the choice of @metadata_key_prefix. Although, an actual key with the same name as a metadata value will hide the metadata value.



361
362
363
364
365
# File 'lib/nodepile/keyed_array.rb', line 361

def deconstruct_keys(keys)
    # Developer note:  I think the below line should work as desired because this object is so
    # much like a hash, but possibly it'll be necessary to support one or more methods.
    return PseudoROHashForDeconstruct.new(self)
end

#dupObject

Copy self, including metadata, source, and ref_num



60
61
62
63
64
# File 'lib/nodepile/keyed_array.rb', line 60

def dup 
    self.class.new(@keys,@vals.dup,extensible: @extensible, metadata: @meta, 
                   source: @source, ref_num: @ref_num,metadata_key_prefix: @meta_key_prefix
                  )
end

#each {|key, value| ... } ⇒ Object

Yields:

  • (key, value)


148
149
150
151
# File 'lib/nodepile/keyed_array.rb', line 148

def each
    return enum_for(:each) unless block_given?
    @keys.each_with_index{|k,i| yield(k,@vals[i]) }
end

#each_empty_key(yield_index = false) ⇒ Void, Enumerator

An empty key is a key whose value is nil.

Parameters:

  • yield_index (Boolean) (defaults to: false)

    If true, yields the internal storage index number rather than the key name. Note that internal index number is the same for objects that #conforms?()

Returns:

  • (Void, Enumerator)


163
164
165
166
167
# File 'lib/nodepile/keyed_array.rb', line 163

def each_empty_key(yield_index = false)
    return enum_for(:each_key_blank, yield_index) unless block_given?
    @keys.each_with_index{|k,i| yield(yield_index ? i : k) if @vals[i].nil?}
    return nil
end

#each_filled_pair(yield_index_instead_of_val = false) ⇒ Void, Enumerator

A filled key is a key whose value is not nil. The block is yielded with the key and value (or index and value depending on yield_index parameter).

Parameters:

  • yield_index_instead_of_val (Boolean) (defaults to: false)

    If true, yields the internal storage index number rather than the key name. Note that internal index number is the same for objects that #conforms?()

Returns:

  • (Void, Enumerator)


175
176
177
178
179
# File 'lib/nodepile/keyed_array.rb', line 175

def each_filled_pair(yield_index_instead_of_val = false)
    return enum_for(:each_key_nonblank, yield_index_instead_of_val) unless block_given?
    @keys.each_with_index{|k,i| yield((yield_index_instead_of_val ? i : k),@vals[i]) unless @vals[i].nil?}
    return nil
end

#each_keyObject



134
135
136
137
# File 'lib/nodepile/keyed_array.rb', line 134

def each_key
    return enum_for(:each_key) unless block_given?
    @keys.each{|k| yield k }
end

#each_value(&block) ⇒ Object



153
154
155
156
# File 'lib/nodepile/keyed_array.rb', line 153

def each_value(&block)
    return enum_for(:each_value) unless block_given?
    @vals.each{|v| yield(v)}
end

#include?(key) ⇒ Boolean

Returns:

  • (Boolean)


228
# File 'lib/nodepile/keyed_array.rb', line 228

def include?(key) =  return @keys.include?(key)

#keysObject



131
# File 'lib/nodepile/keyed_array.rb', line 131

def keys = @keys

#kv_map!(&kv_receiver) ⇒ void

This method returns an undefined value.

Similar to a Hash’s #map! function



141
142
143
144
145
# File 'lib/nodepile/keyed_array.rb', line 141

def kv_map!(&kv_receiver)
    raise "Block required" unless block_given?
    @keys.each_with_index{|k,i| @vals[i] = yield(k,@vals[i])}
    return nil
end

#lengthObject



189
# File 'lib/nodepile/keyed_array.rb', line 189

def length = self.size

#merge(otr_hashlike) ⇒ Object



237
# File 'lib/nodepile/keyed_array.rb', line 237

def merge(otr_hashlike) = self.dup.merge!(otr_hashlike)

#merge!(otr_hashlike) ⇒ Object

Other object must support Note: metadata is left unchanged by this method.



232
233
234
235
236
# File 'lib/nodepile/keyed_array.rb', line 232

def merge!(otr_hashlike)
    raise "Block handling not yet supported by this method" if block_given?
    otr_hashlike.each_key{|k| self[k] = otr_hashlike[k]}
    return self
end

#metadata(key) ⇒ Object

retrieve metadata value

Parameters:

  • key (String)

    Note that the key passed in should start with the metadata_key_prefix if one has been specified.



97
# File 'lib/nodepile/keyed_array.rb', line 97

def (key) = @meta[key]

#metadata_include?(key) ⇒ Boolean

Returns:

  • (Boolean)


98
# File 'lib/nodepile/keyed_array.rb', line 98

def (key) = @meta.include?(key)

#metadata_key_prefixObject



99
# File 'lib/nodepile/keyed_array.rb', line 99

def  = @metadata_key_prefix

#overlay(lower_kaa) ⇒ Object

See #overlay!() except this creates a copy rather than altering self.



315
# File 'lib/nodepile/keyed_array.rb', line 315

def overlay(lower_kaa) = lower_kaa.underlay(self)

#overlay!(lower_kaa) ⇒ self

See #underlay() An important difference between overlay and underlay is the ordering of columns in the result. Column order is in the order of the lower object plus any (non-nil) additions appearing to the right. This operation is not particularly efficient except when working with conforming arrays.

Note: metadata is unchanged by this method.

Returns:

  • (self)


327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/nodepile/keyed_array.rb', line 327

def overlay!(lower_kaa)
    return self if self.equal?(lower_kaa) #no-op
    if conforms?(lower_kaa)
        lower_kaa._each_value_with_index(true){|lower_val,ix| @vals[ix] ||= lower_val}
    else
        new_arr = lower_kaa.dup.underlay!(self)
        @keys = new_arr._internal_keys
        @vals = new_arr._internal_vals
        @key_count = nil
    end
    return self
end

#reset_metadata(pair_enumerable, metadata_key_prefix: :leave_prefix_unchanged) ⇒ void

This method returns an undefined value.

Dump any existing metadata and replace it with the provided metadata

Parameters:

  • pair_enumerable (#each)

    Enumerable that should return key,value pairs



69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/nodepile/keyed_array.rb', line 69

def (pair_enumerable, metadata_key_prefix: :leave_prefix_unchanged)
  @meta_key_prefix =  unless  == :leave_prefix_unchanged 
  pfx = @metadata_key_prefix || ''
  if pair_enumerable.is_a?(Hash) && pair_enumerable.each_key.all?{|k| k.start_with?(pfx)}
    @meta = pair_enumerable.dup
  else
    @meta = Hash.new
    pair_enumerable&.each{|(k,v)|
                          key = (@meta_key_prefix + k) unless @meta_key_prefix.nil? || k.start_with?(@meta_key_prefix)
                          @meta[key] = v
                         }
  end
  nil
end

#sizeObject

Note that duplications of the same key are counted toward this number



188
# File 'lib/nodepile/keyed_array.rb', line 188

def size = keys.length

#to_aObject

alias for #values



185
# File 'lib/nodepile/keyed_array.rb', line 185

def to_a = values()

#to_hObject

Note that if the same key name is duplicated multiple times, the leftmost value is used



124
125
126
127
128
129
# File 'lib/nodepile/keyed_array.rb', line 124

def to_h
    h = Hash.new
    # reverse order so that in the case of duplicates the leftmost dominates
    (-1..-@keys.length).step(-1).each{|i| h[@keys[i]] = @vals[i] }
    return h
end

#underlay(upper_kaa) ⇒ Object



316
# File 'lib/nodepile/keyed_array.rb', line 316

def underlay(upper_kaa) = self.dup.underlay!(upper_kaa)

#underlay!(upper_kaa) ⇒ self

Given a KeyedArrayAccessor objects, update self to form a “merged” KeyedArrayAccessor where self “underlays” an “upper” array to generate a merged data structure. An overlay/underlay follows these rules: If “upper” and self have non-blank for the same element, then the upper element would “overlay” the corresponding entry in self. If the upper is blank, then it does not “overlay”.

Note that the object in array position zero is at the bottom of the overlay. Random Observation: If the upper_kaa is completely populated, the lower_kaa is essentially ignored.

NOTE: When the upper_kaa does not #conforms?(), the result

will have a key set containing the union of the upper and lower keysets.
Also, overlaying non-conforming objects will have worse performance.
Note that if it is possible, for the overlay to be generated without
adding keys, this strategy will be used.

Note: Metadata for self is unchanged by this method.

Parameters:

  • upper_kaa (KeyedArrayAccessor)

    Non-blank entries here will “overlay” entries of the lower kaa. This method is a no-op if it is passed itself

Returns:

  • (self)


304
305
306
307
308
309
310
311
312
# File 'lib/nodepile/keyed_array.rb', line 304

def underlay!(upper_kaa)
    return self if self.equal?(upper_kaa) # return self (no-op)
    if conforms?(upper_kaa)
        upper_kaa._each_value_with_index(true){|upper_val,ix| @vals[ix] = upper_val}
    else
        upper_kaa.each_filled_pair(false){|key,upper_val| self[key] = upper_val}
    end
    return self
end

#update_metadata(key, value) ⇒ Object

Parameters:

  • key (String)

    If the value does not start with the metadata_prefix, it will be appended

  • value (Object, nil)


87
88
89
90
91
# File 'lib/nodepile/keyed_array.rb', line 87

def (key,value)
    @meta = Hash.new if @meta.nil?
    k = key.start_with?(@meta_key_prefix) ? k : (@meta_key_prefix + key)
    @meta&.[]=(k,value)
end

#value_at(index) ⇒ Object



227
# File 'lib/nodepile/keyed_array.rb', line 227

def value_at(index) = @vals[index]

#valuesObject



182
# File 'lib/nodepile/keyed_array.rb', line 182

def values =  return @vals.dup