Skip to content

Commit 2b403bb

Browse files
committed
Fixed: Method generateWithDayStep() returns invalid dates for daylight saving changeover
Fix test run for timezone-sensitive methods
1 parent 89e395c commit 2b403bb

4 files changed

Lines changed: 87 additions & 60 deletions

File tree

.github/workflows/dart.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ jobs:
1919
- name: Install dependencies
2020
run: dart pub get
2121
- name: Run tests
22-
run: dart pub run test
22+
run: TZ=Europe/Ljubljana dart test

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## [1.2.4]
22

33
* Method `isExpiredOrNull()` - same as `isExpired()`, but also returns true if provided `DateTime` is null.
4+
* **Fixed**: Method `generateWithDayStep()` can return invalid dates for daylight saving changeover in some cases.
45

56
## [1.2.3]
67

lib/src/date_time_utils.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,13 +467,17 @@ class DateTimeUtils {
467467
///
468468
/// [DateTime] in result uses [start] timezone.
469469
static Iterable<DateTime> generateWithDayStep(
470-
DateTime start, DateTime end) sync* {
470+
DateTime start,
471+
DateTime end,
472+
) sync* {
471473
if (end.isBefore(start)) return;
472474

473475
var date = start;
476+
var i = 1;
474477
do {
475478
yield date;
476-
date = nextDay(date);
479+
date = copyWith(start, day: start.day + i);
480+
i++;
477481
} while (date.isBefore(end));
478482
}
479483

test/in_date_utils_test.dart

Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,27 @@ import 'package:test/test.dart';
33
import 'package:timezone/data/latest.dart' as tz;
44
import 'package:timezone/standalone.dart' as tz;
55

6+
// to properly run timezone and daylight saving related tests you need run tests in specific timezone
7+
// because the result will be different, based on the rules of that timezone
8+
const _kTimeZoneLocation = 'Europe/Ljubljana';
9+
// DST began:
10+
// 2002: 31 March, 02:00 → 03:00
11+
// 2021: 28 March, 02:00 → 03:00
12+
// DST ended:
13+
// 2002: 27 October, 03:00 → 02:00
14+
// 2021: 31 October, 03:00 → 02:00
15+
616
void main() {
717
tz.initializeTimeZones();
8-
// 31 Mar 2002 - 01:00:00 forward 1 hour to 02:00:00
9-
// 27 Oct 2002 - 02:00:00 backward 1 hour to 01:00:00
10-
const regionLisbon = 'Europe/Lisbon';
1118

1219
void testDaylight(
13-
DateTime date, DateTime expected, DateTime Function(DateTime date) act) {
14-
final res = act(_createInTimezone(date, regionLisbon));
15-
final resTz = _createInTimezone(res, regionLisbon);
16-
final expectedTz = _createInTimezone(expected, regionLisbon);
17-
18-
expect(resTz, expectedTz);
20+
DateTime date,
21+
DateTime expected,
22+
DateTime Function(DateTime date) act,
23+
) {
24+
_verifyTimezone();
25+
final res = act(date);
26+
expect(res, expected);
1927
}
2028

2129
group('getDaysInMonth()', () {
@@ -102,13 +110,19 @@ void main() {
102110
group('should consider daylight saving', () {
103111
test(
104112
'when contains forward changeover',
105-
() => testDaylight(DateTime(2021, 3, 28, 00, 30),
106-
DateTime(2021, 3, 29), DTU.startOfNextDay));
113+
() => testDaylight(
114+
DateTime(2021, 3, 28, 00, 30),
115+
DateTime(2021, 3, 29),
116+
DTU.startOfNextDay,
117+
));
107118

108119
test(
109120
'when contains backward changeover',
110-
() => testDaylight(DateTime(2021, 10, 30, 01, 30),
111-
DateTime(2021, 10, 31), DTU.startOfNextDay));
121+
() => testDaylight(
122+
DateTime(2021, 10, 30, 02, 30), // TODO maybe next day?
123+
DateTime(2021, 10, 31),
124+
DTU.startOfNextDay,
125+
));
112126
});
113127
});
114128

@@ -203,13 +217,13 @@ void main() {
203217
group('should consider daylight saving', () {
204218
test(
205219
'when contains forward changeover',
206-
() => testDaylight(DateTime(2021, 3, 28, 00, 30),
207-
DateTime(2021, 3, 29, 00, 30), DTU.nextDay));
220+
() => testDaylight(DateTime(2021, 3, 28, 01, 30),
221+
DateTime(2021, 3, 29, 01, 30), DTU.nextDay));
208222

209223
test(
210224
'when contains backward changeover',
211-
() => testDaylight(DateTime(2021, 10, 30, 02, 30),
212-
DateTime(2021, 10, 31, 02, 30), DTU.nextDay));
225+
() => testDaylight(DateTime(2021, 10, 30, 03, 30),
226+
DateTime(2021, 10, 31, 03, 30), DTU.nextDay));
213227
});
214228
});
215229

@@ -231,13 +245,13 @@ void main() {
231245
group('should consider daylight saving', () {
232246
test(
233247
'when contains forward changeover',
234-
() => testDaylight(DateTime(2021, 3, 29, 00, 30),
235-
DateTime(2021, 3, 28, 00, 30), DTU.previousDay));
248+
() => testDaylight(DateTime(2021, 3, 29, 01, 30),
249+
DateTime(2021, 3, 28, 01, 30), DTU.previousDay));
236250

237251
test(
238252
'when contains backward changeover',
239-
() => testDaylight(DateTime(2021, 10, 31, 01, 30),
240-
DateTime(2021, 10, 30, 01, 30), DTU.previousDay));
253+
() => testDaylight(DateTime(2021, 10, 31, 02, 30),
254+
DateTime(2021, 10, 30, 02, 30), DTU.previousDay));
241255
});
242256
});
243257

@@ -855,32 +869,28 @@ void main() {
855869

856870
group('should consider daylight saving', () {
857871
test('when contains forward changeover for last month day', () {
858-
final date =
859-
_createInTimezone(DateTime(2002, 3, 31, 00, 30), regionLisbon);
872+
final date = DateTime(2002, 3, 31, 00, 30);
860873

861874
final res = DTU.isLastDayOfMonth(date);
862875
expect(res, true);
863876
});
864877

865878
test('when contains forward changeover for not last month day', () {
866-
final date =
867-
_createInTimezone(DateTime(2002, 3, 30, 23, 30), regionLisbon);
879+
final date = DateTime(2002, 3, 30, 23, 30);
868880

869881
final res = DTU.isLastDayOfMonth(date);
870882
expect(res, false);
871883
});
872884

873885
test('when contains backward changeover for last month day', () {
874-
final date =
875-
_createInTimezone(DateTime(2021, 10, 31, 0, 30), regionLisbon);
886+
final date = DateTime(2021, 10, 31, 0, 30);
876887

877888
final res = DTU.isLastDayOfMonth(date);
878889
expect(res, true);
879890
});
880891

881892
test('when contains backward changeover for not last month day', () {
882-
final date =
883-
_createInTimezone(DateTime(2021, 10, 30, 23, 30), regionLisbon);
893+
final date = DateTime(2021, 10, 30, 23, 30);
884894

885895
final res = DTU.isLastDayOfMonth(date);
886896
expect(res, false);
@@ -1790,39 +1800,45 @@ void main() {
17901800

17911801
group('should consider daylight saving', () {
17921802
void testDaylightSaving(
1793-
DateTime start, DateTime end, List<DateTime> expected) {
1794-
DateTime tz(DateTime d) => _createInTimezone(d, regionLisbon);
1795-
final res = DTU.generateWithDayStep(tz(start), tz(end));
1796-
final resTz = res.map(tz);
1797-
final expectedTz = expected.map(tz);
1803+
DateTime start,
1804+
DateTime end,
1805+
List<DateTime> expected,
1806+
) {
1807+
final res = DTU.generateWithDayStep(start, end);
17981808

1799-
expect(resTz, expectedTz);
1809+
_verifyTimezone();
1810+
expect(
1811+
res,
1812+
expected,
1813+
reason: 'Date range: $start - $end. '
1814+
'Current time zone: ${_currentTimeZone()}}',
1815+
);
18001816
}
18011817

18021818
test(
18031819
'when contains forward changeover',
18041820
() => testDaylightSaving(
1805-
DateTime(2002, 3, 29, 01, 30),
1806-
DateTime(2002, 4, 2, 02, 30),
1821+
DateTime(2002, 3, 29, 02, 30),
1822+
DateTime(2002, 4, 2, 03, 30),
18071823
[
1808-
DateTime(2002, 3, 29, 01, 30),
1809-
DateTime(2002, 3, 30, 01, 30),
1810-
DateTime(2002, 3, 31, 01, 30),
1811-
DateTime(2002, 4, 01, 01, 30),
1812-
DateTime(2002, 4, 02, 01, 30),
1824+
DateTime(2002, 3, 29, 02, 30),
1825+
DateTime(2002, 3, 30, 02, 30),
1826+
DateTime(2002, 3, 31, 02, 30),
1827+
DateTime(2002, 4, 01, 02, 30),
1828+
DateTime(2002, 4, 02, 02, 30),
18131829
],
18141830
));
18151831

18161832
test(
18171833
'when contains backward changeover',
18181834
() => testDaylightSaving(
1819-
DateTime(2021, 10, 30, 01, 30),
1820-
DateTime(2021, 11, 2, 02, 30),
1835+
DateTime(2021, 10, 30, 02, 30),
1836+
DateTime(2021, 11, 2, 03, 30),
18211837
[
1822-
DateTime(2021, 10, 30, 01, 30),
1823-
DateTime(2021, 10, 31, 01, 30),
1824-
DateTime(2021, 11, 01, 01, 30),
1825-
DateTime(2021, 11, 02, 01, 30),
1838+
DateTime(2021, 10, 30, 02, 30),
1839+
DateTime(2021, 10, 31, 02, 30),
1840+
DateTime(2021, 11, 01, 02, 30),
1841+
DateTime(2021, 11, 02, 02, 30),
18261842
],
18271843
));
18281844
});
@@ -1924,15 +1940,21 @@ void main() {
19241940
});
19251941
}
19261942

1927-
/// [locationName] should be value from the database represents
1928-
/// a national region where all clocks keeping local time have agreed since 1970.
1929-
/// TZ database https://www.iana.org/time-zones
1930-
/// You can find a list at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
1931-
DateTime _createInTimezone(DateTime date, String locationName) {
1932-
final location = tz.getLocation(locationName);
1933-
final tzDate = tz.TZDateTime(location, date.year, date.month, date.day,
1934-
date.hour, date.minute, date.second, date.millisecond, date.microsecond);
1935-
return tzDate;
1943+
void _verifyTimezone() {
1944+
final location = tz.getLocation(_kTimeZoneLocation);
1945+
expect(
1946+
DTU.now().timeZoneName,
1947+
location.currentTimeZone.abbreviation,
1948+
reason: 'You should run this test in the $_kTimeZoneLocation timezone. '
1949+
'Use next command to set timezone in tests:\nTZ=$_kTimeZoneLocation dart test',
1950+
);
1951+
}
1952+
1953+
String _currentTimeZone() {
1954+
final d = DateTime.now();
1955+
return '${d.timeZoneName} (UTC${d.timeZoneOffset.isNegative ? '-' : '+'}'
1956+
'${d.timeZoneOffset.inHours.toString().padLeft(2, '0')}:'
1957+
'${(d.timeZoneOffset.inMinutes % 60).toString().padLeft(2, '0')})';
19361958
}
19371959

19381960
class Tuple2<T1, T2> {

0 commit comments

Comments
 (0)