Skip to content

Commit 22c22c0

Browse files
committed
WIP: first draft of locale-invariant parsing and formatting
1 parent 0f4c62d commit 22c22c0

33 files changed

+3021
-524
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime.format
7+
8+
import kotlinx.datetime.internal.format.*
9+
import kotlinx.datetime.internal.format.AlternativesFormatStructure
10+
11+
@DslMarker
12+
public annotation class DateTimeBuilder
13+
14+
/**
15+
* Common functions for all the date-time format builders.
16+
*/
17+
public interface FormatBuilder<out Self> {
18+
/**
19+
* Appends a set of alternative blocks to the format.
20+
*
21+
* When parsing, the blocks are tried in order until one of them succeeds.
22+
*
23+
* When formatting, there is a requirement that the later blocks contain all the fields that are present in the
24+
* earlier blocks. Moreover, the additional fields must have a *default value* defined for them.
25+
* Then, during formatting, the block that has the most information is chosen.
26+
*
27+
* Example:
28+
* ```
29+
* appendAlternatives({
30+
* appendLiteral("Z")
31+
* }, {
32+
* appendOffsetHours()
33+
* appendOptional {
34+
* appendLiteral(":")
35+
* appendOffsetMinutes()
36+
* appendOptional {
37+
* appendLiteral(":")
38+
* appendOffsetSeconds()
39+
* }
40+
* }
41+
* })
42+
* ```
43+
* Here, all values have the default value of zero, so the first block is chosen when formatting `UtcOffset.ZERO`.
44+
*/
45+
public fun appendAlternatives(vararg blocks: Self.() -> Unit)
46+
47+
/**
48+
* Appends a literal string to the format.
49+
* When formatting, the string is appended to the result as is,
50+
* and when parsing, the string is expected to be present in the input.
51+
*/
52+
public fun appendLiteral(string: String)
53+
54+
/**
55+
* Appends a format string to the format.
56+
*
57+
* TODO. For now, see docs for [kotlinx.datetime.internal.format.appendFormatString].
58+
*
59+
* @throws IllegalArgumentException if the format string is invalid.
60+
*/
61+
public fun appendFormatString(formatString: String)
62+
}
63+
64+
/**
65+
* Appends an optional section to the format.
66+
*
67+
* When parsing, the section is parsed if it is present in the input.
68+
*
69+
* When formatting, the section is formatted if the value of any field in the block is not equal to the default value.
70+
* Only [appendOptional] calls where all the fields have default values are permitted when formatting.
71+
*
72+
* Example:
73+
* ```
74+
* appendHours()
75+
* appendLiteral(":")
76+
* appendMinutes()
77+
* appendOptional {
78+
* appendLiteral(":")
79+
* appendSeconds()
80+
* }
81+
* ```
82+
*
83+
* Here, because seconds have the default value of zero, they are formatted only if they are not equal to zero.
84+
*
85+
* This is a shorthand for `appendAlternatives({}, block)`.
86+
*/
87+
public fun <Self> FormatBuilder<Self>.appendOptional(block: Self.() -> Unit): Unit =
88+
appendAlternatives({}, block)
89+
90+
/**
91+
* Appends a literal character to the format.
92+
*
93+
* This is a shorthand for `appendLiteral(char.toString())`.
94+
*/
95+
public fun <Self> FormatBuilder<Self>.appendLiteral(char: Char): Unit = appendLiteral(char.toString())
96+
97+
internal interface AbstractFormatBuilder<Target, out UserSelf, ActualSelf> :
98+
FormatBuilder<UserSelf> where ActualSelf : AbstractFormatBuilder<Target, UserSelf, ActualSelf> {
99+
100+
val actualBuilder: Builder<Target>
101+
fun createEmpty(): ActualSelf
102+
fun castToGeneric(actualSelf: ActualSelf): UserSelf
103+
104+
override fun appendAlternatives(vararg blocks: UserSelf.() -> Unit) {
105+
actualBuilder.add(AlternativesFormatStructure(blocks.map { block ->
106+
createEmpty().also { block(castToGeneric(it)) }.actualBuilder.build()
107+
}))
108+
}
109+
110+
override fun appendLiteral(string: String) = actualBuilder.add(ConstantFormatStructure(string))
111+
112+
override fun appendFormatString(formatString: String) {
113+
val end = actualBuilder.appendFormatString(formatString)
114+
require(end == formatString.length) {
115+
"Unexpected char '${formatString[end]}' in $formatString at position $end"
116+
}
117+
}
118+
119+
fun withSharedSign(outputPlus: Boolean, block: UserSelf.() -> Unit) {
120+
actualBuilder.add(
121+
SignedFormatStructure(
122+
createEmpty().also { block(castToGeneric(it)) }.actualBuilder.build(),
123+
outputPlus
124+
)
125+
)
126+
}
127+
128+
fun build(): Format<Target> = Format(actualBuilder.build())
129+
}
130+
131+
internal interface Copyable<Self> {
132+
fun copy(): Self
133+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime.format
7+
8+
import kotlinx.datetime.*
9+
import kotlinx.datetime.internal.*
10+
import kotlinx.datetime.internal.LruCache
11+
import kotlinx.datetime.internal.format.*
12+
import kotlinx.datetime.internal.format.parser.*
13+
14+
public interface DateFormatBuilderFields {
15+
public fun appendYear(minDigits: Int = 1, outputPlusOnExceededPadding: Boolean = false)
16+
public fun appendMonthNumber(minLength: Int = 1)
17+
public fun appendMonthName(names: List<String>)
18+
public fun appendDayOfMonth(minLength: Int = 1)
19+
}
20+
21+
@DateTimeBuilder
22+
public interface DateFormatBuilder : DateFormatBuilderFields, FormatBuilder<DateFormatBuilder>
23+
24+
public class LocalDateFormat private constructor(private val actualFormat: Format<DateFieldContainer>) {
25+
public companion object {
26+
public fun build(block: DateFormatBuilder.() -> Unit): LocalDateFormat {
27+
val builder = Builder(DateFieldContainerFormatBuilder())
28+
builder.block()
29+
return LocalDateFormat(builder.build())
30+
}
31+
32+
public fun fromFormatString(formatString: String): LocalDateFormat = build { appendFormatString(formatString) }
33+
34+
public val ISO: LocalDateFormat = build {
35+
appendYear(4, outputPlusOnExceededPadding = true)
36+
appendFormatString("'-'mm'-'dd")
37+
}
38+
39+
internal val Cache = LruCache<String, LocalDateFormat>(16) { fromFormatString(it) }
40+
}
41+
42+
public fun format(date: LocalDate): String =
43+
StringBuilder().also {
44+
actualFormat.formatter.format(date.toIncompleteLocalDate(), it)
45+
}.toString()
46+
47+
public fun parse(input: String): LocalDate {
48+
val parser = Parser(::IncompleteLocalDate, IncompleteLocalDate::copy, actualFormat.parser)
49+
try {
50+
return parser.match(input).toLocalDate()
51+
} catch (e: ParseException) {
52+
throw DateTimeFormatException("Failed to parse date from '$input'", e)
53+
} catch (e: IllegalArgumentException) {
54+
throw DateTimeFormatException("Invalid date '$input'", e)
55+
}
56+
}
57+
58+
private class Builder(override val actualBuilder: DateFieldContainerFormatBuilder) :
59+
AbstractFormatBuilder<DateFieldContainer, DateFormatBuilder, Builder>, DateFormatBuilder {
60+
override fun appendYear(minDigits: Int, outputPlusOnExceededPadding: Boolean) =
61+
actualBuilder.add(BasicFormatStructure(YearDirective(minDigits, outputPlusOnExceededPadding)))
62+
63+
override fun appendMonthNumber(minLength: Int) =
64+
actualBuilder.add(BasicFormatStructure(MonthDirective(minLength)))
65+
66+
override fun appendMonthName(names: List<String>) =
67+
actualBuilder.add(BasicFormatStructure(MonthNameDirective(names)))
68+
69+
override fun appendDayOfMonth(minLength: Int) = actualBuilder.add(BasicFormatStructure(DayDirective(minLength)))
70+
71+
override fun createEmpty(): Builder = Builder(DateFieldContainerFormatBuilder())
72+
override fun castToGeneric(actualSelf: Builder): DateFormatBuilder = this
73+
}
74+
75+
}
76+
77+
public fun LocalDate.format(formatString: String): String =
78+
LocalDateFormat.Cache.get(formatString).format(this)
79+
80+
public fun LocalDate.format(format: LocalDateFormat): String = format.format(this)
81+
82+
public fun LocalDate.Companion.parse(input: String, formatString: String): LocalDate =
83+
LocalDateFormat.Cache.get(formatString).parse(input)
84+
85+
public fun LocalDate.Companion.parse(input: String, format: LocalDateFormat): LocalDate = format.parse(input)
86+
87+
internal fun LocalDate.toIncompleteLocalDate(): IncompleteLocalDate =
88+
IncompleteLocalDate(year, monthNumber, dayOfMonth, dayOfWeek.isoDayNumber)
89+
90+
internal fun <T> getParsedField(field: T?, name: String): T {
91+
if (field == null) {
92+
throw DateTimeFormatException("Can not create a $name from the given input: the field $name is missing")
93+
}
94+
return field
95+
}
96+
97+
internal interface DateFieldContainer {
98+
var year: Int?
99+
var monthNumber: Int?
100+
var dayOfMonth: Int?
101+
var isoDayOfWeek: Int?
102+
}
103+
104+
internal object DateFields {
105+
val year = SignedFieldSpec(DateFieldContainer::year, maxAbsoluteValue = null)
106+
val month = UnsignedFieldSpec(DateFieldContainer::monthNumber, minValue = 1, maxValue = 12)
107+
val dayOfMonth = UnsignedFieldSpec(DateFieldContainer::dayOfMonth, minValue = 1, maxValue = 31)
108+
val isoDayOfWeek = UnsignedFieldSpec(DateFieldContainer::isoDayOfWeek, minValue = 1, maxValue = 7)
109+
}
110+
111+
/**
112+
* A [kotlinx.datetime.LocalDate], but potentially incomplete and inconsistent.
113+
*/
114+
internal class IncompleteLocalDate(
115+
override var year: Int? = null,
116+
override var monthNumber: Int? = null,
117+
override var dayOfMonth: Int? = null,
118+
override var isoDayOfWeek: Int? = null
119+
) : DateFieldContainer, Copyable<IncompleteLocalDate> {
120+
fun toLocalDate(): LocalDate {
121+
val date = LocalDate(
122+
getParsedField(year, "year"),
123+
getParsedField(monthNumber, "monthNumber"),
124+
getParsedField(dayOfMonth, "dayOfMonth")
125+
)
126+
isoDayOfWeek?.let {
127+
if (it != date.dayOfWeek.isoDayNumber) {
128+
throw DateTimeFormatException(
129+
"Can not create a LocalDate from the given input: " +
130+
"the day of week is ${DayOfWeek(it)} but the date is $date, which is a ${date.dayOfWeek}"
131+
)
132+
}
133+
}
134+
return date
135+
}
136+
137+
override fun copy(): IncompleteLocalDate = IncompleteLocalDate(year, monthNumber, dayOfMonth, isoDayOfWeek)
138+
139+
override fun toString(): String =
140+
"${year ?: "??"}-${monthNumber ?: "??"}-${dayOfMonth ?: "??"} (day of week is ${isoDayOfWeek ?: "??"})"
141+
}
142+
143+
internal class YearDirective(digits: Int, outputPlusOnExceededPadding: Boolean) :
144+
SignedIntFieldFormatDirective<DateFieldContainer>(
145+
DateFields.year,
146+
minDigits = digits,
147+
maxDigits = null,
148+
outputPlusOnExceededPadding = outputPlusOnExceededPadding,
149+
)
150+
151+
internal class MonthDirective(minDigits: Int) :
152+
UnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.month, minDigits)
153+
154+
internal class MonthNameDirective(names: List<String>) :
155+
NamedUnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.month, names)
156+
157+
internal class DayDirective(minDigits: Int) :
158+
UnsignedIntFieldFormatDirective<DateFieldContainer>(DateFields.dayOfMonth, minDigits)
159+
160+
internal class DateFieldContainerFormatBuilder : AbstractBuilder<DateFieldContainer>() {
161+
162+
companion object {
163+
const val name = "ld"
164+
}
165+
166+
override fun formatFromSubBuilder(
167+
name: String,
168+
block: Builder<*>.() -> Unit
169+
): FormatStructure<DateFieldContainer>? =
170+
if (name == DateFieldContainerFormatBuilder.name)
171+
DateFieldContainerFormatBuilder().apply(block).build()
172+
else null
173+
174+
override fun formatFromDirective(letter: Char, length: Int): FormatStructure<DateFieldContainer>? {
175+
return when (letter) {
176+
'y' -> BasicFormatStructure(YearDirective(length, outputPlusOnExceededPadding = false))
177+
'm' -> BasicFormatStructure(MonthDirective(length))
178+
'd' -> BasicFormatStructure(DayDirective(length))
179+
else -> null
180+
}
181+
}
182+
183+
override fun createSibling(): Builder<DateFieldContainer> = DateFieldContainerFormatBuilder()
184+
}

0 commit comments

Comments
 (0)