How to deal with users mutating DATE! and TIME! to be invalid?

2020 is a leap year, so there's a February 29th:

>> make date! [29 2 2020]
== 29-Feb-2020

But let's say you have a year like 2017, in which there is no February 29th. R3-Alpha and Red are smart enough not to let you MAKE such bad dates, or to load them as literals. e.g. Red:

>> make date! [28 2 2017]
== 28-Feb-2017

>> make date! [29 2 2017]
*** Script Error: cannot MAKE/TO date! from: [29 2 2017]

>> 29-Feb-2017
*** Script Error: cannot MAKE/TO date! from: [day month year]

But let's say you aren't making it directly, but mutating a date. For instance to try to set a February 2017 date's day to the 29th anyway. In R3-Alpha and Red:

>> date: 28-Feb-2017
== 28-Feb-2017

>> date/day: 29
== 29

>> date
== 1-Mar-2017

It gets weirder than that.

>> date: 28-Feb-2017
== 28-Feb-2017

>> date/day: 1000
== 1000

>> date
== 28-Oct-2019

>> date/day: -1000
== -1000

>> date
== 3-Jan-2017

It's doing some kind of off-kilter date math when you assign them. Note that setting the date to 1000 effectively pushed into the future only by the overflow (assumed your 28 was intentional, and everything beyond that wasn't). But then going back -1000 made no such assumptions; so you didn't get back where you started.

Where the odd behavior is coming from is a reuse of the normalization code for date math. Basically, if you start from a valid date and add days / months / years / seconds, you need to reconcile after the math.

But setting date fields item-wise is not date math. I propose it be held to the same rules as MAKE or LOAD of a DATE!.

1 Like

Interestingly, the issue has come up in Red in the past 4 days. This ticket discusses time zones specifically, but the question of "normalization" applies to all of it

"Personally I think it would be more useful for the user to get an error instead of normalisation for out of range values..." --x8x (thumbs up via meijeru, at least)

@rgchris appears to vote for errors...anyone want to make a case in favor of normalization?

Implicit math seems a potential mess, and I don't recall that it was allowed before.

Timezones are a political construct and like dates need to be checked against whatever rules govern those. And I don't see it being useful to do math outside those rules, eg increment timezone so that you're forced to do modulo math.

Anyway I'm in favor of an error.

1 Like

An error make more sense.

I also believe that presence or absence of a zone distinguishes between two meanings.

5-aug-2017/10:00+0:00 is not the same as 5-aug-2017/10:00
With the first we know exactly what moment in time is represented, with the second we'd need to add a timezone before we know what that moment is. See the description for iCalendar's DATE WITH LOCAL TIME.

Looking at the Red discussion referenced above, it appears that Red hides the timezone when zone is set to 0. If that's true, then I'd say it is the wrong way to go. 0 should not mean unset.

1 Like

Repeating what I said in chat:

I'm hearing generally agreement with me, and no real disagreement, so given that DATE! and TIME! math aren't really key interest areas of mine, I'm going to just do what I think is right. The only reason I'm involved here is to help @Brett with getting what he needs to do done... so beyond that, my interest is limited. So it's on everyone else to put together tests and say what they want.

And to sum up "what I think is right": is errors on item-wise setting of date properties which would fail on a MAKE, don't run the normalization code that kicks in after math operations. No time-based math allowed on dates with no time components, and don't consider dates with no time components equal to dates that have a time component. Don't do any time zone math, nor consider dates with time zone components equal to those without.

If people want to compare dates and ignore their time or time zone components, that's up to them to do the dropping of time or time zone components before the comparison.

My proposal for the refinement to make NOW give back a time without a zone is to use /LOCAL, because I actually think it's cool to demonstrate that Ren-C has "pure locals" and doesn't do something silly with a refinement.

I have whipped up these date tests as proposed ren-c date behaviour. I'm wondering what everyone thinks about the implications?

If date has these signatures: DateOnly, DateTime, DateTimeZone then I think a DateOnly needs a time to be compared with a DateTime and a DateTime needs to be "placed" in a zone before it can be compared with a DateTimeZone. I don't see that a DateZone (no time) would be that useful.

I have no idea if the make date! specification will be annoying or fine. I think it is descriptive when aiming to "place" a non-specific Date or DateTime.

Hoping that this is a relatively simple, predictable system of behaviour that can represent different meanings and draw attention to inadvertent mistakes.

1 Like

-bump-

Moreover, I also think that trying to do things like access /utc from a DateOnly should raise an error.

Thoughts?

Bump again. We should get what tests we want/need to pass, to pass...and raise the others as issues...just to keep things moving along.

Speaking of tying up loose ends, there were 3 date-related issues from some "rebol2-specific" items in the test suite:

; date! ignores time portion
[equal? 2-Jul-2009 2-Jul-2009/22:20]

[strict-equal? 2-Jul-2009 2-Jul-2009/22:20]
[strict-equal? 2-Jul-2009 2-Jul-2009/00:00:00+00:00]

So interestingly, Rebol2 said strict-equal? 2-Jul-2009 2-Jul-2009/22:20 was true...but both R3-Alpha and Red say that even plain "lax" equal? 2-Jul-2009 2-Jul-2009/22:20 is false.

We can make arguments from rigor or based on principles like "it can't be an error, because you shouldn't error when comparing values of the same datatype for equality", etc. But I feel like what's missing are the use cases. Why would client code care one way or another? What kinds of bugs or features happen by biasing this one way or another? Does anyone have code that is affected and can show the effect?

It almost feels like on would be an operator to use here. if datetime on 12-Dec-2012 [...]. :-/

I am tying up loose branches and I found this branch from way back in 2017.

As @Brett put the effort into writing the tests, I've always kept it around as a "maybe someday". But working on time zones is unpleasant and the kind of thing I'd rather let someone else worry about, as it doesn't really touch on my interests in the language.

Yet dates have now come up in a couple of applications...something @jhgorse was doing as well as something @BlackATTR is doing.

We've had a feature implemented that I described, of considering dates with no time component or time zone components to have them as missing (NULL). This is a distinct state from the 0:00 time zone, e.g. UTC.

So that piece of the puzzle was done. However, implementing the behaviors was non-trivial and took quite a number of hours. I expect there will be feedback and requests for more changes...so it will cost even more time. But maybe people will find it worth it.

Follows Brett's Tests...Only A Small Difference

One difference is sort of a technical issue with comparison semantics. Right now comparison is based on a function that returns -1 for less than, 0 for equal, or 1 for greater than. This means there's no way to have a different error condition regarding the precision based on whether you're doing a test for equality or greater than / less than, because the comparison function doesn't know which is being requested.

Hence these tests fail, just as doing the other comparisons do:

(not equal? date-110 date-100)
(not equal? date-111 date-100)

The other difference is that I didn't implement the MAKE code for "deriving" a date from another date. That's kind of a broader issue of reviewing the nature of MAKE that should be looked at. I also didn't implement the requested /LOCAL feature yet, to get a local time without a time zone...because I wasn't sure why the /UTC function wouldn't be willing to assume that a time without a time zone was local...there seemed to be some asymmetry there to consider.

Finally, I decided it would be interesting to allow you to assign a time zone to something that did not have a time component...and that this would set the time component to 00:00:00. It seemed like this offered a pretty fast way to get a precise time from one that didn't have one.

>> t: now/date
== 3-Jul-2021

>> t.zone: 0
== 0

>> t
== 3-Jul-2021/0:00+0:00

This can reduce the pain of getting precise dates to perform math on from ones that are less precise. You can also use something like t.zone: default [0] to keep an existing zone but overwrite with 0. I feel this is expedient enough to help make the more cautious behavior in comparisons a win...since you're a stone's throw from the old behavior if you want it.

ren-c/date.test.reb at 45af66ababa7e60eb1bc23f90781b9b392b83c66 · metaeducation/ren-c · GitHub

2 Likes

Pragmatically speaking, it has turned out that not being able to do comparisons or equality on dates with different specificity is a pain. When I actually had to deal with this by working on the Query dialect, it made a mess of things. Being forced to canonize the dates in the code generated ugly output, and then forced every other comparison down the pipe to canonize...it spreads like a virus. :frowning:

So I've instead decided to go with the idea that dates are equal if you compare a less-specific to a more specific one with the same parts.

>> 26-Jul-2021/7:41:45.314 = 26-Jul-2021
== ~true~  ; isotope

If there are time zones involved, then the time is adjusted to UTC before the comparison. There's not really another sensible way to approach the problem--you don't want comparisons to be based on the local machine's time zone (for instance) so different machines would compare the same literal values differently.

I made the greater and less than comparisons come back false:

>> 26-Jul-2021/7:41:45.314 > 26-Jul-2021
== ~false~  ; false

>> 26-Jul-2021/7:41:45.314 < 26-Jul-2021
== ~false~  ; isotope

This break with the tradition in Rebol that plain > and < are based on strict equality (there's no strict-greater-than? and strict-lesser-than?). That generates some odd behavior, where e.g. floating point values can be both less than and equal to each other:

r3-alpha/red>> -4.94065645841247E-324 < 0.0
== true

r3-alpha/red>> -4.94065645841247E-324 = 0.0
== true

Comparisons are still just a slippery topic across the board, which I don't have an answer for (other languages struggle with all this as well). But my hand got forced to dealing with this particular issue in dates, and making it more practical.

1 Like