Times and dates (original) (raw)

Rationale for Ada 2005

Contents Index References Search Previous Next


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 Timecan 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#GMTof interest.

So the function UTC_Time_Offsetapplied in an Ada program in Paris to a value of type Timein 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 Calendaris 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 Calendarset 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_Timewas 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 Clockand 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_Offsetallows for a generous 28 hours.

The package Calendar.Arithmeticprovides 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_Countis 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_Countis 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 Differenceoperates 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 Differencelike 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 Durationand 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.0because 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_Weekwill be much appreciated. It is a nasty calculation.

Then there are functions Year, Month, Day, Hour, Minute, Secondand 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 Calendarprovides 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 Durationwhereas the function in this child package is Secondswhich 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_Seconddo 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_Ofcreates a value of type Duration from components Hour, Minute, Secondand 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, Secondand 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_Ofwhich compose a Time from its various constituents and the Time_Offset (optional). One takes seven components (with individual Hour, Minuteetc) whereas the other takes just four components (with Secondsin the whole day). An interesting feature of these two functions is that they also have a Boolean parameter Leap_Secondwhich 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_Secondis 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 Leapbeing 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 Splitson Time with Leap_Secondbut only one without. This is because the parent package Calendaralready 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_Momentabove

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 Truethus

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 Imageand Value pair work on values of type Durationthus

Image(10_000.0) -- "02:46:40"

with the optional parameter Include_Time_Fractionas before. Again the corresponding function Valueworks 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 and Ada-Europe: Ada-Europe