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