dave08
01/20/2025, 11:53 AMDmitry Khalanskiy [JB]
01/20/2025, 11:59 AMdave08
01/20/2025, 12:01 PMDmitry Khalanskiy [JB]
01/20/2025, 12:02 PMdave08
01/20/2025, 12:02 PMdave08
01/20/2025, 12:04 PM@JvmInline
value class TruncatedInstant private constructor(val value: Instant) {
operator fun compareTo(other: TruncatedInstant): Int =
value.compareTo(other.value)
companion object {
val EMPTY = TruncatedInstant(Instant.DISTANT_PAST)
operator fun invoke(instant: Instant): TruncatedInstant {
val secondsSinceHourStart = instant.epochSeconds % 3600
return TruncatedInstant(instant - secondsSinceHourStart.seconds)
}
}
}
but it seems like millis and nanos mess it up...Dmitry Khalanskiy [JB]
01/20/2025, 12:04 PMdave08
01/20/2025, 12:04 PMdave08
01/20/2025, 12:05 PMkevin.cianfarini
01/20/2025, 12:07 PMfun Instant.getBucketHour(): Int {
return toLocalDateTime(UTC).hour
}
Dmitry Khalanskiy [JB]
01/20/2025, 12:08 PMInstant
with epochSeconds = 1737374400
(it was several minutes ago). Your timezone-oblivious logic would mark it as the start of a new bucket. But in Mumbai, this moment corresponds to 17:30, which is not a beginning of a new hour. Is that okay?dave08
01/20/2025, 12:11 PMdave08
01/20/2025, 12:13 PMdave08
01/20/2025, 12:13 PMDmitry Khalanskiy [JB]
01/20/2025, 12:13 PMdave08
01/20/2025, 12:14 PMdave08
01/20/2025, 12:15 PMDmitry Khalanskiy [JB]
01/20/2025, 12:17 PMdave08
01/20/2025, 12:21 PMIs it not better to ensure that actions are forbidden if too many were already performed in the last 60 minutes?I hear the point, what I said is currently implemented in a very messy way, and I'm trying to refactor w/o changing current behaviour, but I could talk to the project manager if he wants to change... for now, I'd rather stick to what's currently implemented. But it would be nice to see the other way you're proposing. Do you have any suggestions on how to implement both ways with the current library?
kevin.cianfarini
01/20/2025, 12:27 PMDmitry Khalanskiy [JB]
01/20/2025, 12:33 PMimport kotlinx.datetime.*
import kotlin.time.Duration
class SlidingWindow(private val maxEventsPerDuration: Int, private val windowLength: Duration) {
private val events = ArrayDeque<Instant>()
/** Registers a new event, returning `false` if [maxEventsPerDuration] events already happened in the last [windowLength]. */
fun tryRegisterNewEvent(now: Instant): Boolean {
while (events.firstOrNull()?.let { now - it > windowLength } == true) {
events.removeFirst()
}
if (events.size >= maxEventsPerDuration) return false
events.addLast(now)
return true
}
}
and here's how you could implement a bucket that's refreshed hourly:
data class LocalDateHour(val date: LocalDate, val hour: Int)
fun Instant.localDateHourInUtc() = toLocalDateTime(TimeZone.UTC).let {
LocalDateHour(it.date, it.hour)
}
dave08
01/20/2025, 12:42 PMCarter
01/20/2025, 12:42 PMdave08
01/20/2025, 12:44 PMdave08
01/20/2025, 12:46 PMdave08
01/20/2025, 12:46 PMDmitry Khalanskiy [JB]
01/20/2025, 12:47 PMBut how would I save LocalDateHour?Sorry, what do you mean? Where do you want to save it?
Also, they need to be easily comparable.
LocalDateHour
instances can be compared using ==
, because it's a data class
.dave08
01/20/2025, 12:49 PMdave08
01/20/2025, 12:50 PMdave08
01/20/2025, 12:53 PMdata class LocalDateHour(val date: LocalDate, val hour: Int) {
fun toEpochSeconds() = date.atTime(hour, 0).toInstant(TimeZone.UTC).epochSeconds
}
dave08
01/20/2025, 12:53 PMDmitry Khalanskiy [JB]
01/20/2025, 12:53 PMInstant
, your TruncatedInstant
solution should work with this implementation:
operator fun invoke(instant: Instant): TruncatedInstant {
val secondsSinceHourStart = instant.epochSeconds.mod(3600)
return TruncatedInstant(Instant.fromEpochSeconds(instant.epochSeconds - secondsSinceHourStart))
}
This way, the sub-second portion doesn't get stored.dave08
01/20/2025, 12:59 PMdave08
01/20/2025, 1:00 PM