@@ -35,8 +35,9 @@ class TimeZoneTest {
35
35
@Test
36
36
fun available () {
37
37
val allTzIds = TimeZone .availableZoneIds
38
- println (" Available TZs:" )
39
- allTzIds.forEach(::println)
38
+ assertContains(allTzIds, " Europe/Berlin" )
39
+ assertContains(allTzIds, " Europe/Moscow" )
40
+ assertContains(allTzIds, " America/New_York" )
40
41
41
42
assertNotEquals(0 , allTzIds.size)
42
43
assertTrue(TimeZone .currentSystemDefault().id in allTzIds)
@@ -46,7 +47,13 @@ class TimeZoneTest {
46
47
@Test
47
48
fun availableZonesAreAvailable () {
48
49
for (zoneName in TimeZone .availableZoneIds) {
49
- TimeZone .of(zoneName)
50
+ val timezone = try {
51
+ TimeZone .of(zoneName)
52
+ } catch (e: Exception ) {
53
+ throw Exception (" Zone $zoneName is not available" , e)
54
+ }
55
+ Instant .DISTANT_FUTURE .toLocalDateTime(timezone).toInstant(timezone)
56
+ Instant .DISTANT_PAST .toLocalDateTime(timezone).toInstant(timezone)
50
57
}
51
58
}
52
59
@@ -198,6 +205,103 @@ class TimeZoneTest {
198
205
check(" -5" , LocalDateTime (2008 , 11 , 2 , 2 , 0 , 0 , 0 ))
199
206
}
200
207
208
+ @Test
209
+ fun checkKnownTimezoneDatabaseRecords () {
210
+ with (TimeZone .of(" America/New_York" )) {
211
+ checkRegular(this , LocalDateTime (2019 , 3 , 8 , 23 , 0 ), UtcOffset (hours = - 5 ))
212
+ checkGap(this , LocalDateTime (2019 , 3 , 10 , 2 , 0 ))
213
+ checkRegular(this , LocalDateTime (2019 , 6 , 2 , 23 , 0 ), UtcOffset (hours = - 4 ))
214
+ checkOverlap(this , LocalDateTime (2019 , 11 , 3 , 2 , 0 ))
215
+ checkRegular(this , LocalDateTime (2019 , 12 , 5 , 23 , 0 ), UtcOffset (hours = - 5 ))
216
+ }
217
+ with (TimeZone .of(" Europe/Berlin" )) {
218
+ checkRegular(this , LocalDateTime (2019 , 1 , 31 , 1 , 0 ), UtcOffset (hours = 1 ))
219
+ checkGap(this , LocalDateTime (2019 , 3 , 31 , 2 , 0 ))
220
+ checkRegular(this , LocalDateTime (2019 , 6 , 27 , 1 , 0 ), UtcOffset (hours = 2 ))
221
+ checkOverlap(this , LocalDateTime (2019 , 10 , 27 , 3 , 0 ))
222
+ checkRegular(this , LocalDateTime (2019 , 12 , 5 , 23 , 0 ), UtcOffset (hours = 1 ))
223
+ }
224
+ with (TimeZone .of(" Europe/Moscow" )) {
225
+ checkRegular(this , LocalDateTime (2019 , 1 , 31 , 1 , 0 ), UtcOffset (hours = 3 ))
226
+ checkRegular(this , LocalDateTime (2011 , 1 , 31 , 1 , 0 ), UtcOffset (hours = 3 ))
227
+ checkGap(this , LocalDateTime (2011 , 3 , 27 , 2 , 0 ))
228
+ checkRegular(this , LocalDateTime (2011 , 5 , 3 , 1 , 0 ), UtcOffset (hours = 4 ))
229
+ }
230
+ with (TimeZone .of(" Australia/Sydney" )) {
231
+ checkRegular(this , LocalDateTime (2019 , 1 , 31 , 1 , 0 ), UtcOffset (hours = 11 ))
232
+ checkOverlap(this , LocalDateTime (2019 , 4 , 7 , 3 , 0 ))
233
+ checkRegular(this , LocalDateTime (2019 , 10 , 6 , 1 , 0 ), UtcOffset (hours = 10 ))
234
+ checkGap(this , LocalDateTime (2019 , 10 , 6 , 2 , 0 ))
235
+ checkRegular(this , LocalDateTime (2019 , 12 , 5 , 23 , 0 ), UtcOffset (hours = 11 ))
236
+ }
237
+ }
238
+
201
239
private fun LocalDateTime (year : Int , monthNumber : Int , dayOfMonth : Int ) = LocalDateTime (year, monthNumber, dayOfMonth, 0 , 0 )
202
240
203
241
}
242
+
243
+ /* *
244
+ * [gapStart] is the first non-existent moment.
245
+ */
246
+ private fun checkGap (timeZone : TimeZone , gapStart : LocalDateTime ) {
247
+ val instant = gapStart.toInstant(timeZone)
248
+ /* * the first [LocalDateTime] after the gap */
249
+ val adjusted = instant.toLocalDateTime(timeZone)
250
+ try {
251
+ // there is at least a one-second gap
252
+ assertNotEquals(gapStart, adjusted)
253
+ // the offsets before the gap are equal
254
+ assertEquals(
255
+ instant.offsetIn(timeZone),
256
+ instant.plus(1 , DateTimeUnit .SECOND ).offsetIn(timeZone))
257
+ // the offsets after the gap are equal
258
+ assertEquals(
259
+ instant.minus(1 , DateTimeUnit .SECOND ).offsetIn(timeZone),
260
+ instant.minus(2 , DateTimeUnit .SECOND ).offsetIn(timeZone)
261
+ )
262
+ } catch (e: Throwable ) {
263
+ throw Exception (" Didn't find a gap at $gapStart for $timeZone " , e)
264
+ }
265
+ }
266
+
267
+ /* *
268
+ * [overlapStart] is the first non-ambiguous date-time.
269
+ */
270
+ private fun checkOverlap (timeZone : TimeZone , overlapStart : LocalDateTime ) {
271
+ // the earlier occurrence of the overlap
272
+ val instantStart = overlapStart.plusNominalSeconds(- 1 ).toInstant(timeZone).plus(1 , DateTimeUnit .SECOND )
273
+ // the later occurrence of the overlap
274
+ val instantEnd = overlapStart.plusNominalSeconds(1 ).toInstant(timeZone).minus(1 , DateTimeUnit .SECOND )
275
+ try {
276
+ // there is at least a one-second overlap
277
+ assertNotEquals(instantStart, instantEnd)
278
+ // the offsets before the overlap are equal
279
+ assertEquals(
280
+ instantStart.minus(1 , DateTimeUnit .SECOND ).offsetIn(timeZone),
281
+ instantStart.minus(2 , DateTimeUnit .SECOND ).offsetIn(timeZone)
282
+ )
283
+ // the offsets after the overlap are equal
284
+ assertEquals(
285
+ instantStart.offsetIn(timeZone),
286
+ instantEnd.offsetIn(timeZone)
287
+ )
288
+ } catch (e: Throwable ) {
289
+ throw Exception (" Didn't find an overlap at $overlapStart for $timeZone " , e)
290
+ }
291
+ }
292
+
293
+ private fun checkRegular (timeZone : TimeZone , dateTime : LocalDateTime , offset : UtcOffset ) {
294
+ val instant = dateTime.toInstant(timeZone)
295
+ assertEquals(offset, instant.offsetIn(timeZone))
296
+ try {
297
+ // not a gap:
298
+ assertEquals(dateTime, instant.toLocalDateTime(timeZone))
299
+ // not an overlap, or an overlap longer than one hour:
300
+ assertTrue(dateTime.plusNominalSeconds(3600 ) <= instant.plus(1 , DateTimeUnit .HOUR ).toLocalDateTime(timeZone))
301
+ } catch (e: Throwable ) {
302
+ throw Exception (" The date-time at $dateTime for $timeZone was in a gap or overlap" , e)
303
+ }
304
+ }
305
+
306
+ private fun LocalDateTime.plusNominalSeconds (seconds : Int ): LocalDateTime =
307
+ toInstant(UtcOffset .ZERO ).plus(seconds, DateTimeUnit .SECOND ).toLocalDateTime(UtcOffset .ZERO )
0 commit comments