I'm new to Ruby, and I'm trying to implement a comparison between Grades, was shown in the example
include Comparable
class Grade
attr_accessor :grades, :grade
def initialize( grade = "" )
self.grades = { :nil => -1, :"F" => 0, :"D-" => 1, :"D" => 2, :"D+" => 3,
:"C-" => 4, :"C" => 5, :"C+" => 6, :"B-" => 7, :"B" => 8,
:"B+" => 9, :"A-" => 10, "A+" => 11 }
if self.grades[ grade ]
self.grade = grade
else
self.grade = nil
end
end
def <=>( other )
if self.grades[ self.grade ] < self.grades[ other.grade ]
return -1
elsif self.grades[ self.grade ] == self.grades[ other.grade ]
return 0
else
return 1
end
end
end
a_plus = Grade.new("A+")
a = Grade.new("A")
[a_plus, a].sort # should return [a, a_plus]
So, I'm getting:
grade.rb:31:in `<': comparison of Fixnum with nil failed (ArgumentError)
from grade.rb:31:in `<=>'
from grade.rb:43:in `sort'
from grade.rb:43:in `<main>'
I just want to implement Comparison between objects in Ruby
From Ruby Doc module Comparable:
Comparable uses <=> to implement the conventional comparison operators (<, <=, ==, >=, and >) and the method between?.
When you want to implement such thing, only implement <=>. The rest should automatically follow. If you define <, for example, you will mess up the class.
You can do something like this:
class Grade
include Comparable
attr_reader :index
##grades = %w[F D- D D+ C- C C+ B- B B+ A- A+]
def initialize (grade = nil); #index = ##grades.index(grade).to_i end
def <=> (other); #index <=> other.index end
end
a_plus = Grade.new("A+")
a = Grade.new("A")
a_plus > a
# => true
[a_plus, a].sort
# => `[a, a_plus]` will be given in this order
You just need to go as following:
class Foo
include Comparable
attr_reader :bar
def initialize bar
#bar = bar
end
def <=>(another_foo)
self.bar <=> another_foo.bar
end
end
So at the <=>'s definition you can add your own logic.
Comment to your original post : when you have a message like
in '>': undefined method '>' for nil:NilClass (NoMethodError)
just put print statements to display the values. Thus you would have immediately found that in initialize, self.grades[ self.grade ] was returning nil, because the parameter in Grade.new("A+") is a String, but the keys in the hash are symbols, so you need to convert with to_sym.
Your original class rearranged, with print statements (and only showing >(other)) :
class Grade
attr_reader :grade
##grades = { :nil => -1, :"F" => 0, :"D-" => 1, :"D" => 2, :"D+" => 3,
:"C-" => 4, :"C" => 5, :"C+" => 6, :"B-" => 7, :"B" => 8,
:"B+" => 9, :"A-" => 10, :"A+" => 11 }
def initialize( p_grade = "" )
#grade = p_grade if ##grades[ p_grade.to_sym ]
puts "init param=#{p_grade} value=<#{##grades[ p_grade.to_sym ]}> #grades=<#{#grade}>"
end
def >( other )
puts "in >( other ) : key1=#{self.grade} key2=#{other.grade}"
puts "in >( other ) : $#{##grades[ self.grade ]}$ ??? $#{##grades[ other.grade ]}$"
return ##grades[ self.grade ] > ##grades[ other.grade ]
end
end
print '--- Grade.new("A+") : '; a_plus = Grade.new("A+")
print '--- Grade.new("A") : '; a = Grade.new("A")
print '--- a_plus > a : '; p a_plus > a
Execution :
$ ruby -w t.rb
--- Grade.new("A+") : init param=A+ value=<11> #grades=<A+>
--- Grade.new("A") : t.rb:9: warning: instance variable #grade not initialized
init param=A value=<> #grades=<>
--- a_plus > a : in >( other ) : key1=A+ key2=
in >( other ) : $$ ??? $$
t.rb:15:in `>': undefined method `>' for nil:NilClass (NoMethodError)
from t.rb:21:in `<main>'
Grade.new("A") : as A does not exist in the hash, the instance variable #grade is not set, and self.grades[ self.grade ] > ... sends the message > to nil, an instance of NilClass which doesn't define >.
Notice the trick #grades=<#{xyz}>, surrounding the interpolated value with <> or $$ makes the display more obvious when the value is nil.
Note also the -w in ruby -w t.rb, displaying interesting warning messages.
Related
Is there an "easy" way, short of hand-writing the kind of nested Hash/Array traversal performed by Hash#dig, that I can determine if a key is present in a deeply nested Hash? Another way to ask this is to say "determine if any value is assigned".
There is a difference between a Hash having nothing assigned, or it having an explicit nil assigned - especially if the Hash were constructed with a different missing key default value than nil!
h = { :one => { :two => nil }}
h.dig(:one, :two).nil? # => true; but :two *is* present; it is assigned "nil".
h[:one].key?(:two) # => true, because the key exists
h = { :one => {}}
h.dig(:one, :two).nil? # => true; :two *is not* present; no value is assigned.
h[:one].key?(:two) # => FALSE, because the key does not exist
If you are purely checking the existence of a key, you can combine dig and key?. Use key? on the final or last key in your series of keys.
input_hash = {
hello: {
world: {
existing: nil,
}
}
}
# Used !! to make the result boolean
!!input_hash.dig(:hello, :world)&.key?(:existing) # => true
!!input_hash.dig(:hello, :world)&.key?(:not_existing) # => false
!!input_hash.dig(:hello, :universe)&.has_key?(:not_existing) # => false
Inspired by your core extension suggestion I updated the implementation a bit to better mimic that of #dig
requires 1+ arguments
raises TypeError if the dig does not return nil, the resulting object does not respond to dig? and there are additional arguments to be "dug"
module Diggable
def dig?(arg,*args)
return self.member?(arg) if args.empty?
if val = self[arg] and val.respond_to?(:dig?)
val.dig?(*args)
else
val.nil? ? false : raise(TypeError, "#{val.class} does not have a #dig? method")
end
end
end
[Hash,Struct,Array].each { |klass| klass.send(:include,Diggable) }
class Array
def dig?(arg,*args)
return arg.abs < self.size if args.empty?
super
end
end
if defined?(OpenStruct)
class OpenStruct
def dig?(arg,*args)
self.to_h.dig?(arg,*args)
end
end
end
Usage
Foo = Struct.new(:a)
hash = {:one=>1, :two=>[1, 2, 3], :three=>[{:one=>1, :two=>2}, "hello", Foo.new([1,2,3]), {:one=>{:two=>{:three=>3}}}]}
hash.dig? #=> ArgumentError
hash.dig?(:one) #=> true
hash.dig?(:two, 0) #=> true
hash.dig?(:none) #=> false
hash.dig?(:none, 0) #=> false
hash.dig?(:two, -1) #=> true
hash.dig?(:two, 10) #=> false
hash.dig?(:three, 0, :two) #=> true
hash.dig?(:three, 0, :none) #=> false
hash.dig?(:three, 2, :a) #=> true
hash.dig?(:three, 3, :one, :two, :three, :f) #=> TypeError
Example
For reference - taking the unusual step of answering my own question ;-) - here's one of several ways I could solve this if I just wanted to write lots of Ruby.
def dig?(obj, *args)
arg = args.shift()
return case obj
when Array
if args.empty?
arg >= 0 && arg <= obj.size
else
dig?(obj[arg], *args)
end
when Hash
if args.empty?
obj.key?(arg)
else
dig?(obj[arg], *args)
end
when nil
false
else
raise ArgumentError
end
end
Of course, one could also have opened up classes like Array and Hash and added #dig? to those, if you prefer core extensions over explicit methods:
class Hash
def dig?(*args)
arg = args.shift()
if args.empty?
self.key?(arg)
else
self[arg]&.dig?(*args) || false
end
end
end
class Array
def dig?(*args)
arg = args.shift()
if args.empty?
arg >= 0 && arg <= self.size
else
self[arg]&.dig?(*args) || false
end
end
end
...which would raise NoMethodError rather than ArgumentError if the #dig? arguments led to a non-Hash/Array node.
Obviously it would be possible to compress those down into more cunning / elegant solutions that use fewer lines, but the above has the benefit of IMHO being pretty easy to read.
In the scope of the original question, though, the hope was to lean more on anything Ruby has out-of-the-box. We've collectively acknowledged early-on that there is no single-method solution, but the answer from #AmazingRein gets close by reusing #dig to avoid recursion. We might adapt that as follows:
def dig?(obj, *args)
last_arg = args.pop()
obj = obj.dig(*args) unless args.empty?
return case obj
when Array
last_arg >= 0 && last_arg <= obj.size
when Hash
obj.key?(last_arg)
when nil
false
else
raise ArgumentError
end
end
...which isn't too bad, all things considered.
# Example test...
hash = {:one=>1, :two=>[1, 2, 3], :three=>[{:one=>1, :two=>2}, "hello", {:one=>{:two=>{:three=>3}}}]}
puts dig?(hash, :one)
puts dig?(hash, :two, 0)
puts dig?(hash, :none)
puts dig?(hash, :none, 0)
puts dig?(hash, :two, -1)
puts dig?(hash, :two, 10)
puts dig?(hash, :three, 0, :two)
puts dig?(hash, :three, 0, :none)
puts dig?(hash, :three, 2, :one, :two, :three)
puts dig?(hash, :three, 2, :one, :two, :none)
Here is a concise way of doing it which works with nested Array and Hash (and any other object that responds to fetch).
def deep_fetch? obj, *argv
argv.each do |arg|
return false unless obj.respond_to? :fetch
obj = obj.fetch(arg) { return false }
end
true
end
obj = { hello: [ nil, { world: nil } ] }
deep_fetch? obj, :hell # => false
deep_fetch? obj, :hello, 0 # => true
deep_fetch? obj, :hello, 2 # => false
deep_fetch? obj, :hello, 0, :world # => false
deep_fetch? obj, :hello, 1, :world # => true
deep_fetch? obj, :hello, :world
TypeError (no implicit conversion of Symbol into Integer)
The previous code raises an error when accessing an Array element with a non-Integer index (just like Array#dig), which sometimes is not the behavior one is looking for. The following code works well in all cases, but the rescue is not a good practice:
def deep_fetch? obj, *argv
argv.each { |arg| obj = obj.fetch(arg) } and true rescue false
end
obj = { hello: [ nil, { world: nil } ] }
deep_fetch? obj, :hell # => false
deep_fetch? obj, :hello, 0 # => true
deep_fetch? obj, :hello, 2 # => false
deep_fetch? obj, :hello, 0, :world # => false
deep_fetch? obj, :hello, 1, :world # => true
deep_fetch? obj, :hello, :world # => false
Please help to explain what is needed in my code to decipher if the array contains an integer, if it does I need to add 1 to it and if doesn't if will just display the string or symbol.
I have left #notes where my brain stopped working
# possible arrays
# array = [1, "two", :three]
# array = [1, 2, 3]
class Array
def new_map
result = []
self.each do |item|
yield(item)
if #check to see if item is an integer
result << item + 1
else
# add string to result array
end
end
result
end
end
Here is the Rspec test:
describe "Array" do
describe "new_map" do
it "should not call map" do
a = [1, 2, 3]
a.stub(:map) { '' }
a.new_map { |i| i + 1 }.should eq([2, 3, 4])
end
it "should map any object" do
a = [1, "two", :three]
a.new_map { |i| i.class }.should eq([Fixnum, String, Symbol])
end
end
end
if item.is_a? Integer
result << item + 1
class Array
def new_map
result = []
self.each do |item|
yield(item)
if item.class == Integer # or if item.is_a? Integer
result << item + 1
else
# add string to result array
end
end
result
end
end
example:
=> 1.is_a? Integer
=> true
=> "1".is_a? Integer
=> false
=> 1_000_000.is_a? Integer
=> true
=> 1_000_000.class
=> Fixnum
=> 1_000_000.is_a? Integer
=> true
Try this:
class Array
def new_map
map do |item|
yield(item)
end
end
end
Actually you first spec does not make sense. You yield i + 1 to the block. This must fail if ì is not a Fixnum. You must not check if something is an Integer in your method, but in the block. This should work:
describe "Array" do
describe "new_map" do
it "should not call map" do
a = [1, 2, 3]
a.new_map { |i| (i.is_a?(Integer) ? i + 1 : i) }.should eq([2, 3, 4])
end
it "should map any object" do
a = [1, "two", :three]
a.new_map { |i| i.class }.should eq([Fixnum, String, Symbol])
end
end
end
class Array
def new_map
result = []
self.each do |item|
result << yield(item)
end
result
end
end
And both your tests pass, don't check object's class unnecessarily, trust duck typing.
I am just learning ruby and this seems to be an easy mistake I am doing here right?
def palindromic(str)
str.to_s
if str.reverse == str
puts "it is a palindromic number!"
end
end
palindromic(500)
Instead I am getting an error
Project4.rb:5:in `palindromic': undefined method `reverse' for 500:Fixnum (NoMet
hodError)
from Project4.rb:10:in `<main>'
You need to change the line str.to_s to str=str.to_s. One example to show you why so is below :
num = 12
num.to_s # => "12"
num # => 12
num=num.to_s
num # => "12"
Basically String#to_s change the receiver instance to the instance of String.But if the receiver is already the String instance,in that case receiver itself will be returned.
ar = [1,2]
ar.object_id # => 77603090
ar.to_s.object_id # => 77602480
str = 'Hello'
str.object_id # => 77601890
str.to_s.object_id # => 77601890
How do I call a method, given its name, on an element of an array?
For example, I could have:
thing = "each"
I want to be able to do something like:
def do_thing(thing)
array = [object1,object2]
array[0].thing
end
so that do_thing(to_s), for example, would run object1.to_s.
You can use public_send or send. public_send only sends to public methods while send can see public and private methods.
def do_thing(thing)
array = [1,2,3]
array.public_send(thing)
end
do_thing('first')
# => 1
do_thing(:last)
# => 3
Update A more general version:
def do_thing(array, index, method, *args)
array[index].public_send(method, *args)
end
do_thing([1, 2, 3], 0, :to_s)
# => "1"
do_thing([[1,2], [3, 4]], 0, :fetch, 0)
# => 1
require 'ostruct'
o = OpenStruct.new(attribute: 'foo')
do_thing([o], 0, :attribute=, 'bar')
o.attribute == 'bar'
# => true
Object#send
thing = "each"
def do_thing(thing)
array = [1,2,3]
array.send(thing)
end
From the doc:
class Klass
def hello(*args)
"Hello " + args.join(' ')
end
end
k = Klass.new
k.send :hello, "gentle", "readers" #=> "Hello gentle readers"
Here is an example to help you out although I don't have any idea what objects are residing inside your array:
arr = [Array.new(2,10),"abc" ]
arr.each{|i| p i.send(:length)}
#>>2
#>>3
In order to implement auto-vivification of Ruby hash, one can employ the following class
class AutoHash < Hash
def initialize(*args)
super()
#update, #update_index = args[0][:update], args[0][:update_key] unless
args.empty?
end
def [](k)
if self.has_key?k
super(k)
else
AutoHash.new(:update => self, :update_key => k)
end
end
def []=(k, v)
#update[#update_index] = self if #update and #update_index
super
end
def few(n=0)
Array.new(n) { AutoHash.new }
end
end
This class allows to do the following things
a = AutoHash.new
a[:a][:b] = 1
p a[:c] # => {} # key :c has not been created
p a # => {:a=>{:b=>1}} # note, that it does not have key :c
a,b,c = AutoHash.new.few 3
b[:d] = 1
p [a,b,c] # => [{}, {:d=>1}, {}] # hashes are independent
There is a bit more advanced definition of this class proposed by Joshua, which is a bit hard for me to understand.
Problem
There is one situation, where I think the new class can be improved. The following code fails with the error message NoMethodError: undefined method '+' for {}:AutoHash
a = AutoHash.new
5.times { a[:sum] += 10 }
What would you do to handle it? Can one define []+= operator?
Related questions
Is auto-initialization of multi-dimensional hash array possible in Ruby, as it is in PHP?
Multiple initialization of auto-vivifying hashes using a new operator in Ruby
ruby hash initialization r
still open: How to create an operator for deep copy/cloning of objects in Ruby?
There is no way to define a []+= method in ruby. What happens when you type
x[y] += z
is
x[y] = x[y] + z
so both the [] and []= methods are called on x (and + is called on x[y], which in this case is an AutoHash). I think that the best way to handle this problem would be to define a + method on AutoHash, which will just return it's argument. This will make AutoHash.new[:x] += y work for just about any type of y, because the "empty" version of y.class ('' for strings, 0 for numbers, ...) plus y will almost always equal y.
class AutoHash
def +(x); x; end
end
Adding that method will make both of these work:
# Numbers:
a = AutoHash.new
5.times { a[:sum] += 10 }
a[:sum] #=> 50
# Strings:
a = AutoHash.new
5.times { a[:sum] += 'a string ' }
a[:sum] #=> "a string a string a string a string a string "
And by the way, here is a cleaner version of your code:
class AutoHash < Hash
def initialize(args={})
super
#update, #update_index = args[:update], args[:update_key]
end
def [](k)
if has_key? k
super(k)
else
AutoHash.new :update => self, :update_key => k
end
end
def []=(k, v)
#update[#update_index] = self if #update and #update_index
super
end
def +(x); x; end
def self.few(n)
Array.new(n) { AutoHash.new }
end
end
:)
What I think you want is this:
hash = Hash.new { |h, k| h[k] = 0 }
hash['foo'] += 3
# => 3
That will return 3, then 6, etc. without an error, because the the new value is default assigned 0.
require 'xkeys' # on rubygems.org
a = {}.extend XKeys::Hash
a[:a, :b] = 1
p a[:c] # => nil (key :c has not been created)
p a # => { :a => { :b => 1 } }
a.clear
5.times { a[:sum, :else => 0] += 10 }
p a # => { :sum => 50 }