Ada-Rapporteur-Group / User-Community-Input

Ada User Community Input Working Group - Github Mirror Prototype
26 stars 1 forks source link

What is one day? #94

Open jprosen opened 3 months ago

jprosen commented 3 months ago

Ada.Calendar.Arithmetic provides "+" and "-" operations between a Time and a Day_Count. The RM says "Adds (resp. subtracts) a number of days to a time value". This seems to imply that the resulting Time should be the same hour with one more/less day, However there is no definition of what a "day" is, and especially whether it should be understood as a Time or as a Duration. GNAT treats these functions like adding/subtracting 86400 s. to the given time (i.e. considering that a "day" is the same as a duration of 86400s.). In the case where the time is the day before/after switching between regular time and DST, a day is 23 or 25 hours. For example, if the provided time is "2023-03-27 00:00:00" and one day is subtracted, GNAT results in "2023-03-25 23:00:00". Is this intended?

CKWG commented 3 months ago

What do other Ada compilers do?

sttaft commented 3 months ago

The type Time does not inherently have a time zone, so I personally would certainly presume that any change in timezone that occurs in the interval being skipped is irrelevant. Time zone is really only relevant when producing a "local" time, but these operators are operating on Time rather than a split-up local time.

A potentially more controversial question relates to leap seconds ... ;-)

ARG-Editor commented 3 months ago

The type Time does not inherently have a time zone, ...

We decided to leave that unspecified, so it may or may not. For Janus/Ada, "Time" is a local time value; the values are stored "split up" (since that's how they came from MS-DOS or Windows). If one asks for something in a specific time zone, the values are adjusted accordingly.

... so I personally would certainly presume that any change in timezone that occurs in the interval being skipped is irrelevant.

That's also unspecified, I think. For Janus/Ada, math occurs in whatever timezone the original value was produced. It doesn't change unless someone asks for it to change.

Time zone is really only relevant when producing a "local" time, but these operators are operating on Time rather than a split-up local time.

That is a very Unix-centric view. The Janus/Ada representation is better for Windows (closer to the OS values, so Clock calls are faster), better for many typical uses (those that don't use a lot of math, but do lots of splits (such as happen when you output times), and moreover doesn't require any expensive 64-bit math. (This latter was probably more of a consideration back in the day, but whatever.)

A potentially more controversial question relates to leap seconds ... ;-)

I think the sensible answer is the same here; math doesn't change the number of leap seconds. They come from Clock values; I don't think it is practical for them to come from anywhere else. (It makes no sense to keep tables of leap seconds in Ada runtimes.)

        Randy.
ARG-Editor commented 3 months ago

Janus/Ada uses a version of the code from Claw.Time (which is generic in the sense that it doesn't depend in any way on the representation of Ada.Calendar.Time -- it only uses the functions defined in Ada.Calendar). The code determines the "Julian day" for a Time value, adds the number of days to it, and creates a new time value from the resulting "Julian day" and the seconds value of the original.

Janus/Ada stores the results of Year, Day, Month, and Seconds in a Time value (this is close to what MS-DOS and Windows return for time queries). Time zones are only applied if a time zone parameter is involved. Note that I have never fixed the library for AI12-0336-1 (which changed the meaning to Time_Zone to what everyone [else!] thought it was as opposed to what it actually said; that definition had the Time_Zone of local time = 0, which means that Time never needs adjusting to produce local time); I don't know how to do that accurately without completely changing the implementation of all time operations (and worst of all, the representation of those values).

Anyway, the result of this is that the time zone is irrelevant for math purposes in Janus/Ada (unless, of course, you explicitly provide one).

             Randy.

From: CKWG @.*** Sent: Friday, April 19, 2024 4:58 AM To: Ada-Rapporteur-Group/User-Community-Input Cc: Subscribed Subject: Re: [Ada-Rapporteur-Group/User-Community-Input] What is one day? (Issue #94)

What do other Ada compilers do?

- Reply to this email directly, view https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues/94#issu ecomment-2066236226 it on GitHub, or unsubscribe https://github.com/notifications/unsubscribe-auth/AT65YNZPYPAICFH7LAEBHC3Y6 DTCFAVCNFSM6AAAAABGOQ5BXSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANRWGIZTM MRSGY . You are receiving this because you are subscribed to this thread. https://github.com/notifications/beacon/AT65YN6SEBNLEPXPD4PK5XTY6DTCFA5CNFS M6AAAAABGOQ5BXSWGG33NNVSW45C7OR4XAZNMJFZXG5LFINXW23LFNZ2KUY3PNVWWK3TUL5UWJTT 3FBBUE.gif Message ID: @.***>

jprosen commented 3 months ago

We are moving away from the issue here... The real question here is: 1) Is "one day" in the "+" and "-" operations of Ada.Calendar.Arithmetic equivalent to a duration of 86_400s. or 2) Is "one day" the day before or after, with the same "hour" as the original day (and some tweaking if the time does not exist in the resulting time)

Note that if option 1) is chosen, these operations become useless since the operations in Ada.Calendar provide the same functionnality, while option 2) is far from trivial to code by user.

sttaft commented 3 months ago

Note that if option 1) is chosen, these operations become useless since the operations in Ada.Calendar provide the same functionnality, while option 2) is far from trivial to code by user.

I am not sure how you reach that conclusion. In fact in package Calendar, the "+" and "-", and more generally the notion of Time, are described in the AARM as being implementation-defined as far as time-zone interactions (see AARM 9.6(24.b/3) and http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ai05s/ai05-0119-1.txt?rev=1.10).

By contrast, when we defined the Ada.Calendar.Arithmetic and Ada.Calendar.Formatting, I believe we were trying to cleanly separate the notion of time from the notion of time zone, and operations where the time zone mattered were placed in Formatting and given a Time_Zone parameter (of type Time_Offset). If an operation did not have a Time_Zone parameter then it was intended to be independent of time zone. In particular, the Difference operation and the "+" and "-" operators in Ada.Calendar.Arithmetic are intended to be time zone independent. See http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ais/ai-00351.txt?rev=1.17 and http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ais/ai-00427.txt?rev=1.8 for the original rationale. In particular, Ada.Calendar.Arithmetic was designed to help determine the elapsed time between two values of type Time, so time zones should not affect the result at this level (even if they are somehow used behind the scenes).

If you want to get the Time corresponding to the next day, at the same hours/minutes in the local time zone, and there is a chance the local time zone might have changed in that period, you will need to use Split, Time_Of, and Local_Time_Offset, and worry about the fact that there might actually be two different times (e.g. 1:30AM at the end of summer time) that satisfy your requirements (or none, such as 2:30AM at the beginning of summer time). This is another reason why expecting Calendar.Arithmetic."+" to do all of this seems inappropriate.

ARG-Editor commented 2 months ago

Jean-Pierre Rosen writes:

We are moving away from the issue here...

No, we're not. When it comes to Time_Zones, the behavior of Time is implementation-defined unless you use a operations with a Time_Zone parameter. So when your original question asks "what is one day", it is an undefined question unless you explain precisely which sequence of operations you are talking about. My understanding of your question implies that you are using the functions in Calendar to determine the result, and those are always of an implementation-defined time zone (and not necessarily one that makes sense!).

The real question here is:

  1. Is "one day" in the "+" and "-" operations of Ada.Calendar.Arithmetic equivalent to a duration of 86_400s.

Surely. What else could it mean? But you have to be careful that you are using a consistent time zone in order to see that. Otherwise you are comparing apples to oranges. If you are allowing the time zone to change, it's hard to say anything sensible (and I don't see why anyone would want to try).

or

  1. Is "one day" the day before or after, with the same "hour" as the original day (and some tweaking if the time does not exist in the resulting time)

This operation is the same as (1) unless you are changing time zones as well, and no one ought to be doing that. If that's really a requirement (highly doubtful), you should do it yourself).

Note that if option 1) is chosen, these operations become useless since the operations in Ada.Calendar provide the same functionality, ...

Why do you say that? The entire purpose of the operations in Ada.Calendar.Arithmetic is to be able to add multiple days, which you cannot do (portably) with the operations on Ada.Calendar (because the operations in Ada.Calendar are based on values Duration, which is not guaranteed to have a range of more than one day). (Writing a loop to add 5 days is an insane requirement.) The operations are redundant if you are just adding one, that's not the reason these operations were needed.

The whole point of Ada.Calendar.Arithmetic was to provide a mechanism for dealing with time differences that might exceed one day (specifically, that might exceed the range of Duration). I would expect the results to be identical if the operations are less than the range of Duration. (But of course if you are not controlling the output Time_Zone, it might be hard to see that.)

(Note: Unlike Tucker's answer, I think that these operations always do the exact mathematical result, but the time zone of the answer is likely to be implementation-defined unless you specify it somewhere. For Janus/Ada, a math operation won't change the time zone of the result, but that time zone might no longer be the local time zone if Summer time started or ended in between. If you care about those things, you need to use Time_Zones consistently throughout.)

... while option 2) is far from trivial to code by user.

It is trivial, just output the result with the same time zone as the original. Otherwise, you are talking about a nonsense operation, and I don't see what use it could possibly be. The number of hours in a day doesn't change just because your clock hands are jumped forward or back. :-)

         Randy.
jprosen commented 2 months ago

Here is the concrete problem that triggered that issue. I had a program that scanned through the whole year, starting at Dec. 31, back to January 1st. Since I was not interested in the time of day part, I normalized all the dates to 00:00:00, and subtracted 1 day in the loop. I had a hard time understanding why March 26th was skipped... My understanding was that subtracting one day would give me the day before. Otherwise, that very simple need (scanning through the days in a year) becomes quite complicated... (for the story: I solved the problem by normalizing times to 12:00:00 instead of 00:00:00 - but that's more of a kludge than a solution)

sttaft commented 2 months ago

Could you post the actual program, so we could test it on our own favorite Ada compiler or Ada compiler version?

sttaft commented 2 months ago

In any case, I trust you agree with my analysis that expecting the adding of one day to produce the same time in the potentially new timezone could produces cases where there were either two answers or zero answers.

joshua-c-fletcher commented 2 months ago

Ada.Calendar.Formatting provides several functions similar to GNAT.Calendar, including the Day_Of_Week function, and the splitting of Sub_Seconds from seconds. The Language package added the time zone parameters, which GNAT.Calendar didn't have, but GNAT.Calendar has a Day_In_Year function which only calculates one way.

As noted in Issue #15, Day_Of_Week should have a time zone parameter (certainly for consistency with the rest of the Ada.Calendar.Formatting package), and a Day_In_Year function would need one, too, if included in Ada.Calendar.Formatting. With the appropriate time zone parameter, one could have a Day_In_Year function (otherwise similar to the GNAT.Calendar version) as well as a Split function to convert the other way. The Time_Zone parameter when converting from a Time type would resolve the ambiguity inherent in the problem. The function profiles could look like this:

function Day_In_Year
  (Date      : Time;
   Time_Zone : Time_Zones.Time_Offset := 0) return Day_In_Year_Number;

function Day_In_Year
  (Year        : Year_Number;
   Month       : Month_Number;
   Day         : Day_Number) return Day_In_Year_Number;

procedure Split
  (Year        : in  Year_Number;
   Day_In_Year : in  Day_In_Year_Number;
   Month       : out Month_Number;
   Day         : out Day_Number);

-- optionally a version of the Time_Of and Split subprograms
-- from Ada.Calendar.Formatting could use Day_In_Year in place
-- of Month and Day, and they'd have the optional Time_Zone parameter
-- to resolve ambiguity.
-- However, the above functions could also be used with the existing ones.

with these functions, scanning through the days in the year, as @jprosen wants to do in his program would be unambiguous, and the Time_Of function with the time zone parameter would keep the time zone consistent (if used consistently)

declare
   Year : constant Year_Number := Year (Clock);
   Month : Month_Number;
   Day : Day_Number;
   Last_Day : constant Day_In_Year_Number := Day_In_Year (Year => Year, Month => 12, Day => 31);
   Date : Ada.Calendar.Time;
begin
   for Day_In_Year in reverse 1 .. Last_Day loop
      Split (Year => Year, Day_In_Year => Day_In_Year, Month => Month, Day => Day);
      Date := Time_Of (Year => Year, Month => Month, Day => Day); -- other params defaulted
      -- do some processing for this date.
   end loop;
end;

... it's a little verbose, but no days would be missed, and time zone can be explicit, if provided in Time_Of. (note existing Time_Of function is the one from Ada.Calendar.Formatting...)

briot commented 2 months ago

Could you post the actual program, so we could test it on our own favorite Ada compiler or Ada compiler version?

It would be interesting indeed.  I tried the following:

with Ada.Text_IO;              use Ada.Text_IO; with Ada.Calendar.Arithmetic;  use Ada.Calendar.Arithmetic; with Ada.Calendar.Formatting;  use Ada.Calendar.Formatting; use Ada.Calendar; procedure Main is    T : Time := Ada.Calendar.Formatting.Time_Of (2024, 4, 1, 0.0);  begin    Put_Line (Image (T));    for J in 1 .. 4 loop       T := T - 1;       Put_Line (Image (T));    end loop; end Main;

and the output (with GNAT Pro 20240108) looks correct and as expected to me:

2024-04-01 00:00:00 2024-03-31 00:00:00 2024-03-30 00:00:00 2024-03-29 00:00:00 2024-03-28 00:00:00

In our own code we have actually introduced a full dates package which only manipulates dates, rather than using a special time of day like Jean-Pierre chose to use.  This has the benefit of making it clear in the subprogram profiles that we do not care about the time.  Maybe that's something that Ada.Calendar could consider (though there is of course always the backward compatibility to think of, so the current questions definitely need answering).

I agree with Tucker that adding or removing one day could end up with two possible times (in languages like python, there is an extra parameter to indicate whether you would want the first or second in such cases), or no time at all (in this case I guess we should raise an exception).  PostgreSQL simply skips the time in this case:

=> select '2024-04-01T02:30:00 Europe/Paris'::timestamptz - '1 day'::interval, '2024-03-31T02:30:00 Europe/Paris'::timestamptz - '2 day'::interval,  '2024-03-30T02:30:00 Europe/Paris'::timestamptz - '3 day'::interval; ┌────────────────────────┬────────────────────────┬────────────────────────┐ │        ?column?        │        ?column?        │ ?column?        │ ├────────────────────────┼────────────────────────┼────────────────────────┤ │ 2024-03-31 03:30:00+02 │ 2024-03-29 03:30:00+01 │ 2024-03-27 02:30:00+01 │ └────────────────────────┴────────────────────────┴────────────────────────┘

jprosen commented 2 months ago

Here is a simple program showing the problem. Run, and note that there is no 26/03/2023.

And try to find a simple solution for avoiding this problem...

with Ada.Calendar, Ada.Calendar.Arithmetic, Ada.Calendar.Formatting, Ada.Text_IO; procedure Date_Issue is use Ada.Calendar, Ada.Calendar.Arithmetic, Ada.Text_IO;

Jan_1st : constant Time := Time_Of (2023, 01, 01); Generated_Date : Time := Time_Of (2023, 12, 31);

function Image (T : Time) return String is Iso_Date : constant String := Ada.Calendar.Formatting.Local_Image (T); -- YYYY-MM-DD HH:MM:SS -- 1234 67 910 begin return Iso_Date (9 .. 10) & '/' & Iso_Date (6..7) & '/' & Iso_Date (1..4); end Image;

function Normalize (Left : Time) return Time is -- Reset time of date to 00:00:00.0 to make comparisons on date only begin return Time_Of (Year (Left), Month (Left), Day (Left), 0.0); end Normalize;


begin while Generated_Date >= Jan_1st loop Put_Line (Image (Generated_Date)); Generated_Date := Normalize (Generated_Date - 1); end loop; end Date_Issue;

ARG-Editor commented 2 months ago

Here is a simple program showing the problem. Run, and note that there is no 26/03/2023.

I did this using Janus/Ada, and there is indeed a 26/03/2023. So...

And try to find a simple solution for avoiding this problem...

Use Janus/Ada??? ;-)

I've always thought that using a Unix epoch is an incorrect implementation of Ada. I couldn't convince anyone else of that, thus we left it unspecified.

But it really doesn't make sense to use local time for any long-running time calculation, since it jumps around. (My web server reboots every November because the heartbeat timer doesn't get serviced for an hour when the time changes. Ada didn't have timezones when I wrote that timer manager. I've never fixed it because it is only once per year, but...)

And your program specifically made it jump around by using Local_Image. Why didn't you use Image using the UTC time zone instead for this purpose? It doesn't jump around, and you don't care about the hours anyway. I'd guess that using Image with Time_Zone => 0 would not exhibit this anomaly.

          Randy.
ARG-Editor commented 2 months ago

...

I've always thought that using a Unix epoch is an incorrect implementation of Ada. ----------------------^ I meant Ada.Calendar.Time.

sttaft commented 2 months ago

The date that disappears depends on your local time zone. So in US Eastern Time, 03/12/2023 disappears. In US Central Time, it is probably some other date.

I believe the main problem with your program is that you are mixing operations from Ada.Calendar with operations from Ada.Calendar.Formatting, where Ada.Calendar uses some implementation-defined timezone with no specified semantics, while Ada.Calendar.Formatting has a well-defined notion of timezone, UTC, etc.

Here is a program that uses operations from Ada.Calendar.Formatting and Ada.Calendar.Time_Zones exclusively:

with Ada.Calendar,
Ada.Calendar.Arithmetic,
Ada.Calendar.Formatting,
Ada.Text_IO;
with Ada.Calendar.Time_Zones;
procedure Date_Issue_2 is
   use Ada.Calendar.Arithmetic, Ada.Calendar.Formatting, Ada.Text_IO;
   use Ada.Calendar.Time_Zones;
   subtype Time is Ada.Calendar.Time;
   use type Time;

   Jan_1st : constant Time := Time_Of (2023, 01, 01);
   Generated_Date : Time := Time_Of (2023, 12, 31);

   function My_Image (T : Time) return String is
      Iso_Date : constant String := Ada.Calendar.Formatting.Image (T);
      -- YYYY-MM-DD HH:MM:SS
      -- 1234 67 910
   begin
      return Iso_Date (9 .. 10) & '/' & Iso_Date (6..7) &
        '/' & Iso_Date (1..4);
   end My_Image;

   function Normalize (Left : Time) return Time is
   -- Reset time of date to 00:00:00.0 to make comparisons on date only
   begin
      return Time_Of (Year (Left), Month (Left), Day (Left), 0.0);
   end Normalize;

begin
   while Generated_Date >= Jan_1st loop
      Put_Line (My_Image (Generated_Date));
      Generated_Date := Normalize (Generated_Date - 1);
   end loop;
end Date_Issue_2;

This program doesn't skip any dates.

ARG-Editor commented 2 months ago

The date that disappears depends on your local time zone. So in US Eastern Time, 03/12/2023 disappears. In US Central Time, it is probably some other date.

To reiterate, in Janus/Ada, all of the dates appear. (At least all of them in March, April, and May.)

That's what I would expect, since there is no timezone information in an Ada.Calendar.Time value in Janus/Ada. So when one does math on such a value, the result is in the same timezone it originally was in (unless, of course, you use an explicit timezone value). To me, that is the only reasonable implementation for Ada.Calendar.Time.

However, others didn't see it that way and thus we have an unspecified implementation of Ada.Calendar.Time vis-a-vis time zones. Unless you explicitly specify a timezone when you do any operation on a Time value, the result is essentially implementation-defined. Thus you MUST use a timezone value with Image if you want consistent results (I think any timezone value will do for your program, so long as it is constant).

That's essentially what Tucker's program does; I don't think anything is necessary other than replacing Local_Image(T) with Image(T, Time_Zone => 0); (or just Image(T), since the default is 0).

It's always dubious to do any math on local time values, as they necessarily jump back and forth as the year goes along. Janus/Ada's definition made them work usefully for delays and the like, but unfortunately the change to the meaning of timezone probably will make it impossible to keep that the case (thus potentially breaking all existing Janus/Ada code and making timing much less reliable [Ada.Calendar Split operations will be many times more expensive]).

In hindsight, I think Ada.Calendar.Time should have been required to be in UTC time, but (a) that wouldn't have been possible on old Oses like MS-DOS and CP/M, and (b) it would be way too incompatible today.

The only reasonable workaround is to to NEVER mix math and Split/Time_Of/Image without time zones. The versions without time zones should be restricted to display purposes only.

                  Randy.

                Randy.
jprosen commented 2 months ago

I confess that I didn't get the subtilities between Calendar and its children wrt time zones, but still different compilers behave differently, and it would be strange to state that "one day" is implementation defined... There must be a decision if -1 day is the day before, or 86400s. before.

sttaft commented 2 months ago

The unambiguous answer is that one day is 86400 seconds. Sorry if that wasn't clear through all of the discussion.

ARG-Editor commented 2 months ago

The unambiguous answer is that one day is 86400 seconds. Sorry if that wasn't clear through all of the discussion.

I agree. Timezones can confuse the issue, but there never was any intent that it had anything to do with days on a calendar or what Split does.

                     Randy.