holden
holden

Reputation: 13591

ruby object array... or hash

I have an object now:

class Items
  attr_accessor :item_id, :name, :description, :rating

  def initialize(options = {})
      options.each {
        |k,v|
        self.send( "#{k.to_s}=".intern, v)
      }
  end

end

I have it being assigned as individual objects into an array...

@result = []

some loop>>
   @result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>

But instead of assigning my singular object to an array... how could I make the object itself a collection?

Basically want to have the object in such a way so that I can define methods such as

def self.names
   @items.each do |item|
      item.name
   end
end

I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.

Upvotes: 2

Views: 7358

Answers (4)

Whoever
Whoever

Reputation: 43

Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by

def method_missing(symbol_s, *arguments)
  symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
  if s == 's' and arguments.empty?
    select do |i|
      i.respond_to?(symbol) && i.instance_variables.include?("@#{symbol}")
    end.map {|i| i.send(symbol)}
  else
    super
  end
end

For his example data you will get following outputs:

puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems

As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.

Upvotes: 1

tadman
tadman

Reputation: 211740

A few observations before I post an example of how to rework that.

  • Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
  • Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.

One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:

class Item
  attr_accessor :item_id, :name, :description, :rating

  def initialize(options = { })
    options.each do |k,v|
      method = :"#{k}="

      # Check that the method call is valid before making it
      if (respond_to?(method))
        self.send(method, v)
      else
        # If not, produce a meaningful error
        raise "Unknown attribute #{k}"
      end
    end
  end
end

class ItemsCollection < Array
  # This collection does everything an Array does, plus
  # you can add utility methods like names.

  def names
    collect do |i|
      i.name
    end
  end
end

# Example

# Create a custom collection
items = ItemsCollection.new

# Build a few basic examples
[
  {
    :item_id => 1,
    :name => 'Fastball',
    :description => 'Faster than a slowball',
    :rating => 2
  },
  {
    :item_id => 2,
    :name => 'Jack of Nines',
    :description => 'Hypothetical playing card',
    :rating => 3
  },
  {
    :item_id => 3,
    :name => 'Ruby Book',
    :description => 'A book made entirely of precious gems',
    :rating => 1
  }
].each do |example|
  items << Item.new(example)
end

puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book

Upvotes: 6

Whoever
Whoever

Reputation: 43

Do you know the Ruby key word yield?

I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:

class Items
  @items = []
  class << self
    attr_accessor :items
  end
  attr_accessor :name, :description
  def self.each(&args)
    @items.each(&args)
  end
  def initialize(name, description)
    @name, @description = name, description
    Items.items << self
  end
  def each(&block)
    yield name
    yield description
  end
end

a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}

This outputs

mug
cup

mug
a big cup

Did you ask for something like Items.each or a.each or for something completely different?

Upvotes: 2

Ariejan
Ariejan

Reputation: 11079

The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.

Add 'return self' as the last line of initialize and you're golden.

Class Item
  def initialize(options = {})
    ## Do all kinds of stuff. 

    return self
  end
end

Upvotes: -1

Related Questions