Skip to content

Commit ba99d81

Browse files
committed
Define the API for building an UTC-offset-based format
1 parent fba4050 commit ba99d81

File tree

2 files changed

+302
-0
lines changed

2 files changed

+302
-0
lines changed

core/common/src/format/DateTimeFormatBuilder.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,48 @@ public sealed interface DateTimeFormatBuilder {
200200
*/
201201
public fun time(format: DateTimeFormat<LocalTime>)
202202
}
203+
204+
/**
205+
* Functions specific to the date-time format builders containing the UTC-offset fields.
206+
*/
207+
public sealed interface WithUtcOffset : DateTimeFormatBuilder {
208+
/**
209+
* The total number of hours in the UTC offset, including the sign.
210+
*
211+
* By default, it's zero-padded to two digits, but this can be changed with [padding].
212+
*
213+
* This field has the default value of 0. If you want to omit it, use [optional].
214+
*/
215+
public fun offsetHours(padding: Padding = Padding.ZERO)
216+
217+
/**
218+
* The minute-of-hour of the UTC offset.
219+
*
220+
* By default, it's zero-padded to two digits, but this can be changed with [padding].
221+
*
222+
* This field has the default value of 0. If you want to omit it, use [optional].
223+
*/
224+
public fun offsetMinutesOfHour(padding: Padding = Padding.ZERO)
225+
226+
/**
227+
* The second-of-minute of the UTC offset.
228+
*
229+
* By default, it's zero-padded to two digits, but this can be changed with [padding].
230+
*
231+
* This field has the default value of 0. If you want to omit it, use [optional].
232+
*/
233+
public fun offsetSecondsOfMinute(padding: Padding = Padding.ZERO)
234+
235+
/**
236+
* An existing [DateTimeFormat] for the UTC offset part.
237+
*
238+
* Example:
239+
* ```
240+
* offset(UtcOffset.Formats.FOUR_DIGITS)
241+
* ```
242+
*/
243+
public fun offset(format: DateTimeFormat<UtcOffset>)
244+
}
203245
}
204246

205247
/**
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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.format.*
10+
import kotlinx.datetime.internal.format.parser.Copyable
11+
import kotlin.math.*
12+
13+
internal interface UtcOffsetFieldContainer {
14+
var isNegative: Boolean?
15+
var totalHoursAbs: Int?
16+
var minutesOfHour: Int?
17+
var secondsOfMinute: Int?
18+
}
19+
20+
internal interface AbstractWithOffsetBuilder : DateTimeFormatBuilder.WithUtcOffset {
21+
fun addFormatStructureForOffset(structure: FormatStructure<UtcOffsetFieldContainer>)
22+
23+
override fun offsetHours(padding: Padding) =
24+
addFormatStructureForOffset(
25+
SignedFormatStructure(
26+
BasicFormatStructure(UtcOffsetWholeHoursDirective(padding)),
27+
withPlusSign = true
28+
)
29+
)
30+
31+
override fun offsetMinutesOfHour(padding: Padding) =
32+
addFormatStructureForOffset(BasicFormatStructure(UtcOffsetMinuteOfHourDirective(padding)))
33+
34+
override fun offsetSecondsOfMinute(padding: Padding) =
35+
addFormatStructureForOffset(BasicFormatStructure(UtcOffsetSecondOfMinuteDirective(padding)))
36+
37+
@Suppress("NO_ELSE_IN_WHEN")
38+
override fun offset(format: DateTimeFormat<UtcOffset>) = when (format) {
39+
is UtcOffsetFormat -> addFormatStructureForOffset(format.actualFormat)
40+
}
41+
}
42+
43+
internal class UtcOffsetFormat(override val actualFormat: CachedFormatStructure<UtcOffsetFieldContainer>) :
44+
AbstractDateTimeFormat<UtcOffset, IncompleteUtcOffset>() {
45+
companion object {
46+
fun build(block: DateTimeFormatBuilder.WithUtcOffset.() -> Unit): UtcOffsetFormat {
47+
val builder = Builder(AppendableFormatStructure())
48+
builder.block()
49+
return UtcOffsetFormat(builder.build())
50+
}
51+
}
52+
53+
private class Builder(override val actualBuilder: AppendableFormatStructure<UtcOffsetFieldContainer>) :
54+
AbstractDateTimeFormatBuilder<UtcOffsetFieldContainer, Builder>, AbstractWithOffsetBuilder {
55+
56+
override fun addFormatStructureForOffset(structure: FormatStructure<UtcOffsetFieldContainer>) {
57+
actualBuilder.add(structure)
58+
}
59+
60+
override fun createEmpty(): Builder = Builder(AppendableFormatStructure())
61+
}
62+
63+
override fun intermediateFromValue(value: UtcOffset): IncompleteUtcOffset =
64+
IncompleteUtcOffset().apply { populateFrom(value) }
65+
66+
override fun valueFromIntermediate(intermediate: IncompleteUtcOffset): UtcOffset = intermediate.toUtcOffset()
67+
68+
override val emptyIntermediate: IncompleteUtcOffset get() = emptyIncompleteUtcOffset
69+
70+
}
71+
72+
internal enum class WhenToOutput {
73+
NEVER,
74+
IF_NONZERO,
75+
ALWAYS;
76+
}
77+
78+
internal fun <T : DateTimeFormatBuilder> T.outputIfNeeded(whenToOutput: WhenToOutput, format: T.() -> Unit) {
79+
when (whenToOutput) {
80+
WhenToOutput.NEVER -> {}
81+
WhenToOutput.IF_NONZERO -> {
82+
optional {
83+
format()
84+
}
85+
}
86+
87+
WhenToOutput.ALWAYS -> {
88+
format()
89+
}
90+
}
91+
}
92+
93+
internal fun DateTimeFormatBuilder.WithUtcOffset.isoOffset(
94+
zOnZero: Boolean,
95+
useSeparator: Boolean,
96+
outputMinute: WhenToOutput,
97+
outputSecond: WhenToOutput
98+
) {
99+
require(outputMinute >= outputSecond) { "Seconds cannot be included without minutes" }
100+
fun DateTimeFormatBuilder.WithUtcOffset.appendIsoOffsetWithoutZOnZero() {
101+
offsetHours()
102+
outputIfNeeded(outputMinute) {
103+
if (useSeparator) {
104+
char(':')
105+
}
106+
offsetMinutesOfHour()
107+
outputIfNeeded(outputSecond) {
108+
if (useSeparator) {
109+
char(':')
110+
}
111+
offsetSecondsOfMinute()
112+
}
113+
}
114+
}
115+
if (zOnZero) {
116+
optional("Z") {
117+
alternativeParsing({
118+
char('z')
119+
}) {
120+
appendIsoOffsetWithoutZOnZero()
121+
}
122+
}
123+
} else {
124+
appendIsoOffsetWithoutZOnZero()
125+
}
126+
}
127+
128+
private object OffsetFields {
129+
private val sign = object : FieldSign<UtcOffsetFieldContainer> {
130+
override val isNegative = PropertyAccessor(UtcOffsetFieldContainer::isNegative)
131+
override fun isZero(obj: UtcOffsetFieldContainer): Boolean =
132+
(obj.totalHoursAbs ?: 0) == 0 && (obj.minutesOfHour ?: 0) == 0 && (obj.secondsOfMinute ?: 0) == 0
133+
}
134+
val totalHoursAbs = UnsignedFieldSpec(
135+
PropertyAccessor(UtcOffsetFieldContainer::totalHoursAbs),
136+
defaultValue = 0,
137+
minValue = 0,
138+
maxValue = 18,
139+
sign = sign,
140+
)
141+
val minutesOfHour = UnsignedFieldSpec(
142+
PropertyAccessor(UtcOffsetFieldContainer::minutesOfHour),
143+
defaultValue = 0,
144+
minValue = 0,
145+
maxValue = 59,
146+
sign = sign,
147+
)
148+
val secondsOfMinute = UnsignedFieldSpec(
149+
PropertyAccessor(UtcOffsetFieldContainer::secondsOfMinute),
150+
defaultValue = 0,
151+
minValue = 0,
152+
maxValue = 59,
153+
sign = sign,
154+
)
155+
}
156+
157+
internal class IncompleteUtcOffset(
158+
override var isNegative: Boolean? = null,
159+
override var totalHoursAbs: Int? = null,
160+
override var minutesOfHour: Int? = null,
161+
override var secondsOfMinute: Int? = null,
162+
) : UtcOffsetFieldContainer, Copyable<IncompleteUtcOffset> {
163+
164+
fun toUtcOffset(): UtcOffset {
165+
val sign = if (isNegative == true) -1 else 1
166+
return UtcOffset(
167+
totalHoursAbs?.let { it * sign }, minutesOfHour?.let { it * sign }, secondsOfMinute?.let { it * sign }
168+
)
169+
}
170+
171+
fun populateFrom(offset: UtcOffset) {
172+
isNegative = offset.totalSeconds < 0
173+
val totalSecondsAbs = offset.totalSeconds.absoluteValue
174+
totalHoursAbs = totalSecondsAbs / 3600
175+
minutesOfHour = (totalSecondsAbs / 60) % 60
176+
secondsOfMinute = totalSecondsAbs % 60
177+
}
178+
179+
override fun equals(other: Any?): Boolean =
180+
other is IncompleteUtcOffset && isNegative == other.isNegative && totalHoursAbs == other.totalHoursAbs &&
181+
minutesOfHour == other.minutesOfHour && secondsOfMinute == other.secondsOfMinute
182+
183+
override fun hashCode(): Int =
184+
isNegative.hashCode() + totalHoursAbs.hashCode() + minutesOfHour.hashCode() + secondsOfMinute.hashCode()
185+
186+
override fun copy(): IncompleteUtcOffset =
187+
IncompleteUtcOffset(isNegative, totalHoursAbs, minutesOfHour, secondsOfMinute)
188+
189+
override fun toString(): String =
190+
"${isNegative?.let { if (it) "-" else "+" } ?: " "}${totalHoursAbs ?: "??"}:${minutesOfHour ?: "??"}:${secondsOfMinute ?: "??"}"
191+
}
192+
193+
internal class UtcOffsetWholeHoursDirective(private val padding: Padding) :
194+
UnsignedIntFieldFormatDirective<UtcOffsetFieldContainer>(
195+
OffsetFields.totalHoursAbs,
196+
minDigits = padding.minDigits(2),
197+
spacePadding = padding.spaces(2)
198+
) {
199+
override fun equals(other: Any?): Boolean = other is UtcOffsetWholeHoursDirective && padding == other.padding
200+
override fun hashCode(): Int = padding.hashCode()
201+
}
202+
203+
private class UtcOffsetMinuteOfHourDirective(private val padding: Padding) :
204+
UnsignedIntFieldFormatDirective<UtcOffsetFieldContainer>(
205+
OffsetFields.minutesOfHour,
206+
minDigits = padding.minDigits(2), spacePadding = padding.spaces(2)
207+
) {
208+
override fun equals(other: Any?): Boolean = other is UtcOffsetMinuteOfHourDirective && padding == other.padding
209+
override fun hashCode(): Int = padding.hashCode()
210+
}
211+
212+
private class UtcOffsetSecondOfMinuteDirective(private val padding: Padding) :
213+
UnsignedIntFieldFormatDirective<UtcOffsetFieldContainer>(
214+
OffsetFields.secondsOfMinute,
215+
minDigits = padding.minDigits(2), spacePadding = padding.spaces(2)
216+
) {
217+
override fun equals(other: Any?): Boolean = other is UtcOffsetSecondOfMinuteDirective && padding == other.padding
218+
override fun hashCode(): Int = padding.hashCode()
219+
}
220+
221+
// these are constants so that the formats are not recreated every time they are used
222+
internal val ISO_OFFSET by lazy {
223+
UtcOffsetFormat.build {
224+
alternativeParsing({ chars("z") }) {
225+
optional("Z") {
226+
offsetHours()
227+
char(':')
228+
offsetMinutesOfHour()
229+
optional {
230+
char(':')
231+
offsetSecondsOfMinute()
232+
}
233+
}
234+
}
235+
}
236+
}
237+
internal val ISO_OFFSET_BASIC by lazy {
238+
UtcOffsetFormat.build {
239+
alternativeParsing({ chars("z") }) {
240+
optional("Z") {
241+
offsetHours()
242+
optional {
243+
offsetMinutesOfHour()
244+
optional {
245+
offsetSecondsOfMinute()
246+
}
247+
}
248+
}
249+
}
250+
}
251+
}
252+
253+
internal val FOUR_DIGIT_OFFSET by lazy {
254+
UtcOffsetFormat.build {
255+
offsetHours()
256+
offsetMinutesOfHour()
257+
}
258+
}
259+
260+
private val emptyIncompleteUtcOffset = IncompleteUtcOffset()

0 commit comments

Comments
 (0)