|
| 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