88
99package kotlinx.datetime
1010
11+ import kotlinx.datetime.format.*
1112import kotlinx.datetime.internal.*
1213import kotlinx.datetime.serializers.InstantIso8601Serializer
1314import kotlinx.serialization.Serializable
14- import kotlin.math.*
1515import kotlin.time.*
1616import kotlin.time.Duration.Companion.nanoseconds
1717import kotlin.time.Duration.Companion.seconds
@@ -26,95 +26,6 @@ public actual enum class DayOfWeek {
2626 SUNDAY ;
2727}
2828
29- /* * A parser for the string representation of [ZoneOffset] as seen in `OffsetDateTime`.
30- *
31- * We can't just reuse the parsing logic of [ZoneOffset.of], as that version is more lenient: here, strings like
32- * "0330" are not considered valid zone offsets, whereas [ZoneOffset.of] sees treats the example above as "03:30". */
33- private val zoneOffsetParser: Parser <UtcOffset >
34- get() = (concreteCharParser(' z' ).or (concreteCharParser(' Z' )).map { UtcOffset .ZERO })
35- .or (
36- concreteCharParser(' +' ).or (concreteCharParser(' -' ))
37- .chain(intParser(2 , 2 ))
38- .chain(
39- optional(
40- // minutes
41- concreteCharParser(' :' ).chainSkipping(intParser(2 , 2 ))
42- .chain(optional(
43- // seconds
44- concreteCharParser(' :' ).chainSkipping(intParser(2 , 2 ))
45- ))))
46- .map {
47- val (signHours, minutesSeconds) = it
48- val (sign, hours) = signHours
49- val minutes: Int
50- val seconds: Int
51- if (minutesSeconds == null ) {
52- minutes = 0
53- seconds = 0
54- } else {
55- minutes = minutesSeconds.first
56- seconds = minutesSeconds.second ? : 0
57- }
58- try {
59- if (sign == ' -' )
60- UtcOffset .ofHoursMinutesSeconds(- hours, - minutes, - seconds)
61- else
62- UtcOffset .ofHoursMinutesSeconds(hours, minutes, seconds)
63- } catch (e: IllegalArgumentException ) {
64- throw DateTimeFormatException (e)
65- }
66- }
67- )
68-
69- // This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5
70- // org.threeten.bp.format.DateTimeFormatter#ISO_OFFSET_DATE_TIME
71- private val instantParser: Parser <Instant >
72- get() = localDateParser
73- .chainIgnoring(concreteCharParser(' T' ).or (concreteCharParser(' t' )))
74- .chain(intParser(2 , 2 )) // hour
75- .chainIgnoring(concreteCharParser(' :' ))
76- .chain(intParser(2 , 2 )) // minute
77- .chainIgnoring(concreteCharParser(' :' ))
78- .chain(intParser(2 , 2 )) // second
79- .chain(optional(
80- concreteCharParser(' .' )
81- .chainSkipping(fractionParser(0 , 9 , 9 )) // nanos
82- ))
83- .chain(zoneOffsetParser)
84- .map {
85- val (localDateTime, offset) = it
86- val (dateHourMinuteSecond, nanosVal) = localDateTime
87- val (dateHourMinute, secondsVal) = dateHourMinuteSecond
88- val (dateHour, minutesVal) = dateHourMinute
89- val (dateVal, hoursVal) = dateHour
90-
91- val nano = nanosVal ? : 0
92- val (days, hours, min, seconds) = if (hoursVal == 24 && minutesVal == 0 && secondsVal == 0 && nano == 0 ) {
93- listOf (1 , 0 , 0 , 0 )
94- } else if (hoursVal == 23 && minutesVal == 59 && secondsVal == 60 ) {
95- // TODO: throw an error on leap seconds to match what the other platforms do
96- listOf (0 , 23 , 59 , 59 )
97- } else {
98- listOf (0 , hoursVal, minutesVal, secondsVal)
99- }
100-
101- // never fails: 9_999 years are always supported
102- val localDate = dateVal.withYear(dateVal.year % 10000 ).plus(days, DateTimeUnit .DAY )
103- val localTime = LocalTime .of(hours, min, seconds, 0 )
104- val secDelta: Long = try {
105- safeMultiply((dateVal.year / 10000 ).toLong(), SECONDS_PER_10000_YEARS )
106- } catch (e: ArithmeticException ) {
107- throw DateTimeFormatException (e)
108- }
109- val epochDay = localDate.toEpochDays().toLong()
110- val instantSecs = epochDay * 86400 - offset.totalSeconds + localTime.toSecondOfDay() + secDelta
111- try {
112- Instant (instantSecs, nano)
113- } catch (e: IllegalArgumentException ) {
114- throw DateTimeFormatException (e)
115- }
116- }
117-
11829/* *
11930 * The minimum supported epoch second.
12031 */
@@ -184,7 +95,6 @@ public actual class Instant internal constructor(public actual val epochSeconds:
18495 override fun hashCode (): Int =
18596 (epochSeconds xor (epochSeconds ushr 32 )).toInt() + 51 * nanosecondsOfSecond
18697
187- // org.threeten.bp.format.DateTimeFormatterBuilder.InstantPrinterParser#print
18898 actual override fun toString (): String = toStringWithOffset(UtcOffset .ZERO )
18999
190100 public actual companion object {
@@ -226,8 +136,11 @@ public actual class Instant internal constructor(public actual val epochSeconds:
226136 public actual fun fromEpochSeconds (epochSeconds : Long , nanosecondAdjustment : Int ): Instant =
227137 fromEpochSeconds(epochSeconds, nanosecondAdjustment.toLong())
228138
229- public actual fun parse (isoString : String ): Instant =
230- instantParser.parse(isoString)
139+ public actual fun parse (isoString : String ): Instant = try {
140+ DateTimeComponents .parse(isoString, DateTimeComponents .Formats .ISO_DATE_TIME_OFFSET ).toInstantUsingOffset()
141+ } catch (e: IllegalArgumentException ) {
142+ throw DateTimeFormatException (" Failed to parse an instant from '$isoString '" , e)
143+ }
231144
232145 public actual val DISTANT_PAST : Instant = fromEpochSeconds(DISTANT_PAST_SECONDS , 999_999_999 )
233146
@@ -329,65 +242,31 @@ public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti
329242 }
330243 }
331244
332- internal actual fun Instant.toStringWithOffset (offset : UtcOffset ): String {
333- val buf = StringBuilder ()
334- val inNano: Int = nanosecondsOfSecond
335- val seconds = epochSeconds + offset.totalSeconds
336- if (seconds >= - SECONDS_0000_TO_1970 ) { // current era
337- val zeroSecs: Long = seconds - SECONDS_PER_10000_YEARS + SECONDS_0000_TO_1970
338- val hi: Long = zeroSecs.floorDiv(SECONDS_PER_10000_YEARS ) + 1
339- val lo: Long = zeroSecs.mod(SECONDS_PER_10000_YEARS )
340- val ldt: LocalDateTime = Instant (lo - SECONDS_0000_TO_1970 , 0 )
341- .toLocalDateTime(TimeZone .UTC )
342- if (hi > 0 ) {
343- buf.append(' +' ).append(hi)
344- }
345- buf.append(ldt)
346- if (ldt.second == 0 ) {
347- buf.append(" :00" )
348- }
349- } else { // before current era
350- val zeroSecs: Long = seconds + SECONDS_0000_TO_1970
351- val hi: Long = zeroSecs / SECONDS_PER_10000_YEARS
352- val lo: Long = zeroSecs % SECONDS_PER_10000_YEARS
353- val ldt: LocalDateTime = Instant (lo - SECONDS_0000_TO_1970 , 0 )
354- .toLocalDateTime(TimeZone .UTC )
355- val pos = buf.length
356- buf.append(ldt)
357- if (ldt.second == 0 ) {
358- buf.append(" :00" )
359- }
360- if (hi < 0 ) {
361- when {
362- ldt.year == - 10000 -> {
363- buf.deleteAt(pos)
364- buf.deleteAt(pos)
365- buf.insert(pos, (hi - 1 ).toString())
366- }
367- lo == 0L -> {
368- buf.insert(pos, hi)
369- }
370- else -> {
371- buf.insert(pos + 1 , abs(hi))
372- }
373- }
374- }
245+ internal actual fun Instant.toStringWithOffset (offset : UtcOffset ): String =
246+ ISO_DATE_TIME_OFFSET_WITH_TRAILING_ZEROS .format {
247+ setDateTimeOffset(this @toStringWithOffset, offset)
375248 }
376- // fraction
377- if (inNano != 0 ) {
378- buf.append(' .' )
379- when {
380- inNano % 1000000 == 0 -> {
381- buf.append((inNano / 1000000 + 1000 ).toString().substring(1 ))
382- }
383- inNano % 1000 == 0 -> {
384- buf.append((inNano / 1000 + 1000000 ).toString().substring(1 ))
385- }
386- else -> {
387- buf.append((inNano + 1000000000 ).toString().substring(1 ))
388- }
389- }
249+
250+ private val ISO_DATE_TIME_OFFSET_WITH_TRAILING_ZEROS = DateTimeComponents .Format {
251+ date(ISO_DATE )
252+ alternativeParsing({
253+ char(' t' )
254+ }) {
255+ char(' T' )
256+ }
257+ hour()
258+ char(' :' )
259+ minute()
260+ char(' :' )
261+ second()
262+ optional {
263+ char(' .' )
264+ secondFractionInternal(1 , 9 , FractionalSecondDirective .GROUP_BY_THREE )
390265 }
391- buf.append(offset)
392- return buf.toString()
266+ isoOffset(
267+ zOnZero = true ,
268+ useSeparator = true ,
269+ outputMinute = WhenToOutput .IF_NONZERO ,
270+ outputSecond = WhenToOutput .IF_NONZERO
271+ )
393272}
0 commit comments