I am trying to put custom objects in a set. I tried this:
require 'set'
class Person
include Comparable
def initialize(name, age)
#name = name
#age = age
end
attr_accessor :name, :age
def ==(other)
#name == other.name
end
alias eql? ==
end
a = Person.new("a", 18)
b = Person.new("a", 18)
people = Set[]
people << a
people << b
puts a == b # true
It seems that Set does not identify same objects with Object#eql? or == methods:
puts people # #<Set: {#<Person:0x00007f9e09843df8 #name="a", #age=18>, #<Person:0x00007f9e09843da8 #name="a", #age=18>}>
How does Set identify two same objects?
From the docs:
Set uses Hash as storage, so you must note the following points:
Equality of elements is determined according to Object#eql? and Object#hash. [...]
That said: If you want two people to be equal when they have the same name, then you must implement hash accordingly:
def hash
#name.hash
end
Ruby's built-in Set stores items in a Hash. So for your objects to be treated as the "same" by Set, you also need to define a custom hash method. Something like this would work:
def hash
#name.hash
end
Use gem which set.rb to see where the source code for Set is stored, and try reading through it. It's clear and well-written.
Related
How can I refactor the following? I have some values stored in my YAML file as nested arrays, but I want to pull all my transactions into two get and set methods. This works, but is obviously limited and bulky. It feels wrong.
module Persistance
#store = YAML::Store.new('store.yml')
def self.get_transaction(key)
#store.transaction { #store[key] }
end
def self.get_nested_transaction(key, sub)
#store.transaction { #store[key][sub] }
end
end
Bonus credit: I also have an additional method for incrementing values in my YAML file. Is there a further way to refactor this code? Does it make sense to just pass blocks to a single database accessing method?
Hey I remember thinking about this when I was practicing PStore a little while ago. I didn't figure out a working approach then but I managed to get one now. By the way, yaml/store is pretty cool and you can take credit for introducing me to it.
Anyway, on with the code. Basically here's a couple important concepts:
The #store is similar to a hash in that you can use [] and []= but it's not actually a hash, it's a YAML::Store.
Ruby 2.3 has a method Hash#dig which is kind of the missing puzzle piece here. You provide a list of keys and it treats each as successive keys. You can use this for both get and set, as my code shows
If #store were a true hash that would be the end of it but's not, so for this answer I added a YAML::Store#dig method which has the same usage as the original.
require 'yaml/store'
class YAML::Store
def dig(*keys)
first_val = self[keys.shift]
if keys.empty?
first_val
else
keys.reduce(first_val) do |result, key|
first_val[key]
end
end
end
end
class YamlStore
attr_reader :store
def initialize filename
#store = YAML::Store.new filename
end
def get *keys
#store.transaction do
#store.dig *keys
end
end
def set *keys, val
#store.transaction do
final_key = keys.pop
hash_to_set = keys.empty? ? #store : #store.dig(*keys)
hash_to_set.send :[]=, final_key, val
end
end
end
filename = 'store.yml'
db = YamlStore.new filename
db.set :a, {}
puts db.get :a
# => {}
db.set :a, :b, 1
puts db.get :a, :b
# => 1
I have a class in which the data is stored as a set and I want to be able to compare objects of that class such that the letter case of the elements is of no matter. For example if the set contains elements that are strings there should be no difference of "a" and "A".
To do this I have tried to define the eql? method of the set members to be insensitive to case but this has no effect on the method - (alias difference) in Set. So, how should I go about to make - insensitive to case?
The following code illustrates the problem:
require 'set'
class SomeSet
include Enumerable
def initialize; #elements = Set.new; end
def add(o)
#elements.add(o)
self
end
def each(&block) # To enable +Enumerable+
#elements.each(&block)
end
def difference(compared_list)
#elements - compared_list
end
end
class Element
attr_reader :element
def initialize(element); #element = element; end
# This seems to have no effect on +difference+
def eql?(other_element)
element.casecmp(other_element.element) == 0
end
end
set1 = SomeSet.new
set2 = SomeSet.new
set1.add("a")
set2.add("A")
# The following turns out false but I want it to turn out true as case
# should not matter.
puts set1.difference(set2).empty?
Ok, firstly, you're just storing strings from SomeSet#add, you need to store an instance of Element, like so:
def add(o)
#elements.add(Element.new(o))
self
end
And you need to implement a hash method in your Element class.
You can convert Element##element to lowercase, and pass on its hash.
def hash
element.downcase.hash
end
Full code and demo: http://codepad.org/PffThml2
Edit: For my O(n) insertion comment, above:
Insertions are O(1). From what I can see, eql? is only used with the hash of 2 elements is same. As we're doing hash on the downcased version of the element, it will be fairly well distributed, and eql? shouldn't be called much (if it is called at all).
From the docs:
The equality of each couple of elements is determined according to Object#eql? and Object#hash, since Set uses Hash as storage.
Perhaps you need to implement Object#hash as well.
require 'set'
class String2
attr_reader :value
def initialize v
#value = v
end
def eql? v
value.casecmp(v.value) == 0
end
def hash
value.downcase.hash
end
end
set1 = Set.new
set2 = Set.new
set1.add(String2.new "a")
set2.add(String2.new "A")
puts set1.difference(set2).empty?
class A
attr_accessor :dab
....
end
Now I have an array of instances of A, say
arr = [A.new, A.new, A.new]
And now I want to set a value to all instances of class A present in the array arr. Is there a shortcut in ruby/rails to do this?
In addition, I do have A inherited from ActiveRecord::Base
And my actual need is:
A.find_all_by_some_condition.all.dabs = 2
So, all found objects will have dab set to 2.
Is there shortcut for this?
To get the items of class A from an array you can use select/find_all
arr.select { |el| el.class == A } or arr.select { |el| A === el }
To achieve your actual result though you are looking to assign a value to several objects, not their corresponding class. class A does not define the actual objects it just defines the blueprint that the objects use when getting created. So finding a way to assign a value of all instances of A is not what you are after (although I might have missed the point of what you were asking for)
To assign a value to an array of object this works:
A.find_all_by_some_condition.each { |a| a.dab = 2 }
Perhaps you want to save them after that, now arr.each(&:save) might come in handy. Go look up the ampersand if you don't know it already. Very useful.
You can't do that directly by default, however you could build something like that using Ruby's method_missing.
Two solutions:
Solution 1 - Use a wrapper class
We'll call this class MArray for multi-assign-array.
class MArray
def initialize(inner_array)
#inner = inner_array
end
def method_missing(meth, value)
# Check if assignement, and if it is then run mass-assign
if meth.to_s =~ /^\w+=$/
#inner.each { |itm| itm.send(meth, value) }
else
raise ArgumentError, "MArray: not an assignment"
end
end
end
We also need to add support for MArray in Array, so that the wrapping will take place. We'll call the method mas for "mass-assignment":
class Array
def mas
# Wrap MArray around self
MArray.new(self)
end
end
Usage is simple:
Blob = Struct.new(:dab)
arr = [Blob.new] * 3
arr.mas.dab = 123
arr
=> [#<struct Blob dab=123>, #<struct Blob dab=123>, #<struct Blob dab=123>]
Solution 2 - Create mass-assignment support directly into Array
This is a bit more "dangerous" since we directly modify method_missing in Array. It could create some strange side-effects (for example if method_missing has already been redefined by some other library, or you accidentally call a mass-assign while you didn't mean to).
It works by trying to detect assignments with plural words (words ending with s), and then triggering the mass-assignment:
class Array
def method_missing(meth, *args, &block)
# Check for plural assignment, and as an added safety check also
# see if all elements in the array support the assignment:
if meth.to_s =~ /^(\w+)s=$/ &&
self.all? { |itm| itm.respond_to?("#{$1}=") }
self.each { |itm| itm.send("#{$1}=", *args) }
else
super
end
end
end
Usage then becomes even shorter than with MArray:
Blob = Struct.new(:dab)
arr = [Blob.new] * 3
arr.dabs = 123
arr
=> [#<struct Blob dab=123>, #<struct Blob dab=123>, #<struct Blob dab=123>]
I have the following code I am using to turn a hash collection into methods on my classes (somewhat like active record). The problem I am having is that my setter is not working. I am still quite new to Ruby and believe I've gotten myself turned around a bit.
class TheClass
def initialize
#properties = {"my hash"}
self.extend #properties.to_methods
end
end
class Hash
def to_methods
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value
end
define_method("#{key}=") do |val|
instance_variable_set("##{key}", val)
end
end
end
end
end
The methods are created and I can read them on my class but setting them does not work.
myClass = TheClass.new
item = myClass.property # will work.
myClass.property = item # this is what is currently not working.
If your goal is to set dynamic properties then you could use OpenStruct.
require 'ostruct'
person = OpenStruct.new
person.name = "Jennifer Tilly"
person.age = 52
puts person.name
# => "Jennifer Tilly"
puts person.phone_number
# => nil
It even has built-in support to create them from a hash
hash = { :name => "Earth", :population => 6_902_312_042 }
planet = OpenStruct.new(hash)
Your getter method always returns the value in the original hash. Setting the instance variable won't change that; you need to make the getter refer to the instance variable. Something like:
hash.each_pair do |key, value|
define_method key do
instance_variable_get("##{key}")
end
# ... define the setter as before
end
And you also need to set the instance variables at the start, say by putting
#properties.each_pair do |key,val|
instance_variable_set("##{key}",val)
end
in the initialize method.
Note: I do not guarantee that this is the best way to do it; I am not a Ruby expert. But it does work.
It works just fine for me (after fixing the obvious syntax errors in your code, of course):
myClass.instance_variable_get(:#property) # => nil
myClass.property = 42
myClass.instance_variable_get(:#property) # => 42
Note that in Ruby instance variables are always private and you never define a getter for them, so you cannot actually look at them from the outside (other than via reflection), but that doesn't mean that your code doesn't work, it only means that you cannot see that it works.
This is essentially what I was suggesting with method_missing. I'm not familiar enough with either route to say why or why not to use it which is why I asked above. Essentially this will auto-generate properties for you:
def method_missing sym, *args
name = sym.to_s
aname = name.sub("=","")
self.class.module_eval do
attr_accessor aname
end
send name, args.first unless aname == name
end
If I create two String instances with the same content separately they are identical. This is not the case with custom classes by default (see example below).
If I have my own class (Test below) and I have a variable (#v below) which is unique, ie. two Test instances with the same #v should be treated as identical, then how would I go about telling Ruby this is the case?
Consider this example:
class Test
def initialize(v)
#v = v
end
end
a = {Test.new('a') => 1, Test.new('b') => 2}
a.delete(Test.new('a'))
p a
# # Desired output:
# => {#<Test:0x100124ef8 #v="b">=>2}
You need to define an == method that defines what equality means for your class. In this case, you would want:
class Test
def initialize(v)
#v = v
end
def ==(other)
#v == other.instance_variable_get(:#v)
end
end
You are using objects of class Test as keys for the hash. In order for that to function properly (and consequently a.delete), you need to define two methods inside Test: Test#hash and Test#eql?
From: http://ruby-doc.org/core/classes/Hash.html
Hash uses key.eql? to test keys for
equality. If you need to use instances
of your own classes as keys in a Hash,
it is recommended that you define both
the eql? and hash methods. The hash
method must have the property that
a.eql?(b) implies a.hash == b.hash.
I found a different way to tackle this, by keeping track of all the instances of Test internally I can return the premade instance rather than making a new one and telling ruby they're equivalent:
class Test
def self.new(v)
begin
return ##instances[v] if ##instances[v]
rescue
end
new_test = self.allocate
new_test.instance_variable_set(:#v,v)
(##instances ||= {})[v] = new_test
end
end
Now Test.new('a') == Test.new('a') and Test.new('a') === Test.new('a') :)
Most of the time, an object you need to be comparable and/or hashable is composed of member variables which are either primitives (integers, strings, etc) or are themselves comparable/hashable. In those cases, this module:
module Hashable
include Comparable
def ==(other)
other.is_a?(self.class) && other.send(:parts) == parts
end
alias_method :eql?, :==
def hash
parts.hash
end
end
can simply be included in your class to take care of all of the busywork. All you have to do is define a "parts" method that returns all of the values that comprise the object's state:
class Foo
include Hashable
def initialize(a, b)
#a = a
#b = b
end
private
def parts
[#a, #b]
end
end
Objects built this way are comparable (they have <, <=, ==, >=, >, != and equ?) and they can be hash keys.