Skip to content

Commit a730a7e

Browse files
authored
Merge pull request #599 from fthomas/fix/598
Fix off-by-one error in fromNowUntilNext
2 parents 8d08871 + 99a4706 commit a730a7e

File tree

2 files changed

+32
-8
lines changed

2 files changed

+32
-8
lines changed

modules/core/src/main/scala/eu/timepit/fs2cron/ZonedDateTimeScheduler.scala

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package eu.timepit.fs2cron
1818

1919
import cats.effect.{Sync, Temporal}
20-
import cats.syntax.all._
20+
import cats.syntax.all.*
2121

2222
import java.time.temporal.ChronoUnit
2323
import java.time.{Instant, ZoneId, ZoneOffset, ZonedDateTime}
@@ -29,14 +29,20 @@ abstract class ZonedDateTimeScheduler[F[_], Schedule](zoneId: F[ZoneId])(implici
2929
) extends Scheduler[F, Schedule] {
3030
def next(from: ZonedDateTime, schedule: Schedule): F[ZonedDateTime]
3131

32-
override def fromNowUntilNext(schedule: Schedule): F[FiniteDuration] =
33-
now.flatMap { from =>
34-
next(from, schedule).map { to =>
35-
val durationInMillis = from.until(to, ChronoUnit.MILLIS)
36-
FiniteDuration(durationInMillis, TimeUnit.MILLISECONDS)
37-
}
32+
def durationUntilNext(from: ZonedDateTime, schedule: Schedule): F[FiniteDuration] =
33+
next(from, schedule).map { to =>
34+
val durationInMillis = from
35+
// Since `until` only returns complete units between `from` and `to`,
36+
// we truncate `from` to milliseconds, so that `durationInMillis` + `from`
37+
// is never before `to`. See https://github.com/fthomas/fs2-cron/issues/598.
38+
.truncatedTo(ChronoUnit.MILLIS)
39+
.until(to, ChronoUnit.MILLIS)
40+
FiniteDuration(durationInMillis, TimeUnit.MILLISECONDS)
3841
}
3942

43+
override def fromNowUntilNext(schedule: Schedule): F[FiniteDuration] =
44+
now.flatMap(from => durationUntilNext(from, schedule))
45+
4046
private val now: F[ZonedDateTime] =
4147
(temporal.realTime, zoneId).mapN((d, z) => Instant.EPOCH.plusNanos(d.toNanos).atZone(z))
4248
}

modules/core/src/test/scala/eu/timepit/fs2cron/ZonedDateTimeSchedulerSuite.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import cats.effect.IO
2020
import fs2.Stream
2121
import munit.CatsEffectSuite
2222

23-
import java.time.{Instant, ZoneId, ZoneOffset}
23+
import java.time.{Instant, ZoneId, ZoneOffset, ZonedDateTime}
24+
import java.util.concurrent.TimeUnit
25+
import scala.concurrent.duration.FiniteDuration
2426

2527
trait ZonedDateTimeSchedulerSuite[Schedule] extends CatsEffectSuite {
2628
def schedulerCompanion: ZonedDateTimeScheduler.Companion[Schedule]
@@ -76,4 +78,20 @@ trait ZonedDateTimeSchedulerSuite[Schedule] extends CatsEffectSuite {
7678
val s2 = s1.map(instantSeconds).take(2).forall(!isEven(_))
7779
assertIO(s2.compile.last, Some(true))
7880
}
81+
82+
test("durationUntilNext: from + duration >= next") {
83+
val scheduler = schedulerUtc.asInstanceOf[ZonedDateTimeScheduler[IO, Schedule]]
84+
85+
val from = ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 100000, ZoneOffset.UTC)
86+
val duration = scheduler.durationUntilNext(from, everySecond)
87+
88+
val next = scheduler.next(from, everySecond)
89+
val fromPlusDuration = duration.map(d => from.plusNanos(d.toNanos))
90+
val fromPlusDurationGtEqNext = fromPlusDuration.flatMap(fpd => next.map(n => !fpd.isBefore(n)))
91+
92+
for {
93+
_ <- assertIO(duration, FiniteDuration(1000L, TimeUnit.MILLISECONDS))
94+
_ <- assertIOBoolean(fromPlusDurationGtEqNext)
95+
} yield ()
96+
}
7997
}

0 commit comments

Comments
 (0)