Skip to content
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

Implement LocalDate.fromEpochDays #214

Merged
merged 4 commits into from
Jun 24, 2022
Merged
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
2 changes: 1 addition & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ kotlin {
nodejs {
testTask {
useMocha {
timeout = "5s"
timeout = "30s"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The highest recorded test duration value was 8.6s, so probably 10-15 s should be fine

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I just don't see the point in this timeout at all. Don't understand what it brings to the table. So, I increased it to the point where it will most certainly not bother us. Do you have some ideas about why this limit could be useful? If so, maybe it makes sense to add it to the other targets as well?

}
}
}
Expand Down
1 change: 1 addition & 0 deletions core/common/src/DateTimePeriod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package kotlinx.datetime

import kotlinx.datetime.internal.*
import kotlinx.datetime.serializers.DatePeriodIso8601Serializer
import kotlinx.datetime.serializers.DateTimePeriodIso8601Serializer
import kotlin.math.*
Expand Down
1 change: 1 addition & 0 deletions core/common/src/DateTimeUnit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package kotlinx.datetime

import kotlinx.datetime.internal.safeMultiply
import kotlinx.datetime.serializers.*
import kotlinx.serialization.Serializable
import kotlin.time.*
Expand Down
3 changes: 3 additions & 0 deletions core/common/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package kotlinx.datetime

import kotlinx.datetime.internal.*
import kotlinx.datetime.serializers.InstantIso8601Serializer
import kotlinx.serialization.Serializable
import kotlin.time.*
Expand Down Expand Up @@ -126,6 +127,8 @@ public expect class Instant : Comparable<Instant> {
* Returns an [Instant] that is [epochMilliseconds] number of milliseconds from the epoch instant `1970-01-01T00:00:00Z`.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
*
* @see Instant.toEpochMilliseconds
*/
public fun fromEpochMilliseconds(epochMilliseconds: Long): Instant

Expand Down
18 changes: 18 additions & 0 deletions core/common/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public expect class LocalDate : Comparable<LocalDate> {
*/
public fun parse(isoString: String): LocalDate

/**
* Returns a [LocalDate] that is [epochDays] number of days from the epoch day `1970-01-01`.
*
* @throws IllegalArgumentException if the result exceeds the platform-specific boundaries of [LocalDate].
*
* @see LocalDate.toEpochDays
*/
public fun fromEpochDays(epochDays: Int): LocalDate

internal val MIN: LocalDate
internal val MAX: LocalDate
}
Expand Down Expand Up @@ -79,6 +88,15 @@ public expect class LocalDate : Comparable<LocalDate> {
/** Returns the day-of-year component of the date. */
public val dayOfYear: Int

/**
* Returns the number of days since the epoch day `1970-01-01`.
*
* If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result or [Int.MIN_VALUE] for a negative result.
*
* @see LocalDate.fromEpochDays
*/
public fun toEpochDays(): Int

/**
* Compares `this` date with the [other] date.
* Returns zero if this date represent the same day as the other (i.e. equal to other),
Expand Down
41 changes: 41 additions & 0 deletions core/common/src/internal/dateCalculations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2019-2022 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.internal

internal const val SECONDS_PER_HOUR = 60 * 60

internal const val SECONDS_PER_MINUTE = 60

internal const val MINUTES_PER_HOUR = 60

internal const val HOURS_PER_DAY = 24

internal const val SECONDS_PER_DAY: Int = SECONDS_PER_HOUR * HOURS_PER_DAY

internal const val NANOS_PER_ONE = 1_000_000_000
internal const val NANOS_PER_MILLI = 1_000_000
internal const val MILLIS_PER_ONE = 1_000

internal const val NANOS_PER_DAY: Long = NANOS_PER_ONE * SECONDS_PER_DAY.toLong()

internal const val NANOS_PER_MINUTE: Long = NANOS_PER_ONE * SECONDS_PER_MINUTE.toLong()

internal const val NANOS_PER_HOUR = NANOS_PER_ONE * SECONDS_PER_HOUR.toLong()

internal const val MILLIS_PER_DAY: Int = SECONDS_PER_DAY * MILLIS_PER_ONE

// org.threeten.bp.chrono.IsoChronology#isLeapYear
internal fun isLeapYear(year: Int): Boolean {
val prolepticYear: Long = year.toLong()
return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L)
}

internal fun Int.monthLength(isLeapYear: Boolean): Int =
when (this) {
2 -> if (isLeapYear) 29 else 28
4, 6, 9, 11 -> 30
else -> 31
}
26 changes: 2 additions & 24 deletions core/common/src/math.kt → core/common/src/internal/math.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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
package kotlinx.datetime.internal

internal fun Long.clampToInt(): Int =
when {
Expand All @@ -12,28 +12,6 @@ internal fun Long.clampToInt(): Int =
else -> toInt()
}

internal const val SECONDS_PER_HOUR = 60 * 60

internal const val SECONDS_PER_MINUTE = 60

internal const val MINUTES_PER_HOUR = 60

internal const val HOURS_PER_DAY = 24

internal const val SECONDS_PER_DAY: Int = SECONDS_PER_HOUR * HOURS_PER_DAY

internal const val NANOS_PER_ONE = 1_000_000_000
internal const val NANOS_PER_MILLI = 1_000_000
internal const val MILLIS_PER_ONE = 1_000

internal const val NANOS_PER_DAY: Long = NANOS_PER_ONE * SECONDS_PER_DAY.toLong()

internal const val NANOS_PER_MINUTE: Long = NANOS_PER_ONE * SECONDS_PER_MINUTE.toLong()

internal const val NANOS_PER_HOUR = NANOS_PER_ONE * SECONDS_PER_HOUR.toLong()

internal const val MILLIS_PER_DAY: Int = SECONDS_PER_DAY * MILLIS_PER_ONE

internal expect fun safeMultiply(a: Long, b: Long): Long
internal expect fun safeMultiply(a: Int, b: Int): Int
internal expect fun safeAdd(a: Long, b: Long): Long
Expand Down Expand Up @@ -200,4 +178,4 @@ internal fun multiplyAndAdd(d: Long, n: Long, r: Long): Long {
mr -= n
}
return safeAdd(safeMultiply(md, n), mr)
}
}
1 change: 1 addition & 0 deletions core/common/test/InstantTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package kotlinx.datetime.test

import kotlinx.datetime.*
import kotlinx.datetime.Clock // currently, requires an explicit import due to a conflict with the deprecated Clock from kotlin.time
import kotlinx.datetime.internal.*
import kotlin.random.*
import kotlin.test.*
import kotlin.time.*
Expand Down
90 changes: 87 additions & 3 deletions core/common/test/LocalDateTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package kotlinx.datetime.test

import kotlinx.datetime.*
import kotlinx.datetime.internal.*
import kotlin.random.*
import kotlin.test.*

Expand Down Expand Up @@ -36,8 +37,9 @@ class LocalDateTest {

@Test
fun parseIsoString() {
fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int, dayOfYear: Int) {
fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int? = null, dayOfYear: Int? = null) {
checkComponents(LocalDate.parse(value), year, month, day, dayOfWeek, dayOfYear)
assertEquals(value, LocalDate(year, month, day).toString())
}
checkParsedComponents("2019-10-01", 2019, 10, 1, 2, 274)
checkParsedComponents("2016-02-29", 2016, 2, 29, 1, 60)
Expand All @@ -49,6 +51,17 @@ class LocalDateTest {
assertInvalidFormat { LocalDate.parse("2017-10--01") }
// this date is currently larger than the largest representable one any of the platforms:
assertInvalidFormat { LocalDate.parse("+1000000000-10-01") }
// threetenbp
checkParsedComponents("2008-07-05", 2008, 7, 5)
checkParsedComponents("2007-12-31", 2007, 12, 31)
checkParsedComponents("0999-12-31", 999, 12, 31)
checkParsedComponents("-0001-01-02", -1, 1, 2)
checkParsedComponents("9999-12-31", 9999, 12, 31)
checkParsedComponents("-9999-12-31", -9999, 12, 31)
checkParsedComponents("+10000-01-01", 10000, 1, 1)
checkParsedComponents("-10000-01-01", -10000, 1, 1)
checkParsedComponents("+123456-01-01", 123456, 1, 1)
checkParsedComponents("-123456-01-01", -123456, 1, 1)
}

@Test
Expand Down Expand Up @@ -221,9 +234,61 @@ class LocalDateTest {
assertEquals(Int.MIN_VALUE, LocalDate.MAX.until(LocalDate.MIN, DateTimeUnit.DAY))
}
}
}

@Test
fun fromEpochDays() {
/** This test uses [LocalDate.next] and [LocalDate.previous] and not [LocalDate.plus] because, on Native,
* [LocalDate.plus] is implemented via [LocalDate.toEpochDays]/[LocalDate.fromEpochDays], and so it's better to
* test those independently. */
if (LocalDate.fromEpochDays(0).daysUntil(LocalDate.MIN) > Int.MIN_VALUE) {
assertEquals(LocalDate.MIN, LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays()))
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays() - 1) }
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(Int.MIN_VALUE) }
}
if (LocalDate.fromEpochDays(0).daysUntil(LocalDate.MAX) < Int.MAX_VALUE) {
assertEquals(LocalDate.MAX, LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays()))
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays() + 1) }
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(Int.MAX_VALUE) }
}
val eraBeginning = -678941 - 40587
assertEquals(LocalDate(1970, 1, 1), LocalDate.fromEpochDays(0))
assertEquals(LocalDate(0, 1, 1), LocalDate.fromEpochDays(eraBeginning))
assertEquals(LocalDate(-1, 12, 31), LocalDate.fromEpochDays(eraBeginning - 1))
var test = LocalDate(0, 1, 1)
for (i in eraBeginning..699999) {
assertEquals(test, LocalDate.fromEpochDays(i))
test = test.next
}
test = LocalDate(0, 1, 1)
for (i in eraBeginning downTo -2000000 + 1) {
assertEquals(test, LocalDate.fromEpochDays(i))
test = test.previous
}
}

// threetenbp
@Test
fun toEpochDays() {
/** This test uses [LocalDate.next] and [LocalDate.previous] and not [LocalDate.plus] because, on Native,
* [LocalDate.plus] is implemented via [LocalDate.toEpochDays]/[LocalDate.fromEpochDays], and so it's better to
* test those independently. */
val startOfEra = -678941 - 40587
var date = LocalDate(0, 1, 1)
for (i in startOfEra..699999) {
assertEquals(i, date.toEpochDays())
date = date.next
}
date = LocalDate(0, 1, 1)
for (i in startOfEra downTo -2000000 + 1) {
assertEquals(i, date.toEpochDays())
date = date.previous
}
assertEquals(-40587, LocalDate(1858, 11, 17).toEpochDays())
assertEquals(-678575 - 40587, LocalDate(1, 1, 1).toEpochDays())
assertEquals(49987 - 40587, LocalDate(1995, 9, 27).toEpochDays())
assertEquals(0, LocalDate(1970, 1, 1).toEpochDays())
assertEquals(-678942 - 40587, LocalDate(-1, 12, 31).toEpochDays())
}
}

fun checkInvalidDate(constructor: (year: Int, month: Int, day: Int) -> LocalDate) {
assertFailsWith<IllegalArgumentException> { constructor(2007, 2, 29) }
Expand All @@ -236,3 +301,22 @@ fun checkInvalidDate(constructor: (year: Int, month: Int, day: Int) -> LocalDate
assertFailsWith<IllegalArgumentException> { constructor(2007, 0, 1) }
assertFailsWith<IllegalArgumentException> { constructor(2007, 13, 1) }
}

private val LocalDate.next: LocalDate get() =
if (dayOfMonth != monthNumber.monthLength(isLeapYear(year))) {
LocalDate(year, monthNumber, dayOfMonth + 1)
} else if (monthNumber != 12) {
LocalDate(year, monthNumber + 1, 1)
} else {
LocalDate(year + 1, 1, 1)
}

private val LocalDate.previous: LocalDate get() =
if (dayOfMonth != 1) {
LocalDate(year, monthNumber, dayOfMonth - 1)
} else if (monthNumber != 1) {
val newMonthNumber = monthNumber - 1
LocalDate(year, newMonthNumber, newMonthNumber.monthLength(isLeapYear(year)))
} else {
LocalDate(year - 1, 12, 31)
}
1 change: 1 addition & 0 deletions core/common/test/LocalTimeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package kotlinx.datetime.test

import kotlinx.datetime.*
import kotlinx.datetime.internal.*
import kotlin.math.*
import kotlin.random.*
import kotlin.test.*
Expand Down
2 changes: 1 addition & 1 deletion core/common/test/MultiplyAndDivideTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
package kotlinx.datetime.test
import kotlin.random.*
import kotlin.test.*
import kotlinx.datetime.*
import kotlinx.datetime.internal.*

class MultiplyAndDivideTest {

Expand Down
2 changes: 2 additions & 0 deletions core/js/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import kotlinx.datetime.internal.JSJoda.OffsetDateTime as jtOffsetDateTime
import kotlinx.datetime.internal.JSJoda.Duration as jtDuration
import kotlinx.datetime.internal.JSJoda.Clock as jtClock
import kotlinx.datetime.internal.JSJoda.ChronoUnit
import kotlinx.datetime.internal.safeAdd
import kotlinx.datetime.internal.*
import kotlinx.datetime.serializers.InstantIso8601Serializer
import kotlinx.serialization.Serializable
import kotlin.time.*
Expand Down
9 changes: 9 additions & 0 deletions core/js/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa

internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN)
internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX)

public actual fun fromEpochDays(epochDays: Int): LocalDate = try {
LocalDate(jtLocalDate.ofEpochDay(epochDays))
} catch (e: Throwable) {
if (e.isJodaDateTimeException()) throw IllegalArgumentException(e)
throw e
}
}

public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) :
Expand Down Expand Up @@ -49,6 +56,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
actual override fun toString(): String = value.toString()

actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value).toInt()

public actual fun toEpochDays(): Int = value.toEpochDay().toInt()
}

public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plusNumber(1, unit)
Expand Down
1 change: 1 addition & 0 deletions core/js/src/LocalTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package kotlinx.datetime

import kotlinx.datetime.internal.*
import kotlinx.datetime.serializers.LocalTimeIso8601Serializer
import kotlinx.serialization.Serializable
import kotlinx.datetime.internal.JSJoda.LocalTime as jtLocalTime
Expand Down
4 changes: 2 additions & 2 deletions core/js/src/mathJs.kt → core/js/src/internal/mathJs.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/*
* Copyright 2019-2020 JetBrains s.r.o.
* Copyright 2019-2022 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
package kotlinx.datetime.internal

/**
* Safely adds two long values.
Expand Down
2 changes: 2 additions & 0 deletions core/jvm/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

package kotlinx.datetime

import kotlinx.datetime.internal.safeMultiply
import kotlinx.datetime.internal.*
import kotlinx.datetime.serializers.InstantIso8601Serializer
import kotlinx.serialization.Serializable
import java.time.DateTimeException
Expand Down
Loading