Methods to create deep copy of objects without the help of Marshal - ruby

I have 3 simple classes CashRegister, Bill and Position. A CashRegister is composed of Bill objects and a Bill object is composed of Position objects. They're implemented as followed
class CashRegister
def initialize
#bills = []
end
def clone
#?
end
end
class Bill
def initialize(nr)
#nr = nr
#positions = []
end
def clone
#?
end
end
class Position
def initialize(product, price)
#product = product
#price = price
end
def clone
#?
end
end
How do I create methods that can deep copy the objects of these classes. The use of Marshal.load(Marshal.dump(an_obj)) is not allowed.
Edit: So far I've got this:
class CashRegister
def initialize
#bills = []
end
def clone
#bills.map { |bill| bill.clone}
end
end
class Bill
def initialize(nr)
#nr = nr
#positions = []
end
def clone
cloned = super
cloned.positions = #positions.map{ |pos| pos.clone}
cloned
end
end
class Position
attr_reader :preis
# this method is given
def produkt
#produkt.clone()
end
def initialize(product, price)
#product = product
#price = price
end
def clone
cloned = super
cloned.product
cloned
end
end
The clone method in class Position seems to be ok (no compile error). But there is an error in the one in class Bill, it says "undefined method 'positions=', so the problem must be in the line cloned.positions = #positions.map{ |pos| pos.clone}. But I don't understand, can't we call cloned.positions like that?

It's just the instance variables you have to worry about.
class Position
attr_accessor :product, :price
def initialize(product, price)
#product = product
#price = price
end
end
p1 = Position.new("lima beans", 2.31)
#=> #<Position:0x000000027587b0 #product="lima beans", #price=2.31>
p2 = Position.new(p1.product, p1.price)
#=> #<Position:0x0000000273dd48 #product="lima beans", #price=2.31>
We can confirm that p2 is a deep copy of p1.
p1.product = "lettuce"
p1.price = 1.49
p1 #=> #<Position:0x0000000271f870 #product="lettuce", #price=1.49>
p2 #=> #<Position:0x000000026e9e00 #product="lima beans", #price=2.31>
p2.product = "spinach"
p2.price = 2.10
p1 #=> #<Position:0x0000000271f870 #product="lettuce", #price=1.49>
p2 #=> #<Position:0x000000026e9e00 #product="spinach", #price=2.1>
It's more complex if, for example, the class were defined as follows (where products is an array).
p1 = Position.new ["carrots", "onions"]
#=> #<Position:0x000000025b8928 #products=["carrots", "onions"]>
p2 = Position.new p1.products
#=> #<Position:0x000000025b0048 #products=["carrots", "onions"]>
p1.products << "beets"
p1 #=> #<Position:0x000000025b8928 #products=["carrots", "onions", "beets"]>
p2 #=> #<Position:0x000000025b0048 #products=["carrots", "onions", "beets"]>
p2 is not what we want. We would need to write
p1 = Position.new ["carrots", "onions"]
#=> #<Position:0x00000002450900 #products=["carrots", "onions"]>
p2 = Position.new p1.products.dup
#=> #<Position:0x0000000243aa88 #products=["carrots", "onions"]>
(note the .dup) so that
p1.products << "beets"
#=> ["carrots", "onions", "beets"]
p1 #=> #<Position:0x00000002450900 #products=["carrots", "onions", "beets"]>
p2 #=> #<Position:0x0000000243aa88 #products=["carrots", "onions"]>
More generally, we need to make deep copies of the instance variables.

This solution works
class CashRegister
attr_accessor :bills
def initialize
#bills = []
end
def clone
cloned = super
cloned.bills = #bills.map { |bill| bill.clone }
cloned
end
end
class Bill
attr_accessor :positions
def initialize(nr)
#nr = nr
#positions = []
end
def clone
cloned = super
cloned.positions = #positions.map{ |pos| pos.clone }
cloned
end
end
class Position
attr_reader :price
attr_writer :product
# this method is given
def product
#product.clone
end
def initialize(product, price)
#product = product
#price = price
end
def clone
cloned = super
cloned.product = product
cloned
end
end

Another possible answer is to use the full_dup gem (full disclosure, written by me) then simply use:
p2 = p1.full_dup
Now full_dup, like regular dup, does not copy any singleton methods. If that is important, try the full_clone gem (yup, by me too) instead.
If there are fields that need to excluded from the dup (or clone process), the optional full_dup_exclude (or full_clone_exclude) method can be defined to list fields to be excluded from processing.
Note there is no need to worry about trying to clone numbers, symbols, and other non-clonable things that may exist in your object. These are handled safely be the gems.

Related

How to make equal objects on ruby?

The challenge is to make a class that build equals objects for the same value for example:
class Person
att_reader :name
def initialize(name)
#name = name
end
end
I want to make
p1 = Person.new('joe')
p2 = Person.new('joe')
p3 = Person.new('mary')
p1 == p2 #true
p2 == p3 #false
I already tried to look for singleton but I can't make method new to be private
Let's use Comparable mixin
class Person
include Comparable
attr_reader :name
def initialize(name)
#name = name
end
def <=>(other)
name <=> other.name
end
end

Adding new attribute to Object after it's created using method (Ruby)

I am trying to add an attribute to an object after it's been created using a method from within the object Class. I'd like to put this code in the def set_sell_by and def get_sell_by methods, if this is possible. So, in the end I'd like to do apple.set_sell_by(10) and then get that value later by doing apple.get_sell_by to check if the item has 5 days or less left to sell it.
class Grocery_Inventory
attr_accessor :product, :store_buy, :quantity, :serial_number, :customer_buy
def initialize(product, store_buy, quantity, serial_number, customer_buy)
#product = product
#store_buy = store_buy
#quantity = quantity + 5
#serial_number = serial_number
#customer_buy = customer_buy
end
def get_product_name
p product
self
end
def get_cost_customer
p "$#{customer_buy}"
self
end
def get_product_quantity
p "You have #{quantity} #{product}"
self
end
def set_sell_by
#some code...
self
end
def get_sell_by
if sell_by < 5
p "You need to sell this item within five days."
self
else
p "Item doesn't currently need to be sold."
self
end
end
end
apples = Grocery_Inventory.new("apples", 1.00, 5, 123, 0.25)
apples.get_product_name
apples.get_cost_customer
apples.get_product_quantity
Ruby is very lax in this regard. Simply access a variable with #and if it doesn't exist it will be created.
def set_sell_by
#sell_by = value
self
end

Classes in Ruby - Creating parameters that will accept multiple inputs

Ok guys, I am learning ruby and I am having a little bit of trouble with the tutorial. I was wondering if you could help me out!
Take the following code:
class Dish
def initialize(name, ingred, descrip)
#name = name
#ingred = ingred
#descrip = descrip
end
def name
#name
end
def name=(new_name)
#name = new_name
end
def ingred
#ingred
end
def ingred=(new_ingred)
#ingred = new_ingred
end
def descrip
#descrip
end
def descrip=(new_descrip)
#descrip = new_descrip
end
def display
puts "I am a #{#name} and my ingredient is #{#ingred} and my description is #{descrip}"
end
end
dis1 = Dish.new('Pizza', 'sauce', 'put sauce on that thing')
dis1.display
Ok so here is my question and I hope I explain it well enough. So far I have learned to take enter one parameter when making a new instance of a class (i.e. (name, ingred, descrip)). What I am wondering is if a dish has multiple ingredients, how would I add that to my class? Also, if I wanted to count the number of ingredients or the number of names, how would I do that. I am just learning about classes and I am having trouble matching the exact wording I would Google for. Thanks!
I'll try to answer some of your questions. To simplify, I removed your variable, descrip and its associated methods. You see I've put a * in front of ingred in initialize. This means that a variable number of arguments are passed after name. This is one way of dealing with your question about having multiple ingredients. Here ingred is an array. Since #ingred is set equal to ingred, #ingred is also an array. If you look at the various methods and what some print when invoked (shown at the bottom), you should be able to see how this works. (Edited to add a bit of functionality. You may need to scroll down at the bottom.)
class Dish
def initialize(name, *ingred)
#name = name
#ingred = ingred
end
def name
#name
end
def name=(new_name)
#name = new_name
end
def ingred
#ingred
end
def ingred=(*ingred)
#ingred = ingred
end
def add_ingred(ingred)
#ingred << ingred
end
def remove_ingred(ingred)
#ingred.delete(ingred)
end
def nbr_ingred
#ingred.count
end
end
dis1 = Dish.new("Pizza", "sauce", "crust", "cheese", "anchovies")
p dis1.ingred #=> ["sauce", "crust", "cheese", "anchovies"]
dis1.add_ingred("olives")
p dis1.ingred #=> ["sauce", "crust", "cheese", "anchovies", "olives"]
dis1.add_ingred(["capers", "mushrooms"])
p dis1.ingred #=> ["sauce", "crust", "cheese", "anchovies", "olives", ["capers", "mushrooms"]]
dis1.ingred.flatten!
p dis1.ingred #=> ["sauce", "crust", "cheese", "anchovies", "olives", "capers", "mushrooms"]
dis1.remove_ingred("anchovies")
p dis1.ingred #=> ["sauce", "crust", "cheese", "olives", "capers", "mushrooms"]
p dis1.nbr_ingred #=> 6
dis1.ingred = "olives", "pineapple" # treated as ["olives", "pineapple"]
p dis1.ingred #=> [["olives", "pineapple"]]
dis1.ingred = ["cheese", "crust"]
p dis1.ingred #=> [["olives", "pineapple"]]
dis1.ingred.flatten!
p dis1.ingred #=> ["olives", "pineapple"]
Use arrays.
class Dish
class Ingredient
attr_accessor :name, :description
def initialize(name, description)
#name = name
#description = description
end
def to_s
"#{name} - #{description}"
end
end
attr_accessor :name, :description, :ingredients
def initialize(name, description)
#name = name
#description = description
#ingredients = []
end
def to_s
"#{name} - #{description}\n #{ingredients.join("\n").to_s}"
end
end
pizza = Dish.new("Pizza", "Italian style pizza")
pizza.ingredients << Dish::Ingredient.new("Tomato Sauce", "Juicy Juicy Tomato Sauce.")
pizza.ingredients << Dish::Ingredient.new("Cheese", "Cheese, duh.")
puts pizza.to_s
As the two answers before me did both leave out the description parameter i will stealCary Swoveland's answer and add the descrip parameter:
class Dish
attr_accessor :name, :descrip
def initialize(name, *ingred, descrip) # Only in Ruby 1.9+
#name = name
#ingred = *ingred
#descrip = descrip
end
def ingred
#ingred
end
def ingred=(*ingred)
#ingred = ingred
end
def add_ingred(ingred)
#ingred << ingred
end
def remove_ingred(ingred)
#ingred.delete(ingred)
end
def nbr_ingred
#ingred.count
end
def display
puts "I am a #{#name} and my ingredient is #{#ingred.join(', ')} and my description is #{descrip}"
end
end
dis1 = Dish.new('Pizza', 'sauce', 'ham', 'put ingredients on that thing.')
dis1.add_ingred('fish')
dis1.display #=> I am a Pizza and my ingredient is sauce, ham, fish and my description is put ingredients on that thing.

how to get (and set) variables inside a ruby class?

I update this question to better reflect what I have problems to grasp. The example below kind of work but how can I access the Sub class then I have defined it inside the Base class? Should it not be better to do the call outside the class? If so how do I do that? The second question I have in this example is how to grab values so I can use them in another class. Here I store the values in an array that I later need to unpack in another class. Should I not be able to use a proc for this?
Basically what I want to do is to sort the methods into two different classes depending on if they are nested or not.
class Sub
def initialize(base_class_method)
#base_class_method = base_class_method
#sub_methods = []
end
# omitted code here
def base_class_method
#base_class_method
end
def sub_actions(method)
#sub_methods << method
end
def return_sub_methods
#sub_methods
end
def method_missing(sub_method, &block)
if sub_method
sub_method
else
super
end
end
end
class Base
def initialize
#base_methods = []
end
# omitted code here
def base_actions(method)
#base_methods << method
end
def return_base_methods
#base_methods
end
def method_missing(method, &block)
if block_given?
Sub.new(method).instance_eval(&block)
elsif method
base_actions(method)
else
super
end
end
end
base = Base.new
base.instance_eval do
something1
something_with_a_block do
something_inside_block1_1
something_inside_block1_2
end
something2
something_with_a_block2_2 do
something_inside_block2_1
end
end
p base.return_base_methods #=> [:something1, :something2] works!
You can do something like this.
class Test
# reserved method to instantiate object
def initialize(a,b,c)
#a = a
#b = b
#c = c
end
# getters
def a
#a
end
def b
#b
end
def c
#c
end
def abc
[#a, #b, #c] # returns an array
end
# setters
def a=(var)
#a = var
end
def b=(var)
#b = var
end
def c=(var)
#c = var
end
# set values all at once
def update(a, b, c)
#a = a
#b = b
#c = c
end
end
z = Test.new('something','something','something')
z.update('something!','nothing!',"a thing!")
z.a
z.b
z.c
z.a = 'wow, new value!'

Ruby Hash whose key is a function of the object?

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

Resources