Skip to content

Commit

Permalink
DateTime: Support throwing without adjusting for invalid dates (#1447)
Browse files Browse the repository at this point in the history
  • Loading branch information
soywiz committed Mar 23, 2023
1 parent 72179db commit ffc3b05
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 32 deletions.
6 changes: 3 additions & 3 deletions klock/src/commonMain/kotlin/com/soywiz/klock/DateFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.soywiz.klock
/** Allows to [format] and [parse] instances of [Date], [DateTime] and [DateTimeTz] */
interface DateFormat {
fun format(dd: DateTimeTz): String
fun tryParse(str: String, doThrow: Boolean = false): DateTimeTz?
fun tryParse(str: String, doThrow: Boolean = false, doAdjust: Boolean = true): DateTimeTz?

companion object {
val DEFAULT_FORMAT = DateFormat("EEE, dd MMM yyyy HH:mm:ss z")
Expand All @@ -29,8 +29,8 @@ interface DateFormat {
}
}

fun DateFormat.parse(str: String): DateTimeTz =
tryParse(str, doThrow = true) ?: throw DateException("Not a valid format: '$str' for '$this'")
fun DateFormat.parse(str: String, doAdjust: Boolean = true): DateTimeTz =
tryParse(str, doThrow = true, doAdjust = doAdjust) ?: throw DateException("Not a valid format: '$str' for '$this'")
fun DateFormat.parseDate(str: String): Date = parse(str).local.date

fun DateFormat.parseUtc(str: String): DateTime = parse(str).utc
Expand Down
32 changes: 16 additions & 16 deletions klock/src/commonMain/kotlin/com/soywiz/klock/ISO8601.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ object ISO8601 {

override fun format(dd: TimeSpan): String = dateTimeFormat.format(ref + dd)

override fun tryParse(str: String, doThrow: Boolean): TimeSpan? =
dateTimeFormat.tryParse(str, doThrow)?.let { it.utc - ref }
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? =
dateTimeFormat.tryParse(str, doThrow, doAdjust)?.let { it.utc - ref }
}

data class BaseIsoDateTimeFormat(val format: String, val twoDigitBaseYear: Int = 1900) : DateFormat {
Expand Down Expand Up @@ -87,8 +87,8 @@ object ISO8601 {
}
}

override fun tryParse(str: String, doThrow: Boolean): DateTimeTz? {
return tryParse(str).also {
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? {
return _tryParse(str, doAdjust).also {
if (doThrow && it == null) throw DateException("Can't parse $str with $format")
}
}
Expand All @@ -98,7 +98,7 @@ object ISO8601 {
return null
}

private fun tryParse(str: String): DateTimeTz? {
private fun _tryParse(str: String, doAdjust: Boolean): DateTimeTz? {
var sign = +1
var tzOffset: TimeSpan? = null
var year = twoDigitBaseYear
Expand Down Expand Up @@ -263,8 +263,8 @@ object ISO8601 {
val extended = BaseIsoTimeFormat(extendedFormat ?: basicFormat ?: TODO())

override fun format(dd: TimeSpan): String = extended.format(dd)
override fun tryParse(str: String, doThrow: Boolean): TimeSpan? =
basic.tryParse(str, false) ?: extended.tryParse(str, false)
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? =
basic.tryParse(str, false, doAdjust) ?: extended.tryParse(str, false, doAdjust)
?: (if (doThrow) throw DateException("Invalid format $str") else null)
}

Expand All @@ -273,9 +273,9 @@ object ISO8601 {
val extended = BaseIsoDateTimeFormat(extendedFormat ?: basicFormat ?: TODO())

override fun format(dd: DateTimeTz): String = extended.format(dd)
override fun tryParse(str: String, doThrow: Boolean): DateTimeTz? = null
?: basic.tryParse(str, false)
?: extended.tryParse(str, false)
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? = null
?: basic.tryParse(str, false, doAdjust)
?: extended.tryParse(str, false, doAdjust)
?: (if (doThrow) throw DateException("Invalid format $str") else null)
}

Expand Down Expand Up @@ -383,13 +383,13 @@ object ISO8601 {
val DATE = object : DateFormat {
override fun format(dd: DateTimeTz): String = DATE_CALENDAR_COMPLETE.format(dd)

override fun tryParse(str: String, doThrow: Boolean): DateTimeTz? {
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? {
DATE_ALL.fastForEach { format ->
val result = format.extended.tryParse(str, false)
val result = format.extended.tryParse(str, false, doAdjust)
if (result != null) return result
}
DATE_ALL.fastForEach { format ->
val result = format.basic.tryParse(str, false)
val result = format.basic.tryParse(str, false, doAdjust)
if (result != null) return result
}
return if (doThrow) throw DateException("Invalid format") else null
Expand All @@ -398,13 +398,13 @@ object ISO8601 {
val TIME = object : TimeFormat {
override fun format(dd: TimeSpan): String = TIME_LOCAL_FRACTION0.format(dd)

override fun tryParse(str: String, doThrow: Boolean): TimeSpan? {
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? {
TIME_ALL.fastForEach { format ->
val result = format.extended.tryParse(str, false)
val result = format.extended.tryParse(str, false, doAdjust)
if (result != null) return result
}
TIME_ALL.fastForEach { format ->
val result = format.basic.tryParse(str, false)
val result = format.basic.tryParse(str, false, doAdjust)
if (result != null) return result
}
return if (doThrow) throw DateException("Invalid format") else null
Expand Down
16 changes: 12 additions & 4 deletions klock/src/commonMain/kotlin/com/soywiz/klock/PatternDateFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ data class PatternDateFormat @JvmOverloads constructor(
return out
}

override fun tryParse(str: String, doThrow: Boolean): DateTimeTz? {
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? {
var millisecond = 0
var second = 0
var minute = 0
Expand All @@ -228,9 +228,9 @@ data class PatternDateFormat @JvmOverloads constructor(
"y", "yyyy", "YYYY" -> fullYear = value.toInt()
"yy" -> if (doThrow) throw RuntimeException("Not guessing years from two digits.") else return null
"yyy" -> fullYear = value.toInt() + if (value.toInt() < 800) 2000 else 1000 // guessing year...
"H", "HH", "k", "kk" -> hour = value.toInt() umod 24
"H", "HH", "k", "kk" -> hour = value.toInt()
"h", "hh", "K", "KK" -> {
hour = value.toInt() umod 24
hour = value.toInt()
is12HourFormat = true
}
"m", "mm" -> minute = value.toInt()
Expand Down Expand Up @@ -285,7 +285,15 @@ data class PatternDateFormat @JvmOverloads constructor(
}
}
}
val dateTime = DateTime.createAdjusted(fullYear, month, day, hour, minute, second, millisecond)
if (!doAdjust) {
if (month !in 1..12) if (doThrow) error("Invalid month $month") else return null
if (day !in 1..32) if (doThrow) error("Invalid day $day") else return null
if (hour !in 0..24) if (doThrow) error("Invalid hour $hour") else return null
if (minute !in 0..59) if (doThrow) error("Invalid minute $minute") else return null
if (second !in 0..59) if (doThrow) error("Invalid second $second") else return null
if (millisecond !in 0..999) if (doThrow) error("Invalid millisecond $millisecond") else return null
}
val dateTime = DateTime.createAdjusted(fullYear, month, day, hour umod 24, minute, second, millisecond)
return dateTime.toOffsetUnadjusted(offset ?: 0.hours)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ data class PatternTimeFormat(
return out
}

override fun tryParse(str: String, doThrow: Boolean): TimeSpan? {
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? {
var millisecond = 0
var second = 0
var minute = 0
Expand Down
6 changes: 3 additions & 3 deletions klock/src/commonMain/kotlin/com/soywiz/klock/TimeFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.soywiz.klock

interface TimeFormat {
fun format(dd: TimeSpan): String
fun tryParse(str: String, doThrow: Boolean): TimeSpan?
fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean = true): TimeSpan?

companion object {
val DEFAULT_FORMAT = TimeFormat("HH:mm:ss.SSS")
Expand All @@ -26,8 +26,8 @@ interface TimeFormat {
}
}

fun TimeFormat.parse(str: String): TimeSpan =
tryParse(str, doThrow = true) ?: throw DateException("Not a valid format: '$str' for '$this'")
fun TimeFormat.parse(str: String, doAdjust: Boolean = true): TimeSpan =
tryParse(str, doThrow = true, doAdjust = doAdjust) ?: throw DateException("Not a valid format: '$str' for '$this'")
fun TimeFormat.parseTime(str: String): Time = Time(parse(str))

fun TimeFormat.format(time: Double): String = format(time.milliseconds)
Expand Down
15 changes: 10 additions & 5 deletions klock/src/commonTest/kotlin/com/soywiz/klock/DateTimeTest.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package com.soywiz.klock

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.*

class DateTimeTest {
val HttpDate by lazy { DateFormat("EEE, dd MMM yyyy HH:mm:ss z") }
Expand Down Expand Up @@ -523,4 +519,13 @@ class DateTimeTest {
assertEquals(dtmilli, fmt.parseLong(match.groups[1]!!.value), message = "datestamp parsed from log line has correct value")
assertEquals("Example log message", msg.drop(match.value.length), message = "total match length for composed regex")
}

@Test
fun testThrowOnInvalid() {
assertEquals("Invalid hour 25", assertFailsWith<RuntimeException> {
DateFormat("yyyy-MM-dd HH:mm:ss").parse("2023-02-01 25:00:00", doAdjust = false)
}.message)

DateFormat("yyyy-MM-dd HH:mm:ss").parse("2023-02-01 25:00:00", doAdjust = true)
}
}

0 comments on commit ffc3b05

Please sign in to comment.