Skip to content

Commit 8c73b5f

Browse files
committed
Implement parsing and formatting in Native with the new system
1 parent a03691b commit 8c73b5f

File tree

8 files changed

+106
-495
lines changed

8 files changed

+106
-495
lines changed

core/common/src/format/DateTimeFormatBuilder.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,35 @@ public sealed interface DateTimeFormatBuilder {
283283
}
284284
}
285285

286+
/**
287+
* The fractional part of the second without the leading dot.
288+
*
289+
* When formatting, the decimal fraction will round the number to fit in the specified [maxLength] and will add
290+
* trailing zeroes to the specified [minLength].
291+
*
292+
* Additionally, [grouping] is a list, where the i'th (1-based) element specifies how many trailing zeros to add during
293+
* formatting when the number would have i digits.
294+
*
295+
* When parsing, the parser will require that the fraction is at least [minLength] and at most [maxLength]
296+
* digits long.
297+
*
298+
* This field has the default value of 0. If you want to omit it, use [optional].
299+
*
300+
* @throws IllegalArgumentException if [minLength] is greater than [maxLength] or if either is not in the range 1..9.
301+
*/
302+
internal fun DateTimeFormatBuilder.WithTime.secondFractionInternal(
303+
minLength: Int,
304+
maxLength: Int,
305+
grouping: List<Int>
306+
) {
307+
@Suppress("NO_ELSE_IN_WHEN")
308+
when (this) {
309+
is AbstractWithTimeBuilder -> addFormatStructureForTime(
310+
BasicFormatStructure(FractionalSecondDirective(minLength, maxLength, grouping))
311+
)
312+
}
313+
}
314+
286315
/**
287316
* A format along with other ways to parse the same portion of the value.
288317
*

core/native/src/Instant.kt

Lines changed: 31 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
package kotlinx.datetime
1010

11+
import kotlinx.datetime.format.*
1112
import kotlinx.datetime.internal.*
1213
import kotlinx.datetime.serializers.InstantIso8601Serializer
1314
import kotlinx.serialization.Serializable
14-
import kotlin.math.*
1515
import kotlin.time.*
1616
import kotlin.time.Duration.Companion.nanoseconds
1717
import 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
}

core/native/src/LocalDate.kt

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,14 @@
88

99
package kotlinx.datetime
1010

11+
import kotlinx.datetime.format.*
1112
import kotlinx.datetime.internal.*
1213
import kotlinx.datetime.internal.safeAdd
1314
import kotlinx.datetime.internal.safeMultiply
1415
import kotlinx.datetime.serializers.LocalDateIso8601Serializer
1516
import kotlinx.serialization.Serializable
1617
import kotlin.math.*
1718

18-
// This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5
19-
// org.threeten.bp.format.DateTimeFormatter#ISO_LOCAL_DATE
20-
internal val localDateParser: Parser<LocalDate>
21-
get() = intParser(4, 10, SignStyle.EXCEEDS_PAD)
22-
.chainIgnoring(concreteCharParser('-'))
23-
.chain(intParser(2, 2))
24-
.chainIgnoring(concreteCharParser('-'))
25-
.chain(intParser(2, 2))
26-
.map {
27-
val (yearMonth, day) = it
28-
val (year, month) = yearMonth
29-
try {
30-
LocalDate(year, month, day)
31-
} catch (e: IllegalArgumentException) {
32-
throw DateTimeFormatException(e)
33-
}
34-
}
35-
3619
internal const val YEAR_MIN = -999_999
3720
internal const val YEAR_MAX = 999_999
3821

@@ -59,8 +42,7 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
5942
public actual constructor(year: Int, month: Month, dayOfMonth: Int) : this(year, month.number, dayOfMonth)
6043

6144
public actual companion object {
62-
public actual fun parse(isoString: String): LocalDate =
63-
localDateParser.parse(isoString)
45+
public actual fun parse(isoString: String): LocalDate = ISO_DATE.parse(isoString)
6446

6547
// org.threeten.bp.LocalDate#toEpochDay
6648
public actual fun fromEpochDays(epochDays: Int): LocalDate {
@@ -126,13 +108,6 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
126108
return total - DAYS_0000_TO_1970
127109
}
128110

129-
// org.threeten.bp.LocalDate#withYear
130-
/**
131-
* @throws IllegalArgumentException if the result exceeds the boundaries
132-
*/
133-
internal fun withYear(newYear: Int): LocalDate =
134-
if (newYear == year) this else resolvePreviousValid(newYear, monthNumber, dayOfMonth)
135-
136111
public actual val month: Month
137112
get() = Month(monthNumber)
138113

@@ -206,30 +181,7 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
206181
}
207182

208183
// org.threeten.bp.LocalDate#toString
209-
actual override fun toString(): String {
210-
val yearValue = year
211-
val monthValue: Int = monthNumber
212-
val dayValue: Int = dayOfMonth
213-
val absYear: Int = abs(yearValue)
214-
val buf = StringBuilder(10)
215-
if (absYear < 1000) {
216-
if (yearValue < 0) {
217-
buf.append(yearValue - 10000).deleteAt(1)
218-
} else {
219-
buf.append(yearValue + 10000).deleteAt(0)
220-
}
221-
} else {
222-
if (yearValue > 9999) {
223-
buf.append('+')
224-
}
225-
buf.append(yearValue)
226-
}
227-
return buf.append(if (monthValue < 10) "-0" else "-")
228-
.append(monthValue)
229-
.append(if (dayValue < 10) "-0" else "-")
230-
.append(dayValue)
231-
.toString()
232-
}
184+
actual override fun toString(): String = ISO_DATE.format(this)
233185
}
234186

235187
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))

core/native/src/LocalDateTime.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,16 @@
88

99
package kotlinx.datetime
1010

11+
import kotlinx.datetime.format.*
1112
import kotlinx.datetime.internal.*
12-
import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer
13-
import kotlinx.serialization.Serializable
14-
15-
// This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5
16-
// org.threeten.bp.format.DateTimeFormatter#ISO_LOCAL_DATE_TIME
17-
internal val localDateTimeParser: Parser<LocalDateTime>
18-
get() = localDateParser
19-
.chainIgnoring(concreteCharParser('T').or(concreteCharParser('t')))
20-
.chain(localTimeParser)
21-
.map { (date, time) ->
22-
LocalDateTime(date, time)
23-
}
13+
import kotlinx.datetime.serializers.*
14+
import kotlinx.serialization.*
2415

2516
@Serializable(with = LocalDateTimeIso8601Serializer::class)
2617
public actual class LocalDateTime
2718
public actual constructor(public actual val date: LocalDate, public actual val time: LocalTime) : Comparable<LocalDateTime> {
2819
public actual companion object {
29-
public actual fun parse(isoString: String): LocalDateTime =
30-
localDateTimeParser.parse(isoString)
20+
public actual fun parse(isoString: String): LocalDateTime = ISO_DATETIME.parse(isoString)
3121

3222
internal actual val MIN: LocalDateTime = LocalDateTime(LocalDate.MIN, LocalTime.MIN)
3323
internal actual val MAX: LocalDateTime = LocalDateTime(LocalDate.MAX, LocalTime.MAX)
@@ -68,7 +58,7 @@ public actual constructor(public actual val date: LocalDate, public actual val t
6858
}
6959

7060
// org.threeten.bp.LocalDateTime#toString
71-
actual override fun toString(): String = date.toString() + 'T' + time.toString()
61+
actual override fun toString(): String = ISO_DATETIME_OPTIONAL_SECONDS_TRAILING_ZEROS.format(this)
7262

7363
// org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond
7464
internal fun toEpochSecond(offset: UtcOffset): Long {
@@ -127,3 +117,11 @@ internal fun LocalDateTime.plusSeconds(seconds: Int): LocalDateTime
127117
val newTime: LocalTime = if (newNanoOfDay == currentNanoOfDay) time else LocalTime.ofNanoOfDay(newNanoOfDay)
128118
return LocalDateTime(date.plusDays(totalDays.toInt()), newTime)
129119
}
120+
121+
private val ISO_DATETIME_OPTIONAL_SECONDS_TRAILING_ZEROS by lazy {
122+
LocalDateTimeFormat.build {
123+
date(ISO_DATE)
124+
alternativeParsing({ char('t') }) { char('T') }
125+
time(ISO_TIME_OPTIONAL_SECONDS_TRAILING_ZEROS)
126+
}
127+
}

0 commit comments

Comments
 (0)