ruby_newby
ruby_newby

Reputation: 25

Initialize ruby object with a hash with default values?

I'm trying to figure out how to assign a default value for a hash key in my initialize method. I've made it work using a helper method to assign the date, but believe there has to be a more efficient way of doing this to avoid a separate helper method. Any thoughts appreciated!

def initialize(details)
  @name = details[:name]
  @age = details[:age]
  @admission_date = get_date(details)
end

def get_date(details)
  if details[:date].nil?
    @admission_date = Date.today.strftime("%d%m%y")
  else
    @admission_date = details[:date]
  end
end

Upvotes: 1

Views: 583

Answers (3)

PhiAgent
PhiAgent

Reputation: 158

Oh i now see, you can use the or sign, it'll return the first truthy value:

require 'date'

details1={name:"Will", age:19}
details2={name:"Will", age:19, date:'has_date'}

class Test
  attr_reader :admission_date, :age, :name
def initialize(details)
  @name = details[:name]
  @age = details[:age]
  @admission_date = details[:date]||Date.today.strftime("%d%m%y")
end
end

t1=Test.new(details1)
p t1.admission_date #"190920"

t2=Test.new(details2)
p t2.admission_date #"has_date"

Upvotes: 1

Jörg W Mittag
Jörg W Mittag

Reputation: 369458

In Ruby, the conditional expression is, well, an expression, which means it evaluates to a value. (In Ruby, everything is an expression, there are no statements, ergo, everything evaluates to a value.) The value of a conditional expression is the value of the branch that was taken.

So, everywhere where you see something like this:

if foo
  bar(baz)
else
  bar(quux)
end

You can always replace that with

bar(
  if foo
    baz
  else
    quux
  end
)

# more conventionally written as

bar(if foo then baz else quux end)

And everywhere where you see something like this:

if foo
  bar = baz
else
  bar = quux
end

You can always replace that with

bar = if foo
  baz
else
  quux
end

So, let's do that here:

def get_date(details)
  @admission_date = if details[:date].nil?
    Date.today.strftime("%d%m%y")
  else
    details[:date]
  end
end

Let's look at this further: your get_date method is really strange. It performs a side-effect (it assigns to @admission_date) but it also returns a value. Normally, a method should either perform a side-effect (and return nothing, i.e. in Ruby nil) or return something but not both.

Also, the fact that a method named get something is actually setting something is incredibly confusing and misleading, and confusing and misleading method names are very dangerous because they lead to bugs where the programmer thinks the method is doing one thing but it actually does another.

I, personally, would not at all expect that it is unsafe to call a method named get something. But this method is unsafe: if I call it just to check what the current date is (after all, it is called get_date, so what could it possibly do other than, you know, get the date), I will actually overwrite the value of @admission_date!

What is even weirder, though, is the way that the method is used: even though the method already assigns the instance variable @admission_date when it is called, the return value of the method (which is just the value that was assigned to @admission_date) is used to assign @admission_date again, so that it immediately gets overwritten with the exact same value that it already has.

It seems obvious that even the author of the code was confused and mislead by the name of the method! The method name is so misleading that the author didn't even see the double assignment despite the fact that the two assignments are literally within 5 lines of each other.

So, let's remove one of the redundant assignments. I would prefer to remove the one in the get_date method to bring its behavior further in line with its name:

def get_date(details)
  if details[:date].nil?
    Date.today.strftime("%d%m%y")
  else
    details[:date]
  end
end

Furthermore, it looks like details[:date] can be either nil or a Date but it can never be false and nil is not a valid value. So, we can use the well-known || idiom here instead:

def get_date(details)
  details[:date] || Date.today.strftime("%d%m%y")
end

Or, probably even better, we can use Hash#fetch:

def get_date(details)
  details.fetch(:date, Date.today.strftime("%d%m%y"))
end

Since your helper method isn't really doing anything complex at this point, and is only called from one single place in your code, we can just inline it:

def initialize(details)
  @name = details[:name]
  @age = details[:age]
  @admission_date = details.fetch(:date, Date.today.strftime("%d%m%y"))
end

Note that since Ruby 2.0 (released on Ruby's 20th birthday in 2013), Ruby supports keyword parameters, both mandatory keyword parameters and optional keyword parameters with default argument values, so an even better way to write this might be:

def initialize(name:, age:, date: Date.today.strftime("%d%m%y"))
  @name, @age, @date = name, age, date
end

But that depends on your API design and your callers.

Upvotes: 1

max
max

Reputation: 102036

You're looking for keyword arguments:

class Thing
  def initialize(date: Date.today.strftime("%d%m%y"), **details)
    @name = details[:name]
    @age = details[:age]
    @admission_date = date
  end
end

The double splat (**) gathers the remaining keyword arguments into a hash.

You can create also create non optional-arguments by omitting the default value:

class Thing
  def initialize(name:, date: Date.today.strftime("%d%m%y"), **details)
    @name = name
    @age = details[:age]
    @admission_date = date
  end
end

Upvotes: 3

Related Questions