@@ -35,8 +35,9 @@ class TimeZoneTest {
3535 @Test
3636 fun available () {
3737 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" )
4041
4142 assertNotEquals(0 , allTzIds.size)
4243 assertTrue(TimeZone .currentSystemDefault().id in allTzIds)
@@ -46,7 +47,13 @@ class TimeZoneTest {
4647 @Test
4748 fun availableZonesAreAvailable () {
4849 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)
5057 }
5158 }
5259
@@ -198,6 +205,103 @@ class TimeZoneTest {
198205 check(" -5" , LocalDateTime (2008 , 11 , 2 , 2 , 0 , 0 , 0 ))
199206 }
200207
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+
201239 private fun LocalDateTime (year : Int , monthNumber : Int , dayOfMonth : Int ) = LocalDateTime (year, monthNumber, dayOfMonth, 0 , 0 )
202240
203241}
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