3

I'm quite new to Rust and chrono library.

I checked the https://docs.rs/chrono/0.4.19/chrono/struct.Duration.html#method.num_weeks, but there's no num_years() or num_months() API in chrono::Duration.

Is there any work around solution for this ?

E_net4
  • 27,810
  • 13
  • 101
  • 139
linrongbin
  • 2,967
  • 6
  • 31
  • 59
  • 2
    Note that expressing a duration in months doesn't really make any sense because months have different lengths. You can get an approximate value with `num_weeks() / 4` or `num_days() / 30`. – Jmb Dec 21 '21 at 07:35
  • In C++ `2020y/December + months{1} == 2021y/January`. `months{1}` is a duration of 1 month. – Howard Hinnant Dec 21 '21 at 14:58
  • @HowardHinnant Now try that with `2020y/February/1 + months{1} == 2020y/March/1`. In C++, [`months{1}` is a duration of 2629746s](https://en.cppreference.com/w/cpp/chrono/duration) or 30.437 days which may give unexpected results. – Jmb Dec 21 '21 at 15:05
  • 1
    Is `2020y/March/1` not expected? That's what I get. More info here: https://stackoverflow.com/a/43018120/576911 – Howard Hinnant Dec 21 '21 at 15:08
  • Days can have leap seconds too and therefore can have different lengths. The chrono Duration does not take this into account. Currently the only real solution is to revert to [ICU](https://docs.rs/icu_calendar/0.6.0/icu_calendar/struct.DateDuration.html). – Code4R7 Jul 24 '22 at 20:46

3 Answers3

1

I had to solve this problem, too. The problem with years and months, as others have noted, is that you need to provide a starting date, or you'll get different answers depending on days in months and leap days in years. chrono::Duration doesn't remember the original base date you used, so it cannot provide these answers.

But if you have access to the 2 dates from which you calculated the duration, you can get years via <end_datetime>.years_since(<start_datetime).

Chrono does not have any function to compute months. Is this because it's (gulp) straightforward? Too trivial to include in an otherwise comprehensive calendar package? dunno. They do provide other month calculations, such as <datetime>.checked_add_months(..). But (at least, I think) here's how I'm doing it myself:

type BaseDT = DateTime<FixedOffset>;
type UnitSize = i64;

pub fn signed_month_difference(start: BaseDT, end: BaseDT) -> UnitSize {
    let end_naive = end.date_naive();
    let start_naive = start.date_naive();

    let month_diff = end_naive.month() as UnitSize - start_naive.month() as UnitSize;
    let years_diff = (end_naive.year() - start_naive.year()) as UnitSize;
    if month_diff >= 0 {
        (years_diff * 12) + month_diff
    } else {
        (years_diff - 1) * 12 + (month_diff + 12)
    }
}
BobHy
  • 1,575
  • 10
  • 23
0

chrono::Duration provides date and time duration in "ISO 8601 time duration with nanosecond precision", which implies it is representing the duration internally as a number of nanoseconds, and then providing convenience methods to convert into other duration units such as days, weeks, milliseconds, etc.

This is a little at odds with the actual ISO 8601 duration standard, which is a standard of representations and formats. The standard represents durations by the format P[n]Y[n]M[n]DT[n]H[n]M[n]S - which might give you what you want. But this is not what chronos::Duration was designed to provide.

The problem is that in order to represent a duration of months or years, more information than the number of nanoseconds is needed. Durations define the amount of intervening time in a time interval: the time between two points. The start or end time is important, because months or years are not standard durations. There are months of 28, 29 30, 31 days, and years of 365 and 366 days.

If you were to write your own algorithm to format durations in terms of years, months, days, hours, minutes, seconds, etc.. you would have to know the start date. In addition, time zone is important, because daylight savings needs to be taken into account. You would also have to make decisions about how to represent parts of months or years. For example the month of January has 31 days, and February 28, say. What would it mean to represent a duration of 1.75 months from January 1? Would that mean 31 days for January then 0.75 * 28 days in February?

Or you could represent the duration from a start date in a cascading unit format: e.g., 5 years, 4 months and 3 days, 2 hours, 12 minutes and 3 seconds from 1 Jan 1970 12:00Z. Just like the ISO 8601 standard.

So, its not an easy solution, and it all depends what your requirements are. I can understand why the developers of chronos:Duration left off providing num_months() and num_years()!

  • Don't days have different lengths too, with **leap seconds**? Then just leave out days too? A well defined Duration `struct` needs to hold the number of days, months, years, etc. *regardless* of the duration offset. The offset only matters when you start using the Duration in calculations. Now we have to revert to [ICU](https://crates.io/crates/icu) components to do the job, for I see no alternative solution here. – Code4R7 Jul 24 '22 at 19:40
  • True facts, and good issues raised, but not an answer, nor a pointer to an answer for the question. – BobHy May 27 '23 at 22:01
0

The crate chronoutil provides RelativeDuration which is "extending Chrono’s Duration to add months and years".

Example from docs.rs:

let one_day = RelativeDuration::days(1);
let one_month = RelativeDuration::months(1);
let delta = one_month + one_day;
let start = NaiveDate::from_ymd(2020, 1, 30);
assert_eq!(start + delta, NaiveDate::from_ymd(2020, 3, 1));

Note that one can not extract the duration in months or years from the duration alone. These properties can vary, depending on the point in time on the calendar that you are using.

For example, in the Gregorian calender, not all months are equally long (28 up to 31 days), it will depend on the starting date/time how many months fall within a duration.

The date_component crate takes this in to account. Given two dates, it calculates the months and years for you.

Example (from the documentation):

use chrono::prelude::*;
use date_component::date_component;

fn main() {
    let date1 = Utc.ymd(2015, 4, 20).and_hms(0, 0, 0);
    let date2 =  Utc.ymd(2015, 12, 19).and_hms(0, 0, 0);
    let date_interval = date_component::calculate(&date1, &date2);
    println!("{:?}", date_interval);
}
// DateComponent { year: 0, month: 7, week: 4, modulo_days: 1, day: 29, hour: 0, minute: 0, second: 0, interval_seconds: 20995200, interval_minutes: 349920, interval_hours: 5832, interval_days: 243, invert: false }
Code4R7
  • 2,600
  • 1
  • 19
  • 42
  • But this library doesn't let you *extract* the number of months accumulated in the RelativeDuration. You could add N months to a given date (accurately), but given 2 dates, nothing I've found in chrono or this library will tell me how many months are between the 2. – BobHy May 27 '23 at 22:26
  • Yeah you're right. I've updated the answer. – Code4R7 May 31 '23 at 13:30