Skip to content

Implement YearMonth #457

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

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The library provides a basic set of types for working with date and time:
- `Clock` to obtain the current instant;
- `LocalDateTime` to represent date and time components without a reference to the particular time zone;
- `LocalDate` to represent the components of date only;
- `YearMonth` to represent only the year and month components;
- `LocalTime` to represent the components of time only;
- `TimeZone` and `FixedOffsetTimeZone` provide time zone information to convert between `Instant` and `LocalDateTime`;
- `Month` and `DayOfWeek` enums;
Expand Down Expand Up @@ -67,6 +68,9 @@ Here is some basic advice on how to choose which of the date-carrying types to u

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

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

- Use `LocalTime` to represent the time of an event that does not have a specific date associated with it.

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

### Getting year and month components

A `YearMonth` represents a year and month without a day. You can obtain one from a `LocalDate`
by taking its `yearMonth` property.

```kotlin
val day = LocalDate(2020, 2, 21)
val yearMonth: YearMonth = day.yearMonth
```

### Getting local time components

A `LocalTime` represents local time without date. You can obtain one from an `Instant`
Expand Down Expand Up @@ -273,10 +287,10 @@ collection of all datetime 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 monthDay = DateTimeComponents.Format { monthNumber(); char('/'); dayOfMonth() }
.parse("12/25")
println(monthDay.dayOfMonth) // 25
println(monthDay.monthNumber) // 12

val dateTimeOffset = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
.parse("2023-01-07T23:16:15.53+02:00")
Expand Down
173 changes: 166 additions & 7 deletions core/api/kotlinx-datetime.api

Large diffs are not rendered by default.

153 changes: 148 additions & 5 deletions core/api/kotlinx-datetime.klib.api

Large diffs are not rendered by default.

41 changes: 7 additions & 34 deletions core/common/src/LocalDateRange.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@

package kotlinx.datetime

import kotlinx.datetime.internal.clampToInt
import kotlinx.datetime.internal.safeAdd
import kotlinx.datetime.internal.safeMultiplyOrClamp
import kotlinx.datetime.internal.*
import kotlin.random.Random
import kotlin.random.nextLong

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

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

return longProgression.contains(value.toEpochDays())
return longProgression.containsUnsafe(value.toEpochDays())
}

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

/**
* Returns a random [LocalDate] within the bounds of the [LocalDateProgression] or null if the progression is empty.
Expand All @@ -277,29 +274,5 @@ public fun LocalDateProgression.random(random: Random = Random): LocalDate =
*
* @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random
*/
public fun LocalDateProgression.randomOrNull(random: Random = Random): LocalDate? = longProgression.randomOrNull(random)
?.let(LocalDate.Companion::fromEpochDays)

// this implementation is incorrect in general
// (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).random()` throws an exception),
// but for the range of epoch days in LocalDate it's good enough
private fun LongProgression.random(random: Random = Random): Long =
random.nextLong(0L..(last - first) / step) * step + first

// incorrect in general; see `random` just above
private fun LongProgression.randomOrNull(random: Random = Random): Long? = if (isEmpty()) null else random(random)

// this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).step(5).contains(2)`
// returns `false` incorrectly https://www.wolframalpha.com/input?i=-2%5E63+%2B+1844674407370955162+*+5),
// but for the range of epoch days in LocalDate it's good enough
private fun LongProgression.contains(value: Long): Boolean =
value in (if (step > 0) first..last else last..first) && (value - first) % step == 0L

// this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0),
// but for the range of epoch days in LocalDate it's good enough
private val LongProgression.size: Int
get() = if (isEmpty()) 0 else try {
(safeAdd(last, -first) / step + 1).clampToInt()
} catch (e: ArithmeticException) {
Int.MAX_VALUE
}
public fun LocalDateProgression.randomOrNull(random: Random = Random): LocalDate? = longProgression.randomUnsafeOrNull(random)
?.let(LocalDate.Companion::fromEpochDays)
Loading