Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Add DateTime.addYears, DateTime.addMonths, DateTime.addDays, ... #27245

Open
jolleekin opened this issue Sep 4, 2016 · 27 comments
Open
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. core-2 library-core type-enhancement A request for a change that isn't a bug

Comments

@jolleekin
Copy link

jolleekin commented Sep 4, 2016

Problem

It is really inconvenient to work with Dart's DateTime because it misses some important methods for date-time manipulation. Some good examples are

  1. There's no easy way to add years to a DateTime
  2. There's no easy way to add months to a DateTime
  3. There's no easy way to subtract years from a DateTime
  4. There's no easy way to subtract months from a DateTime
  5. There's no easy way to subtract a DateTime from another
  6. I have to deal with low level stuff (leap years; months have different number of days) when I want to do one of the above
  7. Using Duration is more cumbersome than int or num

Proposal

Given the limitations above, I propose adding the following methods to Dart's DateTime

  • DateTime addDays(num value)
  • DateTime addHours(num value)
  • DateTime addMilliseconds(num value)
  • DateTime addMinutes(num value)
  • DateTime addMonths(int value)
  • DateTime addSeconds(num value)
  • DateTime addTicks(int value)
  • DateTime addYears(int value)
  • Duration subtractAnother(DateTime other)

Reference

C#'s DateTime has the following methods

  • AddDays(Double)
  • AddHours(Double)
  • AddMilliseconds(Double)
  • AddMinutes(Double)
  • AddMonths(Int32)
  • AddSeconds(Double)
  • AddTicks(Int64)
  • AddYears(Int32)
  • Subtract(DateTime)
@lrhn
Copy link
Member

lrhn commented Sep 5, 2016

I think that should be fixed with an replace method: date.repace(month: date.month + moreMonths).

You can already use the format date + new Duration(hours: 23) for everything below a month, and you can subtract two DateTime instances as date1.difference(date2).

There is a semantic problem with adding months and years in that "a month" and "a year" isn't a specific amount of time. Years vary by one day, months by up to three days. Adding "one month" to the 30th of January is ambiguous. We can do it, we just have to pick some arbitrary day between the 27th of February and the 2nd of March. That's why we haven't added month and year to Duration - they do not describe durations.

An replace method would allow you to write the actual date that you want - if you end up writing date.update(month: 2, day: 30), we have already decided how to interpret that (it's the 2nd of March, 1st of March in leap years).

(We had a CL to add the replace method, but blocked on naming: https://codereview.chromium.org/1472803003/)

@lrhn lrhn added area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-core type-enhancement A request for a change that isn't a bug labels Sep 5, 2016
@jolleekin
Copy link
Author

In a subscription management system, recurring billing is the core. After charges of a period are collected, a task is scheduled to run next month, next quarter, next six months, next year, and so on. Without addMonths and addYears, it would be difficult to calculate the correct date for the next task.

This is how C# handles leap years and the number of days in a month

The AddMonths method calculates the resulting month and year, taking into account leap years and the number of days in a month, then adjusts the day part of the resulting DateTime object. If the resulting day is not a valid day in the resulting month, the last valid day of the resulting month is used. For example, March 31st + 1 month = April 30th, and March 31st - 1 month = February 28 for a non-leap year and February 29 for a leap year.

@lrhn
Copy link
Member

lrhn commented Sep 6, 2016

I guess there is a reason many subscriptions work in increments of 30 days, not "months" :)

The C# choice is not a bad one. It's one choice and it may or may not be the right choice for any particular use case.
Doing something like that is definitely possible. It works when you just add a number of months or years (or days, for that matter, since daylight savings means that not all days are 24 hours).
We can't get the same effect by having an add({int year, int month, int day, int hour, int minute, int second, ...}) method instead (which I would otherwise prefer). Adding both a day and a month isn't indifferent to the order: adding 1 month and 3 days to the 26th of February can yield either 29th of March or 1st of April, depending on whether you add the day first or the month first (because you do want days to wrap over into the next month).
That's also why it doesn't work very well with Duration - it's not clear whether the difference between 24th of February and 25th of March is "29 days" or "1 month and 1 day".

@jolleekin
Copy link
Author

My company specializes in subscription commerce, so I can confirm that subscriptions are charged mostly on a monthly or yearly basis rather than every 30 days. For example, if you sign up for a monthly charged subscription on Jan 1, you'll get charged again on Feb 1, Mar 1, and so on. From Jan 1 to Feb 1 is 31 days and from Feb 1 to Mar 1 is 28 or 29 days, but those periods are all considered as one month.

Below is my implementation of addMonths. By the way, it would be nice to add static methods isLeapYear and daysInMonth to DateTime.

const _daysInMonth = const [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

bool isLeapYear(int value) =>
    value % 400 == 0 || (value % 4 == 0 && value % 100 != 0);

int daysInMonth(int year, int month) {
  var result = _daysInMonth[month];
  if (month == 2 && isLeapYear(year)) result++;
  return result;
}

DateTime addMonths(DateTime dt, int value) {
  var r = value % 12;
  var q = (value - r) ~/ 12;
  var newYear = dt.year + q;
  var newMonth = dt.month + r;
  if (newMonth > 12) {
    newYear++;
    newMonth -= 12;
  }
  var newDay = min(dt.day, daysInMonth(newYear, newMonth));
  if (dt.isUtc) {
    return new DateTime.utc(
        newYear,
        newMonth,
        newDay,
        dt.hour,
        dt.minute,
        dt.second,
        dt.millisecond,
        dt.microsecond);
  } else {
    return new DateTime(
        newYear,
        newMonth,
        newDay,
        dt.hour,
        dt.minute,
        dt.second,
        dt.millisecond,
        dt.microsecond);
  }
}

@johnpryan
Copy link
Contributor

I'm not saying this is the way to do it, but moment.js calculates an average: https://github.com/moment/moment/blob/b8a297c1f99b3918f44dd9e3d7bd59379a876ba6/moment.js#L3997

in dart this is something like:

int monthsToDays(int months) {
  return (months * 146097 / 4800) as int;
}

@eseidelGoogle
Copy link

An internal Flutter customer (customer: fast) had an issue with their shipping app last week due to using DateTime.add(new Duration(days: 2)) not accounting for daylight savings. Having this API might have helped them avoid typing that bug in the first place.

@johnpryan
Copy link
Contributor

@jolleekin @eseidelGoogle I would be happy to contribute to a 3rd party package. Maybe that's the first step to getting native SDK support.

@floitschG
Copy link
Contributor

Our plan is to work on improving the libraries in Q2. This issue is on our list.

@Casper1131
Copy link

I have found that if you just add or subtract delta dart will do the rest
new DateTime(
Year+delta,
Month+delta,
day+delta,
hour+delta,
minute+delta,
second+delta
);

@sir-boformer
Copy link

@Casper1131

Not if the date is January 31. When you add a month like you suggested, you end up with March 03 (or 02):

DateTime.utc(2018, 1+1, 31) --> 2018-03-03

@d-silveira
Copy link

d-silveira commented Sep 7, 2018

@lrhn in my view dateTime calculations should not average anything, DateTime should be aware of the yearly calendars, and be able to accurately calculate for instance a year ago as being the same day of the same month, but a year ago. and to return the difference between now and 7 years ago in days, for instance, take into account leap years, and month sizes appropriately.

@lrhn
Copy link
Member

lrhn commented Sep 10, 2018

I agree that date calculations should be calendar aware, but taking leap years and month sizes into account doesn't define a unique answer for all computations. You have to say how they are handled exactly.
What is one year before 2016-02-29? Since there is no 2015-02-29, we need to pick some other date, likely either 2015-02-28 or 2015-03-01. That's a decision to make.
What is one month after 2015-01-30?
What is one year and seven days after 2016-02-24?
Do we add days before years?
Do we do it in the opposite order when we subtract?

I can absolutely guarantee that no matter which algorithm you pick, you cannot guarantee that date + interval - interval == date if you take the calendar into account.

We can definitely make some choices (I already have some ideas). We can't add it to DateTime now since someone might be implementing the interface.

@alan-knight
Copy link
Contributor

And locale aware. Where you can also run into issues because we don't have a Date, just a DateTime. And it may or may not be important to a particular usage that November 4 at midnight does not exist if you're in Brazil so the best you can do is 1:00am.

@cosinus84
Copy link

cosinus84 commented Feb 20, 2019

I am in trouble, not having this date&time methods. All about creating apps with flutter goes away. I need to rewrite the code from RN (momentjs) to flutter, and I was thinking that dart has all this (kind of)libs available. Now I am glad that I have some buttons, lists and images...

@duzenko
Copy link

duzenko commented May 24, 2019

floitschG commented on Mar 23, 2017

Our plan is to work on improving the libraries in Q2. This issue is on our list.

Could you follow up on this please

@lrhn
Copy link
Member

lrhn commented May 24, 2019

These operations were scheduled for Dart 2.0, but didn't make the final cut.
Changing the class is a breaking change, so we are not currently planning on adding methods right now.

My best guess at when something will happen is that if/when we get static extension methods, someone (perhaps even myself) will write a library which extends DateTime with a particular choice of calendar operations.

@l-k22
Copy link

l-k22 commented Jun 19, 2019

Sorry to ask again, is there any update on this issue?

@brettclutch
Copy link

Ran into this myself. Needed clean simple way to add a month to a DateTime.

@chuhaienko
Copy link

chuhaienko commented Sep 9, 2019

3 years.
Dart version 2.4.1
:(

@GregorySech
Copy link

There is an open issue on the Flutter repo about porting functionalities from moment.js. If DateTime changes for a future release are being discussed here maybe the analysis did by the other issuer might help. The issue in question is: flutter/flutter#31523

@lrhn
Copy link
Member

lrhn commented Oct 3, 2019

Well, the extension methods are coming "soon", so adding functionality from the outside will be an option.
The advantage of that solution is that there can be more than one month-overflow strategy to pick from, rather than choosing one for the platform API and not solving the problems of people wanting a different strategy.

@johnpryan
Copy link
Contributor

There is now a package that adds these types of extensions: https://github.com/jogboms/time.dart

@brokenalarms
Copy link

brokenalarms commented Apr 4, 2020

To be clear the time extension above adds very nice syntax to the existing Duration functionality, but doesn't in any way address the problems that momentjs addresses, such as relative time differences taking into account leap years, etc. As a new user to dart I was led here upon trying to find out and understand why months and years did not seem to be supported natively, given how great the rest of the DateTime type is..

@lrhn
Copy link
Member

lrhn commented Mar 11, 2021

This can now be done in a non-breaking way using extension methods. I'd still prefer if we had interface default methods. It seems unnecessarily complicated to have extensions on a type declared in the same library as the type.

@lrhn
Copy link
Member

lrhn commented Apr 17, 2023

The best syntax for this behavior would be date = date.add(days: 3). That's not possible because add is taken by the Duration version, and because we cannot have both optional positional and named parameters, it can't even be changed to add([Duration? duration], {int years = 0, int months = 0, int days = 0, ...}).

So we need a new name, with the same meaning as add, but not being add. (Without being confusingly similar to add, like fold and reduce where I can never remember which is which.)

Assume we have such a name, the implementation would then be:

extension DateTimeExtensions on DateTime {
  /// Adds time units to the calendar date and/or clock time.
  ///
  /// Creates a new [DateTime] object with a calendar date offset from
  /// that of the the current oneby the provided number of years, months, and/or days,
  /// and a wall clock time offset from that of the current one by the provided
  /// hours, minutes, seconds, milliseconds and/or microseconds.
  ///
  /// The provided time units can be positive or negative, or any combination.
  /// Overflowing, say by adding more than 30 days to a any date, works like
  /// in the [DateTime] constructor.
  /// The resulting day and time must be withing the supported range for
  /// the `DateTime` class.
  ///
  /// The new `DateTime` object is an UTC time if [isUtc] is `true` ,
  /// and it is local-time if [isUtc] is `false`. 
  /// If [isUtc] is not provided, the created `DateTime` object uses the same
  /// UTC/local-time choice as the original.
  DateTime shift({int years = 0, int months = 0, int days = 0, 
      int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0, 
      bool? isUtc}) =>
    ((isUtc ?? this.isUtc) ? DateTime.utc : DateTime.new)(
      year + years,
      month + months,
      day + days,
      hour + hours,
      minute + minutes,
      second + seconds,
      millisecond + milliseconds,
      microsecond + microseconds,
    );
}

That looks very much like copyWith, and you can write var newDate = date.copyWith(day: date.day + days); instead of var newDate = date.shift(days: days);.

@FMorschel
Copy link
Contributor

There is now a package that adds these types of extensions: https://github.com/jogboms/time.dart

And now my package due_date creates this functionality of "addMonths" in a more specific way depending on what you mean as a month (or whatever time period you want).

@FMorschel
Copy link
Contributor

Now, time already has the solution proposed by lrhn just above (shift extension method). See here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. core-2 library-core type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests