Skip to content

Commit fba4050

Browse files
committed
Define the API for building a time-based format
1 parent 677e329 commit fba4050

File tree

2 files changed

+359
-0
lines changed

2 files changed

+359
-0
lines changed

core/common/src/format/DateTimeFormatBuilder.kt

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,110 @@ public sealed interface DateTimeFormatBuilder {
9696
*/
9797
public fun date(format: DateTimeFormat<LocalDate>)
9898
}
99+
100+
/**
101+
* Functions specific to the date-time format builders containing the local-time fields.
102+
*/
103+
public sealed interface WithTime : DateTimeFormatBuilder {
104+
/**
105+
* The hour of the day, from 0 to 23.
106+
*
107+
* By default, it's zero-padded to two digits, but this can be changed with [padding].
108+
*/
109+
public fun hour(padding: Padding = Padding.ZERO)
110+
111+
/**
112+
* The hour of the day in the 12-hour clock:
113+
*
114+
* * Midnight is 12,
115+
* * Hours 1-11 are 1-11,
116+
* * Noon is 12,
117+
* * Hours 13-23 are 1-11.
118+
*
119+
* To disambiguate between the first and the second halves of the day, [amPmMarker] should be used.
120+
*
121+
* By default, it's zero-padded to two digits, but this can be changed with [padding].
122+
*
123+
* @see [amPmMarker]
124+
*/
125+
public fun amPmHour(padding: Padding = Padding.ZERO)
126+
127+
/**
128+
* The AM/PM marker, using the specified strings.
129+
*
130+
* [am] is used for the AM marker (0-11 hours), [pm] is used for the PM marker (12-23 hours).
131+
*
132+
* @see [amPmHour]
133+
*/
134+
public fun amPmMarker(am: String, pm: String)
135+
136+
/**
137+
* The minute of hour.
138+
*
139+
* By default, it's zero-padded to two digits, but this can be changed with [padding].
140+
*/
141+
public fun minute(padding: Padding = Padding.ZERO)
142+
143+
/**
144+
* The second of minute.
145+
*
146+
* By default, it's zero-padded to two digits, but this can be changed with [padding].
147+
*
148+
* This field has the default value of 0. If you want to omit it, use [optional].
149+
*/
150+
public fun second(padding: Padding = Padding.ZERO)
151+
152+
/**
153+
* The fractional part of the second without the leading dot.
154+
*
155+
* When formatting, the decimal fraction will be rounded to fit in the specified [maxLength] and will add
156+
* trailing zeroes to the specified [minLength].
157+
* Rounding is performed using the round-toward-zero rounding mode.
158+
*
159+
* When parsing, the parser will require that the fraction is at least [minLength] and at most [maxLength]
160+
* digits long.
161+
*
162+
* This field has the default value of 0. If you want to omit it, use [optional].
163+
*
164+
* See also the [secondFraction] overload that accepts just one parameter, the exact length of the fractional
165+
* part.
166+
*
167+
* @throws IllegalArgumentException if [minLength] is greater than [maxLength] or if either is not in the range 1..9.
168+
*/
169+
public fun secondFraction(minLength: Int = 1, maxLength: Int = 9)
170+
171+
/**
172+
* The fractional part of the second without the leading dot.
173+
*
174+
* When formatting, the decimal fraction will add trailing zeroes or be rounded as necessary to always output
175+
* exactly the number of digits specified in [fixedLength].
176+
* Rounding is performed using the round-toward-zero rounding mode.
177+
*
178+
* When parsing, exactly [fixedLength] digits will be consumed.
179+
*
180+
* This field has the default value of 0. If you want to omit it, use [optional].
181+
*
182+
* See also the [secondFraction] overload that accepts two parameters, the minimum and maximum length of the
183+
* fractional part.
184+
*
185+
* @throws IllegalArgumentException if [fixedLength] is not in the range 1..9.
186+
*
187+
* @see secondFraction that accepts two parameters.
188+
*/
189+
public fun secondFraction(fixedLength: Int) {
190+
secondFraction(fixedLength, fixedLength)
191+
}
192+
193+
/**
194+
* An existing [DateTimeFormat] for the time part.
195+
*
196+
* Example:
197+
* ```
198+
* time(LocalTime.Formats.ISO)
199+
* ```
200+
*/
201+
public fun time(format: DateTimeFormat<LocalTime>)
202+
}
99203
}
100204

101205
/**
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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.LocalTime
9+
import kotlinx.datetime.DateTimeFormatException
10+
import kotlinx.datetime.internal.DecimalFraction
11+
import kotlinx.datetime.internal.format.*
12+
import kotlinx.datetime.internal.format.parser.Copyable
13+
14+
/**
15+
* The AM/PM marker that indicates whether the hour in range `1..12` is before or after noon.
16+
*/
17+
public enum class AmPmMarker {
18+
/** The time is before noon. */
19+
AM,
20+
21+
/** The time is after noon. */
22+
PM,
23+
}
24+
25+
internal interface TimeFieldContainer {
26+
var minute: Int?
27+
var second: Int?
28+
var nanosecond: Int?
29+
var hour: Int?
30+
var hourOfAmPm: Int?
31+
var amPm: AmPmMarker?
32+
33+
var fractionOfSecond: DecimalFraction?
34+
get() = nanosecond?.let { DecimalFraction(it, 9) }
35+
set(value) {
36+
nanosecond = value?.fractionalPartWithNDigits(9)
37+
}
38+
}
39+
40+
private object TimeFields {
41+
val hour = UnsignedFieldSpec(PropertyAccessor(TimeFieldContainer::hour), minValue = 0, maxValue = 23)
42+
val minute = UnsignedFieldSpec(PropertyAccessor(TimeFieldContainer::minute), minValue = 0, maxValue = 59)
43+
val second =
44+
UnsignedFieldSpec(PropertyAccessor(TimeFieldContainer::second), minValue = 0, maxValue = 59, defaultValue = 0)
45+
val fractionOfSecond =
46+
GenericFieldSpec(PropertyAccessor(TimeFieldContainer::fractionOfSecond), defaultValue = DecimalFraction(0, 9))
47+
val amPm = GenericFieldSpec(PropertyAccessor(TimeFieldContainer::amPm))
48+
val hourOfAmPm = UnsignedFieldSpec(PropertyAccessor(TimeFieldContainer::hourOfAmPm), minValue = 1, maxValue = 12)
49+
}
50+
51+
internal class IncompleteLocalTime(
52+
override var hour: Int? = null,
53+
override var hourOfAmPm: Int? = null,
54+
override var amPm: AmPmMarker? = null,
55+
override var minute: Int? = null,
56+
override var second: Int? = null,
57+
override var nanosecond: Int? = null
58+
) : TimeFieldContainer, Copyable<IncompleteLocalTime> {
59+
fun toLocalTime(): LocalTime {
60+
val hour: Int = hour?.let { hour ->
61+
hourOfAmPm?.let {
62+
require((hour + 11) % 12 + 1 == it) { "Inconsistent hour and hour-of-am-pm: hour is $hour, but hour-of-am-pm is $it" }
63+
}
64+
amPm?.let { amPm ->
65+
require((amPm == AmPmMarker.PM) == (hour >= 12)) {
66+
"Inconsistent hour and the AM/PM marker: hour is $hour, but the AM/PM marker is $amPm"
67+
}
68+
}
69+
hour
70+
} ?: hourOfAmPm?.let { hourOfAmPm ->
71+
amPm?.let { amPm ->
72+
hourOfAmPm.let { if (it == 12) 0 else it } + if (amPm == AmPmMarker.PM) 12 else 0
73+
}
74+
} ?: throw DateTimeFormatException("Incomplete time: missing hour")
75+
return LocalTime(
76+
hour,
77+
requireParsedField(minute, "minute"),
78+
second ?: 0,
79+
nanosecond ?: 0,
80+
)
81+
}
82+
83+
fun populateFrom(localTime: LocalTime) {
84+
hour = localTime.hour
85+
hourOfAmPm = (localTime.hour + 11) % 12 + 1
86+
amPm = if (localTime.hour >= 12) AmPmMarker.PM else AmPmMarker.AM
87+
minute = localTime.minute
88+
second = localTime.second
89+
nanosecond = localTime.nanosecond
90+
}
91+
92+
override fun copy(): IncompleteLocalTime = IncompleteLocalTime(hour, hourOfAmPm, amPm, minute, second, nanosecond)
93+
94+
override fun equals(other: Any?): Boolean =
95+
other is IncompleteLocalTime && hour == other.hour && hourOfAmPm == other.hourOfAmPm && amPm == other.amPm &&
96+
minute == other.minute && second == other.second && nanosecond == other.nanosecond
97+
98+
override fun hashCode(): Int =
99+
(hour ?: 0) * 31 + (hourOfAmPm ?: 0) * 31 + (amPm?.hashCode() ?: 0) * 31 + (minute ?: 0) * 31 +
100+
(second ?: 0) * 31 + (nanosecond ?: 0)
101+
102+
override fun toString(): String =
103+
"${hour ?: "??"}:${minute ?: "??"}:${second ?: "??"}.${
104+
nanosecond?.let { nanos ->
105+
nanos.toString().let { it.padStart(9 - it.length, '0') }
106+
} ?: "???"
107+
}"
108+
}
109+
110+
internal interface AbstractWithTimeBuilder : DateTimeFormatBuilder.WithTime {
111+
fun addFormatStructureForTime(structure: FormatStructure<TimeFieldContainer>)
112+
113+
override fun hour(padding: Padding) = addFormatStructureForTime(BasicFormatStructure(HourDirective(padding)))
114+
override fun amPmHour(padding: Padding) =
115+
addFormatStructureForTime(BasicFormatStructure(AmPmHourDirective(padding)))
116+
117+
override fun amPmMarker(am: String, pm: String) =
118+
addFormatStructureForTime(BasicFormatStructure(AmPmMarkerDirective(am, pm)))
119+
120+
override fun minute(padding: Padding) = addFormatStructureForTime(BasicFormatStructure(MinuteDirective(padding)))
121+
override fun second(padding: Padding) = addFormatStructureForTime(BasicFormatStructure(SecondDirective(padding)))
122+
override fun secondFraction(minLength: Int, maxLength: Int) =
123+
addFormatStructureForTime(BasicFormatStructure(FractionalSecondDirective(minLength, maxLength)))
124+
125+
@Suppress("NO_ELSE_IN_WHEN")
126+
override fun time(format: DateTimeFormat<LocalTime>) = when (format) {
127+
is LocalTimeFormat -> addFormatStructureForTime(format.actualFormat)
128+
}
129+
}
130+
131+
private class HourDirective(private val padding: Padding) :
132+
UnsignedIntFieldFormatDirective<TimeFieldContainer>(
133+
TimeFields.hour,
134+
minDigits = padding.minDigits(2),
135+
spacePadding = padding.spaces(2)
136+
) {
137+
override fun equals(other: Any?): Boolean = other is HourDirective && padding == other.padding
138+
override fun hashCode(): Int = padding.hashCode()
139+
}
140+
141+
private class AmPmHourDirective(private val padding: Padding) :
142+
UnsignedIntFieldFormatDirective<TimeFieldContainer>(
143+
TimeFields.hourOfAmPm, minDigits = padding.minDigits(2),
144+
spacePadding = padding.spaces(2)
145+
) {
146+
override fun equals(other: Any?): Boolean = other is AmPmHourDirective && padding == other.padding
147+
override fun hashCode(): Int = padding.hashCode()
148+
}
149+
150+
private class AmPmMarkerDirective(private val amString: String, private val pmString: String) :
151+
NamedEnumIntFieldFormatDirective<TimeFieldContainer, AmPmMarker>(
152+
TimeFields.amPm, mapOf(
153+
AmPmMarker.AM to amString,
154+
AmPmMarker.PM to pmString,
155+
),
156+
"AM/PM marker"
157+
) {
158+
override fun equals(other: Any?): Boolean =
159+
other is AmPmMarkerDirective && amString == other.amString && pmString == other.pmString
160+
161+
override fun hashCode(): Int = 31 * amString.hashCode() + pmString.hashCode()
162+
}
163+
164+
private class MinuteDirective(private val padding: Padding) :
165+
UnsignedIntFieldFormatDirective<TimeFieldContainer>(
166+
TimeFields.minute,
167+
minDigits = padding.minDigits(2),
168+
spacePadding = padding.spaces(2)
169+
) {
170+
override fun equals(other: Any?): Boolean = other is MinuteDirective && padding == other.padding
171+
override fun hashCode(): Int = padding.hashCode()
172+
}
173+
174+
private class SecondDirective(private val padding: Padding) :
175+
UnsignedIntFieldFormatDirective<TimeFieldContainer>(
176+
TimeFields.second,
177+
minDigits = padding.minDigits(2),
178+
spacePadding = padding.spaces(2)
179+
) {
180+
override fun equals(other: Any?): Boolean = other is SecondDirective && padding == other.padding
181+
override fun hashCode(): Int = padding.hashCode()
182+
}
183+
184+
internal class FractionalSecondDirective(
185+
private val minDigits: Int,
186+
private val maxDigits: Int,
187+
zerosToAdd: List<Int> = NO_EXTRA_ZEROS,
188+
) :
189+
DecimalFractionFieldFormatDirective<TimeFieldContainer>(
190+
TimeFields.fractionOfSecond,
191+
minDigits,
192+
maxDigits,
193+
zerosToAdd
194+
) {
195+
override fun equals(other: Any?): Boolean =
196+
other is FractionalSecondDirective && minDigits == other.minDigits && maxDigits == other.maxDigits
197+
198+
override fun hashCode(): Int = 31 * minDigits + maxDigits
199+
200+
companion object {
201+
val NO_EXTRA_ZEROS = listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)
202+
val GROUP_BY_THREE = listOf(2, 1, 0, 2, 1, 0, 2, 1, 0)
203+
}
204+
}
205+
206+
internal class LocalTimeFormat(override val actualFormat: CachedFormatStructure<TimeFieldContainer>) :
207+
AbstractDateTimeFormat<LocalTime, IncompleteLocalTime>() {
208+
override fun intermediateFromValue(value: LocalTime): IncompleteLocalTime =
209+
IncompleteLocalTime().apply { populateFrom(value) }
210+
211+
override fun valueFromIntermediate(intermediate: IncompleteLocalTime): LocalTime = intermediate.toLocalTime()
212+
213+
override val emptyIntermediate: IncompleteLocalTime get() = emptyIncompleteLocalTime
214+
215+
companion object {
216+
fun build(block: DateTimeFormatBuilder.WithTime.() -> Unit): LocalTimeFormat {
217+
val builder = Builder(AppendableFormatStructure())
218+
builder.block()
219+
return LocalTimeFormat(builder.build())
220+
}
221+
222+
}
223+
224+
private class Builder(override val actualBuilder: AppendableFormatStructure<TimeFieldContainer>) :
225+
AbstractDateTimeFormatBuilder<TimeFieldContainer, Builder>, AbstractWithTimeBuilder {
226+
227+
override fun addFormatStructureForTime(structure: FormatStructure<TimeFieldContainer>) {
228+
actualBuilder.add(structure)
229+
}
230+
231+
override fun createEmpty(): Builder = Builder(AppendableFormatStructure())
232+
}
233+
234+
}
235+
236+
// these are constants so that the formats are not recreated every time they are used
237+
internal val ISO_TIME by lazy {
238+
LocalTimeFormat.build {
239+
hour()
240+
char(':')
241+
minute()
242+
alternativeParsing({
243+
// intentionally empty
244+
}) {
245+
char(':')
246+
second()
247+
optional {
248+
char('.')
249+
secondFraction(1, 9)
250+
}
251+
}
252+
}
253+
}
254+
255+
private val emptyIncompleteLocalTime = IncompleteLocalTime()

0 commit comments

Comments
 (0)