Skip to content

Commit fea8ce8

Browse files
authored
Implement LocalDate.fromEpochDays (#214)
1 parent 66e663d commit fea8ce8

31 files changed

+264
-241
lines changed

core/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ kotlin {
7070
nodejs {
7171
testTask {
7272
useMocha {
73-
timeout = "5s"
73+
timeout = "30s"
7474
}
7575
}
7676
}

core/common/src/DateTimePeriod.kt

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package kotlinx.datetime
77

8+
import kotlinx.datetime.internal.*
89
import kotlinx.datetime.serializers.DatePeriodIso8601Serializer
910
import kotlinx.datetime.serializers.DateTimePeriodIso8601Serializer
1011
import kotlin.math.*

core/common/src/DateTimeUnit.kt

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package kotlinx.datetime
77

8+
import kotlinx.datetime.internal.safeMultiply
89
import kotlinx.datetime.serializers.*
910
import kotlinx.serialization.Serializable
1011
import kotlin.time.*

core/common/src/Instant.kt

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package kotlinx.datetime
77

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

core/common/src/LocalDate.kt

+18
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ public expect class LocalDate : Comparable<LocalDate> {
3232
*/
3333
public fun parse(isoString: String): LocalDate
3434

35+
/**
36+
* Returns a [LocalDate] that is [epochDays] number of days from the epoch day `1970-01-01`.
37+
*
38+
* @throws IllegalArgumentException if the result exceeds the platform-specific boundaries of [LocalDate].
39+
*
40+
* @see LocalDate.toEpochDays
41+
*/
42+
public fun fromEpochDays(epochDays: Int): LocalDate
43+
3544
internal val MIN: LocalDate
3645
internal val MAX: LocalDate
3746
}
@@ -79,6 +88,15 @@ public expect class LocalDate : Comparable<LocalDate> {
7988
/** Returns the day-of-year component of the date. */
8089
public val dayOfYear: Int
8190

91+
/**
92+
* Returns the number of days since the epoch day `1970-01-01`.
93+
*
94+
* If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result or [Int.MIN_VALUE] for a negative result.
95+
*
96+
* @see LocalDate.fromEpochDays
97+
*/
98+
public fun toEpochDays(): Int
99+
82100
/**
83101
* Compares `this` date with the [other] date.
84102
* Returns zero if this date represent the same day as the other (i.e. equal to other),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2019-2022 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.internal
7+
8+
internal const val SECONDS_PER_HOUR = 60 * 60
9+
10+
internal const val SECONDS_PER_MINUTE = 60
11+
12+
internal const val MINUTES_PER_HOUR = 60
13+
14+
internal const val HOURS_PER_DAY = 24
15+
16+
internal const val SECONDS_PER_DAY: Int = SECONDS_PER_HOUR * HOURS_PER_DAY
17+
18+
internal const val NANOS_PER_ONE = 1_000_000_000
19+
internal const val NANOS_PER_MILLI = 1_000_000
20+
internal const val MILLIS_PER_ONE = 1_000
21+
22+
internal const val NANOS_PER_DAY: Long = NANOS_PER_ONE * SECONDS_PER_DAY.toLong()
23+
24+
internal const val NANOS_PER_MINUTE: Long = NANOS_PER_ONE * SECONDS_PER_MINUTE.toLong()
25+
26+
internal const val NANOS_PER_HOUR = NANOS_PER_ONE * SECONDS_PER_HOUR.toLong()
27+
28+
internal const val MILLIS_PER_DAY: Int = SECONDS_PER_DAY * MILLIS_PER_ONE
29+
30+
// org.threeten.bp.chrono.IsoChronology#isLeapYear
31+
internal fun isLeapYear(year: Int): Boolean {
32+
val prolepticYear: Long = year.toLong()
33+
return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L)
34+
}
35+
36+
internal fun Int.monthLength(isLeapYear: Boolean): Int =
37+
when (this) {
38+
2 -> if (isLeapYear) 29 else 28
39+
4, 6, 9, 11 -> 30
40+
else -> 31
41+
}

core/common/src/math.kt renamed to core/common/src/internal/math.kt

+2-24
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
55

6-
package kotlinx.datetime
6+
package kotlinx.datetime.internal
77

88
internal fun Long.clampToInt(): Int =
99
when {
@@ -12,28 +12,6 @@ internal fun Long.clampToInt(): Int =
1212
else -> toInt()
1313
}
1414

15-
internal const val SECONDS_PER_HOUR = 60 * 60
16-
17-
internal const val SECONDS_PER_MINUTE = 60
18-
19-
internal const val MINUTES_PER_HOUR = 60
20-
21-
internal const val HOURS_PER_DAY = 24
22-
23-
internal const val SECONDS_PER_DAY: Int = SECONDS_PER_HOUR * HOURS_PER_DAY
24-
25-
internal const val NANOS_PER_ONE = 1_000_000_000
26-
internal const val NANOS_PER_MILLI = 1_000_000
27-
internal const val MILLIS_PER_ONE = 1_000
28-
29-
internal const val NANOS_PER_DAY: Long = NANOS_PER_ONE * SECONDS_PER_DAY.toLong()
30-
31-
internal const val NANOS_PER_MINUTE: Long = NANOS_PER_ONE * SECONDS_PER_MINUTE.toLong()
32-
33-
internal const val NANOS_PER_HOUR = NANOS_PER_ONE * SECONDS_PER_HOUR.toLong()
34-
35-
internal const val MILLIS_PER_DAY: Int = SECONDS_PER_DAY * MILLIS_PER_ONE
36-
3715
internal expect fun safeMultiply(a: Long, b: Long): Long
3816
internal expect fun safeMultiply(a: Int, b: Int): Int
3917
internal expect fun safeAdd(a: Long, b: Long): Long
@@ -200,4 +178,4 @@ internal fun multiplyAndAdd(d: Long, n: Long, r: Long): Long {
200178
mr -= n
201179
}
202180
return safeAdd(safeMultiply(md, n), mr)
203-
}
181+
}

core/common/test/InstantTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package kotlinx.datetime.test
77

88
import kotlinx.datetime.*
99
import kotlinx.datetime.Clock // currently, requires an explicit import due to a conflict with the deprecated Clock from kotlin.time
10+
import kotlinx.datetime.internal.*
1011
import kotlin.random.*
1112
import kotlin.test.*
1213
import kotlin.time.*

core/common/test/LocalDateTest.kt

+87-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.test
77

88
import kotlinx.datetime.*
9+
import kotlinx.datetime.internal.*
910
import kotlin.random.*
1011
import kotlin.test.*
1112

@@ -36,8 +37,9 @@ class LocalDateTest {
3637

3738
@Test
3839
fun parseIsoString() {
39-
fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int, dayOfYear: Int) {
40+
fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int? = null, dayOfYear: Int? = null) {
4041
checkComponents(LocalDate.parse(value), year, month, day, dayOfWeek, dayOfYear)
42+
assertEquals(value, LocalDate(year, month, day).toString())
4143
}
4244
checkParsedComponents("2019-10-01", 2019, 10, 1, 2, 274)
4345
checkParsedComponents("2016-02-29", 2016, 2, 29, 1, 60)
@@ -49,6 +51,17 @@ class LocalDateTest {
4951
assertInvalidFormat { LocalDate.parse("2017-10--01") }
5052
// this date is currently larger than the largest representable one any of the platforms:
5153
assertInvalidFormat { LocalDate.parse("+1000000000-10-01") }
54+
// threetenbp
55+
checkParsedComponents("2008-07-05", 2008, 7, 5)
56+
checkParsedComponents("2007-12-31", 2007, 12, 31)
57+
checkParsedComponents("0999-12-31", 999, 12, 31)
58+
checkParsedComponents("-0001-01-02", -1, 1, 2)
59+
checkParsedComponents("9999-12-31", 9999, 12, 31)
60+
checkParsedComponents("-9999-12-31", -9999, 12, 31)
61+
checkParsedComponents("+10000-01-01", 10000, 1, 1)
62+
checkParsedComponents("-10000-01-01", -10000, 1, 1)
63+
checkParsedComponents("+123456-01-01", 123456, 1, 1)
64+
checkParsedComponents("-123456-01-01", -123456, 1, 1)
5265
}
5366

5467
@Test
@@ -221,9 +234,61 @@ class LocalDateTest {
221234
assertEquals(Int.MIN_VALUE, LocalDate.MAX.until(LocalDate.MIN, DateTimeUnit.DAY))
222235
}
223236
}
224-
}
225-
237+
@Test
238+
fun fromEpochDays() {
239+
/** This test uses [LocalDate.next] and [LocalDate.previous] and not [LocalDate.plus] because, on Native,
240+
* [LocalDate.plus] is implemented via [LocalDate.toEpochDays]/[LocalDate.fromEpochDays], and so it's better to
241+
* test those independently. */
242+
if (LocalDate.fromEpochDays(0).daysUntil(LocalDate.MIN) > Int.MIN_VALUE) {
243+
assertEquals(LocalDate.MIN, LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays()))
244+
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays() - 1) }
245+
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(Int.MIN_VALUE) }
246+
}
247+
if (LocalDate.fromEpochDays(0).daysUntil(LocalDate.MAX) < Int.MAX_VALUE) {
248+
assertEquals(LocalDate.MAX, LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays()))
249+
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays() + 1) }
250+
assertFailsWith<IllegalArgumentException> { LocalDate.fromEpochDays(Int.MAX_VALUE) }
251+
}
252+
val eraBeginning = -678941 - 40587
253+
assertEquals(LocalDate(1970, 1, 1), LocalDate.fromEpochDays(0))
254+
assertEquals(LocalDate(0, 1, 1), LocalDate.fromEpochDays(eraBeginning))
255+
assertEquals(LocalDate(-1, 12, 31), LocalDate.fromEpochDays(eraBeginning - 1))
256+
var test = LocalDate(0, 1, 1)
257+
for (i in eraBeginning..699999) {
258+
assertEquals(test, LocalDate.fromEpochDays(i))
259+
test = test.next
260+
}
261+
test = LocalDate(0, 1, 1)
262+
for (i in eraBeginning downTo -2000000 + 1) {
263+
assertEquals(test, LocalDate.fromEpochDays(i))
264+
test = test.previous
265+
}
266+
}
226267

268+
// threetenbp
269+
@Test
270+
fun toEpochDays() {
271+
/** This test uses [LocalDate.next] and [LocalDate.previous] and not [LocalDate.plus] because, on Native,
272+
* [LocalDate.plus] is implemented via [LocalDate.toEpochDays]/[LocalDate.fromEpochDays], and so it's better to
273+
* test those independently. */
274+
val startOfEra = -678941 - 40587
275+
var date = LocalDate(0, 1, 1)
276+
for (i in startOfEra..699999) {
277+
assertEquals(i, date.toEpochDays())
278+
date = date.next
279+
}
280+
date = LocalDate(0, 1, 1)
281+
for (i in startOfEra downTo -2000000 + 1) {
282+
assertEquals(i, date.toEpochDays())
283+
date = date.previous
284+
}
285+
assertEquals(-40587, LocalDate(1858, 11, 17).toEpochDays())
286+
assertEquals(-678575 - 40587, LocalDate(1, 1, 1).toEpochDays())
287+
assertEquals(49987 - 40587, LocalDate(1995, 9, 27).toEpochDays())
288+
assertEquals(0, LocalDate(1970, 1, 1).toEpochDays())
289+
assertEquals(-678942 - 40587, LocalDate(-1, 12, 31).toEpochDays())
290+
}
291+
}
227292

228293
fun checkInvalidDate(constructor: (year: Int, month: Int, day: Int) -> LocalDate) {
229294
assertFailsWith<IllegalArgumentException> { constructor(2007, 2, 29) }
@@ -236,3 +301,22 @@ fun checkInvalidDate(constructor: (year: Int, month: Int, day: Int) -> LocalDate
236301
assertFailsWith<IllegalArgumentException> { constructor(2007, 0, 1) }
237302
assertFailsWith<IllegalArgumentException> { constructor(2007, 13, 1) }
238303
}
304+
305+
private val LocalDate.next: LocalDate get() =
306+
if (dayOfMonth != monthNumber.monthLength(isLeapYear(year))) {
307+
LocalDate(year, monthNumber, dayOfMonth + 1)
308+
} else if (monthNumber != 12) {
309+
LocalDate(year, monthNumber + 1, 1)
310+
} else {
311+
LocalDate(year + 1, 1, 1)
312+
}
313+
314+
private val LocalDate.previous: LocalDate get() =
315+
if (dayOfMonth != 1) {
316+
LocalDate(year, monthNumber, dayOfMonth - 1)
317+
} else if (monthNumber != 1) {
318+
val newMonthNumber = monthNumber - 1
319+
LocalDate(year, newMonthNumber, newMonthNumber.monthLength(isLeapYear(year)))
320+
} else {
321+
LocalDate(year - 1, 12, 31)
322+
}

core/common/test/LocalTimeTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.test
77

88
import kotlinx.datetime.*
9+
import kotlinx.datetime.internal.*
910
import kotlin.math.*
1011
import kotlin.random.*
1112
import kotlin.test.*

core/common/test/MultiplyAndDivideTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
package kotlinx.datetime.test
77
import kotlin.random.*
88
import kotlin.test.*
9-
import kotlinx.datetime.*
9+
import kotlinx.datetime.internal.*
1010

1111
class MultiplyAndDivideTest {
1212

core/js/src/Instant.kt

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import kotlinx.datetime.internal.JSJoda.OffsetDateTime as jtOffsetDateTime
1111
import kotlinx.datetime.internal.JSJoda.Duration as jtDuration
1212
import kotlinx.datetime.internal.JSJoda.Clock as jtClock
1313
import kotlinx.datetime.internal.JSJoda.ChronoUnit
14+
import kotlinx.datetime.internal.safeAdd
15+
import kotlinx.datetime.internal.*
1416
import kotlinx.datetime.serializers.InstantIso8601Serializer
1517
import kotlinx.serialization.Serializable
1618
import kotlin.time.*

core/js/src/LocalDate.kt

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
2222

2323
internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN)
2424
internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX)
25+
26+
public actual fun fromEpochDays(epochDays: Int): LocalDate = try {
27+
LocalDate(jtLocalDate.ofEpochDay(epochDays))
28+
} catch (e: Throwable) {
29+
if (e.isJodaDateTimeException()) throw IllegalArgumentException(e)
30+
throw e
31+
}
2532
}
2633

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

5158
actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value).toInt()
59+
60+
public actual fun toEpochDays(): Int = value.toEpochDay().toInt()
5261
}
5362

5463
public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plusNumber(1, unit)

core/js/src/LocalTime.kt

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
package kotlinx.datetime
66

7+
import kotlinx.datetime.internal.*
78
import kotlinx.datetime.serializers.LocalTimeIso8601Serializer
89
import kotlinx.serialization.Serializable
910
import kotlinx.datetime.internal.JSJoda.LocalTime as jtLocalTime

core/js/src/mathJs.kt renamed to core/js/src/internal/mathJs.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/*
2-
* Copyright 2019-2020 JetBrains s.r.o.
2+
* Copyright 2019-2022 JetBrains s.r.o. and contributors.
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
55

6-
package kotlinx.datetime
6+
package kotlinx.datetime.internal
77

88
/**
99
* Safely adds two long values.

core/jvm/src/Instant.kt

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
package kotlinx.datetime
88

9+
import kotlinx.datetime.internal.safeMultiply
10+
import kotlinx.datetime.internal.*
911
import kotlinx.datetime.serializers.InstantIso8601Serializer
1012
import kotlinx.serialization.Serializable
1113
import java.time.DateTimeException

0 commit comments

Comments
 (0)