-
Notifications
You must be signed in to change notification settings - Fork 112
Ensure better ISO 8601 compliance #330
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
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ package kotlinx.datetime | |
import kotlinx.datetime.internal.* | ||
import kotlinx.datetime.serializers.InstantIso8601Serializer | ||
import kotlinx.serialization.Serializable | ||
import kotlin.math.* | ||
import kotlin.time.* | ||
|
||
/** | ||
|
@@ -221,7 +222,17 @@ public fun String.toInstant(): Instant = Instant.parse(this) | |
* @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in | ||
* [LocalDateTime]. | ||
*/ | ||
public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant | ||
public fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant { | ||
val ldt = toLocalDateTimeFailing(timeZone) | ||
return with(period) { | ||
val newDate = ldt.date | ||
.run { if (totalMonths != 0) plus(totalMonths, DateTimeUnit.MONTH) else this } | ||
.run { if (days != 0) plus(days, DateTimeUnit.DAY) else this } | ||
LocalDateTime(newDate, ldt.time).toInstant(timeZone).run { | ||
if (totalNanoseconds != 0L) plus(totalNanoseconds, DateTimeUnit.NANOSECOND) else this | ||
}.check(timeZone) | ||
} | ||
} | ||
|
||
/** | ||
* Returns an instant that is the result of subtracting components of [DateTimePeriod] from this instant. The components | ||
|
@@ -255,7 +266,23 @@ public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant = | |
* @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. | ||
* Or (only on the JVM) if the number of months between the two dates exceeds an Int. | ||
*/ | ||
public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod | ||
public fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { | ||
val thisLdt = toLocalDateTimeFailing(timeZone) | ||
val otherLdt = other.toLocalDateTimeFailing(timeZone) | ||
var thisDate = thisLdt.date | ||
val otherDate = otherLdt.date | ||
val months = thisDate.until(otherDate, DateTimeUnit.MONTH) // `until` on dates never fails | ||
thisDate = thisDate.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid | ||
val days = thisDate.until(otherDate, DateTimeUnit.DAY) // `until` on dates never fails | ||
if (days.absoluteValue > 31) { | ||
throw DateTimeArithmeticException("The number of months between $this and $other does not fit in an Int") | ||
} | ||
thisDate = thisDate.plus(days, DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt | ||
val thisInstant = LocalDateTime(thisDate, thisLdt.time).toInstant(timeZone) | ||
val nanoseconds = thisInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h | ||
|
||
return buildDateTimePeriod(months, days, nanoseconds) | ||
} | ||
|
||
/** | ||
* Returns the whole number of the specified date or time [units][unit] between `this` and [other] instants | ||
|
@@ -521,3 +548,16 @@ internal const val DISTANT_FUTURE_SECONDS = 3093527980800 | |
* Be careful: this function may throw for some values of the [Instant]. | ||
*/ | ||
internal expect fun Instant.toStringWithOffset(offset: UtcOffset): String | ||
|
||
/** Check that [Instant] fits in [LocalDateTime]. | ||
* This is done on the results of computations for consistency with other platforms. | ||
*/ | ||
internal fun Instant.check(zone: TimeZone): Instant = [email protected] { | ||
toLocalDateTimeFailing(zone) | ||
} | ||
|
||
internal fun Instant.toLocalDateTimeFailing(zone: TimeZone): LocalDateTime = try { | ||
toLocalDateTime(zone) | ||
} catch (e: IllegalArgumentException) { | ||
throw DateTimeArithmeticException("Can not convert instant $this to LocalDateTime to perform computations", e) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import kotlinx.datetime.internal.* | |
import kotlinx.datetime.serializers.InstantIso8601Serializer | ||
import kotlinx.serialization.Serializable | ||
import java.time.DateTimeException | ||
import java.time.ZonedDateTime | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accidental import? |
||
import java.time.format.DateTimeParseException | ||
import java.time.temporal.ChronoUnit | ||
import kotlin.time.* | ||
|
@@ -107,20 +108,6 @@ private fun Instant.atZone(zone: TimeZone): java.time.ZonedDateTime = try { | |
throw DateTimeArithmeticException(e) | ||
} | ||
|
||
public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant { | ||
try { | ||
val thisZdt = atZone(timeZone) | ||
return with(period) { | ||
thisZdt | ||
.run { if (totalMonths != 0) plusMonths(totalMonths.toLong()) else this } | ||
.run { if (days != 0) plusDays(days.toLong()) else this } | ||
.run { if (totalNanoseconds != 0L) plusNanos(totalNanoseconds) else this } | ||
}.toInstant().let(::Instant) | ||
} catch (e: DateTimeException) { | ||
throw DateTimeArithmeticException(e) | ||
} | ||
} | ||
|
||
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)")) | ||
public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant = | ||
plus(1L, unit, timeZone) | ||
|
@@ -157,20 +144,6 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta | |
if (value > 0) Instant.MAX else Instant.MIN | ||
} | ||
|
||
public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { | ||
var thisZdt = this.atZone(timeZone) | ||
val otherZdt = other.atZone(timeZone) | ||
|
||
val months = thisZdt.until(otherZdt, ChronoUnit.MONTHS); thisZdt = thisZdt.plusMonths(months) | ||
val days = thisZdt.until(otherZdt, ChronoUnit.DAYS); thisZdt = thisZdt.plusDays(days) | ||
val nanoseconds = thisZdt.until(otherZdt, ChronoUnit.NANOS) | ||
|
||
if (months > Int.MAX_VALUE || months < Int.MIN_VALUE) { | ||
throw DateTimeArithmeticException("The number of months between $this and $other does not fit in an Int") | ||
} | ||
return buildDateTimePeriod(months.toInt(), days.toInt(), nanoseconds) | ||
} | ||
|
||
public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = try { | ||
val thisZdt = this.atZone(timeZone) | ||
val otherZdt = other.atZone(timeZone) | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -252,27 +252,6 @@ private fun Instant.toZonedDateTime(zone: TimeZone): ZonedDateTime { | |
return ZonedDateTime(toLocalDateTimeImpl(currentOffset), zone, currentOffset) | ||
} | ||
|
||
/** Check that [Instant] fits in [ZonedDateTime]. | ||
* This is done on the results of computations for consistency with other platforms. | ||
*/ | ||
private fun Instant.check(zone: TimeZone): Instant = [email protected] { | ||
toZonedDateTimeFailing(zone) | ||
} | ||
|
||
public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { | ||
with(period) { | ||
val withDate = toZonedDateTimeFailing(timeZone) | ||
.run { if (totalMonths != 0) plus(totalMonths, DateTimeUnit.MONTH) else this } | ||
.run { if (days != 0) plus(days, DateTimeUnit.DAY) else this } | ||
withDate.toInstant() | ||
.run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } | ||
}.check(timeZone) | ||
} catch (e: ArithmeticException) { | ||
throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e) | ||
} catch (e: IllegalArgumentException) { | ||
throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e) | ||
} | ||
|
||
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)")) | ||
public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant = | ||
plus(1L, unit, timeZone) | ||
|
@@ -307,19 +286,6 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta | |
if (value > 0) Instant.MAX else Instant.MIN | ||
} | ||
|
||
public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { | ||
var thisLdt = toZonedDateTimeFailing(timeZone) | ||
val otherLdt = other.toZonedDateTimeFailing(timeZone) | ||
|
||
val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH).toInt() // `until` on dates never fails | ||
thisLdt = thisLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid | ||
val days = thisLdt.until(otherLdt, DateTimeUnit.DAY).toInt() // `until` on dates never fails | ||
thisLdt = thisLdt.plus(days, DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt | ||
val nanoseconds = thisLdt.until(otherLdt, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h | ||
|
||
return buildDateTimePeriod(months, days, nanoseconds) | ||
} | ||
|
||
public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = | ||
when (unit) { | ||
is DateTimeUnit.DateBased -> | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this
toInstant
use the same resolve mode asZDT.plus(days)
that was used before?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly.
Imagine a situation:
+1:00
;+0:00
.If we subtract, say, a day and land into this overlap, then, as is currently implemented, the
+0:00
offset will be used. In this PR, the UTC offset+1:00
will be used instead.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it would be better to preserve resolve behavior in these operations, so that
Instant + DatePeriod(days = 1) == Instant.plus(1, DAY)