Skip to content

Commit 797c7e4

Browse files
authored
Refer to DateTimeComponents field names in errors for null values (#472)
Fixes #471
1 parent e69f92f commit 797c7e4

File tree

6 files changed

+96
-44
lines changed

6 files changed

+96
-44
lines changed

core/common/src/format/DateTimeComponents.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,9 @@ public class DateTimeComponents internal constructor(internal val contents: Date
301301
* @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.dayOfWeek
302302
*/
303303
public var dayOfWeek: DayOfWeek?
304-
get() = contents.date.isoDayOfWeek?.let { DayOfWeek(it) }
304+
get() = contents.date.dayOfWeek?.let { DayOfWeek(it) }
305305
set(value) {
306-
contents.date.isoDayOfWeek = value?.isoDayNumber
306+
contents.date.dayOfWeek = value?.isoDayNumber
307307
}
308308

309309
/**
@@ -366,28 +366,28 @@ public class DateTimeComponents internal constructor(internal val contents: Date
366366
* True if the offset is negative.
367367
* @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset
368368
*/
369-
public var offsetIsNegative: Boolean? by contents.offset::isNegative
369+
public var offsetIsNegative: Boolean? by contents.offset::offsetIsNegative
370370

371371
/**
372372
* The total amount of full hours in the UTC offset, in the range [0; 18].
373373
* @throws IllegalArgumentException during assignment if the value is outside the `0..99` range.
374374
* @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset
375375
*/
376-
public var offsetHours: Int? by TwoDigitNumber(contents.offset::totalHoursAbs)
376+
public var offsetHours: Int? by TwoDigitNumber(contents.offset::offsetHours)
377377

378378
/**
379379
* The amount of minutes that don't add to a whole hour in the UTC offset, in the range [0; 59].
380380
* @throws IllegalArgumentException during assignment if the value is outside the `0..99` range.
381381
* @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset
382382
*/
383-
public var offsetMinutesOfHour: Int? by TwoDigitNumber(contents.offset::minutesOfHour)
383+
public var offsetMinutesOfHour: Int? by TwoDigitNumber(contents.offset::offsetMinutesOfHour)
384384

385385
/**
386386
* The amount of seconds that don't add to a whole minute in the UTC offset, in the range [0; 59].
387387
* @throws IllegalArgumentException during assignment if the value is outside the `0..99` range.
388388
* @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset
389389
*/
390-
public var offsetSecondsOfMinute: Int? by TwoDigitNumber(contents.offset::secondsOfMinute)
390+
public var offsetSecondsOfMinute: Int? by TwoDigitNumber(contents.offset::offsetSecondsOfMinute)
391391

392392
/**
393393
* The timezone identifier, for example, "Europe/Berlin".

core/common/src/format/LocalDateFormat.kt

+9-9
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,15 @@ internal interface DateFieldContainer {
200200
var year: Int?
201201
var monthNumber: Int?
202202
var dayOfMonth: Int?
203-
var isoDayOfWeek: Int?
203+
var dayOfWeek: Int?
204204
var dayOfYear: Int?
205205
}
206206

207207
private object DateFields {
208208
val year = GenericFieldSpec(PropertyAccessor(DateFieldContainer::year))
209209
val month = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::monthNumber), minValue = 1, maxValue = 12)
210210
val dayOfMonth = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::dayOfMonth), minValue = 1, maxValue = 31)
211-
val isoDayOfWeek = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::isoDayOfWeek), minValue = 1, maxValue = 7)
211+
val isoDayOfWeek = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::dayOfWeek), minValue = 1, maxValue = 7)
212212
val dayOfYear = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::dayOfYear), minValue = 1, maxValue = 366)
213213
}
214214

@@ -219,7 +219,7 @@ internal class IncompleteLocalDate(
219219
override var year: Int? = null,
220220
override var monthNumber: Int? = null,
221221
override var dayOfMonth: Int? = null,
222-
override var isoDayOfWeek: Int? = null,
222+
override var dayOfWeek: Int? = null,
223223
override var dayOfYear: Int? = null,
224224
) : DateFieldContainer, Copyable<IncompleteLocalDate> {
225225
fun toLocalDate(): LocalDate {
@@ -253,7 +253,7 @@ internal class IncompleteLocalDate(
253253
}
254254
}
255255
}
256-
isoDayOfWeek?.let {
256+
dayOfWeek?.let {
257257
if (it != date.dayOfWeek.isoDayNumber) {
258258
throw DateTimeFormatException(
259259
"Can not create a LocalDate from the given input: " +
@@ -268,25 +268,25 @@ internal class IncompleteLocalDate(
268268
year = date.year
269269
monthNumber = date.monthNumber
270270
dayOfMonth = date.dayOfMonth
271-
isoDayOfWeek = date.dayOfWeek.isoDayNumber
271+
dayOfWeek = date.dayOfWeek.isoDayNumber
272272
dayOfYear = date.dayOfYear
273273
}
274274

275275
override fun copy(): IncompleteLocalDate =
276-
IncompleteLocalDate(year, monthNumber, dayOfMonth, isoDayOfWeek, dayOfYear)
276+
IncompleteLocalDate(year, monthNumber, dayOfMonth, dayOfWeek, dayOfYear)
277277

278278
override fun equals(other: Any?): Boolean =
279279
other is IncompleteLocalDate && year == other.year && monthNumber == other.monthNumber &&
280-
dayOfMonth == other.dayOfMonth && isoDayOfWeek == other.isoDayOfWeek && dayOfYear == other.dayOfYear
280+
dayOfMonth == other.dayOfMonth && dayOfWeek == other.dayOfWeek && dayOfYear == other.dayOfYear
281281

282282
override fun hashCode(): Int = year.hashCode() * 923521 +
283283
monthNumber.hashCode() * 29791 +
284284
dayOfMonth.hashCode() * 961 +
285-
isoDayOfWeek.hashCode() * 31 +
285+
dayOfWeek.hashCode() * 31 +
286286
dayOfYear.hashCode()
287287

288288
override fun toString(): String =
289-
"${year ?: "??"}-${monthNumber ?: "??"}-${dayOfMonth ?: "??"} (day of week is ${isoDayOfWeek ?: "??"})"
289+
"${year ?: "??"}-${monthNumber ?: "??"}-${dayOfMonth ?: "??"} (day of week is ${dayOfWeek ?: "??"})"
290290
}
291291

292292
private class YearDirective(private val padding: Padding, private val isYearOfEra: Boolean = false) :

core/common/src/format/LocalTimeFormat.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private object TimeFields {
4343
val second =
4444
UnsignedFieldSpec(PropertyAccessor(TimeFieldContainer::second), minValue = 0, maxValue = 59, defaultValue = 0)
4545
val fractionOfSecond =
46-
GenericFieldSpec(PropertyAccessor(TimeFieldContainer::fractionOfSecond), defaultValue = DecimalFraction(0, 9))
46+
GenericFieldSpec(PropertyAccessor(TimeFieldContainer::fractionOfSecond, "nanosecond"), defaultValue = DecimalFraction(0, 9))
4747
val amPm = GenericFieldSpec(PropertyAccessor(TimeFieldContainer::amPm))
4848
val hourOfAmPm = UnsignedFieldSpec(PropertyAccessor(TimeFieldContainer::hourOfAmPm), minValue = 1, maxValue = 12)
4949
}

core/common/src/format/UtcOffsetFormat.kt

+24-24
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import kotlinx.datetime.internal.format.parser.Copyable
1111
import kotlin.math.*
1212

1313
internal interface UtcOffsetFieldContainer {
14-
var isNegative: Boolean?
15-
var totalHoursAbs: Int?
16-
var minutesOfHour: Int?
17-
var secondsOfMinute: Int?
14+
var offsetIsNegative: Boolean?
15+
var offsetHours: Int?
16+
var offsetMinutesOfHour: Int?
17+
var offsetSecondsOfMinute: Int?
1818
}
1919

2020
internal interface AbstractWithOffsetBuilder : DateTimeFormatBuilder.WithUtcOffset {
@@ -127,26 +127,26 @@ internal fun DateTimeFormatBuilder.WithUtcOffset.isoOffset(
127127

128128
private object OffsetFields {
129129
private val sign = object : FieldSign<UtcOffsetFieldContainer> {
130-
override val isNegative = PropertyAccessor(UtcOffsetFieldContainer::isNegative)
130+
override val isNegative = PropertyAccessor(UtcOffsetFieldContainer::offsetIsNegative)
131131
override fun isZero(obj: UtcOffsetFieldContainer): Boolean =
132-
(obj.totalHoursAbs ?: 0) == 0 && (obj.minutesOfHour ?: 0) == 0 && (obj.secondsOfMinute ?: 0) == 0
132+
(obj.offsetHours ?: 0) == 0 && (obj.offsetMinutesOfHour ?: 0) == 0 && (obj.offsetSecondsOfMinute ?: 0) == 0
133133
}
134134
val totalHoursAbs = UnsignedFieldSpec(
135-
PropertyAccessor(UtcOffsetFieldContainer::totalHoursAbs),
135+
PropertyAccessor(UtcOffsetFieldContainer::offsetHours),
136136
defaultValue = 0,
137137
minValue = 0,
138138
maxValue = 18,
139139
sign = sign,
140140
)
141141
val minutesOfHour = UnsignedFieldSpec(
142-
PropertyAccessor(UtcOffsetFieldContainer::minutesOfHour),
142+
PropertyAccessor(UtcOffsetFieldContainer::offsetMinutesOfHour),
143143
defaultValue = 0,
144144
minValue = 0,
145145
maxValue = 59,
146146
sign = sign,
147147
)
148148
val secondsOfMinute = UnsignedFieldSpec(
149-
PropertyAccessor(UtcOffsetFieldContainer::secondsOfMinute),
149+
PropertyAccessor(UtcOffsetFieldContainer::offsetSecondsOfMinute),
150150
defaultValue = 0,
151151
minValue = 0,
152152
maxValue = 59,
@@ -155,39 +155,39 @@ private object OffsetFields {
155155
}
156156

157157
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,
158+
override var offsetIsNegative: Boolean? = null,
159+
override var offsetHours: Int? = null,
160+
override var offsetMinutesOfHour: Int? = null,
161+
override var offsetSecondsOfMinute: Int? = null,
162162
) : UtcOffsetFieldContainer, Copyable<IncompleteUtcOffset> {
163163

164164
fun toUtcOffset(): UtcOffset {
165-
val sign = if (isNegative == true) -1 else 1
165+
val sign = if (offsetIsNegative == true) -1 else 1
166166
return UtcOffset(
167-
totalHoursAbs?.let { it * sign }, minutesOfHour?.let { it * sign }, secondsOfMinute?.let { it * sign }
167+
offsetHours?.let { it * sign }, offsetMinutesOfHour?.let { it * sign }, offsetSecondsOfMinute?.let { it * sign }
168168
)
169169
}
170170

171171
fun populateFrom(offset: UtcOffset) {
172-
isNegative = offset.totalSeconds < 0
172+
offsetIsNegative = offset.totalSeconds < 0
173173
val totalSecondsAbs = offset.totalSeconds.absoluteValue
174-
totalHoursAbs = totalSecondsAbs / 3600
175-
minutesOfHour = (totalSecondsAbs / 60) % 60
176-
secondsOfMinute = totalSecondsAbs % 60
174+
offsetHours = totalSecondsAbs / 3600
175+
offsetMinutesOfHour = (totalSecondsAbs / 60) % 60
176+
offsetSecondsOfMinute = totalSecondsAbs % 60
177177
}
178178

179179
override fun equals(other: Any?): Boolean =
180-
other is IncompleteUtcOffset && isNegative == other.isNegative && totalHoursAbs == other.totalHoursAbs &&
181-
minutesOfHour == other.minutesOfHour && secondsOfMinute == other.secondsOfMinute
180+
other is IncompleteUtcOffset && offsetIsNegative == other.offsetIsNegative && offsetHours == other.offsetHours &&
181+
offsetMinutesOfHour == other.offsetMinutesOfHour && offsetSecondsOfMinute == other.offsetSecondsOfMinute
182182

183183
override fun hashCode(): Int =
184-
isNegative.hashCode() + totalHoursAbs.hashCode() + minutesOfHour.hashCode() + secondsOfMinute.hashCode()
184+
offsetIsNegative.hashCode() + offsetHours.hashCode() + offsetMinutesOfHour.hashCode() + offsetSecondsOfMinute.hashCode()
185185

186186
override fun copy(): IncompleteUtcOffset =
187-
IncompleteUtcOffset(isNegative, totalHoursAbs, minutesOfHour, secondsOfMinute)
187+
IncompleteUtcOffset(offsetIsNegative, offsetHours, offsetMinutesOfHour, offsetSecondsOfMinute)
188188

189189
override fun toString(): String =
190-
"${isNegative?.let { if (it) "-" else "+" } ?: " "}${totalHoursAbs ?: "??"}:${minutesOfHour ?: "??"}:${secondsOfMinute ?: "??"}"
190+
"${offsetIsNegative?.let { if (it) "-" else "+" } ?: " "}${offsetHours ?: "??"}:${offsetMinutesOfHour ?: "??"}:${offsetSecondsOfMinute ?: "??"}"
191191
}
192192

193193
internal class UtcOffsetWholeHoursDirective(private val padding: Padding) :

core/common/src/internal/format/FieldSpec.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ internal interface Accessor<in Object, Field>: AssignableField<Object, Field> {
3030
/**
3131
* An implementation of [Accessor] for a mutable property of an object.
3232
*/
33-
internal class PropertyAccessor<Object, Field>(private val property: KMutableProperty1<Object, Field?>): Accessor<Object, Field> {
34-
override val name: String get() = property.name
35-
33+
internal class PropertyAccessor<Object, Field>(
34+
private val property: KMutableProperty1<Object, Field?>,
35+
override val name: String = property.name
36+
): Accessor<Object, Field> {
3637
override fun trySetWithoutReassigning(container: Object, newValue: Field): Field? {
3738
val oldValue = property.get(container)
3839
return when {

core/common/test/format/DateTimeComponentsFormatTest.kt

+52-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ package kotlinx.datetime.test.format
77

88
import kotlinx.datetime.*
99
import kotlinx.datetime.format.*
10+
import kotlin.reflect.KMutableProperty1
11+
import kotlin.reflect.KProperty
1012
import kotlin.test.*
1113

1214
class DateTimeComponentsFormatTest {
13-
1415
@Test
1516
fun testErrorHandling() {
1617
val format = DateTimeComponents.Formats.RFC_1123
@@ -198,6 +199,56 @@ class DateTimeComponentsFormatTest {
198199
assertNull(bagWithAlternative.nanosecond)
199200
}
200201

202+
@Test
203+
fun testFormattingWithUnsetFields() {
204+
class PropertyAndItsValue<Target, Value>(val property: KMutableProperty1<Target, Value>, val value: Value) {
205+
fun set(target: Target) {
206+
property.set(target, value)
207+
}
208+
}
209+
val fields = listOf<PropertyAndItsValue<DateTimeComponents, *>>(
210+
PropertyAndItsValue(DateTimeComponents::timeZoneId, "Europe/Berlin"),
211+
PropertyAndItsValue(DateTimeComponents::year, 2020),
212+
PropertyAndItsValue(DateTimeComponents::monthNumber, 3),
213+
PropertyAndItsValue(DateTimeComponents::dayOfMonth, 16),
214+
PropertyAndItsValue(DateTimeComponents::dayOfWeek, DayOfWeek.MONDAY),
215+
PropertyAndItsValue(DateTimeComponents::dayOfYear, 76),
216+
PropertyAndItsValue(DateTimeComponents::hour, 23),
217+
PropertyAndItsValue(DateTimeComponents::hourOfAmPm, 11),
218+
PropertyAndItsValue(DateTimeComponents::amPm, AmPmMarker.PM),
219+
PropertyAndItsValue(DateTimeComponents::minute, 59),
220+
PropertyAndItsValue(DateTimeComponents::second, 45),
221+
PropertyAndItsValue(DateTimeComponents::nanosecond, 123_456_789),
222+
PropertyAndItsValue(DateTimeComponents::offsetHours, 3),
223+
PropertyAndItsValue(DateTimeComponents::offsetMinutesOfHour, 30),
224+
PropertyAndItsValue(DateTimeComponents::offsetSecondsOfMinute, 15),
225+
)
226+
val formatWithEverything = DateTimeComponents.Format {
227+
timeZoneId()
228+
dateTime(LocalDateTime.Formats.ISO)
229+
char(' ')
230+
dayOfWeek(DayOfWeekNames.ENGLISH_FULL)
231+
char(' ')
232+
dayOfYear()
233+
char(' ')
234+
amPmHour(); char(' '); amPmMarker("AM", "PM")
235+
char(' ')
236+
offset(UtcOffset.Formats.ISO)
237+
}
238+
for (index in fields.indices) {
239+
val propertyName = fields[index].property.name
240+
assertFailsWith<IllegalStateException>("No error if the field $propertyName is unset") {
241+
formatWithEverything.format {
242+
for (i in fields.indices) {
243+
if (i != index) fields[i].set(this)
244+
}
245+
}
246+
}.let {
247+
assertTrue(it.message!!.contains(fields[index].property.name), "Error message '$it' does not contain $propertyName")
248+
}
249+
}
250+
}
251+
201252
@OptIn(FormatStringsInDatetimeFormats::class)
202253
@Test
203254
fun testByUnicodePatternDoc() {

0 commit comments

Comments
 (0)