# # The ArrayFields module implements methods which allow an Array to be indexed # by String or Symbol. It is not required to manually use this module to extend # Array's - they are auto-extended when Array#fields= is called # module ArrayFields #{{{ VERSION = '3.5.0' # # multiton cache of fields - wraps fields and fieldpos map to save memory # class FieldSet #{{{ class << self #{{{ def new fields #{{{ @sets ||= {} obj = @sets[fields] unless obj obj = super @sets[fields] = obj end obj #}}} end #}}} end attr :fields attr :fieldpos def initialize fields #{{{ raise ArgumentError, "<#{ fields.inspect }> not inject-able" unless fields.respond_to? :inject @fieldpos = fields.inject({}) do |h, f| unless String === f or Symbol === f raise ArgumentError, "<#{ f.inspect }> neither String nor Symbol" end h[f] = h.size h end @fields = fields #}}} end def pos field #{{{ @fieldpos[field] #}}} end #}}} end # # methods redefined to work with fields as well as numeric indexes # def [](idx, *args) #{{{ if @fieldset and (String === idx or Symbol === idx) pos = @fieldset.pos idx return nil unless pos super(pos, *args) else super end #}}} end alias slice [] def []=(idx, *args) #{{{ if @fieldset and (String === idx or Symbol === idx) pos = @fieldset.pos idx unless pos @fieldset.fields << idx @fieldset.fieldpos[idx] = pos = size end super(pos, *args) else super end #}}} end def at idx #{{{ if @fieldset and (String === idx or Symbol === idx) pos = @fieldset.pos idx return nil unless pos super pos else super end #}}} end def delete_at idx #{{{ if @fieldset and (String === idx or Symbol === idx) pos = @fieldset.pos idx return nil unless pos super pos else super end #}}} end def fill(obj, *args) #{{{ idx = args.first if idx and @fieldset and (String === idx or Symbol === idx) idx = args.shift pos = @fieldset.pos idx super(obj, pos, *args) else super end #}}} end def values_at(*idxs) #{{{ idxs.flatten! if @fieldset idxs.map!{|i| (String === i or Symbol === i) ? @fieldset.pos(i) : i} end super(*idxs) #}}} end alias indices values_at alias indexes values_at def slice!(*args) #{{{ ret = self[*args] self[*args] = nil ret #}}} end def each_with_field #{{{ each_with_index do |elem, i| yield elem, @fieldset.fields[i] end #}}} end # # methods which give a hash-like interface # def each_pair #{{{ each_with_index do |elem, i| yield @fieldset.fields[i], elem end #}}} end def each_key #{{{ @fieldset.each{|field| yield field} #}}} end def each_value(*args, &block) #{{{ each(*args, &block) #}}} end def fetch key #{{{ self[key] or raise IndexError, 'key not found' #}}} end def has_key? key #{{{ @fieldset.fields.include? key #}}} end alias member? has_key? alias key? has_key? def has_value? value #{{{ if respond_to? 'include?' self.include? value else a = [] each{|val| a << val} a.include? value end #}}} end alias value? has_value? def keys #{{{ fields #}}} end def store key, value #{{{ self[key] = value #}}} end def values #{{{ if respond_to? 'to_ary' self.to_ary else a = [] each{|val| a << val} a end #}}} end def to_hash #{{{ if respond_to? 'to_ary' h = {} @fieldset.fields.zip(to_ary){|f,e| h[f] = e} h else a = [] each{|val| a << val} h = {} @fieldset.fields.zip(a){|f,e| h[f] = e} h end #}}} end alias to_h to_hash def update other #--{{{ other.each{|k,v| self[k] = v} to_hash #--}}} end def replace other #--{{{ Hash === other ? update(other) : super #--}}} end def invert #--{{{ to_hash.invert #--}}} end #}}} end # # Fieldable encapsulates methods in common for classes which may have their # fields set # module Fieldable #{{{ # # sets fields an dynamically extends this Array instance with methods for # keyword access # def fields= fields #{{{ extend ArrayFields unless defined? @fieldset @fieldset = if ArrayFields::FieldSet === fields fields else ArrayFields::FieldSet.new fields end #}}} end # # access to fieldset # attr_reader :fieldset # # access to field list # def fields #{{{ @fieldset and @fieldset.fields #}}} end #}}} end # # The Array class is extened with a methods to allow keyword access # class Array #{{{ include Fieldable #}}} end # # proxy class that allows an array to be wrapped in a way that still allows # # keyword access. also facilitate usage of ArrayFields with arraylike objects. # thnx to Sean O'Dell for the suggestion. # # sample usage # # fa = FieldedArray.new %w(zero one two), [0,1,2] # p fa['zero'] #=> 0 # # class FieldedArray #{{{ include Fieldable class << self def [](*pairs) #{{{ pairs.flatten! raise ArgumentError, "argument must be key/val paris" unless (pairs.size % 2 == 0 and pairs.size >= 2) fields, elements = [], [] #pairs.each do |f,e| while((f = pairs.shift) and (e = pairs.shift)) raise ArgumentError, "field must be String or Symbol" unless (String === f or Symbol === f) fields << f and elements << e end new fields, elements #}}} end end def initialize fields, array #{{{ @a = array self.fields = fields #}}} end def method_missing(meth, *args, &block) #{{{ @a.send(meth, *args, &block) #}}} end delegates = #{{{ %w( to_s to_str inspect ) #}}} delegates.each do |meth| class_eval "def #{ meth }(*a,&b); @a.#{ meth }(*a,&b);end" end #}}} end