Rationale for Ada 2005
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.
© 2005, 2006 John Barnes Informatics.
Sponsored in part by: