jiffy icon indicating copy to clipboard operation
jiffy copied to clipboard

Incorrect behaviour of add method when traversing a date in which daylight saving time transition occurs

Open amato-gianluca opened this issue 4 years ago • 9 comments

Hi, I have recently stumbled over this problem. My current timezone is 'Europe/Rome', where DST switches off the last Sunday of October. If I execute

Jiffy('2020-10-19 00:00:00').add(weeks: 1)

due to the DST transition, the result is not 2020-10-26 00:00:00, as I would expect, but 2020-10-25 23:00:00. This breaks all the rest of my computation.

I think the behavior of Jiffy is incorrect. Adding a week should return 2020-10-26 00:00:00, independently from the actual number of hours of difference. As a comparison, the momentjs library with

moment('2020-10-19 00:00:00').add(1, 'weeks')

correctly returns Moment<2020-10-26T00:00:00+01:00>.

This is not the only instance of this problem. For example,

Jiffy('2020-10-25 00:00:00').add(days: 1)

returns again 2020-10-25 23:00:00 instead of ``2020-10-26 00:00:00```.

amato-gianluca avatar Oct 25 '20 12:10 amato-gianluca

I'm having the exact problem when subtracting weeks.

Input: 2020-10-30 00:00:00.000 Output: 2020-10-23 01:00:00.000 Should be: 2020-10-23 00:00:00.000

anisalibegic avatar Oct 30 '20 09:10 anisalibegic

I believe Jiffy has nothing to do with this. I solved it by using utc() constructor when creating DateTime objects.

DateTime.now() -> DateTime.now().toUtc() DateTime(2020...) -> DateTime.utc(2020...)

anisalibegic avatar Oct 30 '20 09:10 anisalibegic

In fact, what @anisalibegic found is correct: the bug is in Dart SDK itself.

I've replicated this with this Dart code:

//let's start with a date before the DST transition
DateTime dt = DateTime(2020, 10, 25, 16, 10, 00, 999);
print(dt);
//and let's go to the start of week. this code is the same used by Jiffy
DateTime newDate = dt.subtract(Duration(days: dt.weekday - 1));
DateTime dt2 = DateTime(newDate.year, newDate.month, newDate.day);
print(dt2);
//then, let's add a week. again, I'm using the same code that Jiffy uses
dt2 = dt2.add(Duration(days: 7));
print(dt2);

In my timezone, this is going to be 2020-10-25 23:00:00.000, but that's wrong, because I would expect it to go to the end of the day, so to 2020-10-26 00:00:00.000. If we try to change the starting date, assuming it to be, for example, 2020-11-25, we will get a correct behaviour.

I'll try to search in Dart repository to see if there already is any related issue. Otherwise, I'll open one.

gregoriopalama avatar Nov 01 '20 15:11 gregoriopalama

I've just opened an issue on Dart's SDK repository: https://github.com/dart-lang/sdk/issues/44014

gregoriopalama avatar Nov 01 '20 17:11 gregoriopalama

Jiffy would work correctly (ignoring DST changes) when adding days or weeks if its implementation had special code for it, the way it has special code for adding months and years. It is up to the Jiffy package maintainers if they want to change adding days and weeks to work this way.

It is also solved by using UTC times when working with calendar days, since a calendar day has no time of day associated with it and is independent of time zones.

whesse avatar Nov 02 '20 10:11 whesse

I am from Android developer. Kotlin(or Java) has 2 class named LocalDateTime and LocalDate. LocalDate is only for Date calculation. It is more simple and difficult to misunderstand design With DateTime or Jiffy in Dart, many people may try first add days, but will realize this issue later. (after publishing app if unlucky) How about creating new class for date calculation?

wph144 avatar Feb 13 '21 10:02 wph144

I made a class in my project only for date calculation. This class is wrapping Jiffy. You can modify and use it if need.

import 'package:jiffy/jiffy.dart';

class UtcDate {
  DateTime _dateTime;

  UtcDate.now(): this(DateTime.now());

  UtcDate(DateTime dateTime) {
    _dateTime = DateTime.utc(dateTime.year, dateTime.month, dateTime.day);
  }

  UtcDate.ymd(int year, int month, int day) {
    _dateTime = DateTime.utc(year, month, day);
  }

  UtcDate.parse(String formattedString): this(DateTime.parse(formattedString));

  int get day => _dateTime.day;
  int get month => _dateTime.month;
  int get year => _dateTime.year;
  int get weekday => _dateTime.weekday;
  int get daysInMonth => Jiffy(_dateTime).daysInMonth;
  int get week => Jiffy(_dateTime).week;

  DateTime get dateTime => _dateTime;

  static Future<String> setLocale([String locale]) async {
    return await Jiffy.locale(locale);
  }

  UtcDate withDayOfMonth(int dayOfMonth) {
    return UtcDate.ymd(this.year, this.month, dayOfMonth);
  }

  UtcDate addDays(int days) {
    return UtcDate(_dateTime.add(Duration(days: days)));
  }

  UtcDate addMonths(int months) {
    return UtcDate(Jiffy(_dateTime).add(months: months));
  }

  int diffInDays(UtcDate utcDate) {
    return Jiffy(_dateTime).diff(utcDate.dateTime, Units.DAY);
  }

  int diffInWeek(UtcDate utcDate) {
    return Jiffy(_dateTime).diff(utcDate.dateTime, Units.WEEK);
  }

  int diffInMonths(UtcDate utcDate) {
    return Jiffy(_dateTime).diff(utcDate.dateTime, Units.MONTH);
  }

  UtcDate startOfWeek() {
    return UtcDate(Jiffy(_dateTime).startOf(Units.WEEK));
  }

  UtcDate endOfWeek() {
    return UtcDate(Jiffy(_dateTime).endOf(Units.WEEK));
  }

  bool operator ==(dynamic other) {
    assert(other is UtcDate);
    return year == other.year && month == other.month && day == other.day;
  }

  bool isBefore(UtcDate other) {
    return _dateTime.isBefore(other.dateTime);
  }

  bool isAfter(UtcDate other) {
    return _dateTime.isAfter(other.dateTime);
  }

  bool isSameDate(UtcDate other) {
    return this == other;
  }

  bool isSameMonth(UtcDate other) {
    return this.year == other.year && this.month == other.month;
  }

  String toDateString() {
    return "$year-${month.toString().padLeft(2,'0')}-${day.toString().padLeft(2,'0')}";
  }

  String toMonthString() {
    return '$year.${month.toString().padLeft(2, '0')}';
  }

  @override
  String toString() {
    return toDateString();
  }
}

wph144 avatar Feb 18 '21 14:02 wph144

@whesse I agree, Jiffy could do its own implementation to correct this, maybe within the subtract and add function we could add utc implementation to manipulate the date and later return its local time. A similar approach to @wonpyohong solution

jama5262 avatar Mar 01 '21 14:03 jama5262

This is expected behaviour since jiffy's add method simply calls the Dart DateTime.add method. The documentation states that whole days are not added, but instead seconds are added.

https://api.flutter.dev/flutter/dart-core/DateTime/add.html

Be careful when working with dates in local time.

This should either be better documented in jiffy's add method, or jiffy should better handle daylight savings by adding whole days like this

DateTime now = DateTime.now();
DateTime fiveDaysFromNow = DateTime(now.year, now.month, now.day + 5);

Magnuti avatar Oct 11 '23 09:10 Magnuti