-
Notifications
You must be signed in to change notification settings - Fork 31
kn: Implement Instant
using kotlinx.datetime.Instant
#1201
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
Conversation
Instant
Instant
using kotlinx.datetime.Instant
* ISO-8601/RFC5399 timestamp including fractional seconds at microsecond precision (e.g., | ||
* ISO-8601/RFC3339 timestamp including fractional seconds at microsecond precision (e.g., | ||
* "2022-04-25T16:44:13.667307Z") | ||
* | ||
* Prefers RFC5399 when formatting | ||
* Prefers RFC3339 when formatting |
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.
Comment: Oh jeez, good catch. 🤦♂️
// Handle leap seconds (23:59:59) | ||
parsed.second = parsed.second?.coerceAtMost(59) |
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.
Correctness: This looks like it's discarding leap seconds not handling them. 60
is a valid leap second.
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.
Yes this is what we're currently expecting to happen: https://github.com/smithy-lang/smithy-kotlin/blob/kn-time/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/time/InstantTest.kt#L57-L58
Do you want to change the logic here?
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.
Almost certainly, yes. Treating x:y:60 and x:y:59 as the same instant is incorrect. The problem gets worse when subsecond precision is considered and timestamps increment like :59.998, :59.999, :59.000, :59.001.
We should verify with other SDK implementations but I believe our existing test (and therefore likely our existing JVM implementation) is flawed.
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.
We spoke offline and agreed to match Java's Instant
behavior here, which effectively truncates the leap second 23:59:60 to 23:59:59.
// Handle leap hours (24:00:00) | ||
return if (parsed.hour == 24) { | ||
parsed.hour = 0 | ||
Instant(parsed.toInstantUsingOffset() + 1.days) | ||
} else { | ||
Instant(parsed.toInstantUsingOffset()) | ||
} |
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.
Correctness: What's a leap hour?
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.
Sorry not a leap hour, it's a midnight timestamp
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.
OK, I don't think that's a thing with RFC 3339:
From § 5.6:
date-month = 2DIGIT ; 01-12 date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on ; month/year time-hour = 2DIGIT ; 00-23 time-minute = 2DIGIT ; 00-59 time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second ; rules```
Note that time-hour ranges from 0 to 23, not 24.
From § 5.7:
Although ISO 8601 permits the hour to be "24", this profile of ISO 8601 only allows values between "00" and "23" for the hour in order to reduce confusion.
override fun hashCode(): Int { | ||
var result = delegate.hashCode() | ||
result = 31 * result + epochSeconds.hashCode() | ||
result = 31 * result + nanosecondsOfSecond | ||
return result | ||
} |
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.
Nit: epochSeconds
and nanosecondsOfSecond
are derived from delegate
. They shouldn't be part of the hash code calculation.
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.
Question: Because of some of the refactoring, it's a bit tricky for me to see what's functionally changed in these tests. Besides the addition of .fromEpochSeconds(${test.sec}, ${test.ns})
to an assert message, what's changed here?
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.
Nothing else changed, it's just restructured
.format(format) | ||
val expected = getter(test) | ||
assertEquals(expected, actual, "test[$idx]: failed to correctly format Instant as $format") | ||
assertEquals(expected, actual, "test[$idx]: failed to correctly format Instant.fromEpochSeconds(${test.sec}, ${test.ns}) as $format") |
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.
Nit: Stray space in message (") as"
)
private fun KtInstant.truncateToMicros(): KtInstant = KtInstant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond / 1_000 * 1_000) | ||
private fun KtInstant.truncateToMicros(): KtInstant = KtInstant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond / 1000 * 1000) |
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.
Question: Why did this change? I don't particularly need separators in relatively small numbers like this but, to me, they're harmless in cases like this and very helpful in cases of larger numbers.
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.
No reason besides change of personal preference in this case, I'll add it back
override fun hashCode(): Int { | ||
var result = delegate.hashCode() | ||
result = 31 * result + epochSeconds.hashCode() | ||
result = 31 * result + nanosecondsOfSecond | ||
return result | ||
} |
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.
Correctness: This removes too much from the hash calculation. We don't need the entropy from epochSeconds
or nanosecondsOfSecond
since they're derived values but we do need the entropy from delegate
itself. Otherwise, instances will use the default hashcode implementation which is based on instance memory location and will diverge even for objects which are structurally equal.
This comment has been minimized.
This comment has been minimized.
1 similar comment
Affected ArtifactsChanged in size
|
Issue #
Description of changes
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.