6
6
package kotlinx.datetime.internal
7
7
8
8
import kotlinx.datetime.*
9
+ import kotlinx.datetime.format.optional
9
10
10
11
internal class TzFileData (
11
12
val leapSecondRules : List <LeapSecondRule >,
@@ -204,8 +205,34 @@ private fun BinaryDataReader.readPosixTzString(): PosixTzString? {
204
205
fun readName (): String? {
205
206
if (c == ' \n ' ) return null
206
207
val name = StringBuilder ()
208
+ /* This check is a workaround for a bug in our tzdb processor used in kotlinx-datetime-zoneinfo.
209
+ In 2024b+, the tzdb includes the `%z` directive instead of the timezone abbreviations in cases where the
210
+ abbreviation can be inferred from the offset
211
+ (https://lists.iana.org/hyperkitty/list/[email protected] /thread/IZ7AO6WRE3W3TWBL5IR6PMQUL433BQIE/):
212
+ instead of writing "abbreviation = -03, offset = -3", they now write "abbreviation = %z, offset = -3".
213
+ The first-party tzdb compiler zic knows how to support this:
214
+ https://github.com/eggert/tz/blob/271a5784a59e454b659d85948b5e65c17c11516a/zic.8#L590-L602
215
+ The compiler we're using (`tubular_time_tzdb`) doesn't seem to, though, and generates invalid POSIX strings.
216
+ This is a quick and dirty workaround. A proper solution would be to have correct data in `-zoneinfo`, but
217
+ it doesn't matter if we publish broken tzdb info now, as we are not planning on supporting consuming old tzdb
218
+ versions from new library versions, so the workaround can be removed as soon as the `-zoneinfo` artifact is
219
+ fixed. */
220
+ if (c == ' %' ) {
221
+ c = readAsciiChar()
222
+ check(c == ' z' ) { " Invalid directive %$c in the timezone name abbreviation" }
223
+ c = readAsciiChar()
224
+ return GENERATE_NAME
225
+ }
207
226
if (c == ' <' ) {
208
227
c = readAsciiChar()
228
+ if (c == ' %' ) {
229
+ c = readAsciiChar()
230
+ check(c == ' z' ) { " Invalid directive %$c in the timezone name abbreviation" }
231
+ c = readAsciiChar()
232
+ check(c == ' >' ) { " <%z> expected, got %$c " }
233
+ c = readAsciiChar()
234
+ return GENERATE_NAME
235
+ }
209
236
while (c != ' >' ) {
210
237
check(c.isLetterOrDigit() || c == ' -' || c == ' +' ) { " Invalid char '$c ' in the std name in POSIX TZ string" }
211
238
name.append(c)
@@ -218,7 +245,7 @@ private fun BinaryDataReader.readPosixTzString(): PosixTzString? {
218
245
c = readAsciiChar()
219
246
}
220
247
}
221
- check(name.isNotEmpty()) { " Empty std name in POSIX TZ string" }
248
+ check(name.isNotEmpty()) { " Empty std name in POSIX TZ string: got $c " }
222
249
return name.toString()
223
250
}
224
251
@@ -341,13 +368,29 @@ private fun BinaryDataReader.readPosixTzString(): PosixTzString? {
341
368
342
369
val std = readName() ? : return null
343
370
val stdOffset = readOffset() ? : throw IllegalArgumentException (" Could not parse the std offset in POSIX TZ string" )
344
- val dst = readName() ? : return PosixTzString (std to stdOffset, null , null )
371
+ val stdName = if (std == = GENERATE_NAME ) ISO_OFFSET_BASIC_NO_Z .format(stdOffset) else std
372
+ val dst = readName() ? : return PosixTzString (stdName to stdOffset, null , null )
345
373
val dstOffset = readOffset() ? : UtcOffset (seconds = stdOffset.totalSeconds + 3600 )
374
+ val dstName = if (dst == = GENERATE_NAME ) ISO_OFFSET_BASIC_NO_Z .format(dstOffset) else dst
346
375
val startDate = readDate() ? : return PosixTzString (std to stdOffset, dst to dstOffset, null )
347
376
val startTime = readTime() ? : MonthDayTime .TransitionLocaltime (2 , 0 , 0 )
348
377
val endDate = readDate() ? : throw IllegalArgumentException (" Could not parse the end date in POSIX TZ string" )
349
378
val endTime = readTime() ? : MonthDayTime .TransitionLocaltime (2 , 0 , 0 )
350
379
val start = MonthDayTime (startDate, startTime, MonthDayTime .OffsetResolver .WallClockOffset )
351
380
val end = MonthDayTime (endDate, endTime, MonthDayTime .OffsetResolver .WallClockOffset )
352
- return PosixTzString (std to stdOffset, dst to dstOffset, start to end)
381
+ return PosixTzString (stdName to stdOffset, dstName to dstOffset, start to end)
382
+ }
383
+
384
+ private const val GENERATE_NAME = " %z"
385
+
386
+ private val ISO_OFFSET_BASIC_NO_Z by lazy {
387
+ UtcOffset .Format {
388
+ offsetHours()
389
+ optional {
390
+ offsetMinutesOfHour()
391
+ optional {
392
+ offsetSecondsOfMinute()
393
+ }
394
+ }
395
+ }
353
396
}
0 commit comments