Ruby Hash whose key is a function of the object? - ruby

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"]]

Related

Ruby project help. Can't get saved instances from array

TLDR I am working on a project that creates instances using an API. I want to be able to recall all of my instances but can't figure out how. I'm relatively new to Ruby and programming in general so I hope I'm explaining everything well enough. Here's my code.
class Suggestion
attr_accessor :type, :participants
attr_reader :activity, :price, :link, :key, :accessibility
##all = []
def initialize(type, participants)
#type = type
#participants = participants
# #activity = []
# #price = price
# #key = key
# #accessibility = accessibility
##all << self
end
# def save
# ##all << self
# end
def self.all
##all
end
# def events
# ##all.map
# end
def list_events
# binding.pry
Suggestion.all.map #{|event| [:activity, :type, :participants, :price, :link, :key, :accessibility]}
end
end
any and all help would be greatly appreciated
What Already Works
I may be misunderstanding your issue, but it seems like your code mostly works as-is. I refactored it a little to make it a little easier to see what's going on, but your basic approach of self-registration with a class variable during object initialization seems sound. Consider:
class Suggestion
##all = []
def initialize(type, participants)
#type = type
#participants = participants
##all << self
end
def self.all
##all
end
end
s1 = Suggestion.new :foo, %w[Alice Bob]
#=> #<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>
s2 = Suggestion.new :bar, %w[Charlie Dana]
#=> #<Suggestion:0x00007faed7113900 #participants=:bar, #type=:foo>
Suggestion.all
#=>
[#<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>,
#<Suggestion:0x00007f9671089058
#participants=["Charlie", "Dana"],
#type=:bar>]
What Might Need Improvement
You probably just need class or instance methods that "do a thing" with each instance (or selected elements from specific instances) stored in ##all. For example:
# print the object ID of each instance of Suggestion
Suggestion.all.map { |suggestion| suggestion.object_id }
#=> [240, 260]
You might also want to use Array#select to extract specific instances that meet defined criteria, or Array#map to do something with your matching instances. For example:
# re-open the class to add some getter methods
class Suggestion
attr_reader :type, :participants
end
# only operate on instances of a given type
Suggestion.all.select { |s| s.type == :foo }
#=> [#<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>]
# invoke Suggestion#participants on matching instances
Suggestion.all.map { |s| s.participants if s.type == :bar }.compact
#=> [["Charlie", "Dana"]]
As long as you have a collection through which you can iterate (e.g. your class' ##all Array) you can filter, map, or otherwise operate on some or all of your instances at need. There are certainly more complex approaches, but I'd strive to keep it as simple as possible.
Consider the following class definition, where I've made #all a class instance variable, rather than a class variable. Use of the former is generally preferred over use of the latter.
class Suggestion
attr_accessor :type, :participants
#all = []
class << self
attr_accessor :all
end
def initialize(type, participants)
#type = type
#participants = participants
self.class.all << self
end
def list_events
self.class.all.map do |instance|
{ type: instance.type, participants: instance.participants }
end
end
def self.list_events
all.map do |instance|
{ type: instance.type, participants: instance.participants }
end
end
end
The snippet
class << self
attr_accessor :all
end
creates a read-write accessor for #all. class << self causes self to be changed to Suggestion's singleton class. If desired you could replace this with
superclass.public_send(:attr_accessor, :all)
Let's try it.
Suggestion.all
#=> []
s1 = Suggestion.new(1, ['Bob', 'Sally'])
#=> #<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>]
s2 = Suggestion.new(2, ['Ronda', 'Cliff'])
#<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>,
# #<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>]
s3 = Suggestion.new(3, ['Weasel', 'Isobel'])
#=> #<Suggestion:0x00007fc677077598 #type=3, #participants=["Weasel", "Isobel"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>,
# #<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>,
# #<Suggestion:0x00007fc677077598 #type=3, #participants=["Weasel", "Isobel"]>]
s1.list_events
#=> [{:type=>1, :participants=>["Bob", "Sally"]},
# {:type=>2, :participants=>["Ronda", "Cliff"]},
# {:type=>3, :participants=>["Weasel", "Isobel"]}]
Suggestion.list_events
#=> [{:type=>1, :participants=>["Bob", "Sally"]},
# {:type=>2, :participants=>["Ronda", "Cliff"]},
# {:type=>3, :participants=>["Weasel", "Isobel"]}]
I included Suggestion#instance to show how an instance can access (read or write) the class instance variable #all.

How do I randomly select a name from an array and check if it's uppercase

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?

How to remove duplicates from array with custom objects

When I call first_array | second_array on two arrays that contain custom objects:
first_array = [co1, co2, co3]
second_array =[co2, co3, co4]
it returns [co1, co2, co3, co2, co3, co4]. It doesn't remove the duplicates. I tried to call uniq on the result, but it didn't work either. What should I do?
Update:
This is the custom object:
class Task
attr_accessor :status, :description, :priority, :tags
def initiate_task task_line
#status = task_line.split("|")[0]
#description = task_line.split("|")[1]
#priority = task_line.split("|")[2]
#tags = task_line.split("|")[3].split(",")
return self
end
def <=>(another_task)
stat_comp = (#status == another_task.status)
desc_comp = (#description == another_task.description)
prio_comp = (#priority == another_task.priority)
tags_comp = (#tags == another_task.tags)
if(stat_comp&desc_comp&prio_comp&tags_comp) then return 0 end
end
end
and when I create few instances of Task type and drop them into two different arrays and when I try to call '|' on them nothing happens it just returns array including both first and second array's elements without the duplicates removed.
No programming language for itself can be aware if two objects are different if you don't implement the correct equality methods.
In the case of ruby you need to implement eql? and hash in your class definition, as these are the methods that the Array class uses to check for equality as stated on Ruby's Array docs:
def eql?(other_obj)
# Your comparing code goes here
end
def hash
#Generates an unique integer based on instance variables
end
For example:
class A
attr_accessor :name
def initialize(name)
#name = name
end
def eql?(other)
#name.eql?(other.name)
end
def hash
#name.hash
end
end
a = A.new('Peter')
b = A.new('Peter')
arr = [a,b]
puts arr.uniq
Removes b from Array leaving only one object
Hope this helps!
The uniq method can take a block that defines what to compare the objects on. For example:
class Task
attr_accessor :n
def initialize(n)
#n = n
end
end
t1 = Task.new(1)
t2 = Task.new(2)
t3 = Task.new(2)
a = [t1, t2, t3]
a.uniq
#=> [t1, t2, t3] # because all 3 objects are unique
a.uniq { |t| t.n }
#=> [t1, t2] # as it's comparing on the value of n in the object
I tried the solution from fsaravia above and it didn't work out for me. I tried in both Ruby 2.3.1 and Ruby 2.4.0.
The solution I've found is very similar to what fsaravia posted though, with a small tweak. So here it is:
class A
attr_accessor :name
def initialize(name)
#name = name
end
def eql?(other)
hash.eql?(other.hash)
end
def hash
name.hash
end
end
a = A.new('Peter')
b = A.new('Peter')
arr = [a,b]
puts arr.uniq
Please, don't mind that I've removed the # in my example. It won't affect the solution per se. It's just that, IMO, there wasn't any reason to access the instance variable directly, given a reader method was set for that reason.
So...what I really changed is found inside the eql? method, where I used hash instead name. That's it!
If you look at the Array#| operator it says that it uses the eql?-method, which on Object is the same as the == method. You can define that by mixin in the Comparable-module, and then implement the <=>-method, then you'll get lots of comparison-methods for free.
The <=> operator is very easy to implement:
def <=>(obj)
return -1 if this < obj
return 0 if this == obj
return 1 if this > obj
end
Regarding your 'update', is this what you are doing:
a = Task.new # => #<Task:0x007f8d988f1b78>
b = Task.new # => #<Task:0x007f8d992ea300>
c = [a,b] # => [#<Task:0x007f8d988f1b78>, #<Task:0x007f8d992ea300>]
a = Task.new # => #<Task:0x007f8d992d3e48>
d = [a] # => [#<Task:0x007f8d992d3e48>]
e = c|d # => [#<Task:0x007f8d988f1b78>, #<Task:0x007f8d992ea300>, \
#<Task:0x007f8d992d3e48>]
and then suggesting that e = [a, b, a]? If so, that's the problem, because a no longer points to #<Task:0x007f8d988f1b78>. All you can say is e => [#<Task:0x007f8d988f1b78>, b, a]
I took the liberty to rewrite your class and add the methods that needs to be overwritten in order to use uniq (hash and eql?).
class Task
METHODS = [:status, :description, :priority, :tags]
attr_accessor *METHODS
def initialize task_line
#status, #description, #priority, #tags = *task_line.split("|")
#tags = #tags.split(",")
end
def eql? another_task
METHODS.all?{|m| self.send(m)==another_task.send(m)}
end
alias_method :==, :eql? #Strictly not needed for array.uniq
def hash
[#status, #description, #priority, #tags].hash
end
end
x = [Task.new('1|2|3|4'), Task.new('1|2|3|4')]
p x.size #=> 2
p x.uniq.size #=> 1

ruby inheritance

How to implement inheritance in ruby for the following?
class Land
attr_accessor :name, :area
def initialize(name, area)
#name = name
#area = area
end
end
class Forest < Land
attr_accessor :rain_level
attr_reader :name
def name=(_name)
begin
raise "could not set name"
rescue Exception => e
puts e.message
end
end
def initialize(land, rain_level)
#name = land.name
#rain_level = rain_level
end
end
l = Land.new("land", 2300)
f = Forest.new(l, 400)
puts f.name # => "land"
suppose when i change name for land l, then it should change for sub class also
l.name ="new land"
puts f.name # => "land"
what expected is puts f.name # => "new land"
It seems to me that this is not actually inheritance in the OO sense. If you change Forest so that it holds a reference to the Land then you will get the behavior you wanted.
class Forest
attr_accessor :rain_level
def name
#land.name
end
def initialize(land, rain_level)
#land = land
#rain_level = rain_level
end
end
This is kind of an interesting thing you want to build.
Summarizing you want to have two objects that share a value but only one is allowed to edit the value, the other one is only allowed to read it.
I think the easiest way to implement this is in your case to implement a new getter in Forest which returns land.name. By writing l.name = 'meow' will f.name return moew too because it holds a reference to l.
Hope this helps.

Ruby object returned from method loses class type (reverts to base class array)

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')

Resources