ocaml-community / calendar

OCaml library for handling dates and times.
Other
42 stars 9 forks source link

Off-by-one in precise_sub #13

Open loxs opened 6 years ago

loxs commented 6 years ago

Originally reported here by Stephen Dolan

I expected that if I use CalendarLib.Calendar.Precise.precise_sub to compute the difference between two dates, then I could add this difference to the earlier and get the later.

However, if I attempt this on [2025-03-31 17:53:04; 2026-07-19 12:15:23], then the result of add is 2026-07-20 12:15:23, not 2026-07-19 12:15:23.

loxs commented 6 years ago

This seems to have something to do with leap years (the library incorrectly thinks that 2026 is leap).

If we don't cross the boundaries of 2026-02-28, we get correct results:

utop # let d = Date.precise_sub (Date.make 2026 2 28) (Date.make 2025 3 31);;
val d : Date.Period.t = <abstr>
utop # let added = Date.add (Date.make 2025 3 31) d;;
val added : Period.date_field Date.date = <abstr>
utop # Printer.Date.to_string added;;
- : string = "2026-02-28"

But if we try with 2026-02-29 (should not even allow us to do it), we get this:

utop # let d = Date.precise_sub (Date.make 2026 2 29) (Date.make 2025 3 31);;
val d : Date.Period.t = <abstr>
utop # let added = Date.add (Date.make 2025 3 31) d;;
val added : Period.date_field Date.date = <abstr>
utop # Printer.Date.to_string added;;
- : string = "2026-03-01"

So it seems that precise_sub allows for wrong leap years while later add correctly figures out that 2026 is not leap.

loxs commented 6 years ago

I was wrong, doesn't have anything to do with leap years:

let test_precise_sub start ending =
    Date.precise_sub ending start |> Date.add start |> Printer.Date.to_string

For this case it only happens for dates between 2025-07-10 and 2025-07-30:

utop # test_precise_sub (Date.make 2025 3 31) (Date.make 2026 7 9);;
- : string = "2026-07-09"
utop # test_precise_sub (Date.make 2025 3 31) (Date.make 2026 7 10);;
- : string = "2026-07-11"
utop # test_precise_sub (Date.make 2025 3 31) (Date.make 2026 7 30);;
- : string = "2026-07-31"
utop # test_precise_sub (Date.make 2025 3 31) (Date.make 2026 7 31);;
- : string = "2026-07-31"
utop # test_precise_sub (Date.make 2018 5 31) (Date.make 2018 7 2);;
- : string = "2018-07-03"

Seems to happen when jumping from the last day of an odd numbered month into July but not for all days of July:

utop # test_precise_sub (Date.make 2018 3 31) (Date.make 2018 7 20);;
- : string = "2018-07-21"
utop # test_precise_sub (Date.make 2018 3 31) (Date.make 2018 7 6);;
- : string = "2018-07-07"
utop # test_precise_sub (Date.make 2018 3 31) (Date.make 2018 7 2);;
- : string = "2018-07-02"
utop # test_precise_sub (Date.make 2018 3 31) (Date.make 2018 7 20);;
- : string = "2018-07-21"
utop # test_precise_sub (Date.make 2018 3 31) (Date.make 2018 7 6);;
- : string = "2018-07-07"
utop # test_precise_sub (Date.make 2018 3 31) (Date.make 2018 7 2);;
- : string = "2018-07-02"

So far I haven't been able to reproduce it with anything other than July

The smallest reproducible seems to be:

utop # test_precise_sub (Date.make 2018 5 31) (Date.make 2018 7 2);;
- : string = "2018-07-03"