Skip to content

Commit a03691b

Browse files
committed
Allow printing datetime formats as Kotlin code
1 parent bd02834 commit a03691b

File tree

8 files changed

+266
-0
lines changed

8 files changed

+266
-0
lines changed

core/common/src/format/DateTimeComponents.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,10 @@ internal val timeZoneField = GenericFieldSpec(PropertyAccessor(DateTimeComponent
513513
internal class TimeZoneIdDirective(private val knownZones: Set<String>) :
514514
StringFieldFormatDirective<DateTimeComponentsContents>(timeZoneField, knownZones) {
515515

516+
override val builderRepresentation: String
517+
get() =
518+
"${DateTimeFormatBuilder.WithDateTimeComponents::timeZoneId.name}()"
519+
516520
override fun equals(other: Any?): Boolean = other is TimeZoneIdDirective && other.knownZones == knownZones
517521
override fun hashCode(): Int = knownZones.hashCode()
518522
}

core/common/src/format/DateTimeFormat.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ public sealed interface DateTimeFormat<T> {
3636
* @return the parsed value, or `null` if the input string is not in the expected format or the value is invalid.
3737
*/
3838
public fun parseOrNull(input: CharSequence): T?
39+
40+
public companion object {
41+
/**
42+
* Produces Kotlin code that, when pasted into a Kotlin source file, creates a [DateTimeFormat] instance that
43+
* behaves identically to [format].
44+
*
45+
* The typical use case for this is to create a [DateTimeFormat] instance using a non-idiomatic approach and
46+
* then convert it to a builder DSL.
47+
*/
48+
public fun formatAsKotlinBuilderDsl(format: DateTimeFormat<*>): String = when (format) {
49+
is AbstractDateTimeFormat<*, *> -> format.actualFormat.builderString(allFormatConstants)
50+
}
51+
}
3952
}
4053

4154
/**
@@ -111,3 +124,14 @@ internal sealed class AbstractDateTimeFormat<T, U : Copyable<U>> : DateTimeForma
111124
Parser(actualFormat.parser()).matchOrNull(input, emptyIntermediate)?.let { valueFromIntermediateOrNull(it) }
112125

113126
}
127+
128+
private val allFormatConstants: List<Pair<String, CachedFormatStructure<*>>> by lazy {
129+
fun unwrap(format: DateTimeFormat<*>): CachedFormatStructure<*> = (format as AbstractDateTimeFormat<*, *>).actualFormat
130+
// the formats are ordered vaguely by decreasing length, as the topmost among suitable ones is chosen.
131+
listOf(
132+
"${DateTimeFormatBuilder.WithDateTimeComponents::dateTimeComponents.name}(DateTimeComponents.Formats.RFC_1123)" to
133+
unwrap(DateTimeComponents.Formats.RFC_1123),
134+
"${DateTimeFormatBuilder.WithDateTimeComponents::dateTimeComponents.name}(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET)" to
135+
unwrap(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET),
136+
)
137+
}

core/common/src/format/DateTimeFormatBuilder.kt

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,91 @@ internal interface AbstractDateTimeFormatBuilder<Target, ActualSelf> :
382382

383383
fun build(): CachedFormatStructure<Target> = CachedFormatStructure(actualBuilder.build().formats)
384384
}
385+
386+
internal fun <T> FormatStructure<T>.builderString(constants: List<Pair<String, CachedFormatStructure<*>>>): String =
387+
when (this) {
388+
is BasicFormatStructure -> directive.builderRepresentation
389+
is ConstantFormatStructure -> if (string.length == 1) {
390+
"${DateTimeFormatBuilder::char.name}(${string[0].toKotlinCode()})"
391+
} else {
392+
"${DateTimeFormatBuilder::chars.name}(${string.toKotlinCode()})"
393+
}
394+
395+
is SignedFormatStructure -> {
396+
if (format is BasicFormatStructure && format.directive is UtcOffsetWholeHoursDirective) {
397+
format.directive.builderRepresentation
398+
} else {
399+
buildString {
400+
if (withPlusSign) appendLine("withSharedSign(outputPlus = true) {")
401+
else appendLine("withSharedSign {")
402+
appendLine(format.builderString(constants).prependIndent(CODE_INDENT))
403+
append("}")
404+
}
405+
}
406+
}
407+
408+
is OptionalFormatStructure -> buildString {
409+
if (onZero == "") {
410+
appendLine("${DateTimeFormatBuilder::optional.name} {")
411+
} else {
412+
appendLine("${DateTimeFormatBuilder::optional.name}(${onZero.toKotlinCode()}) {")
413+
}
414+
val subformat = format.builderString(constants)
415+
if (subformat.isNotEmpty()) {
416+
appendLine(subformat.prependIndent(CODE_INDENT))
417+
}
418+
append("}")
419+
}
420+
421+
is AlternativesParsingFormatStructure -> buildString {
422+
append("${DateTimeFormatBuilder::alternativeParsing.name}(")
423+
for (alternative in formats) {
424+
appendLine("{")
425+
val subformat = alternative.builderString(constants)
426+
if (subformat.isNotEmpty()) {
427+
appendLine(subformat.prependIndent(CODE_INDENT))
428+
}
429+
append("}, ")
430+
}
431+
if (this[length - 2] == ',') {
432+
repeat(2) {
433+
deleteAt(length - 1)
434+
}
435+
}
436+
appendLine(") {")
437+
appendLine(mainFormat.builderString(constants).prependIndent(CODE_INDENT))
438+
append("}")
439+
}
440+
441+
is ConcatenatedFormatStructure -> buildString {
442+
if (formats.isNotEmpty()) {
443+
var index = 0
444+
loop@ while (index < formats.size) {
445+
searchConstant@ for (constant in constants) {
446+
val constantDirectives = constant.second.formats
447+
if (formats.size - index >= constantDirectives.size) {
448+
for (i in constantDirectives.indices) {
449+
if (formats[index + i] != constantDirectives[i]) {
450+
continue@searchConstant
451+
}
452+
}
453+
append(constant.first)
454+
index += constantDirectives.size
455+
if (index < formats.size) {
456+
appendLine()
457+
}
458+
continue@loop
459+
}
460+
}
461+
if (index == formats.size - 1) {
462+
append(formats.last().builderString(constants))
463+
} else {
464+
appendLine(formats[index].builderString(constants))
465+
}
466+
++index
467+
}
468+
}
469+
}
470+
}
471+
472+
private const val CODE_INDENT = " "

core/common/src/format/LocalDateFormat.kt

Lines changed: 54 additions & 0 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> requireParsedField(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")
@@ -178,6 +190,16 @@ private class YearDirective(private val padding: Padding, private val isYearOfEr
178190
spacePadding = padding.spaces(4),
179191
outputPlusOnExceededWidth = 4,
180192
) {
193+
override val builderRepresentation: String
194+
get() = when (padding) {
195+
Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::year.name}()"
196+
else -> "${DateTimeFormatBuilder.WithDate::year.name}(${padding.toKotlinCode()})"
197+
}.let {
198+
if (isYearOfEra) {
199+
it + YEAR_OF_ERA_COMMENT
200+
} else it
201+
}
202+
181203
override fun equals(other: Any?): Boolean =
182204
other is YearDirective && padding == other.padding && isYearOfEra == other.isYearOfEra
183205

@@ -190,12 +212,23 @@ private class ReducedYearDirective(val base: Int, private val isYearOfEra: Boole
190212
digits = 2,
191213
base = base,
192214
) {
215+
override val builderRepresentation: String
216+
get() =
217+
"${DateTimeFormatBuilder.WithDate::yearTwoDigits.name}($base)".let {
218+
if (isYearOfEra) {
219+
it + YEAR_OF_ERA_COMMENT
220+
} else it
221+
}
222+
193223
override fun equals(other: Any?): Boolean =
194224
other is ReducedYearDirective && base == other.base && isYearOfEra == other.isYearOfEra
195225

196226
override fun hashCode(): Int = base.hashCode() * 31 + isYearOfEra.hashCode()
197227
}
198228

229+
private const val YEAR_OF_ERA_COMMENT =
230+
" /** TODO: the original format had an `y` directive, so the behavior is different on years earlier than 1 AD. See the [kotlinx.datetime.format.byUnicodePattern] documentation for details. */"
231+
199232
/**
200233
* A special directive for year-of-era that behaves equivalently to [DateTimeFormatBuilder.WithDate.year].
201234
* This is the result of calling [byUnicodePattern] on a pattern that uses the ubiquitous "y" symbol.
@@ -232,12 +265,22 @@ private class MonthDirective(private val padding: Padding) :
232265
minDigits = padding.minDigits(2),
233266
spacePadding = padding.spaces(2),
234267
) {
268+
override val builderRepresentation: String
269+
get() = when (padding) {
270+
Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::monthNumber.name}()"
271+
else -> "${DateTimeFormatBuilder.WithDate::monthNumber.name}(${padding.toKotlinCode()})"
272+
}
273+
235274
override fun equals(other: Any?): Boolean = other is MonthDirective && padding == other.padding
236275
override fun hashCode(): Int = padding.hashCode()
237276
}
238277

239278
private class MonthNameDirective(private val names: MonthNames) :
240279
NamedUnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.month, names.names, "monthName") {
280+
override val builderRepresentation: String
281+
get() =
282+
"${DateTimeFormatBuilder.WithDate::monthName.name}(${names.toKotlinCode()})"
283+
241284
override fun equals(other: Any?): Boolean = other is MonthNameDirective && names.names == other.names.names
242285
override fun hashCode(): Int = names.names.hashCode()
243286
}
@@ -248,12 +291,23 @@ private class DayDirective(private val padding: Padding) :
248291
minDigits = padding.minDigits(2),
249292
spacePadding = padding.spaces(2),
250293
) {
294+
override val builderRepresentation: String
295+
get() = when (padding) {
296+
Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::dayOfMonth.name}()"
297+
else -> "${DateTimeFormatBuilder.WithDate::dayOfMonth.name}(${padding.toKotlinCode()})"
298+
}
299+
251300
override fun equals(other: Any?): Boolean = other is DayDirective && padding == other.padding
252301
override fun hashCode(): Int = padding.hashCode()
253302
}
254303

255304
private class DayOfWeekDirective(private val names: DayOfWeekNames) :
256305
NamedUnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.isoDayOfWeek, names.names, "dayOfWeekName") {
306+
307+
override val builderRepresentation: String
308+
get() =
309+
"${DateTimeFormatBuilder.WithDate::dayOfWeek.name}(${names.toKotlinCode()})"
310+
257311
override fun equals(other: Any?): Boolean = other is DayOfWeekDirective && names.names == other.names.names
258312
override fun hashCode(): Int = names.names.hashCode()
259313
}

core/common/src/format/LocalTimeFormat.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ private class HourDirective(private val padding: Padding) :
134134
minDigits = padding.minDigits(2),
135135
spacePadding = padding.spaces(2)
136136
) {
137+
override val builderRepresentation: String
138+
get() = when (padding) {
139+
Padding.ZERO -> "${DateTimeFormatBuilder.WithTime::hour.name}()"
140+
else -> "${DateTimeFormatBuilder.WithTime::hour.name}(${padding.toKotlinCode()})"
141+
}
142+
137143
override fun equals(other: Any?): Boolean = other is HourDirective && padding == other.padding
138144
override fun hashCode(): Int = padding.hashCode()
139145
}
@@ -143,6 +149,12 @@ private class AmPmHourDirective(private val padding: Padding) :
143149
TimeFields.hourOfAmPm, minDigits = padding.minDigits(2),
144150
spacePadding = padding.spaces(2)
145151
) {
152+
override val builderRepresentation: String
153+
get() = when (padding) {
154+
Padding.ZERO -> "${DateTimeFormatBuilder.WithTime::amPmHour.name}()"
155+
else -> "${DateTimeFormatBuilder.WithTime::amPmHour.name}(${padding.toKotlinCode()})"
156+
}
157+
146158
override fun equals(other: Any?): Boolean = other is AmPmHourDirective && padding == other.padding
147159
override fun hashCode(): Int = padding.hashCode()
148160
}
@@ -155,6 +167,11 @@ private class AmPmMarkerDirective(private val amString: String, private val pmSt
155167
),
156168
"AM/PM marker"
157169
) {
170+
171+
override val builderRepresentation: String
172+
get() =
173+
"${DateTimeFormatBuilder.WithTime::amPmMarker.name}($amString, $pmString)"
174+
158175
override fun equals(other: Any?): Boolean =
159176
other is AmPmMarkerDirective && amString == other.amString && pmString == other.pmString
160177

@@ -167,6 +184,13 @@ private class MinuteDirective(private val padding: Padding) :
167184
minDigits = padding.minDigits(2),
168185
spacePadding = padding.spaces(2)
169186
) {
187+
188+
override val builderRepresentation: String
189+
get() = when (padding) {
190+
Padding.ZERO -> "${DateTimeFormatBuilder.WithTime::minute.name}()"
191+
else -> "${DateTimeFormatBuilder.WithTime::minute.name}(${padding.toKotlinCode()})"
192+
}
193+
170194
override fun equals(other: Any?): Boolean = other is MinuteDirective && padding == other.padding
171195
override fun hashCode(): Int = padding.hashCode()
172196
}
@@ -177,6 +201,13 @@ private class SecondDirective(private val padding: Padding) :
177201
minDigits = padding.minDigits(2),
178202
spacePadding = padding.spaces(2)
179203
) {
204+
205+
override val builderRepresentation: String
206+
get() = when (padding) {
207+
Padding.ZERO -> "${DateTimeFormatBuilder.WithTime::second.name}()"
208+
else -> "${DateTimeFormatBuilder.WithTime::second.name}(${padding.toKotlinCode()})"
209+
}
210+
180211
override fun equals(other: Any?): Boolean = other is SecondDirective && padding == other.padding
181212
override fun hashCode(): Int = padding.hashCode()
182213
}
@@ -192,6 +223,20 @@ internal class FractionalSecondDirective(
192223
maxDigits,
193224
zerosToAdd
194225
) {
226+
227+
override val builderRepresentation: String
228+
get() {
229+
val ref = "secondFraction" // can't directly reference `secondFraction` due to resolution ambiguity
230+
// we ignore `grouping`, as it's not representable in the end users' code
231+
return when {
232+
minDigits == 1 && maxDigits == 9 -> "$ref()"
233+
minDigits == 1 -> "$ref(maxLength = $maxDigits)"
234+
maxDigits == 1 -> "$ref(minLength = $minDigits)"
235+
maxDigits == minDigits -> "$ref($minDigits)"
236+
else -> "$ref($minDigits, $maxDigits)"
237+
}
238+
}
239+
195240
override fun equals(other: Any?): Boolean =
196241
other is FractionalSecondDirective && minDigits == other.minDigits && maxDigits == other.maxDigits
197242

core/common/src/format/UtcOffsetFormat.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ internal class UtcOffsetWholeHoursDirective(private val padding: Padding) :
196196
minDigits = padding.minDigits(2),
197197
spacePadding = padding.spaces(2)
198198
) {
199+
200+
override val builderRepresentation: String
201+
get() =
202+
"${DateTimeFormatBuilder.WithUtcOffset::offsetHours.name}(${padding.toKotlinCode()})"
203+
199204
override fun equals(other: Any?): Boolean = other is UtcOffsetWholeHoursDirective && padding == other.padding
200205
override fun hashCode(): Int = padding.hashCode()
201206
}
@@ -205,6 +210,13 @@ private class UtcOffsetMinuteOfHourDirective(private val padding: Padding) :
205210
OffsetFields.minutesOfHour,
206211
minDigits = padding.minDigits(2), spacePadding = padding.spaces(2)
207212
) {
213+
214+
override val builderRepresentation: String
215+
get() = when (padding) {
216+
Padding.NONE -> "${DateTimeFormatBuilder.WithUtcOffset::offsetMinutesOfHour.name}()"
217+
else -> "${DateTimeFormatBuilder.WithUtcOffset::offsetMinutesOfHour.name}(${padding.toKotlinCode()})"
218+
}
219+
208220
override fun equals(other: Any?): Boolean = other is UtcOffsetMinuteOfHourDirective && padding == other.padding
209221
override fun hashCode(): Int = padding.hashCode()
210222
}
@@ -214,6 +226,13 @@ private class UtcOffsetSecondOfMinuteDirective(private val padding: Padding) :
214226
OffsetFields.secondsOfMinute,
215227
minDigits = padding.minDigits(2), spacePadding = padding.spaces(2)
216228
) {
229+
230+
override val builderRepresentation: String
231+
get() = when (padding) {
232+
Padding.NONE -> "${DateTimeFormatBuilder.WithUtcOffset::offsetSecondsOfMinute.name}()"
233+
else -> "${DateTimeFormatBuilder.WithUtcOffset::offsetSecondsOfMinute.name}(${padding.toKotlinCode()})"
234+
}
235+
217236
override fun equals(other: Any?): Boolean = other is UtcOffsetSecondOfMinuteDirective && padding == other.padding
218237
override fun hashCode(): Int = padding.hashCode()
219238
}

core/common/src/internal/format/FieldFormatDirective.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ internal interface FieldFormatDirective<in Target> {
2727
* The parser structure that parses the field.
2828
*/
2929
fun parser(): ParserStructure<Target>
30+
31+
/**
32+
* The string with the code that, when evaluated in the builder context, appends the directive.
33+
*/
34+
val builderRepresentation: String
3035
}
3136

3237
/**

0 commit comments

Comments
 (0)