Skip to content

Commit 02e4e4d

Browse files
authored
Improve error descriptiveness in the parsing API (#360)
Fixes #359 Fixes #361 Additionally, remove delayed initialization of parsing to ensure that creating ambigous formats fails and document the thrown exception.
1 parent cc8121a commit 02e4e4d

15 files changed

+101
-44
lines changed

core/common/src/LocalDate.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public expect class LocalDate : Comparable<LocalDate> {
6565
* (for example, [dayOfMonth] is 31 for February), consider using [DateTimeComponents.Format] instead.
6666
*
6767
* There is a collection of predefined formats in [LocalDate.Formats].
68+
*
69+
* @throws IllegalArgumentException if parsing using this format is ambiguous.
6870
*/
6971
@Suppress("FunctionName")
7072
public fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate>

core/common/src/LocalDateTime.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public expect class LocalDateTime : Comparable<LocalDateTime> {
6262
* (for example, [dayOfMonth] is 31 for February), consider using [DateTimeComponents.Format] instead.
6363
*
6464
* There is a collection of predefined formats in [LocalDateTime.Formats].
65+
*
66+
* @throws IllegalArgumentException if parsing using this format is ambiguous.
6567
*/
6668
@Suppress("FunctionName")
6769
public fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat<LocalDateTime>

core/common/src/LocalTime.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ public expect class LocalTime : Comparable<LocalTime> {
9797
* (for example, [second] is 60), consider using [DateTimeComponents.Format] instead.
9898
*
9999
* There is a collection of predefined formats in [LocalTime.Formats].
100+
*
101+
* @throws IllegalArgumentException if parsing using this format is ambiguous.
100102
*/
101103
@Suppress("FunctionName")
102104
public fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat<LocalTime>

core/common/src/UtcOffset.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public expect class UtcOffset {
6767
* [DateTimeFormatBuilder.WithUtcOffset.offset] in a format builder for a larger data structure.
6868
*
6969
* There is a collection of predefined formats in [UtcOffset.Formats].
70+
*
71+
* @throws IllegalArgumentException if parsing using this format is ambiguous.
7072
*/
7173
@Suppress("FunctionName")
7274
public fun Format(block: DateTimeFormatBuilder.WithUtcOffset.() -> Unit): DateTimeFormat<UtcOffset>

core/common/src/format/DateTimeComponents.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public class DateTimeComponents internal constructor(internal val contents: Date
6969
* Creates a [DateTimeFormat] for [DateTimeComponents] values using [DateTimeFormatBuilder.WithDateTimeComponents].
7070
*
7171
* There is a collection of predefined formats in [DateTimeComponents.Formats].
72+
*
73+
* @throws IllegalArgumentException if parsing using this format is ambiguous.
7274
*/
7375
@Suppress("FunctionName")
7476
public fun Format(block: DateTimeFormatBuilder.WithDateTimeComponents.() -> Unit): DateTimeFormat<DateTimeComponents> {

core/common/src/format/DateTimeFormat.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ internal sealed class AbstractDateTimeFormat<T, U : Copyable<U>> : DateTimeForma
115115
try {
116116
return valueFromIntermediate(matched)
117117
} catch (e: IllegalArgumentException) {
118-
throw DateTimeFormatException(e.message!!)
118+
throw DateTimeFormatException(when (val message = e.message) {
119+
null -> "The value parsed from '$input' is invalid"
120+
else -> "$message (when parsing '$input')"
121+
}, e)
119122
}
120123
}
121124

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ internal abstract class NamedUnsignedIntFieldFormatDirective<in Target>(
108108
override fun parser(): ParserStructure<Target> =
109109
ParserStructure(
110110
listOf(
111-
StringSetParserOperation(values, AssignableString(), "One of $values for $name")
111+
StringSetParserOperation(values, AssignableString(), "one of $values for $name")
112112
), emptyList()
113113
)
114114
}
@@ -142,7 +142,7 @@ internal abstract class NamedEnumIntFieldFormatDirective<in Target, Type>(
142142
override fun parser(): ParserStructure<Target> =
143143
ParserStructure(
144144
listOf(
145-
StringSetParserOperation(mapping.values, AssignableString(), "One of ${mapping.values} for $name")
145+
StringSetParserOperation(mapping.values, AssignableString(), "one of ${mapping.values} for $name")
146146
), emptyList()
147147
)
148148
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ internal open class ConcatenatedFormatStructure<in T>(
231231

232232
internal class CachedFormatStructure<in T>(formats: List<NonConcatenatedFormatStructure<T>>) :
233233
ConcatenatedFormatStructure<T>(formats) {
234-
private val cachedFormatter: FormatterStructure<T> by lazy { super.formatter() }
235-
private val cachedParser: ParserStructure<T> by lazy { super.parser() }
234+
private val cachedFormatter: FormatterStructure<T> = super.formatter()
235+
private val cachedParser: ParserStructure<T> = super.parser()
236236

237237
override fun formatter(): FormatterStructure<T> = cachedFormatter
238238

core/common/src/internal/format/parser/ParserOperation.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ internal class NumberSpanParserOperation<Output>(
5050

5151
init {
5252
require(consumers.all { (it.length ?: Int.MAX_VALUE) > 0 })
53-
require(consumers.count { it.length == null } <= 1)
53+
require(consumers.count { it.length == null } <= 1) {
54+
val fieldNames = consumers.filter { it.length == null }.map { it.whatThisExpects }
55+
"At most one variable-length numeric field in a row is allowed, but got several: $fieldNames. " +
56+
"Parsing is undefined: for example, with variable-length month number " +
57+
"and variable-length day of month, '111' can be parsed as Jan 11th or Nov 1st."
58+
}
5459
}
5560

5661
private val whatThisExpects: String

core/common/test/format/DateTimeComponentsFormatTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ class DateTimeComponentsFormatTest {
3232
setOffset(UtcOffset.ZERO)
3333
},
3434
format.parse("Tue, 40 Jun 2008 11:05:30 GMT"))
35-
assertFailsWith<DateTimeFormatException> { format.parse("Bue, 3 Jun 2008 11:05:30 GMT") }
35+
format.assertCanNotParse("Bue, 3 Jun 2008 11:05:30 GMT")
3636
}
3737

3838
@Test
3939
fun testInconsistentLocalTime() {
4040
val formatTime = LocalTime.Format {
41-
hour(); char(':'); minute();
41+
hour(); char(':'); minute()
4242
chars(" ("); amPmHour(); char(':'); minute(); char(' '); amPmMarker("AM", "PM"); char(')')
4343
}
4444
val format = DateTimeComponents.Format { time(formatTime) }
@@ -53,16 +53,16 @@ class DateTimeComponentsFormatTest {
5353
DateTimeComponents().apply { hour = 23; hourOfAmPm = 11; minute = 15; amPm = AmPmMarker.AM },
5454
format.parse(time2)
5555
)
56-
assertFailsWith<IllegalArgumentException> { formatTime.parse(time2) }
56+
formatTime.assertCanNotParse(time2)
5757
val time3 = "23:15 (10:15 PM)" // a time with an inconsistent number of hours
5858
assertDateTimeComponentsEqual(
5959
DateTimeComponents().apply { hour = 23; hourOfAmPm = 10; minute = 15; amPm = AmPmMarker.PM },
6060
format.parse(time3)
6161
)
62-
assertFailsWith<IllegalArgumentException> { formatTime.parse(time3) }
62+
formatTime.assertCanNotParse(time3)
6363
val time4 = "23:15 (11:16 PM)" // a time with an inconsistently duplicated field
64-
assertFailsWith<IllegalArgumentException> { format.parse(time4) }
65-
assertFailsWith<IllegalArgumentException> { formatTime.parse(time4) }
64+
format.assertCanNotParse(time4)
65+
formatTime.assertCanNotParse(time4)
6666
}
6767

6868
@Test
@@ -95,7 +95,7 @@ class DateTimeComponentsFormatTest {
9595
assertEquals(dateTime, bag.toLocalDateTime())
9696
assertEquals(offset, bag.toUtcOffset())
9797
assertEquals(berlin, bag.timeZoneId)
98-
assertFailsWith<DateTimeFormatException> { format.parse("2008-06-03T11:05:30.123456789+01:00[Mars/New_York]") }
98+
format.assertCanNotParse("2008-06-03T11:05:30.123456789+01:00[Mars/New_York]")
9999
for (zone in TimeZone.availableZoneIds) {
100100
assertEquals(zone, format.parse("2008-06-03T11:05:30.123456789+01:00[$zone]").timeZoneId)
101101
}

0 commit comments

Comments
 (0)