5.1.3 Methods on <time-offset>

Because a <time-offset> can represent future time or past time, it will be useful to provide a convenient way to determine whether a <time-offset> is in the past. We define a new predicate named past? as follows:

define method past? (time :: <time-offset>) => (past? :: <boolean>)
  time.total-seconds < 0;
end method past?;

The past? method returns an instance of <boolean>, which is #t if the time offset is in the past, and otherwise is #f. Here is an example:

? past?(*my-time-offset*)
#f
? past?(*your-time-offset*)
#t

We need a method to describe instances of <time-offset>. The output should look like this:

? say-time-offset(*my-time-offset*);
plus 15:20
? say-time-offset(*your-time-offset*);
minus 6:45

We might define the method in this way:

define method say-time-offset (time :: <time-offset>) => ()
  let(hours, minutes) = decode-total-seconds(time);
  format-out("%s %d:%s%d",
             if (past?(time)) "minus" else "plus" end, 
             hours, 
             if (minutes < 10) "0" else "" end,
             minutes);
end method say-time-offset;

If we test this method in a listener, however, the result is different:

? say-time-offset(*my-time-offset*);
ERROR: No applicable method for decode-total-seconds with argument {instance <time-offset>}

"No applicable method" means that there is no method for this generic function that is appropriate for the arguments. To understand this error, we can look at the methods for decode-total-seconds in Figure 4.1, page 49. One method takes an argument of the type <integer>. Another method takes an argument of the type <time-of-day>. There is no method for instances of <time-offset>, so Dylan signals an error. There are three possible approaches to solving this problem.

As a first approach, we could define the say-time-offset method to call decode-total-seconds with an integer.

// First approach: Call decode-total-seconds with an integer
define method say-time-offset (time :: <time-offset>) => ()	// 1
  let(hours, minutes) = decode-total-seconds(abs(time.total-seconds));	// 2
  format-out("%s %d:%s%d",	// 3
             if (past?(time)) "minus" else "plus" end, 	// 4
             hours, 	// 5
             if (minutes < 10) "0" else "" end,	// 6
             minutes);	// 7
end method say-time-offset;	// 8	

We changed only the call to decode-total-seconds on line 2. Here, we call it with the absolute value (returned by the abs function) of the total-seconds slot.

This approach works, but it is awkward because we need to remember what kinds of arguments decode-total-seconds can take. The convenient calling syntax that we introduced for calling decode-total-seconds with an instance of <time-of-day> is not available for other kinds of time.

As a second approach, we could to define a third method for decode-total-seconds that takes as its argument an instance of <time-offset>:

// Second approach: Define a method on <time-offset>
define method decode-total-seconds (time :: <time-offset>) => ()
  decode-total-seconds(abs(time.total-seconds));
end method decode-total-seconds;

The method for say-time-offset can then call decode-total-seconds, as we did in the first place:

define method say-time-offset (time :: <time-offset>) => ()
  let(hours, minutes) = decode-total-seconds(time);
  format-out("%s %d:%s%d",
             if (past?(time)) "minus" else "plus" end, 
             hours, 
             if (minutes < 10) "0" else "" end,
             minutes);
end method say-time-offset;

This approach works, and it preserves the flexibility of calling decode-total-seconds on instances of <integer>, <time-of-day>, and <time-offset>. However, the body of the method on <time-offset> (defined in this section) is nearly identical to the body of the method on <time-of-day> (defined in Section 4.6.5, page 48). The only difference is that we use abs in the method on <time-offset> but not in the method on <time-of-day>. If we used it in the method on <time-of-day>, it would be harmless. Duplication of code is ugly, adds maintenance overhead, and is particularly undesirable when programming in an object-oriented language, where it may indicate a flaw in the overall design.

The best solution to the problem lies in a third approach — to rethink the classes and methods in a more object-oriented style, using inheritance. We show this solution in the next section.