Skip to content

Commit ea0c1d9

Browse files
authored
Native: make range for Instants and LocalDate a 1000 times narrower (#22)
This is done to support `LocalDate.daysUntil(...): Int` always having a precise value. Additionally, it allows to simplify some parts of the implementation. Before, the Native implementation was using the same representable ranges as the Java implementation; now, it uses the same ranges as the JS implementation, which are narrower so that everything fits in a JS number.
1 parent 5bc86cb commit ea0c1d9

11 files changed

+191
-172
lines changed

core/nativeMain/src/Instant.kt

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private val instantParser: Parser<Instant>
6363
} catch (e: ArithmeticException) {
6464
throw DateTimeFormatException(e)
6565
}
66-
val epochDay: Long = localDate.toEpochDay()
66+
val epochDay = localDate.toEpochDay().toLong()
6767
val instantSecs = epochDay * 86400 + localTime.toSecondOfDay() + secDelta
6868
try {
6969
Instant(instantSecs, nano)
@@ -75,12 +75,12 @@ private val instantParser: Parser<Instant>
7575
/**
7676
* The minimum supported epoch second.
7777
*/
78-
private const val MIN_SECOND = -31557014167219200L
78+
private const val MIN_SECOND = -31619119219200L // -1000000-01-01T00:00:00Z
7979

8080
/**
8181
* The maximum supported epoch second.
8282
*/
83-
private const val MAX_SECOND = 31556889864403199L
83+
private const val MAX_SECOND = 31494816403199L // +1000000-12-31T23:59:59
8484

8585
private fun isValidInstantSecond(second: Long) = second >= MIN_SECOND && second <= MAX_SECOND
8686

@@ -93,21 +93,7 @@ public actual class Instant internal constructor(actual val epochSeconds: Long,
9393

9494
// org.threeten.bp.Instant#toEpochMilli
9595
actual fun toEpochMilliseconds(): Long =
96-
if (epochSeconds >= 0) {
97-
try {
98-
safeAdd(safeMultiply(epochSeconds, MILLIS_PER_ONE.toLong()),
99-
nanosecondsOfSecond / NANOS_PER_MILLI.toLong())
100-
} catch (e: ArithmeticException) {
101-
Long.MAX_VALUE
102-
}
103-
} else {
104-
try {
105-
safeSubtract(safeMultiply(epochSeconds + 1, MILLIS_PER_ONE.toLong()),
106-
MILLIS_PER_ONE.toLong() - nanosecondsOfSecond / NANOS_PER_MILLI)
107-
} catch (e: ArithmeticException) {
108-
Long.MIN_VALUE
109-
}
110-
}
96+
epochSeconds * MILLIS_PER_ONE + nanosecondsOfSecond / NANOS_PER_MILLI
11197

11298
// org.threeten.bp.Instant#plus(long, long)
11399
/**
@@ -240,7 +226,9 @@ public actual class Instant internal constructor(actual val epochSeconds: Long,
240226

241227
// org.threeten.bp.Instant#ofEpochMilli
242228
actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant =
243-
Instant(floorDiv(epochMilliseconds, MILLIS_PER_ONE.toLong()),
229+
if (epochMilliseconds < MIN_SECOND * MILLIS_PER_ONE) MIN
230+
else if (epochMilliseconds > MAX_SECOND * MILLIS_PER_ONE) MAX
231+
else Instant(floorDiv(epochMilliseconds, MILLIS_PER_ONE.toLong()),
244232
(floorMod(epochMilliseconds, MILLIS_PER_ONE.toLong()) * NANOS_PER_MILLI).toInt())
245233

246234
/**
@@ -285,9 +273,9 @@ private fun Instant.check(zone: TimeZone): Instant = [email protected] {
285273
actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try {
286274
with(period) {
287275
val withDate = toZonedLocalDateTimeFailing(zone)
288-
.run { if (years != 0 && months == 0) plus(years.toLong(), DateTimeUnit.YEAR) else this }
289-
.run { if (months != 0) plus(years * 12L + months.toLong(), DateTimeUnit.MONTH) else this }
290-
.run { if (days != 0) plus(days.toLong(), DateTimeUnit.DAY) else this }
276+
.run { if (years != 0 && months == 0) plus(years, DateTimeUnit.YEAR) else this }
277+
.run { if (months != 0) plus(safeAdd(safeMultiply(years, 12), months), DateTimeUnit.MONTH) else this }
278+
.run { if (days != 0) plus(days, DateTimeUnit.DAY) else this }
291279
val secondsToAdd = safeAdd(seconds,
292280
safeAdd(minutes.toLong() * SECONDS_PER_MINUTE, hours.toLong() * SECONDS_PER_HOUR))
293281
withDate.toInstant().plus(secondsToAdd, period.nanoseconds)
@@ -303,9 +291,13 @@ internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone
303291

304292
internal fun Instant.plusDateTimeUnit(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant = try {
305293
when (unit) {
306-
is DateTimeUnit.DateBased -> toZonedLocalDateTimeFailing(zone).plus(value, unit).toInstant()
307-
is DateTimeUnit.TimeBased -> multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let {
308-
(seconds, nanoseconds) -> check(zone).plus(seconds, nanoseconds).check(zone)
294+
is DateTimeUnit.DateBased -> {
295+
if (value < Int.MIN_VALUE || value > Int.MAX_VALUE)
296+
throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow")
297+
toZonedLocalDateTimeFailing(zone).plus(value.toInt(), unit).toInstant()
298+
}
299+
is DateTimeUnit.TimeBased -> multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (seconds, nanoseconds) ->
300+
check(zone).plus(seconds, nanoseconds).check(zone)
309301
}
310302
}
311303
} catch (e: ArithmeticException) {
@@ -319,21 +311,22 @@ actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod {
319311
var thisLdt = toZonedLocalDateTimeFailing(zone)
320312
val otherLdt = other.toZonedLocalDateTimeFailing(zone)
321313

322-
val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails
314+
val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH).toInt() // `until` on dates never fails
323315
thisLdt = thisLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid
324-
val days = thisLdt.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails
316+
val days = thisLdt.until(otherLdt, DateTimeUnit.DAY).toInt() // `until` on dates never fails
325317
thisLdt = thisLdt.plus(days, DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt
326318
val time = thisLdt.until(otherLdt, DateTimeUnit.NANOSECOND).nanoseconds // |otherLdt - thisLdt| < 24h
327319

328320
time.toComponents { hours, minutes, seconds, nanoseconds ->
329-
return DateTimePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong())
321+
return DateTimePeriod((months / 12), (months % 12), days, hours, minutes, seconds.toLong(), nanoseconds.toLong())
330322
}
331323
}
332324

333325
public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long =
334326
when (unit) {
335327
is DateTimeUnit.DateBased ->
336328
toZonedLocalDateTimeFailing(zone).dateTime.until(other.toZonedLocalDateTimeFailing(zone).dateTime, unit)
329+
.toLong()
337330
is DateTimeUnit.TimeBased -> {
338331
check(zone); other.check(zone)
339332
try {

core/nativeMain/src/LocalDate.kt

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ internal val localDateParser: Parser<LocalDate>
2828
}
2929
}
3030

31-
private const val YEAR_MIN = -999_999_999
32-
private const val YEAR_MAX = 999_999_999
31+
internal const val YEAR_MIN = -999_999
32+
internal const val YEAR_MAX = 999_999
3333

3434
private fun isValidYear(year: Int): Boolean =
3535
year >= YEAR_MIN && year <= YEAR_MAX
@@ -58,44 +58,44 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va
5858
/**
5959
* @throws IllegalArgumentException if the result exceeds the boundaries
6060
*/
61-
internal fun ofEpochDay(epochDay: Long): LocalDate {
61+
internal fun ofEpochDay(epochDay: Int): LocalDate {
62+
// LocalDate(-999999, 1, 1).toEpochDay(), LocalDate(999999, 12, 31).toEpochDay()
6263
// Unidiomatic code due to https://github.com/Kotlin/kotlinx-datetime/issues/5
63-
require(epochDay >= -365243219162L && epochDay <= 365241780471L) {
64+
require(epochDay >= -365961662 && epochDay <= 364522971) {
6465
"Invalid date: boundaries of LocalDate exceeded"
6566
}
66-
var zeroDay: Long = epochDay + DAYS_0000_TO_1970
67+
var zeroDay = epochDay + DAYS_0000_TO_1970
6768
// find the march-based year
6869
zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle
6970

70-
var adjust: Long = 0
71+
var adjust = 0
7172
if (zeroDay < 0) { // adjust negative years to positive for calculation
72-
val adjustCycles: Long = (zeroDay + 1) / DAYS_PER_CYCLE - 1
73+
val adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1
7374
adjust = adjustCycles * 400
7475
zeroDay += -adjustCycles * DAYS_PER_CYCLE
7576
}
76-
var yearEst: Long = (400 * zeroDay + 591) / DAYS_PER_CYCLE
77+
var yearEst = ((400 * zeroDay.toLong() + 591) / DAYS_PER_CYCLE).toInt()
7778
var doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
7879
if (doyEst < 0) { // fix estimate
7980
yearEst--
8081
doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
8182
}
8283
yearEst += adjust // reset any negative year
8384

84-
val marchDoy0 = doyEst.toInt()
85+
val marchDoy0 = doyEst
8586

8687
// convert march-based values back to january-based
8788
val marchMonth0 = (marchDoy0 * 5 + 2) / 153
8889
val month = (marchMonth0 + 2) % 12 + 1
8990
val dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1
90-
yearEst += marchMonth0 / 10.toLong()
91-
val year: Int = yearEst.toInt()
91+
yearEst += marchMonth0 / 10
9292

93-
return LocalDate(year, month, dom)
93+
return LocalDate(yearEst, month, dom)
9494
}
9595
}
9696

9797
// org.threeten.bp.LocalDate#toEpochDay
98-
internal fun toEpochDay(): Long {
98+
internal fun toEpochDay(): Int {
9999
val y = year
100100
val m = monthNumber
101101
var total = 0
@@ -129,7 +129,7 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va
129129
// org.threeten.bp.LocalDate#getDayOfWeek
130130
actual val dayOfWeek: DayOfWeek
131131
get() {
132-
val dow0 = floorMod(toEpochDay() + 3, 7).toInt()
132+
val dow0 = floorMod(toEpochDay() + 3, 7)
133133
return DayOfWeek(dow0 + 1)
134134
}
135135

@@ -164,55 +164,42 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va
164164
* @throws IllegalArgumentException if the result exceeds the boundaries
165165
* @throws ArithmeticException if arithmetic overflow occurs
166166
*/
167-
internal fun plusYears(yearsToAdd: Long): LocalDate {
168-
if (yearsToAdd == 0L) {
169-
return this
170-
}
171-
val newYear = safeAdd(year.toLong(), yearsToAdd)
172-
if (newYear < Int.MIN_VALUE || newYear > Int.MAX_VALUE) {
173-
throw ArithmeticException("Addition overflows an Int")
174-
}
175-
return resolvePreviousValid(newYear.toInt(), monthNumber, dayOfMonth)
176-
}
167+
internal fun plusYears(yearsToAdd: Int): LocalDate =
168+
if (yearsToAdd == 0) this
169+
else resolvePreviousValid(safeAdd(year, yearsToAdd), monthNumber, dayOfMonth)
177170

178171
// org.threeten.bp.LocalDate#plusMonths
179172
/**
180173
* @throws IllegalArgumentException if the result exceeds the boundaries
181174
* @throws ArithmeticException if arithmetic overflow occurs
182175
*/
183-
internal fun plusMonths(monthsToAdd: Long): LocalDate {
184-
if (monthsToAdd == 0L) {
176+
internal fun plusMonths(monthsToAdd: Int): LocalDate {
177+
if (monthsToAdd == 0) {
185178
return this
186179
}
187-
val monthCount: Long = year * 12L + (monthNumber - 1)
180+
val monthCount = year * 12 + (monthNumber - 1)
188181
val calcMonths = safeAdd(monthCount, monthsToAdd)
189182
val newYear = floorDiv(calcMonths, 12)
190-
if (newYear < Int.MIN_VALUE || newYear > Int.MAX_VALUE) {
191-
throw ArithmeticException("Addition overflows an Int")
192-
}
193-
val newMonth = floorMod(calcMonths, 12).toInt() + 1
194-
return resolvePreviousValid(newYear.toInt(), newMonth, dayOfMonth)
183+
val newMonth = floorMod(calcMonths, 12) + 1
184+
return resolvePreviousValid(newYear, newMonth, dayOfMonth)
195185
}
196186

197187
// org.threeten.bp.LocalDate#plusWeeks
198188
/**
199189
* @throws IllegalArgumentException if the result exceeds the boundaries
200190
* @throws ArithmeticException if arithmetic overflow occurs
201191
*/
202-
internal fun plusWeeks(value: Long): LocalDate =
192+
internal fun plusWeeks(value: Int): LocalDate =
203193
plusDays(safeMultiply(value, 7))
204194

205195
// org.threeten.bp.LocalDate#plusDays
206196
/**
207197
* @throws IllegalArgumentException if the result exceeds the boundaries
208198
* @throws ArithmeticException if arithmetic overflow occurs
209199
*/
210-
internal fun plusDays(daysToAdd: Long): LocalDate =
211-
if (daysToAdd == 0L) {
212-
this
213-
} else {
214-
ofEpochDay(safeAdd(toEpochDay(), daysToAdd))
215-
}
200+
internal fun plusDays(daysToAdd: Int): LocalDate =
201+
if (daysToAdd == 0) this
202+
else ofEpochDay(safeAdd(toEpochDay(), daysToAdd))
216203

217204
override fun equals(other: Any?): Boolean =
218205
this === other || (other is LocalDate && compareTo(other) == 0)
@@ -253,61 +240,60 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va
253240
}
254241

255242
internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate =
256-
if (unit.dateTimeUnit is DateTimeUnit.DateBased)
257-
plusDateTimeUnit(value, unit.dateTimeUnit as DateTimeUnit.DateBased)
258-
else throw IllegalArgumentException("Only date based units can be added to LocalDate")
243+
if (value > Int.MAX_VALUE || value < Int.MIN_VALUE)
244+
throw DateTimeArithmeticException("Can't add a Long to a LocalDate")
245+
else plus(value.toInt(), unit)
259246

260-
internal fun LocalDate.plusDateTimeUnit(value: Long, unit: DateTimeUnit.DateBased): LocalDate =
247+
internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate =
248+
if (unit.dateTimeUnit !is DateTimeUnit.DateBased)
249+
throw IllegalArgumentException("Only date based units can be added to LocalDate")
250+
else plusDateTimeUnit(value, unit.dateTimeUnit as DateTimeUnit.DateBased)
251+
252+
internal fun LocalDate.plusDateTimeUnit(value: Int, unit: DateTimeUnit.DateBased): LocalDate =
261253
try {
262254
when (unit) {
263-
is DateTimeUnit.DateBased.DayBased -> plusDays(safeMultiply(value, unit.days.toLong()))
264-
is DateTimeUnit.DateBased.MonthBased -> plusMonths(safeMultiply(value, unit.months.toLong()))
255+
is DateTimeUnit.DateBased.DayBased -> plusDays(safeMultiply(value, unit.days))
256+
is DateTimeUnit.DateBased.MonthBased -> plusMonths(safeMultiply(value, unit.months))
265257
}
266258
} catch (e: ArithmeticException) {
267259
throw DateTimeArithmeticException("Arithmetic overflow when adding a value to a date", e)
268260
} catch (e: IllegalArgumentException) {
269261
throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a value", e)
270262
}
271263

272-
internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate =
273-
plus(value.toLong(), unit)
274-
275264
actual operator fun LocalDate.plus(period: DatePeriod): LocalDate =
276265
with(period) {
277266
try {
278267
this@plus
279-
.run { if (years != 0 && months == 0) plusYears(years.toLong()) else this }
280-
.run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this }
281-
.run { if (days != 0) plusDays(days.toLong()) else this }
268+
.run { if (years != 0 && months == 0) plusYears(years) else this }
269+
.run { if (months != 0) plusMonths(safeAdd(safeMultiply(years, 12), months)) else this }
270+
.run { if (days != 0) plusDays(days) else this }
282271
} catch (e: ArithmeticException) {
283272
throw DateTimeArithmeticException("Arithmetic overflow when adding a period to a date", e)
284273
} catch (e: IllegalArgumentException) {
285274
throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a period", e)
286275
}
287276
}
288277

289-
290-
// TODO: ensure range of LocalDate fits in Int number of days
291-
public actual fun LocalDate.daysUntil(other: LocalDate): Int = longDaysUntil(other).toInt()
292-
public actual fun LocalDate.monthsUntil(other: LocalDate): Int = longMonthsUntil(other).toInt()
293-
public actual fun LocalDate.yearsUntil(other: LocalDate): Int = (longMonthsUntil(other) / 12).toInt()
294-
295278
// org.threeten.bp.LocalDate#daysUntil
296-
internal fun LocalDate.longDaysUntil(other: LocalDate): Long =
279+
public actual fun LocalDate.daysUntil(other: LocalDate): Int =
297280
other.toEpochDay() - this.toEpochDay()
298281

299282
// org.threeten.bp.LocalDate#getProlepticMonth
300-
internal val LocalDate.prolepticMonth get() = (year * 12L) + (monthNumber - 1)
283+
internal val LocalDate.prolepticMonth get() = (year * 12) + (monthNumber - 1)
301284

302285
// org.threeten.bp.LocalDate#monthsUntil
303-
internal fun LocalDate.longMonthsUntil(other: LocalDate): Long {
304-
val packed1: Long = prolepticMonth * 32L + dayOfMonth
305-
val packed2: Long = other.prolepticMonth * 32L + other.dayOfMonth
286+
public actual fun LocalDate.monthsUntil(other: LocalDate): Int {
287+
val packed1 = prolepticMonth * 32 + dayOfMonth
288+
val packed2 = other.prolepticMonth * 32 + other.dayOfMonth
306289
return (packed2 - packed1) / 32
307290
}
308291

292+
public actual fun LocalDate.yearsUntil(other: LocalDate): Int =
293+
monthsUntil(other) / 12
294+
309295
actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod {
310-
val months = longMonthsUntil(other)
311-
val days = plusMonths(months).longDaysUntil(other)
312-
return DatePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt())
296+
val months = monthsUntil(other)
297+
val days = plusMonths(months).daysUntil(other)
298+
return DatePeriod(months / 12, months % 12, days)
313299
}

0 commit comments

Comments
 (0)