I use in a genealogy program following method to add a mariage/spouse to a Person.
#mariages is an array of arrays.
def add_spouse(spouse, mariage_date = nil, divorce_date = nil)
#mariages.push([spouse, mariage_date, divorce_date]) unless #mariages.index{|(a, b, c)| a == spouse && b == mariage_date}
spouse.mariages.push(self) unless spouse.mariages.index{|(a, b, c)| a == self && b == mariage_date}
end
With the unless #mariages.index{|(a, b, c)| a == spouse && b == mariage_date} i check if the mariage is not allready in the array.
Now i want to keep my mariages in an array of hashes like this
def add_spouse(spouse, mariage_date = nil, divorce_date = nil)
#mariages.push({:spouse => spouse, :mariage_date => mariage_date, :divorce_date => divorce_date}) unless ...
spouse.mariages.push({:spouse => self, :mariage_date => mariage_date, :divorce_date => divorce_date}) unless ...
end
Can someone help me adapt the unless part to do the check if the hash is not allready presant in the array ?
In the block that goes into index the element getting iterated over is a hash so you should use
.. unless #mariages.index{|h| h[:spouse] == spouse && h[:mariage_date] == mariage_date}
and
.. unless spouse.mariages.index{|h| h[:spouse] == self && h[:mariage_date] == mariage_date}
PS: mariage is misspelled. It should be marriage.
Since your array now contains hashes instead of other arrays, you can't use "array unpacking" (not sure what's the official term for that is). You will get an instance of hash, and you can access it as you normally would.
#mariages.push({:spouse => spouse,
:mariage_date => mariage_date,
:divorce_date => divorce_date}) unless #mariages.index{|h| h[:spouse] == spouse && h[:mariage_date] == mariage_date}
As per your question in the commend I'd do something like the following. Note that Dates should use the date class and the partners in the marriage should be classes too, with births and deaths and whatnot. But hopefully you can see how moving the data into objects that know what to do with it, can simplify the design as things get larger. (I also went with bride and groom for simplicity, feel free to change that in a genealogically approved manner).
Each person would have a Marriages associated with them and if you had a bride and a groom they could share a single Marriage, but have a different list of Marriages.
class Marriage
attr_accessor :marriage_date, :divorce_date, :bride, :groom
def initialize(date, bride, groom)
#marriage_date = date
#bride = bride
#groom = groom
end
def marriage_equals(m)
return (#marriage_date == m.marriage_date) &&
(#bride == m.bride) &&
(#groom == m.groom)
end
end
class Marriages
def initialize
#marriages = []
end
def add_marriage(marriage)
if (#marriages.any? { |m| m.marriage_equals(marriage) })
puts "Marriage of #{marriage.groom} already listed"
return false
else
puts "Added new marriage"
#marriages.push(marriage)
return true
end
end
end
m1 = Marriage.new("1-1-0002", "Wilma", "Fred")
m2 = Marriage.new("6-8-0003", "Betty", "Barney")
m3 = Marriage.new("2-8-8003", "Jane", "George")
marriages = [m1,m2]
p marriages.any? { |m| m.marriage_equals(m1) } # true
p marriages.any? { |m| m.marriage_equals(m3) } # false
m_list = Marriages.new
m_list.add_marriage(m1) # Added new marriage
m_list.add_marriage(m2) # Added new marriage
m_list.add_marriage(m2) # Marriage of Barney already listed
Related
Apparently, my ability to think functional withered over time. I have problems to select a sub-dataset from a dataset. I can solve the problem the hacky imperative style, but I believe, there is a sweet functional solution, which I am unfortunately not able to find.
Consider this data structure (tried to not simplify it beyond usability):
class C
attr_reader :attrC
def initialize(base)
#attrC = { "c1" => base+10 , "c2" => base+20, "c3" => base+30}
end
end
class B
attr_reader :attrB
##counter = 0
def initialize
#attrB = Hash.new
#attrB["b#{##counter}"] = C.new(##counter)
##counter += 1
end
end
class A
attr_reader :attrA
def initialize
#attrA = { "a1" => B.new, "a2" => B.new, "a3" => B.new}
end
end
which is created as a = A.new. The complete data set then would be
#<A: #attrA={"a1"=>#<B: #attrB={"b0"=>#<C: #attrC={"c1"=>10, "c2"=>20, "c3"=>30}>}>,
"a2"=>#<B: #attrB={"b1"=>#<C: #attrC={"c1"=>11, "c2"=>21, "c3"=>31}>}>,
"a3"=>#<B: #attrB={"b2"=>#<C: #attrC={"c1"=>12, "c2"=>22, "c3"=>32}>}>}>
which is subject to a selection. I want to retrieve only those instances of B where attrB's key is "b2".
My hacky way would is:
result = Array.new
A.new.attrA.each do |_,va|
result << va.attrB.select { |kb,_| kb == "b2" }
end
p result.reject { |a| a.empty?} [0]
which results in exactly what I intended:
{"b2"=>#<C: #attrC={"c1"=>12, "c2"=>22, "c3"=>32}>}
but I believe there would be a one-liner using map, fold, zip and reduce.
If you want a one-liner:
a.attrA.values.select { |b| b.attrB.keys == %w(b2) }
This returns instances of B. In your question, you're getting attrB values rather than instances of B. If that's what you want, there's this ugly reduce:
a.attrA.values.reduce([]) { |memo, b| memo << b.attrB if b.attrB.keys == %w(b2) ; memo }
I'm not sure what you're trying to do here, though?
I have the following implementation of a linked list in Ruby:
class Node
attr_accessor :data, :next
def initialize(data = nil)
#data = data
#next = nil
end
end
class LinkedList
def initialize(items)
#head = Node.new(items.shift)
items.inject(#head) { |last, data| #tail = last.next = Node.new(data) }
end
def iterate
return nil if #head.nil?
entry = #head
until entry.nil?
yield entry
entry = entry.next
end
end
def equal?(other_list)
#How do I check if all the data for all the elements in one list are the same in the other one?
end
end
I have tried using the .iterate like this:
def equals?(other_list)
other_list.iterate do |ol|
self.iterate do |sl|
if ol.data != sl.data
return false
end
end
end
return true
end
But this is doing a nested approach. I fail to see how to do it.
You can't do it easily with the methods you have defined currently, as there is no way to access a single next element. Also, it would be extremely useful if you implemented each instead of iterate, which then gives you the whole power of the Enumerable mixin.
class LinkedList
include Enumerable # THIS allows you to use `zip` :)
class Node # THIS because you didn't give us your Node
attr_accessor :next, :value
def initialize(value)
#value = value
#next = nil
end
end
def initialize(items)
#head = Node.new(items.shift)
items.inject(#head) { |last, data| #tail = last.next = Node.new(data) }
end
def each
return enum_for(__method__) unless block_given? # THIS allows block or blockless calls
return if #head.nil?
entry = #head
until entry.nil?
yield entry.value # THIS yields node values instead of nodes
entry = entry.next
end
end
def ==(other_list)
# and finally THIS - get pairs from self and other, and make sure all are equal
zip(other_list).all? { |a, b| a == b }
end
end
a = LinkedList.new([1, 2, 3])
b = LinkedList.new([1, 2, 3])
c = LinkedList.new([1, 2])
puts a == b # => true
puts a == c # => false
EDIT: I missed this on the first run through: equal? is supposed to be referential identity, i.e. two variables are equal? if they contain the reference to the same object. You should not redefine that method, even though it is possible. Rather, == is the general common-language meaning of "equal" as in "having the same value", so I changed it to that.
I think there is something wrong with your initialize method in LinkedList, regardless could this be what you need
...
def equal?(other_list)
other_index = 0
cur_index = 0
hash = Hash.new
other_list.iterate do |ol|
hash[ol.data.data] = other_index
other_index += 1
end
self.iterate do |node|
return false if hash[node.data.data] != cur_index
return false if !hash.has_key?(node.data.data)
cur_index += 1
end
return true
end
...
Assuming this is how you use your code
a = Node.new(1)
b = Node.new(2)
c = Node.new(3)
listA = [a,b,c]
aa = Node.new(1)
bb = Node.new(2)
cc = Node.new(3)
listB = [aa,bb,cc]
linkA = LinkedList.new(listA)
linkB = LinkedList.new(listB)
puts linkA.equal?(linkB)
On this page:
https://rubymonk.com/learning/books/4-ruby-primer-ascent/chapters/45-more-classes/lessons/105-equality_of_objects
I am trying to correct this code, so that it passes its tests.
My attempt is quite bad as I am only just beginning to learn how software logic works.
class Item
attr_reader :item_name, :qty
def initialize(item_name, qty)
#item_name = item_name
#qty = qty
end
def to_s
"Item (#{#item_name}, #{#qty})"
end
def ==(other_item)
end
end
p Item.new("abcd",1) == Item.new("abcd",1)
p Item.new("abcd",2) == Item.new("abcd",1)
This is my solution, but it is not correct:
class Item
attr_reader :item_name, :qty
def initialize(item_name, qty)
#item_name = item_name
#qty = qty
end
def to_s
"Item (#{#item_name}, #{#qty})"
end
def ==(other_item)
if self == other_item
return true
else
return false
end
end
end
p Item.new("abcd",1) == Item.new("abcd",1)
p Item.new("abcd",2) == Item.new("abcd",1)
I was hoping a Rubyist out there could help me solve this exercise. I am unsure of how to solve it.
Thanks for your help
here is the output from the test:
STDOUT:
nil
nil
Items with same item name and quantity should be equal
RSpec::Expectations::ExpectationNotMetError
expected Item (abcd, 1) got Item (abcd, 1) (compared using ==) Diff:
Items with same item name but different quantity should not be equal ✔
Items with same same quantity but different item name should not be equal ✔
When you override the == method, you should give meaning to your comparison. The default == behavior checks that the other item is identical to the compared item (they have the same object_id). Try this:
def ==(other_item)
other_item.is_a?(Item) &&
self.item_name == other_item.item_name &&
self.qty == other_item.qty
end
Can point you in right direction instead of telling the answer.
You are comparing the references of objects for equality, whereas, you are asked to compare only those attributes for equality. That is, compare both objects parameters in such a way that if they are equal, it must return true; else false
When you scroll down past the question you will see that the next example provides a clear solution.
def ==(other_item)
self.item_name == other_item.item_name &&
self.qty == other_item.qty
end
end
I have two hash arrays like this:
hashArray1 = [{"id"=>"1","data"=>"data1"},{"id"=>"2","data"=>"data2"}]
hashArray2 = [{"id"=>"3","data"=>"data1"},{"id"=>"4","data"=>"data2"}]
I want to compare both of them and return true if everything else matches without the "id" key.
I have tried something like this:
hashArray1.each do |h1|
hashArray2.each do |h2|
if h1.select{|h| h!= "id"} == h2.select{|b| b!= "id"}
break
else
return false
end
end
end
But this seems to be incorrect. Does anyone have a better solution. I am on plain ruby 1.9.3, not using rails framework.
I would simply do:
hash1.zip(hash2).all? do |h1,h2|
return false unless h1.keys == h1.keys
h1.keys.each do |key|
return false if h1[key] != h2[key] unless key == 'id'
end
end
If hash1.length != hash2.length then you can bail out immediately as they can't be the same. If they have the same length then you could do something like this:
except_id = ->(h) { h.reject { |k, v| k == 'id' } }
same = hash1.zip(hash2).find { |h1, h2| except_id[h1] != except_id[h2] }.nil?
If same is true then they're the same (while ignore 'id's), otherwise they're different. Using Hash#reject is one pure Ruby way to non-destructively look at the Hash without a particular key. You could also use:
except_id = lambda { |h| h = h.dup; h.delete('id'); h }
if "copy and remove" makes more sense to you than filtering. If you don't like find then all? might read better:
same = hash1.zip(hash2).all? { |h1, h2| except_id[h1] == except_id[h2] }
or even:
same_without_id = lambda { |h1, h2| except_id[h1] == except_id[h2] }
same == hash1.zip(hash2).all?(&same_without_id)
The question is not necessarily clear, but I assume the order of the hashes are taken into account.
hash1.map{|h| h.reject{|k, _| k == "id"}} ==
hash2.map{|h| h.reject{|k, _| k == "id"}}
I have a few arrays of Ruby objects of class UserInfo:
class UserInfo
attr_accessor :name, :title, :age
end
How can I merge these arrays into one array? A user is identified by its name, so I want no duplicate names. If name, title, age, etc. are equal I'd like to have 1 entry in the new array. If names are the same, but any of the other details differ I probably want those 2 users in a different array to manually fix the errors.
Thanks in advance
Redefine equality comparison on your object, and you can get rid of actual duplicates quickly with Array#uniq
class UserInfo
attr_accessor :name, :title, :age
def == other
name==other.name and title==other.title and age==other.age
end
end
# assuming a and b are arrays of UserInfo objects
c = a | b
# c will only contain one of each UserInfo
Then you can sort by name and look for name-only duplicates
d = c.sort{ |p,q| p.name <=> q.name } #sort by name
name = ""
e = []
d.each do |item|
if item.name == name
e[-1] = [e[-1],item].flatten
else
e << item
end
end
A year ago I monkey patched a kind of cryptic instance_variables_compare on Object. I guess you could use that.
class Object
def instance_variables_compare(o)
Hash[*self.instance_variables.map {|v|
self.instance_variable_get(v)!=o.instance_variable_get(v) ?
[v,o.instance_variable_get(v)] : []}.flatten]
end
end
A cheesy example
require 'Date'
class Cheese
attr_accessor :name, :weight, :expire_date
def initialize(name, weight, expire_date)
#name, #weight, #expire_date = name, weight, expire_date
end
end
stilton=Cheese.new('Stilton', 250, Date.parse("2010-12-02"))
gorgonzola=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
irb is my weapon of choice
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola", "#expire_date"=>#<Date: 4910305/2,0,2299161>}
>> gorgonzola.instance_variables_compare(stilton)
=> {"#name"=>"Stilton", "#expire_date"=>#<Date: 4910275/2,0,2299161>}
>> stilton.expire_date=gorgonzola.expire_date
=> #<Date: 4910305/2,0,2299161>
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola"}
>> stilton.instance_variables_compare(stilton)
=> {}
As you can see the instance_variables_compare returns an empty Hash if the two objects has the same content.
An array of cheese
stilton2=Cheese.new('Stilton', 210, Date.parse("2010-12-02"))
gorgonzola2=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
arr=[]<<stilton<<stilton2<<gorgonzola<<gorgonzola2
One hash without problems and one with
h={}
problems=Hash.new([])
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name].instance_variables_compare(c) != {}
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Now the Hash h contains the objects without merging problems and the problems hash contains those that has instance variables that differs.
>> h
=> {"Gorgonzola"=>#<Cheese:0xb375e8 #name="Gorgonzola", #weight=250, #expire_date=#<Date: 2010-12-17 (4911095/2,0,2299161)>>}
>> problems
=> {"Stilton"=>[#<Cheese:0xf54c30 #name="Stilton", #weight=210, #expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>, #<Cheese:0xfdeca8 #name="Stilton", #weight=250,#expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>]}
As far as I can see you will not have to modify this code at all to support an array of UserInfo objects.
It would most probably be much faster to compare the properties directly or with a override of ==. This is how you override ==
def ==(other)
return self.weight == other.weight && self.expire_date == other.expire_date
end
and the loop changes into this
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name] != c
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Finally you might want to convert the Hash back to an Array
result = h.values
Here's another potential way. If you have a way of identifying each UserInfo, say a to_str method that prints out the values:
def to_str()
return "#{#name}:#{#title}:#{#age}"
end
You can use inject and a hash
all_users = a + b # collection of users to "merge"
res = all_users.inject({})do |h,v|
h[v.to_str] = v #save the value indexed on the string output
h # return h for the next iteration
end
merged = res.values #the unique users