`day` parameter overflow for the `date` function, simple leap year handling

I’ve just realized and thought it might be interesting for other community members as well that for the date function there is no invalid day parameter because the function handles overflow in the following way:

daml> date 2019 Feb 29
2019-03-01
daml> date 2020 Aug 32
2020-09-01

I came across this behavior when I set out to implement a function which adds years to a date preserving month and day and started to think about leap year handling.

The simple solution for this is that you don’t care about leap years:

addYearsToDate : Int -> Date -> Date
addYearsToDate yearsToAdd initDate = 
  date resultYear initMonth initDay
  where 
    (initYear, initMonth, initDay) = toGregorian initDate
    resultYear = initYear + yearsToAdd

yearsToAddTest = scenario do 
  assert $ isLeapYear 2020
  assert $ addYearsToDate 4 (date 2020 Feb 29) == date 2024 Feb 29
  assert $ addYearsToDate 5 (date 2020 Feb 29) == date 2025 Feb 29
  assert $ addYearsToDate 5 (date 2020 Feb 29) == date 2025 Mar 1

If I add 5 years to 2020 Feb 29, the result is 2025 Mar 1.

2 Likes

DAML internally uses unix time (micros since Unix Epoch). There are some fun looking DAML function to convert between a number of days since epoch and dates: daml/Date.daml at 1872c668a554e2ec7cff8bc8838e3895a253962f · digital-asset/daml · GitHub

I wonder whether these are standard algorithms, or are adapted from some other Standard Library. Does anyone here know the source of all the magic numbers?

1 Like

It turns out that according to Hungarian law, the simple leap year handling cited above is not OK, I had to implement a more complex function for this:

addYearsToDate : Int -> Date -> Date
addYearsToDate yearsToAdd initDate = 
  if (not . isLeapYear) resultYear &&
     initMonth == Feb && 
     initDay == 29 
  then date resultYear Feb 28
  else date resultYear initMonth initDay
  where 
    (initYear, initMonth, initDay) = toGregorian initDate
    resultYear = initYear + yearsToAdd

addingYearsTest = scenario do 
  assert $ isLeapYear 2020
  assert $ addYearsToDate 4 (date 2020 Feb 29) == date 2024 Feb 29
  assert $ addYearsToDate 5 (date 2020 Feb 29) == date 2025 Feb 28
1 Like

I would consider this a bug. The documentation:

-- Given the three values (year, month, day), constructs a Date
-- value. date (y, m, d) turns the year y, month m, and day d
-- into a Date value.
date: Int -> Month -> Int -> Date

does not mention any kind of “overflow” behaviour, and I would be surprised by it. I would much prefer if it threw an error on invalid inputs.

@bernhard I think we should update either the documentation to mention the overflow behaviour, or the implementation to throw on invalid inputs. Happy to do either.

3 Likes

Yes, makes sense. I guess the easiest way is to convert to check that calling toGregorian on the result of date gives the original inputs. Quite wasteful, though. Mind opening a ticket for discussion?

2 Likes

The similar function in Haskell doesn’t overflow, in case of a too big day number returns the last day of the month:

Prelude Data.Time> fromGregorian 2020 12 40
2020-12-31
Prelude Data.Time> fromGregorian 2020 11 40
2020-11-30
1 Like

This is also incorrect in the docs, the above function would be of type

date : (Int, Month, Int) -> Date

which is not the case. The docs should say date y m d turns the year y, month m, and day d into a Date value.

1 Like

Good catch!

2 Likes

Opened a PR for the minor docstring typo, which I don’t expect to be controversial, and an issue to further discuss how to handle bad inputs.

2 Likes

I merged a PR to add the missing underflow/overflow check on date:

4 Likes