Skip to content

Commit 7a1d97e

Browse files
committed
Document the datetime formatting in README
1 parent f18cb89 commit 7a1d97e

File tree

3 files changed

+332
-15
lines changed

3 files changed

+332
-15
lines changed

README.md

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -172,34 +172,145 @@ val hourMinute = LocalTime(hour = 12, minute = 13)
172172
An `Instant` can be converted to a number of milliseconds since the Unix/POSIX epoch with the `toEpochMilliseconds()` function.
173173
To convert back, use the companion object function `Instant.fromEpochMilliseconds(Long)`.
174174

175-
### Converting instant and local date/time to and from string
175+
### Converting instant and local date/time to and from the ISO 8601 string
176176

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

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

188-
Alternatively, the `String.to...()` extension functions can be used instead of `parse`,
189-
where it feels more convenient:
190-
191188
`LocalDateTime` uses a similar format, but without `Z` UTC time zone designator in the end.
192189

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

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

197194
```kotlin
198-
"2010-06-01T22:19:44.475Z".toInstant()
199-
"2010-06-01T22:19:44".toLocalDateTime()
200-
"2010-06-01".toLocalDate()
201-
"12:01:03".toLocalTime()
202-
"12:0:03.999".toLocalTime()
195+
LocalDateTime.parse("2010-06-01T22:19:44")
196+
LocalDate.parse("2010-06-01")
197+
LocalTime.parse("12:01:03")
198+
LocalTime.parse("12:00:03.999")
199+
LocalTime.parse("12:0:03.999") // fails with an IllegalArgumentException
200+
```
201+
202+
### Working with other string formats
203+
204+
When some data needs to be formatted in some format other than ISO-8601, one
205+
can define their own format or use some of the predefined ones:
206+
207+
```kotlin
208+
// import kotlinx.datetime.format.*
209+
210+
val dateFormat = LocalDate.Format {
211+
monthNumber(padding = Padding.SPACE)
212+
char('/')
213+
dayOfMonth()
214+
char(' ')
215+
year()
216+
}
217+
218+
val date = dateFormat.parse("12/24 2023")
219+
println(date.format(LocalDate.Formats.ISO_BASIC)) // "20231224"
220+
```
221+
222+
#### Using Unicode format strings (like `yyyy-MM-dd`)
223+
224+
Given a constant format string like the ones used by Java's
225+
[DateTimeFormatter.ofPattern](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) can be
226+
converted to Kotlin code using the following invocation:
227+
228+
```kotlin
229+
// import kotlinx.datetime.format.*
230+
231+
println(DateTimeFormat.formatAsKotlinBuilderDsl(DateTimeComponents.Format {
232+
byUnicodePattern("uuuu-MM-dd'T'HH:mm:ss[.SSS]Z")
233+
}))
234+
235+
// will print:
236+
/*
237+
date(LocalDate.Formats.ISO)
238+
char('T')
239+
hour()
240+
char(':')
241+
minute()
242+
char(':')
243+
second()
244+
alternativeParsing({
245+
}) {
246+
char('.')
247+
secondFraction(3)
248+
}
249+
offset(UtcOffset.Formats.FOUR_DIGITS)
250+
*/
251+
```
252+
253+
When your format string is not constant, with the `FormatStringsInDatetimeFormats` opt-in,
254+
you can use the format without converting it to Kotlin code beforehand:
255+
256+
```kotlin
257+
val formatPattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]"
258+
259+
@OptIn(FormatStringsInDatetimeFormats::class)
260+
val dateTimeFormat = LocalDateTime.Format {
261+
byUnicodePattern(formatPattern)
262+
}
263+
264+
dateTimeFormat.parse("2023-12-24T23:59:59")
265+
```
266+
267+
### Parsing and formatting partial, compound or out-of-bounds data
268+
269+
Sometimes, the required string format doesn't fully correspond to any of the
270+
classes `kotlinx-datetime` provides. In these cases, `DateTimeComponents`, a
271+
collection of all date-time fields, can be used instead.
272+
273+
```kotlin
274+
// import kotlinx.datetime.format.*
275+
276+
val yearMonth = DateTimeComponents.Format { year(); char('-'); monthNumber() }
277+
.parse("2024-01")
278+
println(yearMonth.year)
279+
println(yearMonth.monthNumber)
280+
281+
val dateTimeOffset = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
282+
.parse("2023-01-07T23:16:15.53+02:00")
283+
println(dateTimeOffset.toUtcOffset()) // +02:00
284+
println(dateTimeOffset.toLocalDateTime()) // 2023-01-07T23:16:15.53
285+
```
286+
287+
Occasionally, one can encounter strings where the values are slightly off:
288+
for example, `23:59:60`, where `60` is an invalid value for the second.
289+
`DateTimeComponents` allows parsing such values as well and then mutating them
290+
before conversion.
291+
292+
```kotlin
293+
val time = DateTimeComponents.Format { time(LocalTime.Formats.ISO) }
294+
.parse("23:59:60").apply {
295+
if (second == 60) second = 59
296+
}.toLocalTime()
297+
println(time) // 23:59:59
298+
```
299+
300+
Because `DateTimeComponents` is provided specifically for parsing and
301+
formatting, there is no way to construct it normally. If one needs to format
302+
partial, complex or out-of-bounds data, the `format` function allows building
303+
`DateTimeComponents` specifically for formatting it:
304+
305+
```kotlin
306+
DateTimeComponents.Formats.RFC_1123.format {
307+
// the receiver of this lambda is DateTimeComponents
308+
setDate(LocalDate(2023, 1, 7))
309+
hour = 23
310+
minute = 59
311+
second = 60
312+
setOffset(UtcOffset(hours = 2))
313+
} // Sat, 7 Jan 2023 23:59:60 +0200
203314
```
204315

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

390501
After that, the project can be opened in IDEA and built with Gradle.
502+
503+
For building and running benchmarks, see [README.md](benchmarks/README.md)

core/common/test/ReadmeTest.kt

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime.test
7+
8+
import kotlinx.datetime.*
9+
import kotlinx.datetime.format.*
10+
import kotlin.test.*
11+
import kotlin.time.*
12+
13+
/**
14+
* Tests the code snippets in the README.md file.
15+
*/
16+
@Suppress("UNUSED_VARIABLE")
17+
class ReadmeTest {
18+
@Test
19+
fun testGettingCurrentMoment() {
20+
val currentMoment = Clock.System.now()
21+
}
22+
23+
@Test
24+
fun testConvertingAnInstantToLocalDateAndTimeComponents() {
25+
val currentMoment: Instant = Clock.System.now()
26+
val datetimeInUtc: LocalDateTime = currentMoment.toLocalDateTime(TimeZone.UTC)
27+
val datetimeInSystemZone: LocalDateTime = currentMoment.toLocalDateTime(TimeZone.currentSystemDefault())
28+
29+
val tzBerlin = TimeZone.of("Europe/Berlin")
30+
val datetimeInBerlin = currentMoment.toLocalDateTime(tzBerlin)
31+
32+
val kotlinReleaseDateTime = LocalDateTime(2016, 2, 15, 16, 57, 0, 0)
33+
34+
val kotlinReleaseInstant = kotlinReleaseDateTime.toInstant(TimeZone.of("UTC+3"))
35+
}
36+
37+
@Test
38+
fun testGettingLocalDateComponents() {
39+
val now: Instant = Clock.System.now()
40+
val today: LocalDate = now.toLocalDateTime(TimeZone.currentSystemDefault()).date
41+
// or shorter
42+
val today2: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
43+
44+
val knownDate = LocalDate(2020, 2, 21)
45+
}
46+
47+
@Test
48+
fun testGettingLocalTimeComponents() {
49+
val now: Instant = Clock.System.now()
50+
val thisTime: LocalTime = now.toLocalDateTime(TimeZone.currentSystemDefault()).time
51+
52+
val knownTime = LocalTime(hour = 23, minute = 59, second = 12)
53+
val timeWithNanos = LocalTime(hour = 23, minute = 59, second = 12, nanosecond = 999)
54+
val hourMinute = LocalTime(hour = 12, minute = 13)
55+
}
56+
57+
@Test
58+
fun testConvertingInstantToAndFromUnixTime() {
59+
Instant.fromEpochMilliseconds(Clock.System.now().toEpochMilliseconds())
60+
}
61+
62+
@Test
63+
fun testConvertingInstantAndLocalDateTimeToAndFromIso8601String() {
64+
val instantNow = Clock.System.now()
65+
instantNow.toString() // returns something like 2015-12-31T12:30:00Z
66+
val instantBefore = Instant.parse("2010-06-01T22:19:44.475Z")
67+
68+
LocalDateTime.parse("2010-06-01T22:19:44")
69+
LocalDate.parse("2010-06-01")
70+
LocalTime.parse("12:01:03")
71+
LocalTime.parse("12:00:03.999")
72+
assertFailsWith<IllegalArgumentException> { LocalTime.parse("12:0:03.999") }
73+
}
74+
75+
@Test
76+
fun testWorkingWithOtherStringFormats() {
77+
val dateFormat = LocalDate.Format {
78+
monthNumber(padding = Padding.SPACE)
79+
char('/')
80+
dayOfMonth()
81+
char(' ')
82+
year()
83+
}
84+
85+
val date = dateFormat.parse("12/24 2023")
86+
assertEquals("20231224", date.format(LocalDate.Formats.ISO_BASIC))
87+
}
88+
89+
@OptIn(FormatStringsInDatetimeFormats::class)
90+
@Test
91+
fun testUnicodePatterns() {
92+
assertEquals("""
93+
date(LocalDate.Formats.ISO)
94+
char('T')
95+
hour()
96+
char(':')
97+
minute()
98+
char(':')
99+
second()
100+
alternativeParsing({
101+
}) {
102+
char('.')
103+
secondFraction(3)
104+
}
105+
offset(UtcOffset.Formats.FOUR_DIGITS)
106+
""".trimIndent(),
107+
DateTimeFormat.formatAsKotlinBuilderDsl(DateTimeComponents.Format {
108+
byUnicodePattern("uuuu-MM-dd'T'HH:mm:ss[.SSS]Z")
109+
})
110+
)
111+
@OptIn(FormatStringsInDatetimeFormats::class)
112+
val dateTimeFormat = LocalDateTime.Format {
113+
byUnicodePattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]")
114+
}
115+
dateTimeFormat.parse("2023-12-24T23:59:59")
116+
}
117+
118+
@Test
119+
fun testParsingAndFormattingPartialCompoundOrOutOfBoundsData() {
120+
val yearMonth = DateTimeComponents.Format { year(); char('-'); monthNumber() }
121+
.parse("2024-01")
122+
assertEquals(2024, yearMonth.year)
123+
assertEquals(1, yearMonth.monthNumber)
124+
125+
val dateTimeOffset = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
126+
.parse("2023-01-07T23:16:15.53+02:00")
127+
assertEquals("+02:00", dateTimeOffset.toUtcOffset().toString())
128+
assertEquals("2023-01-07T23:16:15.530", dateTimeOffset.toLocalDateTime().toString())
129+
130+
val time = DateTimeComponents.Format { time(LocalTime.Formats.ISO) }
131+
.parse("23:59:60").apply {
132+
if (second == 60) second = 59
133+
}.toLocalTime()
134+
assertEquals(LocalTime(23, 59, 59), time)
135+
136+
assertEquals("Sat, 7 Jan 2023 23:59:60 +0200", DateTimeComponents.Formats.RFC_1123.format {
137+
// the receiver of this lambda is DateTimeComponents
138+
setDate(LocalDate(2023, 1, 7))
139+
hour = 23
140+
minute = 59
141+
second = 60
142+
setOffset(UtcOffset(hours = 2))
143+
})
144+
}
145+
146+
@Test
147+
fun testInstantArithmetic() {
148+
run {
149+
val now = Clock.System.now()
150+
val instantInThePast: Instant = Instant.parse("2020-01-01T00:00:00Z")
151+
val durationSinceThen: Duration = now - instantInThePast
152+
val equidistantInstantInTheFuture: Instant = now + durationSinceThen
153+
154+
val period: DateTimePeriod = instantInThePast.periodUntil(Clock.System.now(), TimeZone.UTC)
155+
156+
instantInThePast.yearsUntil(Clock.System.now(), TimeZone.UTC)
157+
instantInThePast.monthsUntil(Clock.System.now(), TimeZone.UTC)
158+
instantInThePast.daysUntil(Clock.System.now(), TimeZone.UTC)
159+
160+
val diffInMonths = instantInThePast.until(Clock.System.now(), DateTimeUnit.MONTH, TimeZone.UTC)
161+
}
162+
163+
run {
164+
val now = Clock.System.now()
165+
val systemTZ = TimeZone.currentSystemDefault()
166+
val tomorrow = now.plus(2, DateTimeUnit.DAY, systemTZ)
167+
val threeYearsAndAMonthLater = now.plus(DateTimePeriod(years = 3, months = 1), systemTZ)
168+
}
169+
}
170+
171+
@Test
172+
fun testDateArithmetic() {
173+
val date = LocalDate(2023, 1, 7)
174+
val date2 = date.plus(1, DateTimeUnit.DAY)
175+
date.plus(DatePeriod(days = 1))
176+
date.until(date2, DateTimeUnit.DAY)
177+
date.yearsUntil(date2)
178+
date.monthsUntil(date2)
179+
date.daysUntil(date2)
180+
date.periodUntil(date2)
181+
date2 - date
182+
}
183+
184+
@Test
185+
fun testDateTimeArithmetic() {
186+
val timeZone = TimeZone.of("Europe/Berlin")
187+
val localDateTime = LocalDateTime.parse("2021-03-27T02:16:20")
188+
val instant = localDateTime.toInstant(timeZone)
189+
190+
val instantOneDayLater = instant.plus(1, DateTimeUnit.DAY, timeZone)
191+
val localDateTimeOneDayLater = instantOneDayLater.toLocalDateTime(timeZone)
192+
assertEquals(LocalDateTime(2021, 3, 28, 3, 16, 20), localDateTimeOneDayLater)
193+
// 2021-03-28T03:16:20, as 02:16:20 that day is in a time gap
194+
195+
val instantTwoDaysLater = instant.plus(2, DateTimeUnit.DAY, timeZone)
196+
val localDateTimeTwoDaysLater = instantTwoDaysLater.toLocalDateTime(timeZone)
197+
assertEquals(LocalDateTime(2021, 3, 29, 2, 16, 20), localDateTimeTwoDaysLater)
198+
// 2021-03-29T02:16:20
199+
}
200+
}

license/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ The Apache 2 license (given in full in [LICENSE.txt](../LICENSE.txt)) applies to
22
by JetBrains s.r.o. and contributors. The following sections of the repository contain third-party code, to which different licenses
33
may apply:
44

5+
- Path: `core/common/src/internal/dateCalculations.kt`
6+
- Origin: implementation of date/time calculations is based on ThreeTen backport project.
7+
- License: BSD 3-Clause ([license/thirdparty/threetenbp_license.txt][threetenbp])
8+
59
- Path: `core/nativeMain/src`
610
- Origin: implementation of date/time entities is based on ThreeTen backport project.
711
- License: BSD 3-Clause ([license/thirdparty/threetenbp_license.txt][threetenbp])
@@ -13,7 +17,7 @@ may apply:
1317
- Path: `core/commonTest/src`
1418
- Origin: Some tests are derived from tests of ThreeTen backport project
1519
- License: BSD 3-Clause ([license/thirdparty/threetenbp_license.txt][threetenbp])
16-
20+
1721
- Path: `thirdparty/date`
1822
- Origin: https://github.com/HowardHinnant/date library
1923
- License: MIT ([license/thirdparty/cppdate_license.txt](thirdparty/cppdate_license.txt))
@@ -22,6 +26,6 @@ may apply:
2226
- Origin: time zone name mappings for Windows are generated from
2327
https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml
2428
- License: Unicode ([license/thirdparty/unicode_license.txt](thirdparty/unicode_license.txt))
25-
26-
27-
[threetenbp]: thirdparty/threetenbp_license.txt
29+
30+
31+
[threetenbp]: thirdparty/threetenbp_license.txt

0 commit comments

Comments
 (0)