Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

7.3 Times and dates

The first change to note is that the subtype Year_Number in the package Ada.Calendar in Ada 2005 is
subtype Year_Number is Integer range 1901 .. 2399;
In Ada 95 (and in Ada 83) the range is 1901 .. 2099. This avoids the leap year complexity caused by the 400 year rule at the expense of the use of dates in the far future. But, the end of the 21st century is perhaps not so far into the future, so it was decided that the 2.1k problem should be solved now rather than later. However, it was decided not to change the lower bound because some systems are known to have used that as a time datum. The upper bound was chosen in order to avoid difficulties for implementations. For example, with one nanosecond for Duration'Small, the type Time can just be squeezed into 64 bits.
Having grasped the nettle of doing leap years properly Ada 2005 dives in and deals with leap seconds, time zones and other such matters in pitiless detail.
There are three new child packages Calendar.Time_Zones, Calendar.Arithmetic and Calendar.Formatting. We will look at these in turn.
The specification of the first is
package Ada.Calendar.Time_Zones is
   -- Time zone manipulation:
   type Time_Offset is range –28*60 .. 28*60;
   Unknown_Zone_Error: exception;
   function UTC_Time_Offset(Date: Time := Clock) return Time_Offset;
end Ada.Calendar.Time_Zones;
Time zones are described in terms of the number of minutes different from UTC (which curiously is short for Coordinated Universal Time); this is close to but not quite the same as Greenwich Mean Time (GMT) and similarly does not suffer from leaping about in spring and falling about in the autumn. It might have seemed more natural to use hours but some places (for example India) have time zones which are not an integral number of hours different from UTC.
Time is an extraordinarily complex subject. The difference between GMT and UTC is never more than one second but at the moment of writing there is a difference of about 0.577 seconds. The BBC broadcast timesignals based on UTC but call them GMT and with digital broadcasting they turn up late anyway. The chronophile might find the website http://www.merlyn.demon.co.uk/misctime.htm#GMT of interest.
So the function UTC_Time_Offset applied in an Ada program in Paris to a value of type Time in summer should return a time offset of 120 (one hour for European Central Time plus one hour for daylight saving or heure d’ été). Remember that the type Calendar.Time incorporates the date. To find the offset now (that is, at the time of the function call) we simply write 
Offset := UTC_Time_Offset;
and then Clock is called by default.
To find what the offset was on Christmas Day 2000 we write 
Offset := UTC_Time_Offset(Time_Of(2000, 12, 25));
and this should return 60 in Paris. So the poor function has to remember the whole history of local time changes since 1901 and predict them forward to 2399 – these Ada systems are pretty smart! In reality the intent is to use whatever the underlying operating system provides. If the information is not known then it can raise Unknown_Zone_Error.
Note that we are assuming that the package Calendar is set to the local civil (or wall clock) time. It doesn't have to be but one expects that to be the normal situation. Of course it is possible for an Ada system running in California to have Calendar set to the local time in New Zealand but that would be unusual. Equally, Calendar doesn't have to adjust with daylight saving but we expect that it will. (No wonder that Ada.Real_Time was introduced for vital missions such as boiling an egg.)
A useful fact is that
Clock – Duration(UTC_Time_Offset*60)
gives UTC time – provided we don't do this just as daylight saving comes into effect in which case the call of Clock and that of UTC_Time_Offset might not be compatible.
More generally the type Time_Offset can be used to represent the difference between two time zones. If we want to work with the difference between New York and Paris then we could say 
NY_Paris: Time_Offset := –360;
The time offset between two different places can be greater than 24 hours for two reasons. One is that the International Date Line weaves about somewhat and the other is that daylight saving time can extend the difference as well. Differences of 26 hours can easily occur and 27 hours is possible. Accordingly the range of the type Time_Offset allows for a generous 28 hours.
The package Calendar.Arithmetic provides some awkward arithmetic operations and also covers leap seconds. Its specification is
package Ada.Calendar.Arithmetic is
   -- Arithmetic on days:
   type Day_Count is range
     –366*(1+Year_Number'Last – Year_Number'First)
     ..
     +366*(1+Year_Number'Last – Year_Number'First);
   subtype Leap_Seconds_Count is Integer range –2047 .. 2047;
   procedure Difference(
      Left, Right: in Time;
      Days: out Day_Count; Seconds: out Duration;
      Leap_Seconds: out Leap_Seconds_Count);
   function "+" (Left: Time; Right: Day_Count) return Time;
   function "+" (Left: Day_Count; Right: Time) return Time;
   function "–" (Left: Time; Right: Day_Count) return Time;
   function "–" (Left, Right: Time) return Day_Count;
end Ada.Calendar.Arithmetic;
The range for Leap_Seconds_Count is generous. It allows for a leap second at least four times a year for the foreseeable future – the somewhat arbitrary range chosen allows the value to be accommodated in 12 bits. And the 366 in Day_Count is also a bit generous – but the true expression would be very unpleasant.
One of the problems with the old planet is that it is slowing down and a day as measured by the Earth's rotation is now a bit longer than 86,400 seconds. Naturally enough we have to keep the seconds uniform and so in order to keep worldly clocks synchronized with the natural day, an odd leap second has to be added from time to time. This is always added at midnight UTC (which means it can pop up in the middle of the day in other time zones). The existence of leap seconds makes calculations with times somewhat tricky.
The basic trouble is that we want to have our cake and eat it. We want to have the invariant that a day has 86,400 seconds but unfortunately this is not always the case.
The procedure Difference operates on two values of type Time and gives the result in three parts, the number of days (an integer), the number of seconds as a Duration and the number of leap seconds (an integer). If Left is later then Right then all three numbers will be nonnegative; if earlier, then nonpositive.
Remember that Difference like all these other operations always works on local time as defined by the clock in Calendar (unless stated otherwise).
Suppose we wanted to find the difference between noon on June 1st 1982 and 2pm on July 1st 1985 according to a system set to UTC. We might write 
Days: Day_Count;
Secs: Duration;
Leaps: Leap_Seconds_Count;
...
Difference(
      Time_Of(1985, 7, 1, 14*3600.0),
      Time_Of(1982, 6, 1, 12*3600.0), Days, Secs, Leaps);
The results should be 
Days = 365+366+365+30 = 1126,
Secs = 7200.0,
Leaps = 2.
There were leap seconds on 30 June 1983 and 30 June 1985.
The functions "+" and "–" apply to values of type Time and Day_Count (whereas those in the parent Calendar apply only to Time and Duration and thus only work for intervals of a day or so). Note that the function "–" between two values of type Time in this child package produces the same value for the number of days as the corresponding call of the function Difference – leap seconds are completely ignored. Leap seconds are in fact ignored in all the operations "+" and "–" in the child package.
However, it should be noted that Calendar."–" counts the true seconds and so the expression 
Calendar."–" (Time_Of(1985, 7, 1, 1*3600.0), Time_Of(1985, 6, 30, 23*3600.0))
has the Duration value 7201.0 and not 7200.0 because of the leap second at midnight that night. (We are assuming that our Ada system is running at UTC.) The same calculation in New York will produce 7200.0 because the leap second doesn't occur until 4 am in EST (with daylight saving).
Note also that 
Calendar."–" (Time_Of(1985, 7, 1, 0.0), Time_Of(1985, 6, 30, 0.0))
in Paris where the leap second occurs at 10pm returns 86401.0 whereas the same calculation in New York will return 86400.0.
The third child package Calendar.Formatting has a variety of functions. Its specification is
with Ada.Calendar.Time_Zones;
use Ada.Calendar.Time_Zones;
package Ada.Calendar.Formatting is
   -- Day of the week:
   type Day_Name is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
   function Day_Of_Week(Date: Time) return Day_Name;
   -- Hours:Minutes:Seconds access:
   subtype Hour_Number is Natural range 0 .. 23;
   subtype Minute_Number is Natural range 0 .. 59;
   subtype Second_Number is Natural range 0 .. 59;
   subtype Second_Duration is Day_Duration range 0.0 .. 1.0;
   function Year(Date: Time; Time_Zone: Time_Offset := 0) return Year_Number;
   -- similarly functions Month, Day, Hour, Minute
   function Second(Date: Time) return Second_Number;
   function Sub_Second(Date: Time) return Second_Duration;
   function Seconds_Of(
      Hour: Hour_Number;
      Minute: Minute_Number;
      Second: Second_Number := 0;
      Sub_Second: Second_Duration := 0.0) return Day_Duration;
   procedure Split(
      Seconds: in Day_Duration;    -- (1)
      Hour: out Hour_Number;
      Minute: out Minute_Number;
      Second: out Second_Number;
      Sub_Second: out Second_Duration);
   procedure Split(
      Date: in Time;    -- (2)
      Year: out Year_Number;
      Month: out Month_Number;
      Day: out Day_Number;
      Hour: out Hour_Number;
      Minute: out Minute_Number;
      Second: out Second_Number;
      Sub_Second: out Second_Duration;
      Time_Zone: in Time_Offset := 0);
   function Time_Of(
      Year: Year_Number;
      Month: Month_Number;
      Day: Day_Number;
      Hour: Hour_Number;
      Minute: Minute_Number;
      Second: Second_Number;
      Sub_Second: Second_Duration := 0.0;
      Leap_Second: Boolean := False;
      Time_Zone: Time_Offset := 0) return Time;
   function Time_Of(
      Year: Year_Number;
      Month: Month_Number;
      Day: Day_Number;
      Seconds: Day_Duration;
      Leap_Second: Boolean := False;
      Time_Zone: Time_Offset := 0) return Time;
   procedure Split(
      Date: in Time;    --(3)
      ... -- as (2) but with additional parameter
      Leap_Second: out Boolean;
      Time_Zone: in Time_Offset := 0);
   procedure Split(
      Date: in Time;    -- (4)
      ... -- as Calendar.Split
      ... -- but with additional parameters
      Leap_Second: out Boolean;
      Time_Zone: in Time_Offset := 0);
   -- Simple image and value:
   function Image(
      Date: Time;
      Include_Time_Fraction: Boolean := False;
      Time_Zone: Time_Offset := 0) return String;
   function Value(Date: String; Time_Zone: Time_Offset := 0) return Time;
   function Image (
      Elapsed_Time: Duration;
      Include_Time_Fraction: Boolean := False) return String;
   function Value(Elapsed_Time: String) return Duration;
end Ada.Calendar.Formatting;
The function Day_Of_Week will be much appreciated. It is a nasty calculation.
Then there are functions Year, Month, Day, Hour, Minute, Second and Sub_Second which return the corresponding parts of a Time taking account of the time zone given as necessary. It is unfortunate that functions returning the parts of a time (as opposed to the parts of a date) were not provided in Calendar originally. All that Calendar provides is Seconds which gives the number of seconds from midnight and leaves users to chop it up for themselves. Note that Calendar.Second returns a Duration whereas the function in this child package is Seconds which returns an Integer. The fraction of a second is returned by Sub_Second.
Most of these functions have an optional parameter which is a time zone offset. Wherever in the world we are running, if we want to know the hour according to UTC then we write 
Hour(Clock, UTC_Time_Offset)
If we are in New York and want to know the hour in Paris then we write 
Hour(Clock, –360)
since New York is 6 hours (360 minutes) behind Paris.
Note that Second and Sub_Second do not have the optional Time_Offset parameter because offsets are an integral number of minutes and so the number of seconds does not depend upon the time zone.
The package also generously provides four procedures Split and two procedures Time_Of. These have the same general purpose as those in Calendar. There is also a function Seconds_Of. We will consider them in the order of declaration in the package specification above.
The function Seconds_Of creates a value of type Duration from components Hour, Minute, Second and Sub_Second. Note that we can use this together with Calendar.Time_Of to create a value of type Time. For example 
T := Time_Of(2005, 4, 2, Seconds_Of(22, 4, 10, 0.5));
makes the time of the instant when I (originally) typed that last semicolon.
The first procedure Split is the reverse of Seconds_Of. It decomposes a value of type Duration into Hour, Minute, Second and Sub_Second. It is useful with the function Calendar.Split thus 
Split(Some_Time, Y, M, D, Secs);    -- split time
Split(Secs, H, M, S, SS);    -- split secs
The next procedure Split (no 2) takes a Time and a Time_Offset (optional) and decomposes the time into its seven components. Note that the optional parameter is last for convenience. The normal rule for parameters of predefined procedures is that parameters of mode in are first and parameters of mode out are last. But this is a nuisance if parameters of mode in have defaults since this forces named notation if the default is used.
There are then two functions Time_Of which compose a Time from its various constituents and the Time_Offset (optional). One takes seven components (with individual Hour, Minute etc) whereas the other takes just four components (with Seconds in the whole day). An interesting feature of these two functions is that they also have a Boolean parameter Leap_Second which by default is False.
The purpose of this parameter needs to be understood carefully. Making up a typical time will have this parameter as False. But suppose we need to compose the time midway through the leap second that occurred on 30 June 1985 and assign it to a variable Magic_Moment. We will assume that our Calendar is in New York and set to EST with daylight saving (and so midnight UTC is 8pm in New York). We would write 
Magic_Moment: Time := Time_Of(1985, 6, 30, 19, 59, 59, 0.5, True);
In a sense there were two 19:59:59 that day in New York. The proper one and then the leap one; the parameter distinguishes them. So the moment one second earlier is given by 
Normal_Moment: Time := Time_Of(1985, 6, 30, 19, 59, 59, 0.5, False);
We could have followed ISO and used 23:59:60 UTC and so have subtype Second_Number is Natural range 0 .. 60; but this would have produced an incompatibility with Ada 95.
Note that if the parameter Leap_Second is True and the other parameters do not identify a time of a leap second then Time_Error is raised.
There are then two corresponding procedures Split (nos 3 and 4) with an out parameter Leap_Second. One produces seven components and the other just four. The difference between this seven-component procedure Split (no 3) and the earlier Split (no 2) is that this one has the out parameter Leap_Second whereas the other does not. Writing 
Split(Magic_Moment, 0, Y, M, D, H, M, S, SS, Leap);
results in Leap being True whereas 
Split(Normal_Moment, 0, Y, M, D, H, M, S, SS, Leap);
results in Leap being False but gives all the other out parameters (Y, ... , SS) exactly the same values.
On the other hand calling the version of Split (no 2) without the parameter Leap_Second thus 
Split(Magic_Moment, 0, Y, M, D, H, M, S, SS);
Split(Normal_Moment, 0, Y, M, D, H, M, S, SS);
produces exactly the same results for both calls.
The reader might wonder why there are two Splits on Time with Leap_Second but only one without. This is because the parent package Calendar already has the other one (although without the time zone parameter). Another point is that in the case of Time_Of, we can default the Leap parameter being of mode in but in the case of Split the parameter has mode out and cannot be omitted. It would be bad practice to encourage the use of a dummy parameter which is ignored and hence there have to be additional versions of Split.
Finally, there are two pairs of functions Image and Value. The first pair works with values of type Time. A call of Image returns a date and time value in the standard ISO 8601 format. Thus taking the Normal_Moment above 
Image(Normal_Moment)
returns the following string 
"1985-06-30 19:59:59"    -- in New York
If we set the optional parameter Include_Time_Fraction to True thus 
Image(Normal_Moment, True)
then we get 
"1985-06-30 19:59:59.50"
There is also the usual optional Time_Zone parameter so we could produce the time in Paris (from the program in New York) thus 
Image(Normal_Moment, True, –360)
giving 
"1985-07-01 02:59:59.50"    -- in Paris
The matching Value function works in reverse as expected.
We would expect to get exactly the same results with Magic_Moment. However, since some implementations might have an ISO function available in their operating system it is also allowed to produce 
"1985-06-30 19:59:60"    -- in New York
The other Image and Value pair work on values of type Duration thus 
Image(10_000.0)    -- "02:46:40"
with the optional parameter Include_Time_Fraction as before. Again the corresponding function Value works in reverse.

Contents   Index   References   Search   Previous   Next 
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association and its member companies: ARA Members AdaCore Polyspace Technologies Praxis Critical Systems IBM Rational Sofcheck and   Ada-Europe:
Ada-Europe