chrono icon indicating copy to clipboard operation
chrono copied to clipboard

There is no way to add a "day" to a DateTime

Open sparky8251 opened this issue 6 years ago • 8 comments

Just like in #318, this sample code can be used to explain the problem.

use chrono::NaiveDateTime;
use chrono::TimeZone;
use chrono::{DateTime, Duration};
use chrono_tz::US::Eastern;
use chrono_tz::Tz;

fn main() {
    let timestamp = 1572580800;

    let mut dt: DateTime<Tz> = Eastern.from_utc_datetime(&NaiveDateTime::from_timestamp(timestamp, 0));

    println!("Written datetime: {}", dt);

    //increment past the daylight savings change

    dt = dt + Duration::days(3);

    println!("Written datetime: {}", dt);
}

When it's run, the output is:

Written datetime: 2019-11-01 00:00:00 EDT
Written datetime: 2019-11-03 23:00:00 EST

What I would expect adding 3 days is that I would get 2019-11-4 00:00:00 EST. I didn't add 86400 * 3 to the time, I added Duration::days(3) so this feels inconsistent as a consumer of this crate.

I don't know if this belongs here or if it belongs in something like a chrono-utils crate, but I do feel it needs addressing or at least some sort of call out in the docs that it's not actually days. It's non-obvious that it's just an abstraction for "number of seconds in a day * X".

sparky8251 avatar Sep 04 '19 14:09 sparky8251

Hi,

I noticed this problem appears only in newest versions of chrono:

I wrote this test:

#[cfg(test)]
mod tests {
    extern crate chrono_tz;

    use self::chrono_tz::Europe::Warsaw;

    #[test]
    fn test_chrono_add() { // https://github.com/chronotope/chrono/issues/339
        use std::ops::Add;
        use chrono::*;

        let timezone = Warsaw;
        let mut date = timezone.from_utc_datetime(&"2017-10-28T22:00:00Z".parse::<DateTime<Utc>>().unwrap().naive_utc());

        dbg!(&date);
        assert_eq!("2017-10-29", &date.format("%Y-%m-%d").to_string());
        date = date.add(Duration::days(1));
        assert_eq!("2017-10-30", &date.format("%Y-%m-%d").to_string());
        date = date.add(Duration::days(1));
        assert_eq!("2017-10-31", &date.format("%Y-%m-%d").to_string());
    }
}

With chrono = "=0.4.10" it's failing - "Expected 2017-10-30, found 2017-10-29" While with chrono = "=0.4.7" it's working correctly

frondeus avatar Dec 02 '19 10:12 frondeus

2017-10-29T00:00:00CEST + 1 day = 2017-10-29T23:00:00:00CET

frondeus avatar Dec 02 '19 10:12 frondeus

@frondeus that is unfortunately the correct behavior, the old behavior should have preserved the CEST timezone instead of correctly converting it to CET, unless that has gone awry.

quodlibetor avatar Dec 02 '19 18:12 quodlibetor

Hmm so I should probably operate on dates:

#[cfg(test)]
mod tests {
    extern crate chrono_tz;

    use self::chrono_tz::Europe::Warsaw;

    #[test]
    fn test_chrono_add() { // https://github.com/chronotope/chrono/issues/339
        use std::ops::Add;
        use chrono::*;

        let timezone = Warsaw;
        let mut date = timezone.from_utc_datetime(&"2017-10-28T22:00:00Z".parse::<DateTime<Utc>>()
            .unwrap()
            .naive_utc())
            .date(); //Note `.date()` at the end.

        dbg!(&date);
        assert_eq!("2017-10-29", &date.format("%Y-%m-%d").to_string());
        date = date.add(Duration::days(1));
        assert_eq!("2017-10-30", &date.format("%Y-%m-%d").to_string());
        date = date.add(Duration::days(1));
        assert_eq!("2017-10-31", &date.format("%Y-%m-%d").to_string());
    }
}

This works on both versions.

frondeus avatar Dec 03 '19 08:12 frondeus

Interesting. That does make sense, although adding a duration to a date doesn't make sense! I'm glad you've found a workaround.

quodlibetor avatar Dec 03 '19 15:12 quodlibetor

You could also convert to a NaiveDateTime and add a duration to that, which will ignore all timezone issues.

quodlibetor avatar Dec 03 '19 15:12 quodlibetor

I've run in to this too. The JavaScript data library Luxon does a really nice job of this by using 'time math' and 'calendar math'.

For examples like this, it's currently adding using time math (just 1 days worth of seconds), whereas sometimes you want to use calendar math (adding 1 day, regardless of how long that day is).

There's an explanation here: https://moment.github.io/luxon/docs/manual/math.html

oeed avatar Sep 10 '20 03:09 oeed

I have a use case for this too: I generate schedules for running tasks, and DST changes shift all subsequent datetimes, which is (often) undesirable

t-cadet avatar Feb 21 '22 22:02 t-cadet

Fixed in https://github.com/chronotope/chrono/pull/784.

pitdicker avatar Jun 07 '23 04:06 pitdicker