1
1
/*
2
- * Copyright 2019-2023 JetBrains s.r.o. and contributors.
2
+ * Copyright 2019-2025 JetBrains s.r.o. and contributors.
3
3
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4
4
*/
5
5
@@ -8,7 +8,6 @@ package kotlinx.datetime.test.format
8
8
import kotlinx.datetime.*
9
9
import kotlinx.datetime.format.*
10
10
import kotlin.reflect.KMutableProperty1
11
- import kotlin.reflect.KProperty
12
11
import kotlin.test.*
13
12
14
13
class DateTimeComponentsFormatTest {
@@ -268,4 +267,138 @@ class DateTimeComponentsFormatTest {
268
267
}
269
268
}
270
269
}
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
+ }
271
404
}
0 commit comments