11/*
2- * Copyright 2019-2023 JetBrains s.r.o. and contributors.
2+ * Copyright 2019-2025 JetBrains s.r.o. and contributors.
33 * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44 */
55
@@ -8,7 +8,6 @@ package kotlinx.datetime.test.format
88import kotlinx.datetime.*
99import kotlinx.datetime.format.*
1010import kotlin.reflect.KMutableProperty1
11- import kotlin.reflect.KProperty
1211import kotlin.test.*
1312
1413class DateTimeComponentsFormatTest {
@@ -268,4 +267,138 @@ class DateTimeComponentsFormatTest {
268267 }
269268 }
270269 }
270+
271+ private object TimezoneTestData {
272+ val correctParsableOffsets = listOf (
273+ // Single digit hours (H format)
274+ " 1" , " 9" , " 0" ,
275+ // Two-digit hours (HH format)
276+ " 09" , " 11" , " 18" ,
277+ // Hours and minutes without a separator (HHMM format)
278+ " 0110" , " 0230" , " 0930" ,
279+ // Hours, minutes, and seconds without a separator (HHMMSS format)
280+ " 010000" , " 000100" , " 012345" ,
281+ // Hours and minutes with colon separator (HH:MM format)
282+ " 01:15" , " 02:35" , " 09:35" ,
283+ // Hours, minutes, and seconds with colon separators (HH:MM:SS format)
284+ " 01:10:32" , " 15:51:00" , " 17:54:32"
285+ )
286+
287+ val incorrectParsableOffsets = listOf (
288+ // Invalid hours (exceeding typical timezone ranges)
289+ " 19" , " 99" , " 20" ,
290+ // HHMM format with invalid minutes (>59) or hours (>18)
291+ " 2010" , " 0260" , " 0999" , " 9999" ,
292+ // HHMMSS format with invalid hours, minutes, or seconds
293+ " 180001" , " 006000" , " 000099" , " 999999" ,
294+ // HH:MM format with invalid hours or minutes
295+ " 30:10" , " 02:70" , " 99:99" ,
296+ // HH:MM:SS format with invalid hours, minutes, or seconds
297+ " 19:00:00" , " 00:60:00" , " 99:99:99" ,
298+ )
299+
300+ val incorrectUnparsableOffsets = listOf (
301+ // Single non-digit characters
302+ " a" , " _" , " +" ,
303+ // Two characters: letter+digit, letter+symbol, digit+symbol
304+ " a9" , " y!" , " 1#" ,
305+ // Three digits (invalid length - not 2 or 4 digits)
306+ " 110" , " 020" ,
307+ // Five digits (invalid length - not 4 or 6 digits)
308+ " 18000" , " 02300" ,
309+ // HH:MM format violations: single digit hour, missing minute, missing hour
310+ " 3:10" , " 2:70" , " 99:" , " :20" ,
311+ // Invalid colon-separated formats: too many digits in an hour/minute component
312+ " 12:3456" , " 1234:56" ,
313+ // HH:MM:SS format violations: single digit hour, single digit minute, single digit second
314+ " 1:00:00" , " 00:6:00" , " 09:99:9" ,
315+ // Colon placement errors
316+ " :00:00" , " 00::00" , " 09:99:" , " ::00" , " 00::" , " ::" ,
317+ // HH:MM:SS format violations: 3-digit hour, 3-digit minute, 3-digit second
318+ " 180:00:00" , " 00:610:00" , " 99:99:199"
319+ )
320+
321+ val tzPrefixes = listOf (" UTC" , " GMT" , " UT" )
322+
323+ val timezoneDbIdentifiers = listOf (
324+ " America/New_York" , " Europe/London" , " Asia/Tokyo" , " Australia/Sydney" ,
325+ " Pacific/Auckland" , " Africa/Cairo" , " America/Los_Angeles" , " Europe/Paris" ,
326+ " Asia/Singapore" , " Australia/Melbourne" , " Africa/Johannesburg" , " Europe/Isle_of_Man"
327+ )
328+
329+ val invalidTimezoneIds = listOf (" INVALID" , " XYZ" , " ABC/DEF" , " NOT_A_TIMEZONE" , " SYSTEM" )
330+ }
331+
332+ @Test
333+ fun testZuluTimeZone () {
334+ // Replace it to:
335+ // listOf("z", "Z").forEach(::assertParseableAsTimeZone)
336+ // when TimeZone.of("z") works correctly
337+ assertParseableAsTimeZone(" Z" )
338+ assertIncorrectlyParseableAsTimeZone(" z" )
339+ }
340+
341+ @Test
342+ fun testSpecialNamedTimezones () {
343+ TimezoneTestData .tzPrefixes.forEach(::assertParseableAsTimeZone)
344+ }
345+
346+ @Test
347+ fun testPrefixWithCorrectParsableOffset () {
348+ val timezoneIds =
349+ generateTimezoneIds(TimezoneTestData .tzPrefixes + " " , TimezoneTestData .correctParsableOffsets)
350+ timezoneIds.forEach(::assertParseableAsTimeZone)
351+ }
352+
353+ @Test
354+ fun testPrefixWithIncorrectParsableOffset () {
355+ val timezoneIds =
356+ generateTimezoneIds(TimezoneTestData .tzPrefixes + " " , TimezoneTestData .incorrectParsableOffsets)
357+ timezoneIds.forEach(::assertIncorrectlyParseableAsTimeZone)
358+ }
359+
360+ @Test
361+ fun testPrefixWithIncorrectUnparsableOffset () {
362+ val timezoneIds =
363+ generateTimezoneIds(TimezoneTestData .tzPrefixes + " " , TimezoneTestData .incorrectUnparsableOffsets)
364+ timezoneIds.forEach(::assertNonParseableAsTimeZone)
365+ }
366+
367+ @Test
368+ fun testTimezoneDBIdentifiers () {
369+ TimezoneTestData .timezoneDbIdentifiers.forEach(::assertParseableAsTimeZone)
370+ }
371+
372+ @Test
373+ fun testInvalidTimezoneIds () {
374+ TimezoneTestData .invalidTimezoneIds.forEach(::assertNonParseableAsTimeZone)
375+ }
376+
377+ private fun generateTimezoneIds (prefixes : List <String >, offsets : List <String >): List <String > = buildList {
378+ for (prefix in prefixes) {
379+ for (sign in listOf (' +' , ' -' )) {
380+ for (offset in offsets) {
381+ add(" $prefix$sign$offset " )
382+ }
383+ }
384+ }
385+ }
386+
387+ private fun assertParseableAsTimeZone (zoneId : String ) {
388+ TimeZone .of(zoneId)
389+ val result = DateTimeComponents .Format { timeZoneId() }.parse(zoneId)
390+ assertEquals(zoneId, result.timeZoneId)
391+ }
392+
393+ private fun assertIncorrectlyParseableAsTimeZone (zoneId : String ) {
394+ assertFailsWith<IllegalTimeZoneException > { TimeZone .of(zoneId) }
395+ val result = DateTimeComponents .Format { timeZoneId() }.parse(zoneId)
396+ assertEquals(zoneId, result.timeZoneId)
397+ }
398+
399+ private fun assertNonParseableAsTimeZone (zoneId : String ) {
400+ assertFailsWith<DateTimeFormatException > {
401+ DateTimeComponents .Format { timeZoneId() }.parse(zoneId)
402+ }
403+ }
271404}
0 commit comments