korge icon indicating copy to clipboard operation
korge copied to clipboard

korlibs.time parsing millis precision issue

Open ArtRoman opened this issue 11 months ago • 4 comments

I've found an issue with parsing milliseconds using custom format with optional ([.S]).

0.1 sec is 100 ms, but parsing as 1 millisecond; 0.10 sec is 100 ms, but parsing as 10 milliseconds; 0.100 sec is 100 ms, parsing as 100 milliseconds, that's OK.

    @Test
    fun testMillisKorlibs() {
        //val format = ISO8601.DATETIME_UTC_COMPLETE_FRACTION // Can't set variable size for fraction of second
        val format = DateFormat("yyyy-MM-dd'T'HH:mm[:ss[.S]]Z").withOptional()
        val date = korlibs.time.DateTime(2020, 1, 1, 13, 12, 30, 100)

        assertEquals(1, format.parseUtc("2020-01-01T13:12:30.001Z").milliseconds) //1
        assertEquals(10, format.parseUtc("2020-01-01T13:12:30.010Z").milliseconds) //10
        assertEquals(100, format.parseUtc("2020-01-01T13:12:30.100Z").milliseconds) //100
        assertEquals(100, format.parseUtc("2020-01-01T13:12:30.1Z").milliseconds) // ❌ parsed as 1
        assertEquals(100, format.parseUtc("2020-01-01T13:12:30.10Z").milliseconds) // ❌ parsed as 10
        assertEquals(100, format.parseUtc("2020-01-01T13:12:30.100Z").milliseconds) //100

        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.1Z")) // ❌ parsed as 2020-01-01T13:12:30.001Z
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.10Z")) // ❌ parsed as 2020-01-01T13:12:30.010Z
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.100Z"))
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.1000Z"))
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.10000Z"))
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.100000Z"))
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.1000000Z"))
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.10000000Z")) // ❌ parsed as 2020-01-01T13:12:30.099Z
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.100000000Z"))
        // Out of precision
        assertEquals(date, format.parseUtc("2020-01-01T13:12:30.1000000000Z"))

        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.01Z"))
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.001Z"))
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.0001Z"))
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.00001Z"))
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.000001Z"))
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.0000001Z"))
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.00000001Z"))
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.000000001Z"))
        // Out of precision
        assertNotEquals(date, format.parseUtc("2020-01-01T13:12:30.0000000001Z"))
    }

Tested on com.soywiz.korlibs.korio:korio:4.0.10, com.soywiz.korge:korlibs-time:5.3.2 and 5.4.0.

For example, Java Time and kotlinx.datetime same parsing is OK, but I used existing format for each test, because I can't create format from string with variable length for milliseconds:

    @Test
    fun testMillisJavaTime() {
        //val format = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm[:ss[.S]]X") // Can't set variable size for fraction of second
        val format = DateTimeFormatter.ISO_OFFSET_DATE_TIME
        val date = ZonedDateTime.of(2020, 1, 1, 13, 12, 30, 100_000_000, ZoneOffset.UTC) // 100 millis

        // ZonedDateTime:
        assertEquals(date, format.parse("2020-01-01T13:12:30.1Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.10Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.100Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.1000Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.10000Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.100000Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.1000000Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.10000000Z", ZonedDateTime::from))
        assertEquals(date, format.parse("2020-01-01T13:12:30.100000000Z", ZonedDateTime::from))

        assertFailsWith<DateTimeParseException> {
            // Out of precision: Text '2020-01-01T13:12:30.1000000000Z' could not be parsed at index 29
            format.parse("2020-01-01T13:12:30.1000000000Z", ZonedDateTime::from)
        }

        assertNotEquals(date, format.parse("2020-01-01T13:12:30.01Z", ZonedDateTime::from))
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.001Z", ZonedDateTime::from))
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.0001Z", ZonedDateTime::from))
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.00001Z", ZonedDateTime::from))
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.000001Z", ZonedDateTime::from))
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.0000001Z", ZonedDateTime::from))
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.00000001Z", ZonedDateTime::from))
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.000000001Z", ZonedDateTime::from))

        assertFailsWith<DateTimeParseException> {
            // Out of precision: Text '2020-01-01T13:12:30.0000000001Z' could not be parsed at index 29
            format.parse("2020-01-01T13:12:30.0000000001Z", ZonedDateTime::from)
        }
    }

    @Test
    fun testMillisKotlinxDateTime() {
        //val format = DateTimeComponents.Format { byUnicodePattern("yyyy-MM-dd'T'HH:mm[:ss[.SSS]]X") } // Can't set variable size for fraction of second
        val format = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
        val date = LocalDateTime(2020, 1, 1, 13, 12, 30, 100_000_000) //100 millis

        // DateTimeFormat:
        assertEquals(date, format.parse("2020-01-01T13:12:30.1Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.10Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.100Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.1000Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.10000Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.100000Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.1000000Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.10000000Z").toLocalDateTime())
        assertEquals(date, format.parse("2020-01-01T13:12:30.100000000Z").toLocalDateTime())

        assertFailsWith<IllegalArgumentException> {
            // Out of precision: Failed to parse value from '2020-01-01T13:12:30.1000000000Z'
            assertEquals(date, format.parse("2020-01-01T13:12:30.1000000000Z").toLocalDateTime())
        }

        assertNotEquals(date, format.parse("2020-01-01T13:12:30.01Z").toLocalDateTime())
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.001Z").toLocalDateTime())
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.0001Z").toLocalDateTime())
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.00001Z").toLocalDateTime())
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.000001Z").toLocalDateTime())
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.0000001Z").toLocalDateTime())
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.00000001Z").toLocalDateTime())
        assertNotEquals(date, format.parse("2020-01-01T13:12:30.000000001Z").toLocalDateTime())

        assertFailsWith<IllegalArgumentException> {
            // Out of precision: Failed to parse value from '2020-01-01T13:12:30.0000000001Z'
            assertNotEquals(date, format.parse("2020-01-01T13:12:30.0000000001Z").toLocalDateTime())
        }
    }

ArtRoman avatar Mar 11 '24 16:03 ArtRoman