Skip to content

Commit

Permalink
Better time APIs: deprecated effectful code in IzTime, zone can be pr…
Browse files Browse the repository at this point in the history
…ovided to Clock1 calls, better accuracy logic (#2051)

* better time APIs

* better time APIs: zone parameter for all the Clock1 methods

* cleanups
  • Loading branch information
pshirshov authored Jan 17, 2024
1 parent 34f52bb commit e666c70
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,18 @@ trait ZIOTestEnv extends TestInstances with EqThrowable {
implicit def clock2(implicit ticker: Ticker): Clock2[IO] = {
new Clock2[IO] {
override def epoch: IO[Nothing, Long] = ZIO.succeed(ticker.ctx.now().toMillis)
override def now(accuracy: Clock1.ClockAccuracy): IO[Nothing, ZonedDateTime] = ZIO.succeed(
ZonedDateTime.ofInstant(Instant.ofEpochMilli(ticker.ctx.now().toMillis), ZoneOffset.UTC)
override def now(accuracy: Clock1.ClockAccuracy): IO[Nothing, ZonedDateTime] = nowZoned(accuracy)
override def nowLocal(accuracy: Clock1.ClockAccuracy, zone: ZoneId): IO[Nothing, LocalDateTime] = ZIO.succeed(
LocalDateTime.ofInstant(Instant.ofEpochMilli(ticker.ctx.now().toMillis), zone)
)
override def nowLocal(accuracy: Clock1.ClockAccuracy): IO[Nothing, LocalDateTime] = ZIO.succeed(
LocalDateTime.ofInstant(Instant.ofEpochMilli(ticker.ctx.now().toMillis), ZoneOffset.UTC)
)
override def nowOffset(accuracy: Clock1.ClockAccuracy): IO[Nothing, OffsetDateTime] = ZIO.succeed(
OffsetDateTime.ofInstant(Instant.ofEpochMilli(ticker.ctx.now().toMillis), ZoneOffset.UTC)
override def nowOffset(accuracy: Clock1.ClockAccuracy, zone: ZoneId): IO[Nothing, OffsetDateTime] = ZIO.succeed(
OffsetDateTime.ofInstant(Instant.ofEpochMilli(ticker.ctx.now().toMillis), zone)
)
override def monotonicNano: IO[Nothing, Long] = ZIO.succeed(ticker.ctx.now().toNanos)

override def nowZoned(accuracy: Clock1.ClockAccuracy, zone: ZoneId): IO[Nothing, ZonedDateTime] = ZIO.succeed(
ZonedDateTime.ofInstant(Instant.ofEpochMilli(ticker.ctx.now().toMillis), zone)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import izumi.functional.bio.Clock1.ClockAccuracy
import izumi.functional.bio.DivergenceHelper.{Divergent, Nondivergent}
import izumi.fundamentals.platform.functional.Identity

import java.time.temporal.ChronoUnit
import java.time.temporal.{ChronoUnit, TemporalUnit}
import java.time.{LocalDateTime, OffsetDateTime, ZoneId, ZonedDateTime}
import scala.annotation.unused
import scala.language.implicitConversions
Expand All @@ -14,9 +14,14 @@ trait Clock1[F[_]] extends DivergenceHelper {
def epoch: F[Long]

/** Should return current time (UTC timezone) */
def now(accuracy: ClockAccuracy = ClockAccuracy.DEFAULT): F[ZonedDateTime]
def nowLocal(accuracy: ClockAccuracy = ClockAccuracy.DEFAULT): F[LocalDateTime]
def nowOffset(accuracy: ClockAccuracy = ClockAccuracy.DEFAULT): F[OffsetDateTime]
@deprecated("use nowZoned")
def now(accuracy: ClockAccuracy = ClockAccuracy.RAW): F[ZonedDateTime]
private[izumi] def nowLocal(accuracy: ClockAccuracy): F[LocalDateTime] = nowLocal(accuracy, Clock1.TZ_UTC)
private[izumi] def nowOffset(accuracy: ClockAccuracy): F[OffsetDateTime] = nowOffset(accuracy, Clock1.TZ_UTC)

def nowZoned(accuracy: ClockAccuracy = ClockAccuracy.MICROS, zone: ZoneId = Clock1.TZ_UTC): F[ZonedDateTime]
def nowLocal(accuracy: ClockAccuracy = ClockAccuracy.MICROS, zone: ZoneId = Clock1.TZ_UTC): F[LocalDateTime]
def nowOffset(accuracy: ClockAccuracy = ClockAccuracy.MICROS, zone: ZoneId = Clock1.TZ_UTC): F[OffsetDateTime]

/** Should return a never decreasing measure of time, in nanoseconds */
def monotonicNano: F[Long]
Expand All @@ -25,6 +30,8 @@ trait Clock1[F[_]] extends DivergenceHelper {
}

object Clock1 extends LowPriorityClockInstances {
final val TZ_UTC: ZoneId = ZoneId.of("UTC")

def apply[F[_]: Clock1]: Clock1[F] = implicitly

def fromImpure[F[_]: SyncSafe1](impureClock: Clock1[Identity]): Clock1[F] = fromImpureClock(impureClock, SyncSafe1[F])
Expand All @@ -39,50 +46,113 @@ object Clock1 extends LowPriorityClockInstances {
System.nanoTime()
}

override def nowZoned(accuracy: ClockAccuracy, zone: ZoneId): ZonedDateTime = {
ClockAccuracy.applyAccuracy(ZonedDateTime.now(zone), accuracy)
}

@deprecated("use nowZoned")
override def now(accuracy: ClockAccuracy): ZonedDateTime = {
ClockAccuracy.applyAccuracy(ZonedDateTime.now(TZ_UTC), accuracy)
nowZoned(accuracy)
}

override def nowLocal(accuracy: ClockAccuracy): LocalDateTime = {
now(accuracy).toLocalDateTime
override def nowLocal(accuracy: ClockAccuracy, zone: ZoneId): LocalDateTime = {
// do not reuse nowZoned because of sjs
ClockAccuracy.applyAccuracy(LocalDateTime.now(zone), accuracy)
}

override def nowOffset(accuracy: ClockAccuracy): OffsetDateTime = {
now(accuracy).toOffsetDateTime
override def nowOffset(accuracy: ClockAccuracy, zone: ZoneId): OffsetDateTime = {
// do not reuse nowZoned because of sjs
ClockAccuracy.applyAccuracy(OffsetDateTime.now(zone), accuracy)
}

private[this] final val TZ_UTC: ZoneId = ZoneId.of("UTC")
}

@deprecated("Use ConstantZoned/ConstantOffset")
final class Constant(time: ZonedDateTime, nano: Long) extends Clock1[Identity] {
private val underlying = new ConstantZoned(time, nano)

override def epoch: Long = underlying.epoch
@deprecated("use nowZoned")
override def now(accuracy: ClockAccuracy): ZonedDateTime = underlying.now(accuracy)

override def nowLocal(accuracy: ClockAccuracy, zone: ZoneId): LocalDateTime = underlying.nowLocal(accuracy, zone)
override def nowOffset(accuracy: ClockAccuracy, zone: ZoneId): OffsetDateTime = underlying.nowOffset(accuracy, zone)
override def nowZoned(accuracy: ClockAccuracy, zone: ZoneId): Identity[ZonedDateTime] = underlying.nowZoned(accuracy, zone)
override def monotonicNano: Long = nano
}

final class ConstantZoned(time: ZonedDateTime, nano: Long) extends Clock1[Identity] {
override def epoch: Long = time.toEpochSecond
override def now(accuracy: ClockAccuracy): ZonedDateTime = ClockAccuracy.applyAccuracy(time, accuracy)
override def nowLocal(accuracy: ClockAccuracy): LocalDateTime = now(accuracy).toLocalDateTime
override def nowOffset(accuracy: ClockAccuracy): OffsetDateTime = now(accuracy).toOffsetDateTime
@deprecated("use nowZoned")
override def now(accuracy: ClockAccuracy): ZonedDateTime = nowZoned(accuracy)

override def nowLocal(accuracy: ClockAccuracy, zone: ZoneId): LocalDateTime = ClockAccuracy.applyAccuracy(time.toLocalDateTime, accuracy)
override def nowOffset(accuracy: ClockAccuracy, zone: ZoneId): OffsetDateTime = ClockAccuracy.applyAccuracy(time.toOffsetDateTime, accuracy)
override def nowZoned(accuracy: ClockAccuracy, zone: ZoneId): Identity[ZonedDateTime] = ClockAccuracy.applyAccuracy(time, accuracy)
override def monotonicNano: Long = nano
}

final class ConstantOffset(time: OffsetDateTime, nano: Long) extends Clock1[Identity] {
override def epoch: Long = time.toEpochSecond
@deprecated("use nowZoned")
override def now(accuracy: ClockAccuracy): ZonedDateTime = nowZoned(accuracy)

override def nowLocal(accuracy: ClockAccuracy, zone: ZoneId): LocalDateTime = ClockAccuracy.applyAccuracy(time.toLocalDateTime, accuracy)
override def nowOffset(accuracy: ClockAccuracy, zone: ZoneId): OffsetDateTime = ClockAccuracy.applyAccuracy(time, accuracy)
override def nowZoned(accuracy: ClockAccuracy, zone: ZoneId): Identity[ZonedDateTime] = ClockAccuracy.applyAccuracy(time.toZonedDateTime, accuracy)
override def monotonicNano: Long = nano
}

sealed trait ClockAccuracy
object ClockAccuracy {
case object DEFAULT extends ClockAccuracy
@deprecated("Use ClockAccuracy.RAW (but better set the limit explicitly!)")
private[izumi] case object DEFAULT extends ClockAccuracy

/** The accuracy will not be changed. Generally speaking, it's a bad idea because the actual accuracy might differ between JVM versions (e.g. 11 vs 17)
*/
case object RAW extends ClockAccuracy

/** Highest precision, although it might be unsafe to use it because some tools (e.g. PostgreSQL) may be implicitly truncating nanosecond-precision timestamps.
*/
case object NANO extends ClockAccuracy
case object MILLIS extends ClockAccuracy

/**
* This is the safest choice for PostgreSQL which is known to truncate timestamps to microseconds
*/
case object MICROS extends ClockAccuracy
case object SECONDS extends ClockAccuracy
case object MINUTES extends ClockAccuracy
case object HOURS extends ClockAccuracy

def applyAccuracy(now: ZonedDateTime, clockAccuracy: ClockAccuracy): ZonedDateTime = {
private trait TruncatableTime[T] {
def truncatedTo(timestamp: T, unit: TemporalUnit): T
}
private object TruncatableTime {
implicit lazy val TruncatableZoned: TruncatableTime[ZonedDateTime] = (timestamp: ZonedDateTime, unit: TemporalUnit) => timestamp.truncatedTo(unit)
implicit lazy val TruncatableOffset: TruncatableTime[OffsetDateTime] = (timestamp: OffsetDateTime, unit: TemporalUnit) => timestamp.truncatedTo(unit)
implicit lazy val TruncatableLocal: TruncatableTime[LocalDateTime] = (timestamp: LocalDateTime, unit: TemporalUnit) => timestamp.truncatedTo(unit)
}

private def applyTTAccuracy[T: TruncatableTime](now: T, clockAccuracy: ClockAccuracy): T = {
val tt = implicitly[TruncatableTime[T]]
clockAccuracy match {
case ClockAccuracy.DEFAULT => now
case ClockAccuracy.NANO => now.truncatedTo(ChronoUnit.NANOS)
case ClockAccuracy.MILLIS => now.truncatedTo(ChronoUnit.MILLIS)
case ClockAccuracy.MICROS => now.truncatedTo(ChronoUnit.MICROS)
case ClockAccuracy.SECONDS => now.truncatedTo(ChronoUnit.SECONDS)
case ClockAccuracy.MINUTES => now.truncatedTo(ChronoUnit.MINUTES)
case ClockAccuracy.HOURS => now.truncatedTo(ChronoUnit.HOURS)
case ClockAccuracy.RAW => now
case ClockAccuracy.NANO => tt.truncatedTo(now, ChronoUnit.NANOS)
case ClockAccuracy.MILLIS => tt.truncatedTo(now, ChronoUnit.MILLIS)
case ClockAccuracy.MICROS => tt.truncatedTo(now, ChronoUnit.MICROS)
case ClockAccuracy.SECONDS => tt.truncatedTo(now, ChronoUnit.SECONDS)
case ClockAccuracy.MINUTES => tt.truncatedTo(now, ChronoUnit.MINUTES)
case ClockAccuracy.HOURS => tt.truncatedTo(now, ChronoUnit.HOURS)
}
}

def applyAccuracy(now: ZonedDateTime, clockAccuracy: ClockAccuracy): ZonedDateTime = applyTTAccuracy(now, clockAccuracy)

def applyAccuracy(now: OffsetDateTime, clockAccuracy: ClockAccuracy): OffsetDateTime = applyTTAccuracy(now, clockAccuracy)

def applyAccuracy(now: LocalDateTime, clockAccuracy: ClockAccuracy): LocalDateTime = applyTTAccuracy(now, clockAccuracy)
}

@inline implicit final def impureClock: Clock1[Identity] = Standard
Expand Down Expand Up @@ -118,10 +188,13 @@ sealed trait LowPriorityClockInstances {
@inline implicit final def fromImpureClock[F[_]](implicit impureClock: Clock1[Identity], F: SyncSafe1[F]): Clock1[F] = {
new Clock1[F] {
override val epoch: F[Long] = F.syncSafe(impureClock.epoch)
override def now(accuracy: ClockAccuracy): F[ZonedDateTime] = F.syncSafe(impureClock.now(accuracy))
override def nowLocal(accuracy: ClockAccuracy): F[LocalDateTime] = F.syncSafe(impureClock.nowLocal(accuracy))
override def nowOffset(accuracy: ClockAccuracy): F[OffsetDateTime] = F.syncSafe(impureClock.nowOffset(accuracy))
@deprecated("use nowZoned")
override def now(accuracy: ClockAccuracy): F[ZonedDateTime] = nowZoned(accuracy)
override def nowLocal(accuracy: ClockAccuracy, zone: ZoneId): F[LocalDateTime] = F.syncSafe(impureClock.nowLocal(accuracy, zone))
override def nowOffset(accuracy: ClockAccuracy, zone: ZoneId): F[OffsetDateTime] = F.syncSafe(impureClock.nowOffset(accuracy, zone))
override def nowZoned(accuracy: ClockAccuracy, zone: ZoneId): F[ZonedDateTime] = F.syncSafe(impureClock.nowZoned(accuracy, zone))
override val monotonicNano: F[Long] = F.syncSafe(impureClock.monotonicNano)

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package izumi.fundamentals.platform.time

import java.time.{Instant, LocalDateTime, ZoneId, ZonedDateTime}

import java.time.{Instant, LocalDateTime, OffsetDateTime, ZoneId, ZonedDateTime}
import izumi.fundamentals.platform.time.IzTime.TZ_UTC

final class IzLongParsers(private val t: Long) extends AnyVal {
Expand All @@ -15,14 +14,35 @@ final class IzLongParsers(private val t: Long) extends AnyVal {
LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
}

def asEpochSecondUtcZoned: ZonedDateTime = {
val instant = Instant.ofEpochSecond(t)
ZonedDateTime.ofInstant(instant, TZ_UTC)
}

def asEpochMillisUtcZoned: ZonedDateTime = {
val instant = Instant.ofEpochMilli(t)
ZonedDateTime.ofInstant(instant, TZ_UTC)
}

def asEpochSecondsUtcOffset: OffsetDateTime = {
val instant = Instant.ofEpochSecond(t)
OffsetDateTime.ofInstant(instant, TZ_UTC)
}

def asEpochMillisUtcOffset: OffsetDateTime = {
val instant = Instant.ofEpochMilli(t)
OffsetDateTime.ofInstant(instant, TZ_UTC)
}

@deprecated("use asEpochSecondZonedsUtc")
def asEpochSecondsUtc: ZonedDateTime = {
val instant = Instant.ofEpochSecond(t)
ZonedDateTime.ofInstant(instant, TZ_UTC)
}

@deprecated("use asEpochMillisZonedUtc")
def asEpochMillisUtc: ZonedDateTime = {
val instant = Instant.ofEpochMilli(t)
ZonedDateTime.ofInstant(instant, TZ_UTC)
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ trait IzTime extends IzTimeSafe {
// parsers
@inline implicit final def toRichLong(value: Long): IzLongParsers = new IzLongParsers(value)
@inline implicit final def stringToParseableTime(value: String): IzTimeParsers = new IzTimeParsers(value)
@inline implicit final def maybeStringToParseableTime(value: Option[String]): IzOptionalTimeParsers = new IzOptionalTimeParsers(value)

// current time
@deprecated("use Clock1.Standard.now")
def utcNow: ZonedDateTime = ZonedDateTime.now(TZ_UTC)

@deprecated("use Clock1.Standard.nowOffset")
def utcNowOffset: OffsetDateTime = OffsetDateTime.now(TZ_UTC)

@deprecated("use Clock1.Standard.utcNow.isoFormat")
def isoNow: String = utcNow.isoFormat

final lazy val ISO_ZONED_DATE_TIME_3NANO: DateTimeFormatter = {
Expand Down

0 comments on commit e666c70

Please sign in to comment.