Skip to content

Commit 4956c94

Browse files
authored
Implement YearMonth (#457)
Fixes #168 Fixes #184
1 parent 3e68613 commit 4956c94

38 files changed

+3029
-498
lines changed

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ The library provides a basic set of types for working with date and time:
4040
- `Clock` to obtain the current instant;
4141
- `LocalDateTime` to represent date and time components without a reference to the particular time zone;
4242
- `LocalDate` to represent the components of date only;
43+
- `YearMonth` to represent only the year and month components;
4344
- `LocalTime` to represent the components of time only;
4445
- `TimeZone` and `FixedOffsetTimeZone` provide time zone information to convert between `Instant` and `LocalDateTime`;
4546
- `Month` and `DayOfWeek` enums;
@@ -67,6 +68,9 @@ Here is some basic advice on how to choose which of the date-carrying types to u
6768

6869
- Use `LocalDate` to represent the date of an event that does not have a specific time associated with it (like a birth date).
6970

71+
- Use `YearMonth` to represent the year and month of an event that does not have a specific day associated with it
72+
or has a day-of-month that is inferred from the context (like a credit card expiration date).
73+
7074
- Use `LocalTime` to represent the time of an event that does not have a specific date associated with it.
7175

7276
## Operations
@@ -150,6 +154,16 @@ Note, that today's date really depends on the time zone in which you're observin
150154
val knownDate = LocalDate(2020, 2, 21)
151155
```
152156

157+
### Getting year and month components
158+
159+
A `YearMonth` represents a year and month without a day. You can obtain one from a `LocalDate`
160+
by taking its `yearMonth` property.
161+
162+
```kotlin
163+
val day = LocalDate(2020, 2, 21)
164+
val yearMonth: YearMonth = day.yearMonth
165+
```
166+
153167
### Getting local time components
154168

155169
A `LocalTime` represents local time without date. You can obtain one from an `Instant`
@@ -273,10 +287,10 @@ collection of all datetime fields, can be used instead.
273287
```kotlin
274288
// import kotlinx.datetime.format.*
275289

276-
val yearMonth = DateTimeComponents.Format { year(); char('-'); monthNumber() }
277-
.parse("2024-01")
278-
println(yearMonth.year)
279-
println(yearMonth.monthNumber)
290+
val monthDay = DateTimeComponents.Format { monthNumber(); char('/'); day() }
291+
.parse("12/25")
292+
println(monthDay.day) // 25
293+
println(monthDay.monthNumber) // 12
280294

281295
val dateTimeOffset = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
282296
.parse("2023-01-07T23:16:15.53+02:00")

core/api/kotlinx-datetime.api

Lines changed: 166 additions & 7 deletions
Large diffs are not rendered by default.

core/api/kotlinx-datetime.klib.api

Lines changed: 148 additions & 5 deletions
Large diffs are not rendered by default.

core/common/src/LocalDate.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import kotlin.internal.*
3535
*
3636
* ### Platform specifics
3737
*
38+
* The range of supported years is unspecified, but at least is enough to represent dates of all instants between
39+
* [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE].
40+
*
3841
* On the JVM,
3942
* there are `LocalDate.toJavaLocalDate()` and `java.time.LocalDate.toKotlinLocalDate()`
4043
* extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose.

core/common/src/LocalDateRange.kt

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55

66
package kotlinx.datetime
77

8-
import kotlinx.datetime.internal.clampToInt
9-
import kotlinx.datetime.internal.safeAdd
10-
import kotlinx.datetime.internal.safeMultiplyOrClamp
8+
import kotlinx.datetime.internal.*
119
import kotlin.random.Random
12-
import kotlin.random.nextLong
1310

1411
private class LocalDateProgressionIterator(private val iterator: LongIterator) : Iterator<LocalDate> {
1512
override fun hasNext(): Boolean = iterator.hasNext()
@@ -67,7 +64,7 @@ internal constructor(internal val longProgression: LongProgression) : Collection
6764
* Returns [Int.MAX_VALUE] if the number of dates overflows [Int]
6865
*/
6966
override val size: Int
70-
get() = longProgression.size
67+
get() = longProgression.sizeUnsafe
7168

7269
/**
7370
* Returns true iff every element in [elements] is a member of the progression.
@@ -82,7 +79,7 @@ internal constructor(internal val longProgression: LongProgression) : Collection
8279
@Suppress("USELESS_CAST")
8380
if ((value as Any?) !is LocalDate) return false
8481

85-
return longProgression.contains(value.toEpochDays())
82+
return longProgression.containsUnsafe(value.toEpochDays())
8683
}
8784

8885
override fun equals(other: Any?): Boolean =
@@ -261,13 +258,13 @@ public infix fun LocalDate.downTo(that: LocalDate): LocalDateProgression =
261258
* Takes the step into account;
262259
* will not return any value within the range that would be skipped over by the progression.
263260
*
264-
* @throws IllegalArgumentException if the progression is empty.
261+
* @throws NoSuchElementException if the progression is empty.
265262
*
266263
* @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random
267264
*/
268265
public fun LocalDateProgression.random(random: Random = Random): LocalDate =
269266
if (isEmpty()) throw NoSuchElementException("Cannot get random in empty range: $this")
270-
else longProgression.random(random).let(LocalDate.Companion::fromEpochDays)
267+
else longProgression.randomUnsafe(random).let(LocalDate.Companion::fromEpochDays)
271268

272269
/**
273270
* Returns a random [LocalDate] within the bounds of the [LocalDateProgression] or null if the progression is empty.
@@ -277,29 +274,5 @@ public fun LocalDateProgression.random(random: Random = Random): LocalDate =
277274
*
278275
* @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random
279276
*/
280-
public fun LocalDateProgression.randomOrNull(random: Random = Random): LocalDate? = longProgression.randomOrNull(random)
281-
?.let(LocalDate.Companion::fromEpochDays)
282-
283-
// this implementation is incorrect in general
284-
// (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).random()` throws an exception),
285-
// but for the range of epoch days in LocalDate it's good enough
286-
private fun LongProgression.random(random: Random = Random): Long =
287-
random.nextLong(0L..(last - first) / step) * step + first
288-
289-
// incorrect in general; see `random` just above
290-
private fun LongProgression.randomOrNull(random: Random = Random): Long? = if (isEmpty()) null else random(random)
291-
292-
// this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).step(5).contains(2)`
293-
// returns `false` incorrectly https://www.wolframalpha.com/input?i=-2%5E63+%2B+1844674407370955162+*+5),
294-
// but for the range of epoch days in LocalDate it's good enough
295-
private fun LongProgression.contains(value: Long): Boolean =
296-
value in (if (step > 0) first..last else last..first) && (value - first) % step == 0L
297-
298-
// this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0),
299-
// but for the range of epoch days in LocalDate it's good enough
300-
private val LongProgression.size: Int
301-
get() = if (isEmpty()) 0 else try {
302-
(safeAdd(last, -first) / step + 1).clampToInt()
303-
} catch (e: ArithmeticException) {
304-
Int.MAX_VALUE
305-
}
277+
public fun LocalDateProgression.randomOrNull(random: Random = Random): LocalDate? = longProgression.randomUnsafeOrNull(random)
278+
?.let(LocalDate.Companion::fromEpochDays)

core/common/src/LocalDateTime.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import kotlin.jvm.JvmName
6666
*
6767
* ### Platform specifics
6868
*
69-
* The range of supported years is platform-dependent, but at least is enough to represent dates of all instants between
69+
* The range of supported years is unspecified, but at least is enough to represent dates of all instants between
7070
* [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE].
7171
*
7272
* On the JVM, there are `LocalDateTime.toJavaLocalDateTime()` and `java.time.LocalDateTime.toKotlinLocalDateTime()`

0 commit comments

Comments
 (0)