Skip to content

Commit 8a4e47b

Browse files
committed
Implement LocalDate.fromEpochDays
1 parent 3dbe188 commit 8a4e47b

File tree

13 files changed

+149
-164
lines changed

13 files changed

+149
-164
lines changed

core/common/src/Instant.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ public expect class Instant : Comparable<Instant> {
126126
* Returns an [Instant] that is [epochMilliseconds] number of milliseconds from the epoch instant `1970-01-01T00:00:00Z`.
127127
*
128128
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
129+
*
130+
* @see Instant.toEpochMilliseconds
129131
*/
130132
public fun fromEpochMilliseconds(epochMilliseconds: Long): Instant
131133

core/common/src/LocalDate.kt

Lines changed: 18 additions & 0 deletions
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),

core/common/src/math.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,17 @@ internal fun multiplyAndAdd(d: Long, n: Long, r: Long): Long {
200200
mr -= n
201201
}
202202
return safeAdd(safeMultiply(md, n), mr)
203-
}
203+
}
204+
205+
// org.threeten.bp.chrono.IsoChronology#isLeapYear
206+
internal fun isLeapYear(year: Int): Boolean {
207+
val prolepticYear: Long = year.toLong()
208+
return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L)
209+
}
210+
211+
internal fun Int.monthLength(isLeapYear: Boolean): Int =
212+
when (this) {
213+
2 -> if (isLeapYear) 29 else 28
214+
4, 6, 9, 11 -> 30
215+
else -> 31
216+
}

core/common/test/LocalDateTest.kt

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ class LocalDateTest {
3636

3737
@Test
3838
fun parseIsoString() {
39-
fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int, dayOfYear: Int) {
39+
fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int? = null, dayOfYear: Int? = null) {
4040
checkComponents(LocalDate.parse(value), year, month, day, dayOfWeek, dayOfYear)
41+
assertEquals(value, LocalDate(year, month, day).toString())
4142
}
4243
checkParsedComponents("2019-10-01", 2019, 10, 1, 2, 274)
4344
checkParsedComponents("2016-02-29", 2016, 2, 29, 1, 60)
@@ -49,6 +50,17 @@ class LocalDateTest {
4950
assertInvalidFormat { LocalDate.parse("2017-10--01") }
5051
// this date is currently larger than the largest representable one any of the platforms:
5152
assertInvalidFormat { LocalDate.parse("+1000000000-10-01") }
53+
// threetenbp
54+
checkParsedComponents("2008-07-05", 2008, 7, 5)
55+
checkParsedComponents("2007-12-31", 2007, 12, 31)
56+
checkParsedComponents("0999-12-31", 999, 12, 31)
57+
checkParsedComponents("-0001-01-02", -1, 1, 2)
58+
checkParsedComponents("9999-12-31", 9999, 12, 31)
59+
checkParsedComponents("-9999-12-31", -9999, 12, 31)
60+
checkParsedComponents("+10000-01-01", 10000, 1, 1)
61+
checkParsedComponents("-10000-01-01", -10000, 1, 1)
62+
checkParsedComponents("+123456-01-01", 123456, 1, 1)
63+
checkParsedComponents("-123456-01-01", -123456, 1, 1)
5264
}
5365

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

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

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

core/js/src/LocalDate.kt

Lines changed: 9 additions & 0 deletions
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/jvm/src/LocalDate.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
2323

2424
internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN)
2525
internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX)
26+
27+
public actual fun fromEpochDays(epochDays: Int): LocalDate =
28+
LocalDate(jtLocalDate.ofEpochDay(epochDays.toLong()))
2629
}
2730

2831
public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) :
@@ -49,6 +52,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
4952
actual override fun toString(): String = value.toString()
5053

5154
actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value)
55+
56+
public actual fun toEpochDays(): Int = value.toEpochDay().clampToInt()
5257
}
5358

5459
public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate =

core/native/src/Instant.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ private val instantParser: Parser<Instant>
105105
} catch (e: ArithmeticException) {
106106
throw DateTimeFormatException(e)
107107
}
108-
val epochDay = localDate.toEpochDay().toLong()
108+
val epochDay = localDate.toEpochDays().toLong()
109109
val instantSecs = epochDay * 86400 - offset.totalSeconds + localTime.toSecondOfDay() + secDelta
110110
try {
111111
Instant(instantSecs, nano)

core/native/src/LocalDate.kt

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
4242
init {
4343
// org.threeten.bp.LocalDate#create
4444
require(isValidYear(year)) { "Invalid date: the year is out of range" }
45-
require(monthNumber >= 1 && monthNumber <= 12) { "Invalid date: month must be a number between 1 and 12, got $monthNumber" }
46-
require(dayOfMonth >= 1 && dayOfMonth <= 31) { "Invalid date: day of month must be a number between 1 and 31, got $dayOfMonth" }
45+
require(monthNumber in 1..12) { "Invalid date: month must be a number between 1 and 12, got $monthNumber" }
46+
require(dayOfMonth in 1..31) { "Invalid date: day of month must be a number between 1 and 31, got $dayOfMonth" }
4747
if (dayOfMonth > 28 && dayOfMonth > monthNumber.monthLength(isLeapYear(year))) {
4848
if (dayOfMonth == 29) {
4949
throw IllegalArgumentException("Invalid date 'February 29' as '$year' is not a leap year")
@@ -60,16 +60,12 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
6060
localDateParser.parse(isoString)
6161

6262
// org.threeten.bp.LocalDate#toEpochDay
63-
/**
64-
* @throws IllegalArgumentException if the result exceeds the boundaries
65-
*/
66-
internal fun ofEpochDay(epochDay: Int): LocalDate {
63+
public actual fun fromEpochDays(epochDays: Int): LocalDate {
6764
// LocalDate(-999999, 1, 1).toEpochDay(), LocalDate(999999, 12, 31).toEpochDay()
68-
// Unidiomatic code due to https://github.com/Kotlin/kotlinx-datetime/issues/5
69-
require(epochDay >= MIN_EPOCH_DAY && epochDay <= MAX_EPOCH_DAY) {
65+
require(epochDays in MIN_EPOCH_DAY..MAX_EPOCH_DAY) {
7066
"Invalid date: boundaries of LocalDate exceeded"
7167
}
72-
var zeroDay = epochDay + DAYS_0000_TO_1970
68+
var zeroDay = epochDays + DAYS_0000_TO_1970
7369
// find the march-based year
7470
zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle
7571

@@ -106,7 +102,7 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
106102
}
107103

108104
// org.threeten.bp.LocalDate#toEpochDay
109-
internal fun toEpochDay(): Int {
105+
public actual fun toEpochDays(): Int {
110106
val y = year
111107
val m = monthNumber
112108
var total = 0
@@ -140,7 +136,7 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
140136
// org.threeten.bp.LocalDate#getDayOfWeek
141137
public actual val dayOfWeek: DayOfWeek
142138
get() {
143-
val dow0 = (toEpochDay() + 3).mod(7)
139+
val dow0 = (toEpochDays() + 3).mod(7)
144140
return DayOfWeek(dow0 + 1)
145141
}
146142

@@ -170,15 +166,6 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
170166
return LocalDate(year, month, newDay)
171167
}
172168

173-
// org.threeten.bp.LocalDate#plusYears
174-
/**
175-
* @throws IllegalArgumentException if the result exceeds the boundaries
176-
* @throws ArithmeticException if arithmetic overflow occurs
177-
*/
178-
internal fun plusYears(yearsToAdd: Int): LocalDate =
179-
if (yearsToAdd == 0) this
180-
else resolvePreviousValid(safeAdd(year, yearsToAdd), monthNumber, dayOfMonth)
181-
182169
// org.threeten.bp.LocalDate#plusMonths
183170
/**
184171
* @throws IllegalArgumentException if the result exceeds the boundaries
@@ -195,22 +182,14 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu
195182
return resolvePreviousValid(newYear, newMonth, dayOfMonth)
196183
}
197184

198-
// org.threeten.bp.LocalDate#plusWeeks
199-
/**
200-
* @throws IllegalArgumentException if the result exceeds the boundaries
201-
* @throws ArithmeticException if arithmetic overflow occurs
202-
*/
203-
internal fun plusWeeks(value: Int): LocalDate =
204-
plusDays(safeMultiply(value, 7))
205-
206185
// org.threeten.bp.LocalDate#plusDays
207186
/**
208187
* @throws IllegalArgumentException if the result exceeds the boundaries
209188
* @throws ArithmeticException if arithmetic overflow occurs
210189
*/
211190
internal fun plusDays(daysToAdd: Int): LocalDate =
212191
if (daysToAdd == 0) this
213-
else ofEpochDay(safeAdd(toEpochDay(), daysToAdd))
192+
else fromEpochDays(safeAdd(toEpochDays(), daysToAdd))
214193

215194
override fun equals(other: Any?): Boolean =
216195
this === other || (other is LocalDate && compareTo(other) == 0)
@@ -292,7 +271,7 @@ public actual fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased
292271

293272
// org.threeten.bp.LocalDate#daysUntil
294273
public actual fun LocalDate.daysUntil(other: LocalDate): Int =
295-
other.toEpochDay() - this.toEpochDay()
274+
other.toEpochDays() - this.toEpochDays()
296275

297276
// org.threeten.bp.LocalDate#getProlepticMonth
298277
internal val LocalDate.prolepticMonth get() = (year * 12) + (monthNumber - 1)

core/native/src/LocalDateTime.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public actual constructor(public actual val date: LocalDate, public actual val t
7171

7272
// org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond
7373
internal fun toEpochSecond(offset: UtcOffset): Long {
74-
val epochDay = date.toEpochDay().toLong()
74+
val epochDay = date.toEpochDays().toLong()
7575
var secs: Long = epochDay * 86400 + time.toSecondOfDay()
7676
secs -= offset.totalSeconds
7777
return secs

core/native/src/Month.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,3 @@ internal fun Month.firstDayOfYear(leapYear: Boolean): Int {
2727
Month.DECEMBER -> 335 + leap
2828
}
2929
}
30-
31-
// From threetenbp
32-
internal fun Int.monthLength(leapYear: Boolean): Int {
33-
return when (this) {
34-
2 -> if (leapYear) 29 else 28
35-
4, 6, 9, 11 -> 30
36-
else -> 31
37-
}
38-
}

core/native/src/TimeZone.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ internal fun Instant.toLocalDateTimeImpl(offset: UtcOffset): LocalDateTime {
145145
val localSecond: Long = epochSeconds + offset.totalSeconds // overflow caught later
146146
val localEpochDay = localSecond.floorDiv(SECONDS_PER_DAY.toLong()).toInt()
147147
val secsOfDay = localSecond.mod(SECONDS_PER_DAY.toLong()).toInt()
148-
val date: LocalDate = LocalDate.ofEpochDay(localEpochDay) // may throw
148+
val date: LocalDate = LocalDate.fromEpochDays(localEpochDay) // may throw
149149
val time: LocalTime = LocalTime.ofSecondOfDay(secsOfDay, nanosecondsOfSecond)
150150
return LocalDateTime(date, time)
151151
}

core/native/src/mathNative.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,4 @@ internal fun zoneIdByOffset(totalSeconds: Int): String {
117117
}
118118
buf.toString()
119119
}
120-
}
121-
122-
// org.threeten.bp.chrono.IsoChronology#isLeapYear
123-
internal fun isLeapYear(year: Int): Boolean {
124-
val prolepticYear: Long = year.toLong()
125-
return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L)
126-
}
120+
}

0 commit comments

Comments
 (0)