In Progress
Unit 1, Lesson 1
In Progress

Implicit And Explicit Conversion

Video transcript & code

Recently we've been working on making it easier to convert from various types into our new class for representing quantities of feet. We now have a handy "conversion function" that can accept various numeric values, strings, and other Feet objects.

Feet(763)                       # => #<Feet:763.0>
Feet(9.876)                     # => #<Feet:9.876>
Feet("10000")                   # => #<Feet:10000.0>
Feet(Feet(763))                 # => #<Feet:763.0>

Let's turn our attention now to the inverse operation: converting from Feet to other types. Presumably, sooner or later we will need to interface with APIs that expect standard Ruby numeric types. So we'll focus on conversions from Feet to raw numbers.

As things stand now, we can already ask a Feet object what its magnitude is.

require "./feet"

Feet(1234).magnitude            # => 1234.0

We could just leave it at that. But from the point of view of someone using this class, "magnitude" probably isn't the most obvious method name to use to convert a Feet object to a raw numeric type.

Ruby programmers are used to asking for numeric values using explicit conversion methods. For instance, we tell a string to convert itself to a floating point number using #to_f.

"123.4".to_f                    # => 123.4

Since Feet magnitude is always held as a floating point number, #to_f seems like an obvious conversion method to provide. To implement it, we just delegate straight to the #magnitude method.

class Feet
  # ...
  def to_f
    magnitude
  end
end

An even shorter way to write this would be to simply alias the #magnitude method to the #to_f method. We just have to remember that Ruby's alias_method puts the alias name first, and the original name second.

class Feet
  # ...
  alias_method :to_f, :magnitude
  # ...
end

Now we can send a conventional #to_f message to a Feet object and get the floating-point magnitude of the measurement back.

require "./feet"

Feet(1234.5).to_f               # => 1234.5

While we're at it, we might as well provide a #to_i conversion method for converting measurements directly to integers as well. We'll just re-use the #to_f conversion and tack on a #to_i message.

class Feet
  # ...
  def to_i
    to_f.to_i
  end
  # ...
end

Of course, this conversion will lose any fractional part of the magnitude.

I mentioned that these conversions will be useful for interfacing with third-party code that expects raw numeric types. But that isn't the only advantage. Remember back in Episode 194 and Episode 195 when we talked about string formats? Well, you might be pleased to know that with these most recent changes, we've now made the Feet class format-friendly.

What do I mean by format-friendly? Let's look at an example. Supposing we have a dataset that contains dimensions for an inventory of pre-fabricated outbuildings.

BUILDINGS = [
  {name: "The Snoopy", length: Feet(5), width: Feet(4)},
  {name: "The Woodshed", length: Feet(20), width: Feet(15)},
  {name: "The Smokehouse", length: Feet(10), width: Feet(10)},
  {name: "The Jumbo", length: Feet(30), width: Feet(22.25)}
]

We'd like to print out a neatly formatted report showing the available structures. A format string is the natural choice for this kind of tabular layout. Since we just want to dump the text to standard out, we'll use a format string in the form of Ruby's printf method.

We start with a header row, with headings for name, length, and width.

Then we iterate over the building data. For each building, we use named field references in our format specification, each one referring to a field in our data set. By controlling the field widths using formatting flags, we are able to produce neat columns of data.

require "./feet"
require "./data"

printf "%-15s %12s %12s\n", "Name", "Length(ft)", "Width(ft)"

BUILDINGS.each do |building|
  printf "%<name>-15s %<length>12.1f %<width>12.1f\n", building
end
# >> Name              Length(ft)    Width(ft)
# >> The Snoopy               5.0          4.0
# >> The Woodshed            20.0         15.0
# >> The Smokehouse          10.0         10.0
# >> The Jumbo               30.0         22.2

Let's take a closer look at what we just accomplished. In the format specification we used the f field type for the length and width columns. That tells ruby "treat this field as a floating point number". But remember, the actual data is in the form of Feet objects. The fact that Ruby is able to format these user-supplied objects as if they were floating point numbers is a direct consequence of our implementing the #to_f conversion method.

As you might expect, the fact that we implemented #to_i as well means that we could also format these fields as decimal integer values using the d format type.

require "./feet"
require "./data"

printf "%-15s %12s %12s\n", "Name", "Length(ft)", "Width(ft)"

BUILDINGS.each do |building|
  printf "%<name>-15s %<length>12.1d %<width>12.1d\n", building
end
# >> Name              Length(ft)    Width(ft)
# >> The Snoopy                 5            4
# >> The Woodshed              20           15
# >> The Smokehouse            10           10
# >> The Jumbo                 30           22

By implementing standardized explicit conversion methods, we've improved the Feet class' ability to play well with others. Other programmers looking to quickly convert a Feet object to a numeric type will be able to find the conversions they expect, and Ruby's formatting facilities are able to treat Feet values like built-in types. We're one step closer to a quantity class that has feature parity with raw numeric types.

And that's it for today. Happy hacking!

Responses