Skip to content

Commit f18cb89

Browse files
committed
Define the public API endpoints for datetime formatting
Fixes #39 Fixes #58 Fixes #90 Fixes #128 Fixes #133 Fixes #139 Fixes #211 Fixes #240 Fixes #83 Fixes #276
1 parent f83b355 commit f18cb89

35 files changed

+3001
-161
lines changed

core/common/src/Instant.kt

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package kotlinx.datetime
77

8+
import kotlinx.datetime.format.*
89
import kotlinx.datetime.internal.*
910
import kotlinx.datetime.serializers.InstantIso8601Serializer
1011
import kotlinx.serialization.Serializable
@@ -115,6 +116,9 @@ public expect class Instant : Comparable<Instant> {
115116
* where the component for seconds is 60, and for any day, it's possible to observe 23:59:59.
116117
*
117118
* @see Instant.parse
119+
* @see DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET for a very similar format. The difference is that
120+
* [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] will not add trailing zeros for readability to the
121+
* fractional part of the second.
118122
*/
119123
public override fun toString(): String
120124

@@ -149,31 +153,28 @@ public expect class Instant : Comparable<Instant> {
149153
public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant
150154

151155
/**
152-
* Parses a string that represents an instant in ISO-8601 format including date and time components and
153-
* the mandatory time zone offset and returns the parsed [Instant] value.
156+
* A shortcut for calling [DateTimeFormat.parse], followed by [DateTimeComponents.toInstantUsingOffset].
154157
*
155-
* Supports the following ways of specifying the time zone offset:
156-
* - the `Z` designator for the UTC+0 time zone,
157-
* - a custom time zone offset specified with `+hh`, or `+hh:mm`, or `+hh:mm:ss`
158-
* (with `+` being replaced with `-` for the negative offsets)
159-
*
160-
* Examples of instants in the ISO-8601 format:
161-
* - `2020-08-30T18:43:00Z`
162-
* - `2020-08-30T18:43:00.500Z`
163-
* - `2020-08-30T18:43:00.123456789Z`
164-
* - `2020-08-30T18:40:00+03:00`
165-
* - `2020-08-30T18:40:00+03:30:20`
158+
* Parses a string that represents an instant including date and time components and a mandatory
159+
* time zone offset and returns the parsed [Instant] value.
166160
*
167161
* The string is considered to represent time on the UTC-SLS time scale instead of UTC.
168162
* In practice, this means that, even if there is a leap second on the given day, it will not affect how the
169163
* time is parsed, even if it's in the last 1000 seconds of the day.
170164
* Instead, even if there is a negative leap second on the given day, 23:59:59 is still considered valid time.
171165
* 23:59:60 is invalid on UTC-SLS, so parsing it will fail.
172166
*
167+
* If the format is not specified, [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] is used.
168+
*
173169
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Instant] are exceeded.
170+
*
171+
* @see Instant.toString for formatting using the default format.
172+
* @see Instant.format for formatting using a custom format.
174173
*/
175-
public fun parse(isoString: String): Instant
176-
174+
public fun parse(
175+
input: CharSequence,
176+
format: DateTimeFormat<DateTimeComponents> = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
177+
): Instant
177178

178179
/**
179180
* An instant value that is far in the past.
@@ -508,12 +509,20 @@ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone)
508509
public fun Instant.minus(other: Instant, unit: DateTimeUnit.TimeBased): Long =
509510
other.until(this, unit)
510511

511-
internal const val DISTANT_PAST_SECONDS = -3217862419201
512-
internal const val DISTANT_FUTURE_SECONDS = 3093527980800
513-
514512
/**
515-
* Displays the given Instant in the given [offset].
513+
* Formats this value using the given [format] using the given [offset].
514+
*
515+
* Equivalent to calling [DateTimeFormat.format] on [format] and using [DateTimeComponents.setDateTimeOffset] in
516+
* the lambda.
516517
*
517-
* Be careful: this function may throw for some values of the [Instant].
518+
* [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] is a format very similar to the one used by [toString].
519+
* The only difference is that [Instant.toString] adds trailing zeros to the fraction-of-second component so that the
520+
* number of digits after a dot is a multiple of three.
518521
*/
519-
internal expect fun Instant.toStringWithOffset(offset: UtcOffset): String
522+
public fun Instant.format(format: DateTimeFormat<DateTimeComponents>, offset: UtcOffset = UtcOffset.ZERO): String {
523+
val instant = this
524+
return format.format { setDateTimeOffset(instant, offset) }
525+
}
526+
527+
internal const val DISTANT_PAST_SECONDS = -3217862419201
528+
internal const val DISTANT_FUTURE_SECONDS = 3093527980800

core/common/src/LocalDate.kt

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package kotlinx.datetime
77

8+
import kotlinx.datetime.format.*
89
import kotlinx.datetime.serializers.LocalDateIso8601Serializer
910
import kotlinx.serialization.Serializable
1011

@@ -23,14 +24,18 @@ import kotlinx.serialization.Serializable
2324
public expect class LocalDate : Comparable<LocalDate> {
2425
public companion object {
2526
/**
26-
* Parses a string that represents a date in ISO-8601 format
27-
* and returns the parsed [LocalDate] value.
27+
* A shortcut for calling [DateTimeFormat.parse].
2828
*
29-
* An example of a local date in ISO-8601 format: `2020-08-30`.
29+
* Parses a string that represents a date and returns the parsed [LocalDate] value.
30+
*
31+
* If [format] is not specified, [Formats.ISO] is used.
3032
*
3133
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalDate] are exceeded.
34+
*
35+
* @see LocalDate.toString for formatting using the default format.
36+
* @see LocalDate.format for formatting using a custom format.
3237
*/
33-
public fun parse(isoString: String): LocalDate
38+
public fun parse(input: CharSequence, format: DateTimeFormat<LocalDate> = getIsoDateFormat()): LocalDate
3439

3540
/**
3641
* Returns a [LocalDate] that is [epochDays] number of days from the epoch day `1970-01-01`.
@@ -41,10 +46,67 @@ public expect class LocalDate : Comparable<LocalDate> {
4146
*/
4247
public fun fromEpochDays(epochDays: Int): LocalDate
4348

49+
/**
50+
* Creates a new format for parsing and formatting [LocalDate] values.
51+
*
52+
* Example:
53+
* ```
54+
* // 2020 Jan 05
55+
* LocalDate.Format {
56+
* year()
57+
* char(' ')
58+
* monthName(MonthNames.ENGLISH_ABBREVIATED)
59+
* char(' ')
60+
* dayOfMonth()
61+
* }
62+
* ```
63+
*
64+
* Only parsing and formatting of well-formed values is supported. If the input does not fit the boundaries
65+
* (for example, [dayOfMonth] is 31 for February), consider using [DateTimeComponents.Format] instead.
66+
*
67+
* There is a collection of predefined formats in [LocalDate.Formats].
68+
*/
69+
@Suppress("FunctionName")
70+
public fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate>
71+
4472
internal val MIN: LocalDate
4573
internal val MAX: LocalDate
4674
}
4775

76+
/**
77+
* A collection of predefined formats for parsing and formatting [LocalDate] values.
78+
*
79+
* See [LocalDate.Formats.ISO] and [LocalDate.Formats.ISO_BASIC] for popular predefined formats.
80+
* [LocalDate.parse] and [LocalDate.toString] can be used as convenient shortcuts for the
81+
* [LocalDate.Formats.ISO] format.
82+
*
83+
* If predefined formats are not sufficient, use [LocalDate.Format] to create a custom
84+
* [kotlinx.datetime.format.DateTimeFormat] for [LocalDate] values.
85+
*/
86+
public object Formats {
87+
/**
88+
* ISO 8601 extended format, which is the format used by [LocalDate.toString] and [LocalDate.parse].
89+
*
90+
* Examples of dates in ISO 8601 format:
91+
* - `2020-08-30`
92+
* - `+12020-08-30`
93+
* - `0000-08-30`
94+
* - `-0001-08-30`
95+
*/
96+
public val ISO: DateTimeFormat<LocalDate>
97+
98+
/**
99+
* ISO 8601 basic format.
100+
*
101+
* Examples of dates in ISO 8601 basic format:
102+
* - `20200830`
103+
* - `+120200830`
104+
* - `00000830`
105+
* - `-00010830`
106+
*/
107+
public val ISO_BASIC: DateTimeFormat<LocalDate>
108+
}
109+
48110
/**
49111
* Constructs a [LocalDate] instance from the given date components.
50112
*
@@ -77,14 +139,19 @@ public expect class LocalDate : Comparable<LocalDate> {
77139

78140
/** Returns the year component of the date. */
79141
public val year: Int
142+
80143
/** Returns the number-of-month (1..12) component of the date. */
81144
public val monthNumber: Int
145+
82146
/** Returns the month ([Month]) component of the date. */
83147
public val month: Month
148+
84149
/** Returns the day-of-month component of the date. */
85150
public val dayOfMonth: Int
151+
86152
/** Returns the day-of-week component of the date. */
87153
public val dayOfWeek: DayOfWeek
154+
88155
/** Returns the day-of-year component of the date. */
89156
public val dayOfYear: Int
90157

@@ -106,13 +173,21 @@ public expect class LocalDate : Comparable<LocalDate> {
106173
public override fun compareTo(other: LocalDate): Int
107174

108175
/**
109-
* Converts this date to the ISO-8601 string representation.
176+
* Converts this date to the extended ISO-8601 string representation.
110177
*
111-
* @see LocalDate.parse
178+
* @see Formats.ISO for the format details.
179+
* @see parse for the dual operation: obtaining [LocalDate] from a string.
180+
* @see LocalDate.format for formatting using a custom format.
112181
*/
113182
public override fun toString(): String
114183
}
115184

185+
/**
186+
* Formats this value using the given [format].
187+
* Equivalent to calling [DateTimeFormat.format] on [format] with `this`.
188+
*/
189+
public fun LocalDate.format(format: DateTimeFormat<LocalDate>): String = format.format(this)
190+
116191
/**
117192
* @suppress
118193
*/
@@ -159,9 +234,8 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate =
159234
if (period.days != Int.MIN_VALUE && period.months != Int.MIN_VALUE) {
160235
plus(with(period) { DatePeriod(-years, -months, -days) })
161236
} else {
162-
minus(period.years, DateTimeUnit.YEAR).
163-
minus(period.months, DateTimeUnit.MONTH).
164-
minus(period.days, DateTimeUnit.DAY)
237+
minus(period.years, DateTimeUnit.YEAR).minus(period.months, DateTimeUnit.MONTH)
238+
.minus(period.days, DateTimeUnit.DAY)
165239
}
166240

167241
/**
@@ -299,3 +373,6 @@ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): Loc
299373
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
300374
*/
301375
public fun LocalDate.minus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit)
376+
377+
// workaround for https://youtrack.jetbrains.com/issue/KT-65484
378+
internal fun getIsoDateFormat() = LocalDate.Formats.ISO

0 commit comments

Comments
 (0)