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