I have a ruby class like this:
class Table
def initialize(name)
#name = name
#columns = {}
end
end
I'm creating different objects:
table_1 = Table.new("First")
table_2 = Table.new("Second")
table_3 = Table.new("Third")
How can I find among the objects of the Table class the object having "Second" as name attribute ?
Without creating any additional structures of data:
ObjectSpace.each_object(Table).find { |object| object.instance_variable_get(:#name) == "Second" }
=> #<Table:0x007f9f912b0ce0 #columns={}, #name="Second">
You can keep a reference to an array of instances in the class.
class Table
#instances = []
class << self
attr_accessor :instances
end
def initialize(name)
#name = name
#columns = {}
self.class.instances << self
end
end
Then you can get all the instances by
Table.instances
This, however, will prevent all the Table objects being garbage collected, so it is only feasible if you have only a small amount of Tables and that amount never grows, otherwise you'll have a memory leak.
Let's add a getter method for the name attribute
class Table
attr_reader :name
def initialize(name)
#name = name
#columns = {}
end
end
Now, if you have an array of Table objects
arr = [Table.new("First"), Table.new("Second"), Table.new("Third")]
You can find by name
arr.find { |table| table.name == "Second" }
=> #<Table:0x007f862107eb18 #name="Second", #columns={}>
You can use enumerable find or any similar method from the enumerable module:
class Table
attr_reader :name
def initialize(name)
#name = name
#columns = {}
end
end
table_1 = Table.new("First")
table_2 = Table.new("Second")
table_3 = Table.new("Third")
x = [table_1, table_2, table_3].find { |t| t.name == "Second" }
puts x.name => "Second"
Let's say we have reader on name in Table:
class Table
attr_reader :name
def initialize(name)
#name = name
#columns = {}
end
end
I'd encourage you to store these Table classes' objects in a list/array:
table_1 = Table.new("First")
table_2 = Table.new("Second")
table_3 = Table.new("Third")
tables = [table_1, table_2, table_3]
Which can then be used for finding it using find(as mentioned in one of the answers) or detect:
tables.detect { |t| t.name == "Second" } #=> table_2 object
If you'd like to go one more step ahead then we can have another class maintaining this array:
class TableList
attr_reader :tables
def initialize
#tables = tables
end
def add(table)
#tables << table
end
def find_by_name(name)
tables.detect{ |table| table.name == name }
end
end
Which can then be used as:
table_1 = Table.new("First")
table_2 = Table.new("Second")
table_3 = Table.new("Third")
table_list = TableList.new
table_list.add(table_1)
table_list.add(table_2)
table_list.add(table_3)
table_list.find_by_name('Second') #=> table_2 object
Related
I need to randomly pick a name from an array in Ruby and then check if it uppercase. So far I have:
def namegenerator
return #name.sample
end
def namechecker
if name.upcase then
check = TRUE
else
check = FALSE
end
end
It needs to be as two separate methods like this.
Something like this:
def sample_word(words)
words.sample
end
def upcase?(word)
word == word.upcase
end
And then something like:
words = %w[APPLE banana CherRy GRAPE]
word = sample_word(words)
puts word # e.g. BANANA
puts upcase?(word) # will print true
If you just want to check just the first letter:
names = %w(Kirk mccoy scott Spock)
names.sample.then { |name| [name, name[0] == name[0].upcase] }
#=> ["mccoy", false]
Maybe something like this:
class NameGenerator
def initialize(size, items)
#name = ""
#size = size
#items = items
end
def namegenerator
#name = #items.sample(#size).to_s
end
def namechecker?
#name == #name.upcase
end
def name
#name
end
end
ng = NameGenerator.new 1, ["name", "Name", "NAME"]
ng.namegenerator
puts ng.name, ng.namechecker?
Update
I've posted code without much thinking about abstraction and i think it would be much better to encapsulate name and upper case check to separate class and make it immutable, then make generator class that selects one entity from collection.
class NameGenerator
def initialize(items)
#items = items
end
def next
#items.sample
end
end
class Name
attr_reader :name
def initialize(name)
#name = name
end
def is_uppercase?
#name.match(/\p{Lower}/) == nil
end
end
ng = NameGenerator.new [
Name.new("name"),
Name.new("Name"),
Name.new("NAME"),
Name.new("na-me")
]
name = ng.next
puts name.name, name.is_uppercase?
modify this code to create search by first name using user input output must show age and title
# A simple Employee class
class Employee
attr_reader :first_name, :last_name, :title, :age
def initialize(fname, lname, title, age)
#first_name = fname
#last_name = lname
#title = title
#age = age
end
# A string representation of the Employee object
def to_s
"#{first_name} #{last_name}, #{title}, #{age}"
end
end
# The collection class for Employee objects
class Employees
include Enumerable
def initialize
#employees = []
end
# Add Employee objects to the collection
def <<(employee)
#employees << employee
end
# Method mandated by the Enumerable module
def each
#employees.each { |e| yield(e) }
end
end
employees = Employees.new
employees << Employee.new('Anita', 'Baker', 'President', 48)
employees << Employee.new('Frank', 'Gifford', 'Director', 58)
employees << Employee.new('Barbara', 'Eden', 'Secretary', 34)
employees << Employee.new('George', 'Clooney', 'Project Manager', 37)
employees << Employee.new('Emily', 'Davies', 'Programmer', 28)
employees << Employee.new('David', 'Faber', 'Programmer', 55)
employees << Employee.new('Cindy', 'Adams', 'Programmer', 33)
employees << Employee.new('Helen', 'Hamilton', 'Business Analyst', 42)
You have already done all the necessary work by implementing Enumerable interface. Just accept user input and call the Enumerable#find method:
fname = gets.chomp
e = employees.find {|i| i.first_name == fname}
class Employee
EMP = []
attr_reader :name, :hobbies, :friends
def initialize(name)
#name = name
#hobbies = []
#friends = []
EMP << self
end
end
Can we discuss what happens at this line please: EMP << self ?
Obviously an element is added to the existing array (the array called EMP) - that is what is implied by the << symbol.
But, which is the element that is added? Is it only #name and do we know it is only the #name variable because it is the only argument from the initialize method?
What if the initialize method had 2 arg:
def initialize(name, hob)
#name = name
#hobbies = hob
#friends = []
EMP << self
end
What would then be the effect of EMP << self? Thank you in advance.
The keyword self inside an instance method is a reference to the current object. So you are adding the object that is being intialized to the EMP array of the object itself - a thing that doesn't really make sense!. the class Employee.
You may want to add the object to a class variable, which can be defined this way:
class Employee
##EMP = []
def initialize(name)
#name = name
#hobbies = []
#friends = []
##EMP << self
end
end
Thus, every time a new object is initialized, it's added to the ##EMP array of the class itself.
For instance,
s1 = Student.new(1, "Bob", "Podunk High")
hash[1] = s1
puts hash[1].name #produces "Bob"
s1.id = 15
puts hash[15].name #produces "Bob"
puts hash[1].name #fails
This is not exactly Hash-like behavior and insertions with the wrong key still needs to be defined.
While I can certainly roll my own container that behaves this way but it will be hard to make it fast, ie not search through the whole container every time [] is called. Just wondering if someone smarter has already made something I can steal.
EDIT: Some good ideas below helped me focus my requirements:
avoid the O(n) lookup time
allow multiple containers to the same object (association not composition)
have different data types (eg. that might use name instead of id) without too much reimplementation
You can implement it yourself.
Look at the draft solution:
class Campus
attr_reader :students
def initialize
#students = []
end
def [](ind)
students.detect{|s| s.id == ind}
end
def <<(st)
raise "Yarrr, not a student" if st.class != Student
raise "We already have got one with id #{st.id}" if self[st.id]
students << st
end
end
class Student
attr_accessor :id, :name, :prop
def initialize(id, name, prop)
#id, #name, #prop = id, name, prop
end
end
campus = Campus.new
st1 = Student.new(1, "Pedro", "Math")
st2 = Student.new(2, "Maria", "Opera")
campus << st1
campus << st2
campus[1]
#=> Student...id:1,name:pedro...
campus[2].name
#=> Maria
campus[2].id = 10
campus[2]
#=> error
campus[10].name
#=> Maria
Or you can play around Array class (or Hash, if you really need it):
class StrangeArray < Array
def [](ind)
self.detect{|v| v.id == ind} || raise "nothing found" # if you really need to raise an error
end
def <<(st)
raise "Looks like a duplicate" if self[st.id]
self.push(st)
end
end
campus = StrangeArray.new
campus << Student.new(15, 'Michael', 'Music')
campus << Student.new(40, 'Lisa', 'Medicine')
campus[1]
#=> error 'not found'
campus[15].prop
#=> Music
campus[15].id = 20
campus[20].prop
#=> Music
etc
And after #tadman's correct comment you can use reference to your hash right into your Student class:
class Student
attr_accessor :name, :prop
attr_reader :id, :campus
def initialize(id, name, prop, camp=nil)
#id, #name, #prop = id, name, prop
self.campus = camp if camp
end
def id=(new_id)
if campus
rase "this id is already taken in campus" if campus[new_id]
campus.delete id
campus[new_id] = self
end
#id = new_id
end
def campus=(camp)
rase "this id is already taken in campus" if camp[id]
#campus = camp
camp[#id] = self
end
end
campus = {}
st1 = Student.new(1, "John", "Math")
st2 = Student.new(2, "Lisa", "Math", campus)
# so now in campus is only Lisa
st1.campus = campus
# we've just pushed John in campus
campus[1].name
#=> John
campus[1].id = 10
campus[10].name
#=> John
While the Hash object might not behave the way you want it to, you can always customize the objects being inserted to be hashed a particular way.
You can do this by adding two new methods to your existing class:
class Student
def hash
self.id
end
def eql?(student)
self.id == student.id
end
end
By defining hash to return a value based on id, Hash will consider these two candidates for the same spot in the hash. The second definition declares "hash equivalence" between any two objects that have the same hash value.
This will work well provided your id values fit into a conventional 32-bit Fixnum and aren't 64-bit BIGINT database values.
As fl00r points out, this will only work if your id is immutable. For most databases this tends to be the case. Changing id on the fly is probably a really bad idea, though, as it can lead to total chaos and mind-blowing bugs.
This is a hard problem. Database vendors can make money because it is a hard problem. You are basically looking to implement traditional RDBMS indices: search through derived data, to provide fast lookup to the data it was derived from, while allowing that data to change. If you want to access the data from multiple threads, you'll quickly run into all the issues that make it hard to make a database ACID compliant.
I suggest putting the data into a database, adding the necessary indices and letting the database -- an application optimized for this exact purpose -- do the work.
The container must be notified when your key has been changed, otherwise you must search the key on the fly in lg(n).
If you rarely change key and lookup a lot, just rebuild the hash:
def build_hash_on_attribute(objects, attribute)
Hash[objects.collect { |e| [e.send(method), e] }]
end
s1 = OpenStruct.new id: 1, name: 's1'
h = build_hash_on_attribute([s1], :id)
h[1].name # => 's1'
h[1].id = 15
# rebuild the whole index after any key attribute has been changed
h = build_hash_on_attribute(h.values, :id)
h[1] # => nil
h[15].name # => 's1'
Update 02/12: Add a solution using observer pattern
Or you do need such automatically index building, you can use observer pattern like below or decorator pattern. But you need to use the wrapped objects in decorator pattern.
gist: https://gist.github.com/1807324
module AttrChangeEmitter
def self.included(base)
base.extend ClassMethods
base.send :include, InstanceMethods
end
module ClassMethods
def attr_change_emitter(*attrs)
attrs.each do |attr|
class_eval do
alias_method "#{attr}_without_emitter=", "#{attr}="
define_method "#{attr}_with_emitter=" do |v|
previous_value = send("#{attr}")
send "#{attr}_without_emitter=", v
attr_change_listeners_on(attr).each do |listener|
listener.call self, previous_value, v
end
end
alias_method "#{attr}=", "#{attr}_with_emitter="
end
end
end
end
module InstanceMethods
def attr_change_listeners_on(attr)
#attr_change_listeners_on ||= {}
#attr_change_listeners_on[attr.to_sym] ||= []
end
def add_attr_change_listener_on(attr, block)
listeners = attr_change_listeners_on(attr)
listeners << block unless listeners.include?(block)
end
def remove_attr_change_listener_on(attr, block)
attr_change_listeners_on(attr).delete block
end
end
end
class AttrChangeAwareHash
include Enumerable
def initialize(attr = :id)
#attr = attr.to_sym
#hash = {}
end
def each(&block)
#hash.values.each(&block)
end
def on_entity_attr_change(e, previous_value, new_value)
if #hash[previous_value].equal? e
#hash.delete(previous_value)
# remove the original one in slot new_value
delete_by_key(new_value)
#hash[new_value] = e
end
end
def add(v)
delete(v)
v.add_attr_change_listener_on(#attr, self.method(:on_entity_attr_change))
k = v.send(#attr)
#hash[k] = v
end
alias_method :<<, :add
def delete(v)
k = v.send(#attr)
delete_by_key(k) if #hash[k].equal?(v)
end
def delete_by_key(k)
v = #hash.delete(k)
v.remove_attr_change_listener_on(#attr, self.method(:on_entity_attr_change)) if v
v
end
def [](k)
#hash[k]
end
end
class Student
include AttrChangeEmitter
attr_accessor :id, :name
attr_change_emitter :id, :name
def initialize(id, name)
self.id = id
self.name = name
end
end
indexByIDA = AttrChangeAwareHash.new(:id)
indexByIDB = AttrChangeAwareHash.new(:id)
indexByName = AttrChangeAwareHash.new(:name)
s1 = Student.new(1, 'John')
s2 = Student.new(2, 'Bill')
s3 = Student.new(3, 'Kate')
indexByIDA << s1
indexByIDA << s3
indexByIDB << s1
indexByIDB << s2
indexByName << s1
indexByName << s2
indexByName << s3
puts indexByIDA[1].name # => John
puts indexByIDB[2].name # => Bill
puts indexByName['John'].id # => 1
s2.id = 15
s2.name = 'Batman'
p indexByIDB[2] # => nil
puts indexByIDB[15].name # => Batman
indexByName.each do |v|
v.name = v.name.downcase
end
p indexByName['John'] # => nil
puts indexByName['john'].id # => 1
p indexByName.collect { |v| [v.id, v.name] }
# => [[1, "john"], [3, "kate"], [15, "batman"]]
indexByName.delete_by_key 'john'
indexByName.delete(s2)
s2.id = 1 # set batman id to 1 to overwrite john
p indexByIDB.collect { |v| [v.id, v.name] }
# => [[1, "batman"]]
p indexByName.collect { |v| [v.id, v.name] }
# => [[3, "kate"]]
In the following code, the issue is that after calling method .find_name on an object type of LogsCollection, the returned object becomes a native array and does not remain type LogsCollection. I believe the correct approach might be to create a constructor/initializer that accepts an array and return a brand new object of the correct type. But I am not sure there is not a better way to accomplish this?
Can a Ruby-pro eyeball this code and suggest (at the code level) the best way to make the returned object from .find_name remain type LogsCollection (not array)?
class Log
attr_accessor :name, :expense_id, :is_excluded, :amount, :paid_to
def initialize(name, expense_id, is_excluded, amount, paid_to)
#name = name
#expense_id = expense_id
#is_excluded = is_excluded
#amount = amount
#paid_to = paid_to
end
end
class LogsCollection < Array
def names
collect do |i|
i.name
end
end
def find_name(name)
#name = name
self.select { |l| l.name == #name }
end
end
logs = LogsCollection.new
logs.push(Log.new('Smith', 1, false, 323.95, nil))
logs.push(Log.new('Jones', 1, false, 1000, nil))
logs = logs.find_name('Smith')
puts logs.count
unless logs.empty?
puts logs.first.name # works since this is a standard function in native array
puts logs.names # TODO: figure out why this fails (we lost custom class methods--LogsCollection def find_name returns _native_ array, not type LogsCollection)
end
Final code post-answer for anyone searching (note the removal of base class < array):
class Log
attr_accessor :name, :expense_id, :is_excluded, :amount, :paid_to
def initialize(name, expense_id, is_excluded, amount, paid_to)
#name = name
#expense_id = expense_id
#is_excluded = is_excluded
#amount = amount
#paid_to = paid_to
end
end
class LogsCollection
attr_reader :logs
def initialize(logs)
#logs = logs
end
def add(log)
#logs.push(log)
end
def names
#logs.collect { |l| l.name }
end
def find_name(name)
LogsCollection.new(#logs.select { |l| l.name == name })
end
end
logs = LogsCollection.new([])
logs.add(Log.new('Smith', 1, false, 323.95, nil))
logs.add(Log.new('Jones', 1, false, 1000, nil))
puts logs.names
puts '--- post .find_name ---'
puts logs.find_name('Smith').names
As you can see in the docs Enumerable#select with a block always returns an array. E.g.
{:a => 1, :b => 2, :c => 3}.select { |k,v | v > 1 }
=> [[:b, 2], [:c, 3]]
What you could do is have some sort of constructor for LogsCollection that wraps up a normal array as a LogsCollection object and call that in find_name.
As requested here's an example class (I'm at work and writing this while waiting for something to finish, it's completely untested):
class LogsCollection
attr_reader :logs
def initialize(logs)
#logs = logs
end
def names
#logs.collect { |i| i.name }
end
def find_name(n)
name = n
LogsCollection.new(#logs.select { |l| l.name == n })
end
# if we don't know a method, forward it to the #logs array
def method_missing(m, *args, &block)
#logs.send(m, args, block)
end
end
Use like
lc = LogsCollection.new
logs = lc.logs.find_name('Smith')