Skip to content
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

Better time APIs: deprecated effectful code in IzTime, zone can be provided to Clock1 calls, better accuracy logic #2051

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading