Skip to content

Commit 3a2d1ec

Browse files
committed
Implement formatAsKotlinBuilderDsl properly
1 parent 7d5b922 commit 3a2d1ec

12 files changed

+580
-349
lines changed

core/common/src/format/DateTimeComponents.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -430,14 +430,18 @@ internal class DateTimeComponentsContents internal constructor(
430430
@SharedImmutable
431431
internal val timeZoneField = GenericFieldSpec(DateTimeComponentsContents::timeZoneId)
432432

433-
internal class TimeZoneIdDirective(knownZones: Set<String>) :
433+
internal class TimeZoneIdDirective(private val knownZones: Set<String>) :
434434
StringFieldFormatDirective<DateTimeComponentsContents>(timeZoneField, knownZones) {
435435

436-
override val builderRepresentation: String = "${DateTimeFormatBuilder.WithDateTimeComponents::appendTimeZoneId.name}()"
436+
override val builderRepresentation: String get() =
437+
"${DateTimeFormatBuilder.WithDateTimeComponents::appendTimeZoneId.name}()"
438+
439+
override fun equals(other: Any?): Boolean = other is TimeZoneIdDirective && other.knownZones == knownZones
440+
override fun hashCode(): Int = knownZones.hashCode()
437441
}
438442

439-
internal class DateTimeComponentsFormat(val actualFormat: StringFormat<DateTimeComponentsContents>) :
440-
AbstractDateTimeFormat<DateTimeComponents, DateTimeComponentsContents>(actualFormat) {
443+
internal class DateTimeComponentsFormat(override val actualFormat: StringFormat<DateTimeComponentsContents>) :
444+
AbstractDateTimeFormat<DateTimeComponents, DateTimeComponentsContents>() {
441445
override fun intermediateFromValue(value: DateTimeComponents): DateTimeComponentsContents = value.contents
442446

443447
override fun valueFromIntermediate(intermediate: DateTimeComponentsContents): DateTimeComponents = DateTimeComponents(intermediate)
@@ -456,11 +460,11 @@ internal class DateTimeComponentsFormat(val actualFormat: StringFormat<DateTimeC
456460
actualBuilder.add(BasicFormatStructure(MonthDirective(padding)))
457461

458462
override fun appendMonthName(names: MonthNames) =
459-
actualBuilder.add(BasicFormatStructure(MonthNameDirective(names.names)))
463+
actualBuilder.add(BasicFormatStructure(MonthNameDirective(names)))
460464

461465
override fun appendDayOfMonth(padding: Padding) = actualBuilder.add(BasicFormatStructure(DayDirective(padding)))
462466
override fun appendDayOfWeek(names: DayOfWeekNames) =
463-
actualBuilder.add(BasicFormatStructure(DayOfWeekDirective(names.names)))
467+
actualBuilder.add(BasicFormatStructure(DayOfWeekDirective(names)))
464468

465469
override fun appendHour(padding: Padding) = actualBuilder.add(BasicFormatStructure(HourDirective(padding)))
466470
override fun appendAmPmHour(padding: Padding) =
@@ -518,8 +522,6 @@ internal class DateTimeComponentsFormat(val actualFormat: StringFormat<DateTimeC
518522

519523
override fun createEmpty(): Builder = Builder(AppendableFormatStructure())
520524
}
521-
522-
override fun toString(): String = actualFormat.builderString()
523525
}
524526

525527
private class TwoDigitNumber(private val reference: KMutableProperty0<Int?>) {

core/common/src/format/DateTimeFormat.kt

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package kotlinx.datetime.format
88
import kotlinx.datetime.*
99
import kotlinx.datetime.internal.format.*
1010
import kotlinx.datetime.internal.format.parser.*
11+
import kotlin.native.concurrent.*
1112

1213
/**
1314
* A format for parsing and formatting date-time-related values.
@@ -21,7 +22,7 @@ public sealed interface DateTimeFormat<T> {
2122
/**
2223
* Formats the given [value] into the given [appendable], using this format.
2324
*/
24-
public fun <A: Appendable> formatTo(appendable: A, value: T): A
25+
public fun <A : Appendable> formatTo(appendable: A, value: T): A
2526

2627
/**
2728
* Parses the given [input] string as [T], using this format.
@@ -36,6 +37,22 @@ public sealed interface DateTimeFormat<T> {
3637
* @return the parsed value, or `null` if the input string is not in the expected format or the value is invalid.
3738
*/
3839
public fun parseOrNull(input: CharSequence): T?
40+
41+
public companion object {
42+
/**
43+
* Produces Kotlin code that, when pasted into a Kotlin source file, creates a [DateTimeFormat] instance that
44+
* behaves identically to [format].
45+
*
46+
* The typical use case for this is to create a [DateTimeFormat] instance using a non-idiomatic approach and
47+
* then convert it to a builder DSL.
48+
*/
49+
public fun formatAsKotlinBuilderDsl(format: DateTimeFormat<*>): String {
50+
when (format) {
51+
is AbstractDateTimeFormat<*, *> -> return format.actualFormat.builderString(allFormatConstants)
52+
else -> error("Unsupported format: $format")
53+
}
54+
}
55+
}
3956
}
4057

4158
/**
@@ -58,11 +75,19 @@ public enum class Padding {
5875
SPACE
5976
}
6077

78+
internal fun Padding.toKotlinCode(): String = when (this) {
79+
Padding.NONE -> "Padding.NONE"
80+
Padding.ZERO -> "Padding.ZERO"
81+
Padding.SPACE -> "Padding.SPACE"
82+
}
83+
6184
internal inline fun Padding.minDigits(width: Int) = if (this == Padding.ZERO) width else 1
6285
internal inline fun Padding.spaces(width: Int) = if (this == Padding.SPACE) width else null
6386

6487
/** [T] is the user-visible type, whereas [U] is its mutable representation for parsing and formatting. */
65-
internal sealed class AbstractDateTimeFormat<T, U : Copyable<U>>(private val actualFormat: StringFormat<U>): DateTimeFormat<T> {
88+
internal sealed class AbstractDateTimeFormat<T, U : Copyable<U>> : DateTimeFormat<T> {
89+
90+
abstract val actualFormat: StringFormat<U>
6691

6792
abstract fun intermediateFromValue(value: T): U
6893

@@ -80,7 +105,7 @@ internal sealed class AbstractDateTimeFormat<T, U : Copyable<U>>(private val act
80105
actualFormat.formatter.format(intermediateFromValue(value), it)
81106
}.toString()
82107

83-
override fun <A: Appendable> formatTo(appendable: A, value: T): A = appendable.apply {
108+
override fun <A : Appendable> formatTo(appendable: A, value: T): A = appendable.apply {
84109
actualFormat.formatter.format(intermediateFromValue(value), this)
85110
}
86111

@@ -108,3 +133,21 @@ internal sealed class AbstractDateTimeFormat<T, U : Copyable<U>>(private val act
108133
}?.let { valueFromIntermediateOrNull(it) }
109134

110135
}
136+
137+
@SharedImmutable
138+
private val allFormatConstants: List<Pair<String, StringFormat<*>>> = run {
139+
fun unwrap(format: DateTimeFormat<*>): StringFormat<*> = (format as AbstractDateTimeFormat<*, *>).actualFormat
140+
listOf(
141+
"${DateTimeFormatBuilder.WithDate::appendDate.name}(LocalDate.Formats.ISO)" to unwrap(LocalDate.Formats.ISO),
142+
"${DateTimeFormatBuilder.WithDate::appendDate.name}(LocalDate.Formats.ISO_BASIC)" to unwrap(LocalDate.Formats.ISO_BASIC),
143+
"${DateTimeFormatBuilder.WithTime::appendTime.name}(LocalTime.Formats.ISO)" to unwrap(LocalTime.Formats.ISO),
144+
"${DateTimeFormatBuilder.WithTime::appendTime.name}(LocalTime.Formats.ISO_BASIC)" to unwrap(LocalTime.Formats.ISO_BASIC),
145+
"${DateTimeFormatBuilder.WithUtcOffset::appendOffset.name}(UtcOffset.Formats.ISO)" to unwrap(UtcOffset.Formats.ISO),
146+
"${DateTimeFormatBuilder.WithUtcOffset::appendOffset.name}(UtcOffset.Formats.ISO_BASIC)" to unwrap(UtcOffset.Formats.ISO_BASIC),
147+
"${DateTimeFormatBuilder.WithUtcOffset::appendOffset.name}(UtcOffset.Formats.FOUR_DIGITS)" to unwrap(UtcOffset.Formats.FOUR_DIGITS),
148+
"${DateTimeFormatBuilder.WithDateTimeComponents::appendDateTimeComponents.name}(DateTimeComponents.Formats.RFC_1123)" to
149+
unwrap(DateTimeComponents.Formats.RFC_1123),
150+
"${DateTimeFormatBuilder.WithDateTimeComponents::appendDateTimeComponents.name}(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET)" to
151+
unwrap(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET),
152+
)
153+
}

core/common/src/format/DateTimeFormatBuilder.kt

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -338,37 +338,48 @@ internal interface Copyable<Self> {
338338
fun copy(): Self
339339
}
340340

341-
internal inline fun<T> StringFormat<T>.builderString(): String = directives.builderString()
341+
internal inline fun<T> StringFormat<T>.builderString(constants: List<Pair<String, StringFormat<*>>>): String =
342+
directives.builderString(constants)
342343

343-
private fun<T> FormatStructure<T>.builderString(): String = when (this) {
344+
private fun<T> FormatStructure<T>.builderString(constants: List<Pair<String, StringFormat<*>>>): String = when (this) {
344345
is BasicFormatStructure -> directive.builderRepresentation
345-
is ConstantFormatStructure -> "char(${string.toKotlinCode()})"
346+
is ConstantFormatStructure -> if (string.length == 1) {
347+
"${DateTimeFormatBuilder::char.name}(${string[0].toKotlinCode()})"
348+
} else {
349+
"${DateTimeFormatBuilder::chars.name}(${string.toKotlinCode()})"
350+
}
346351
is SignedFormatStructure -> {
347352
if (format is BasicFormatStructure && format.directive is UtcOffsetWholeHoursDirective) {
348353
format.directive.builderRepresentation
349354
} else {
350355
buildString {
351356
if (withPlusSign) appendLine("withSharedSign(outputPlus = true) {")
352357
else appendLine("withSharedSign {")
353-
appendLine(format.builderString().prependIndent(CODE_INDENT))
358+
appendLine(format.builderString(constants).prependIndent(CODE_INDENT))
354359
append("}")
355360
}
356361
}
357362
}
358363
is OptionalFormatStructure -> buildString {
359364
if (onZero == "") {
360-
appendLine("optional {")
365+
appendLine("${DateTimeFormatBuilder::optional.name} {")
361366
} else {
362-
appendLine("optional(${onZero.toKotlinCode()}) {")
367+
appendLine("${DateTimeFormatBuilder::optional.name}(${onZero.toKotlinCode()}) {")
368+
}
369+
val subformat = format.builderString(constants)
370+
if (subformat.isNotEmpty()) {
371+
appendLine(subformat.prependIndent(CODE_INDENT))
363372
}
364-
appendLine(format.builderString().prependIndent(CODE_INDENT))
365373
append("}")
366374
}
367375
is AlternativesParsingFormatStructure -> buildString {
368-
append("appendAlternatives(")
376+
append("${DateTimeFormatBuilder::alternativeParsing.name}(")
369377
for (alternative in formats) {
370378
appendLine("{")
371-
appendLine(alternative.builderString().prependIndent(CODE_INDENT))
379+
val subformat = alternative.builderString(constants)
380+
if (subformat.isNotEmpty()) {
381+
appendLine(subformat.prependIndent(CODE_INDENT))
382+
}
372383
append("}, ")
373384
}
374385
if (this[length - 2] == ',') {
@@ -377,14 +388,35 @@ private fun<T> FormatStructure<T>.builderString(): String = when (this) {
377388
}
378389
}
379390
appendLine(") {")
380-
appendLine(mainFormat.builderString().prependIndent(CODE_INDENT))
391+
appendLine(mainFormat.builderString(constants).prependIndent(CODE_INDENT))
381392
append("}")
382393
}
383394
is ConcatenatedFormatStructure -> buildString {
384-
for (format in formats) {
385-
appendLine(format.builderString())
395+
if (formats.isNotEmpty()) {
396+
var index = 0
397+
loop@while (index < formats.size) {
398+
searchConstant@for (constant in constants) {
399+
val constantDirectives = constant.second.directives.formats
400+
if (formats.size - index >= constantDirectives.size) {
401+
for (i in constantDirectives.indices) {
402+
if (formats[index + i] != constantDirectives[i]) {
403+
continue@searchConstant
404+
}
405+
}
406+
append(constant.first)
407+
index += constantDirectives.size
408+
continue@loop
409+
}
410+
}
411+
if (index == formats.size - 1) {
412+
append(formats.last().builderString(constants))
413+
} else {
414+
appendLine(formats[index].builderString(constants))
415+
}
416+
++index
417+
}
386418
}
387419
}
388420
}
389421

390-
private const val CODE_INDENT = " "
422+
private const val CODE_INDENT = " "

core/common/src/format/LocalDateFormat.kt

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public class MonthNames(
5555
}
5656
}
5757

58+
internal fun MonthNames.toKotlinCode(): String = when (this.names) {
59+
MonthNames.ENGLISH_FULL.names -> "MonthNames.${DayOfWeekNames.Companion::ENGLISH_FULL.name}"
60+
MonthNames.ENGLISH_ABBREVIATED.names -> "MonthNames.${DayOfWeekNames.Companion::ENGLISH_ABBREVIATED.name}"
61+
else -> names.joinToString(", ", "MonthNames(", ")", transform = String::toKotlinCode)
62+
}
63+
5864
/**
5965
* A description of how day of week names are formatted.
6066
*/
@@ -103,6 +109,12 @@ public class DayOfWeekNames(
103109
}
104110
}
105111

112+
internal fun DayOfWeekNames.toKotlinCode(): String = when (this.names) {
113+
DayOfWeekNames.ENGLISH_FULL.names -> "DayOfWeekNames.${DayOfWeekNames.Companion::ENGLISH_FULL.name}"
114+
DayOfWeekNames.ENGLISH_ABBREVIATED.names -> "DayOfWeekNames.${DayOfWeekNames.Companion::ENGLISH_ABBREVIATED.name}"
115+
else -> names.joinToString(", ", "DayOfWeekNames(", ")", transform = String::toKotlinCode)
116+
}
117+
106118
internal fun <T> getParsedField(field: T?, name: String): T {
107119
if (field == null) {
108120
throw DateTimeFormatException("Can not create a $name from the given input: the field $name is missing")
@@ -170,18 +182,21 @@ internal class IncompleteLocalDate(
170182
"${year ?: "??"}-${monthNumber ?: "??"}-${dayOfMonth ?: "??"} (day of week is ${isoDayOfWeek ?: "??"})"
171183
}
172184

173-
internal class YearDirective(padding: Padding) :
185+
internal class YearDirective(private val padding: Padding) :
174186
SignedIntFieldFormatDirective<DateFieldContainer>(
175187
DateFields.year,
176188
minDigits = padding.minDigits(4),
177189
maxDigits = null,
178190
spacePadding = padding.spaces(4),
179191
outputPlusOnExceededWidth = 4,
180192
) {
181-
override val builderRepresentation: String = when (padding) {
193+
override val builderRepresentation: String get() = when (padding) {
182194
Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::appendYear.name}()"
183-
else -> "${DateTimeFormatBuilder.WithDate::appendYear.name}($padding)"
195+
else -> "${DateTimeFormatBuilder.WithDate::appendYear.name}(${padding.toKotlinCode()})"
184196
}
197+
198+
override fun equals(other: Any?): Boolean = other is YearDirective && padding == other.padding
199+
override fun hashCode(): Int = padding.hashCode()
185200
}
186201

187202
internal class ReducedYearDirective(val base: Int) :
@@ -190,48 +205,63 @@ internal class ReducedYearDirective(val base: Int) :
190205
digits = 2,
191206
base = base,
192207
) {
193-
override val builderRepresentation: String = "${DateTimeFormatBuilder.WithDate::appendYearTwoDigits.name}($base)"
208+
override val builderRepresentation: String get() = "${DateTimeFormatBuilder.WithDate::appendYearTwoDigits.name}($base)"
209+
210+
override fun equals(other: Any?): Boolean = other is ReducedYearDirective && base == other.base
211+
override fun hashCode(): Int = base.hashCode()
194212
}
195213

196-
internal class MonthDirective(padding: Padding) :
214+
internal class MonthDirective(private val padding: Padding) :
197215
UnsignedIntFieldFormatDirective<DateFieldContainer>(
198216
DateFields.month,
199217
minDigits = padding.minDigits(2),
200218
spacePadding = padding.spaces(2),
201219
) {
202-
override val builderRepresentation: String = when (padding) {
220+
override val builderRepresentation: String get() = when (padding) {
203221
Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::appendMonthNumber.name}()"
204-
else -> "${DateTimeFormatBuilder.WithDate::appendMonthNumber.name}($padding)"
222+
else -> "${DateTimeFormatBuilder.WithDate::appendMonthNumber.name}(${padding.toKotlinCode()})"
205223
}
224+
225+
override fun equals(other: Any?): Boolean = other is MonthDirective && padding == other.padding
226+
override fun hashCode(): Int = padding.hashCode()
206227
}
207228

208-
internal class MonthNameDirective(names: List<String>) :
209-
NamedUnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.month, names) {
210-
override val builderRepresentation: String =
211-
"${DateTimeFormatBuilder.WithDate::appendMonthName.name}(${names.toKotlinCode(String::toKotlinCode)})"
229+
internal class MonthNameDirective(private val names: MonthNames) :
230+
NamedUnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.month, names.names) {
231+
override val builderRepresentation: String get() =
232+
"${DateTimeFormatBuilder.WithDate::appendMonthName.name}(${names.toKotlinCode()})"
233+
234+
override fun equals(other: Any?): Boolean = other is MonthNameDirective && names.names == other.names.names
235+
override fun hashCode(): Int = names.names.hashCode()
212236
}
213237

214-
internal class DayDirective(padding: Padding) :
238+
internal class DayDirective(private val padding: Padding) :
215239
UnsignedIntFieldFormatDirective<DateFieldContainer>(
216240
DateFields.dayOfMonth,
217241
minDigits = padding.minDigits(2),
218242
spacePadding = padding.spaces(2),
219243
) {
220-
override val builderRepresentation: String = when (padding) {
244+
override val builderRepresentation: String get() = when (padding) {
221245
Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::appendDayOfMonth.name}()"
222-
else -> "${DateTimeFormatBuilder.WithDate::appendDayOfMonth.name}($padding)"
246+
else -> "${DateTimeFormatBuilder.WithDate::appendDayOfMonth.name}(${padding.toKotlinCode()})"
223247
}
248+
249+
override fun equals(other: Any?): Boolean = other is DayDirective && padding == other.padding
250+
override fun hashCode(): Int = padding.hashCode()
224251
}
225252

226-
internal class DayOfWeekDirective(names: List<String>) :
227-
NamedUnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.isoDayOfWeek, names) {
253+
internal class DayOfWeekDirective(private val names: DayOfWeekNames) :
254+
NamedUnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.isoDayOfWeek, names.names) {
228255

229-
override val builderRepresentation: String =
230-
"${DateTimeFormatBuilder.WithDate::appendDayOfWeek.name}(${names.toKotlinCode(String::toKotlinCode)})"
256+
override val builderRepresentation: String get() =
257+
"${DateTimeFormatBuilder.WithDate::appendDayOfWeek.name}(${names.toKotlinCode()})"
258+
259+
override fun equals(other: Any?): Boolean = other is DayOfWeekDirective && names.names == other.names.names
260+
override fun hashCode(): Int = names.names.hashCode()
231261
}
232262

233-
internal class LocalDateFormat(val actualFormat: StringFormat<DateFieldContainer>) :
234-
AbstractDateTimeFormat<LocalDate, IncompleteLocalDate>(actualFormat) {
263+
internal class LocalDateFormat(override val actualFormat: StringFormat<DateFieldContainer>) :
264+
AbstractDateTimeFormat<LocalDate, IncompleteLocalDate>() {
235265
override fun intermediateFromValue(value: LocalDate): IncompleteLocalDate =
236266
IncompleteLocalDate().apply { populateFrom(value) }
237267

@@ -259,11 +289,11 @@ internal class LocalDateFormat(val actualFormat: StringFormat<DateFieldContainer
259289
actualBuilder.add(BasicFormatStructure(MonthDirective(padding)))
260290

261291
override fun appendMonthName(names: MonthNames) =
262-
actualBuilder.add(BasicFormatStructure(MonthNameDirective(names.names)))
292+
actualBuilder.add(BasicFormatStructure(MonthNameDirective(names)))
263293

264294
override fun appendDayOfMonth(padding: Padding) = actualBuilder.add(BasicFormatStructure(DayDirective(padding)))
265295
override fun appendDayOfWeek(names: DayOfWeekNames) =
266-
actualBuilder.add(BasicFormatStructure(DayOfWeekDirective(names.names)))
296+
actualBuilder.add(BasicFormatStructure(DayOfWeekDirective(names)))
267297

268298
@Suppress("NO_ELSE_IN_WHEN")
269299
override fun appendDate(dateFormat: DateTimeFormat<LocalDate>) = when (dateFormat) {
@@ -272,8 +302,6 @@ internal class LocalDateFormat(val actualFormat: StringFormat<DateFieldContainer
272302

273303
override fun createEmpty(): Builder = Builder(AppendableFormatStructure())
274304
}
275-
276-
override fun toString(): String = actualFormat.builderString()
277305
}
278306

279307
// these are constants so that the formats are not recreated every time they are used

0 commit comments

Comments
 (0)