Skip to content

Locale-invariant parsing and formatting #343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cd030f8
Implement a class for representing decimal fractions
dkhalanskyjb Feb 19, 2024
ab131c0
Implement general machinery for formatting entities as text
dkhalanskyjb Feb 19, 2024
6a7b0f8
Implement the general machinery for parsing entities from code
dkhalanskyjb Feb 19, 2024
cba4968
Add a way to describe fields regardless of their storage and format
dkhalanskyjb Feb 20, 2024
c7dce78
Add a way to specify how a field should be parsed and formatted
dkhalanskyjb Feb 20, 2024
48b0f1d
Encode the form of formats for parsing and formatting anything
dkhalanskyjb Feb 20, 2024
eb729db
Implement a mutable container for building formats
dkhalanskyjb Feb 20, 2024
084eae1
Define the public API representation of a format
dkhalanskyjb Feb 20, 2024
dbe06bb
Define the public API representation of a format builder
dkhalanskyjb Feb 20, 2024
5de962b
Define the API for building a date-based format
dkhalanskyjb Feb 20, 2024
03f9b12
Define the API for building a time-based format
dkhalanskyjb Feb 20, 2024
bdcbc3e
Define the API for building an UTC-offset-based format
dkhalanskyjb Feb 20, 2024
71ef2aa
Define the API for building a date-time-based format
dkhalanskyjb Feb 20, 2024
7c080ef
Add DateTimeComponents, a bag of datetime arbitrary values
dkhalanskyjb Feb 20, 2024
58f81e9
Allow printing datetime formats as Kotlin code
dkhalanskyjb Feb 20, 2024
d79683f
Implement parsing and formatting in Native with the new system
dkhalanskyjb Feb 20, 2024
0fa4cc8
Remove String.toSomething
dkhalanskyjb Jan 26, 2024
4362d63
Only allow the ISO extended format in `UtcOffset.parse`
dkhalanskyjb Feb 1, 2024
9b7b49c
Define the public API endpoints for datetime formatting
dkhalanskyjb Feb 20, 2024
a9c1940
Document the datetime formatting in README
dkhalanskyjb Feb 20, 2024
cbdd448
Introduce benchmarks module with basic formatting benchmark
qwwdfsad Nov 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
*.iml
target
build
/local.properties
/local.properties
benchmarks.jar
135 changes: 124 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,34 +172,145 @@ val hourMinute = LocalTime(hour = 12, minute = 13)
An `Instant` can be converted to a number of milliseconds since the Unix/POSIX epoch with the `toEpochMilliseconds()` function.
To convert back, use the companion object function `Instant.fromEpochMilliseconds(Long)`.

### Converting instant and local date/time to and from string
### Converting instant and local date/time to and from the ISO 8601 string

Currently, `Instant`, `LocalDateTime`, `LocalDate` and `LocalTime` only support ISO-8601 format.
`Instant`, `LocalDateTime`, `LocalDate` and `LocalTime` provide shortcuts for
parsing and formatting them using the extended ISO-8601 format.
The `toString()` function is used to convert the value to a string in that format, and
the `parse` function in companion object is used to parse a string representation back.


```kotlin
val instantNow = Clock.System.now()
instantNow.toString() // returns something like 2015-12-31T12:30:00Z
val instantBefore = Instant.parse("2010-06-01T22:19:44.475Z")
```

Alternatively, the `String.to...()` extension functions can be used instead of `parse`,
where it feels more convenient:

`LocalDateTime` uses a similar format, but without `Z` UTC time zone designator in the end.

`LocalDate` uses a format with just year, month, and date components, e.g. `2010-06-01`.

`LocalTime` uses a format with just hour, minute, second and (if non-zero) nanosecond components, e.g. `12:01:03`.

```kotlin
"2010-06-01T22:19:44.475Z".toInstant()
"2010-06-01T22:19:44".toLocalDateTime()
"2010-06-01".toLocalDate()
"12:01:03".toLocalTime()
"12:0:03.999".toLocalTime()
LocalDateTime.parse("2010-06-01T22:19:44")
LocalDate.parse("2010-06-01")
LocalTime.parse("12:01:03")
LocalTime.parse("12:00:03.999")
LocalTime.parse("12:0:03.999") // fails with an IllegalArgumentException
```

### Working with other string formats

When some data needs to be formatted in some format other than ISO-8601, one
can define their own format or use some of the predefined ones:

```kotlin
// import kotlinx.datetime.format.*

val dateFormat = LocalDate.Format {
monthNumber(padding = Padding.SPACE)
char('/')
dayOfMonth()
char(' ')
year()
}

val date = dateFormat.parse("12/24 2023")
println(date.format(LocalDate.Formats.ISO_BASIC)) // "20231224"
```

#### Using Unicode format strings (like `yyyy-MM-dd`)

Given a constant format string like the ones used by Java's
[DateTimeFormatter.ofPattern](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) can be
converted to Kotlin code using the following invocation:

```kotlin
// import kotlinx.datetime.format.*

println(DateTimeFormat.formatAsKotlinBuilderDsl(DateTimeComponents.Format {
byUnicodePattern("uuuu-MM-dd'T'HH:mm:ss[.SSS]Z")
}))

// will print:
/*
date(LocalDate.Formats.ISO)
char('T')
hour()
char(':')
minute()
char(':')
second()
alternativeParsing({
}) {
char('.')
secondFraction(3)
}
offset(UtcOffset.Formats.FOUR_DIGITS)
*/
```

When your format string is not constant, with the `FormatStringsInDatetimeFormats` opt-in,
you can use the format without converting it to Kotlin code beforehand:

```kotlin
val formatPattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]"

@OptIn(FormatStringsInDatetimeFormats::class)
val dateTimeFormat = LocalDateTime.Format {
byUnicodePattern(formatPattern)
}

dateTimeFormat.parse("2023-12-24T23:59:59")
```

### Parsing and formatting partial, compound or out-of-bounds data

Sometimes, the required string format doesn't fully correspond to any of the
classes `kotlinx-datetime` provides. In these cases, `DateTimeComponents`, a
collection of all date-time fields, can be used instead.

```kotlin
// import kotlinx.datetime.format.*

val yearMonth = DateTimeComponents.Format { year(); char('-'); monthNumber() }
.parse("2024-01")
println(yearMonth.year)
println(yearMonth.monthNumber)

val dateTimeOffset = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
.parse("2023-01-07T23:16:15.53+02:00")
println(dateTimeOffset.toUtcOffset()) // +02:00
println(dateTimeOffset.toLocalDateTime()) // 2023-01-07T23:16:15.53
```

Occasionally, one can encounter strings where the values are slightly off:
for example, `23:59:60`, where `60` is an invalid value for the second.
`DateTimeComponents` allows parsing such values as well and then mutating them
before conversion.

```kotlin
val time = DateTimeComponents.Format { time(LocalTime.Formats.ISO) }
.parse("23:59:60").apply {
if (second == 60) second = 59
}.toLocalTime()
println(time) // 23:59:59
```

Because `DateTimeComponents` is provided specifically for parsing and
formatting, there is no way to construct it normally. If one needs to format
partial, complex or out-of-bounds data, the `format` function allows building
`DateTimeComponents` specifically for formatting it:

```kotlin
DateTimeComponents.Formats.RFC_1123.format {
// the receiver of this lambda is DateTimeComponents
setDate(LocalDate(2023, 1, 7))
hour = 23
minute = 59
second = 60
setOffset(UtcOffset(hours = 2))
} // Sat, 7 Jan 2023 23:59:60 +0200
```

### Instant arithmetic
Expand Down Expand Up @@ -388,3 +499,5 @@ For local builds, you can use a later version of JDK if you don't have that
version installed. Specify the version of this JDK with the `java.mainToolchainVersion` Gradle property.

After that, the project can be opened in IDEA and built with Gradle.

For building and running benchmarks, see [README.md](benchmarks/README.md)
28 changes: 28 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
### Benchmarks utility module

Module that provides benchmarking infrastructure for kotlinx-datetime.
Please note that these benchmarks are typically written with the specific target, hypothesis and effect in mind.

They provide numbers, not insights, and shouldn't be used as the generic comparison and statements like
"X implementaiton or format is faster/slower than Y"


#### Usage

```
// Build `benchmarks.jar` into the project's root
./gradlew :benchmarks:jmhJar

// Run all benchmarks
java -jar benchmarks.jar

// Run dedicated benchmark(s)
java -jar benchmarks.jar Formatting
java -jar benchmarks.jar FormattingBenchmark.formatIso

// Run with the specified number of warmup iterations, measurement iterations, timeunit and mode
java -jar benchmarks.jar -wi 5 -i 5 -tu us -bm thrpt Formatting

// Extensive help
java -jar benchmarks.jar -help
```
34 changes: 34 additions & 0 deletions benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

plugins {
id("kotlin")
id("me.champeau.jmh")
}


val mainJavaToolchainVersion by ext(project.property("java.mainToolchainVersion"))
val modularJavaToolchainVersion by ext(project.property("java.modularToolchainVersion"))

sourceSets {
dependencies {
implementation(project(":kotlinx-datetime"))
implementation("org.openjdk.jmh:jmh-core:1.35")
}
}

// Publish benchmarks to the root for the easier 'java -jar benchmarks.jar`
tasks.named<Jar>("jmhJar") {
val nullString: String? = null
archiveBaseName.set("benchmarks")
archiveClassifier.set(nullString)
archiveVersion.set(nullString)
archiveVersion.convention(nullString)
destinationDirectory.set(file("$rootDir"))
}

repositories {
mavenCentral()
}
27 changes: 27 additions & 0 deletions benchmarks/src/jmh/kotlin/FormattingBenchmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime

import org.openjdk.jmh.annotations.*
import java.util.concurrent.*

@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(1)
open class FormattingBenchmark {

private val localDateTime = LocalDateTime(2023, 11, 9, 12, 21, 31, 41)
private val formatted = LocalDateTime.Formats.ISO.format(localDateTime)

@Benchmark
fun formatIso() = LocalDateTime.Formats.ISO.format(localDateTime)

@Benchmark
fun parseIso() = LocalDateTime.Formats.ISO.parse(formatted)
}
20 changes: 4 additions & 16 deletions core/common/src/DateTimePeriod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,9 @@ public sealed class DateTimePeriod {
}

/**
* Parses the ISO-8601 duration representation as a [DateTimePeriod].
*
* See [DateTimePeriod.parse] for examples.
*
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are exceeded.
*
* @see DateTimePeriod.parse
* @suppress
*/
@Deprecated("Removed to support more idiomatic code. See https://github.com/Kotlin/kotlinx-datetime/issues/339", ReplaceWith("DateTimePeriod.parse(this)"), DeprecationLevel.WARNING)
public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this)

/**
Expand Down Expand Up @@ -358,16 +353,9 @@ public class DatePeriod internal constructor(
}

/**
* Parses the ISO-8601 duration representation as a [DatePeriod].
*
* This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not
* zero.
*
* @throws IllegalArgumentException if the text cannot be parsed, the boundaries of [DatePeriod] are exceeded,
* or any time components are not zero.
*
* @see DateTimePeriod.parse
* @suppress
*/
@Deprecated("Removed to support more idiomatic code. See https://github.com/Kotlin/kotlinx-datetime/issues/339", ReplaceWith("DatePeriod.parse(this)"), DeprecationLevel.WARNING)
public fun String.toDatePeriod(): DatePeriod = DatePeriod.parse(this)

private class DateTimePeriodImpl(
Expand Down
Loading