All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cron4zio.package.scala Maven / Gradle / Ivy

import com.cronutils.model.Cron
import com.cronutils.model.definition.{CronConstraintsFactory, CronDefinition, CronDefinitionBuilder}
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import zio.{Duration, IO, Schedule, ZIO}
import java.time.temporal.ChronoUnit
import java.time.{LocalDateTime, ZoneId}
import java.util.TimeZone
import scala.util.Try

package object cron4zio {
  // format: off
  /* Our cron definition uses below cron expressions that go from seconds to day of week in the following order:
   Seconds	0-59	                    - * /
   Minutes	0-59	                    - * /
   Hours	0-23	                      - * /
   Day (of month)	1-31	            * ? / L W
   Month	1-12 or JAN-DEC	            - * /
   Day (of week)	1-7 or SUN-SAT	    - * ? / L #
   Year (optional)	empty, 1970-2099	- * /
 */
  val cronDefinition: CronDefinition = CronDefinitionBuilder.defineCron()
    .withSeconds().withValidRange(0, 59).and()
    .withMinutes().withValidRange(0, 59).and()
    .withHours().withValidRange(0, 23).and()
    .withDayOfMonth().withValidRange(1, 31).supportsL().supportsW().supportsLW().supportsQuestionMark().and()
    .withMonth().withValidRange(1, 12).and()
    .withDayOfWeek().withValidRange(1, 7).withMondayDoWValue(1).supportsHash().supportsL().supportsQuestionMark().and()
    .withYear().withValidRange(1970, 2099).withStrictRange().optional().and()
    .withCronValidation(CronConstraintsFactory.ensureEitherDayOfWeekOrDayOfMonth())
    .instance()
  // format: on

  val cronParser: CronParser = new CronParser(cronDefinition)

  /** @param cron
    * @return
    *   Cron instance, which corresponds to cron expression received as String or throws IllegalArgumentException if expression
    *   does not match cron definition
    */
  def unsafeParse(cron: String): Cron = cronParser.parse(cron)

  /** @param cron
    * @return
    *   Cron instance, which corresponds to cron expression received as String or throws IllegalArgumentException if expression
    *   does not match cron definition
    */
  def parse(cron: String): Try[Cron] = Try(cronParser.parse(cron))

  /** @param cron
    * @param zoneId
    * @return
    *   Duration between current time and next run based on cron,zoneId provided or throws IllegalArgumentException if for a valid
    *   cron next run cannot be generated
    */
  def getNextRunDuration(
      cron: Cron,
      zoneId: ZoneId = TimeZone.getDefault.toZoneId
  ): IO[IllegalArgumentException, Duration] =
    for {
      timeNow <- ZIO.succeed(LocalDateTime.now().atZone(zoneId))
      timeNext <- ZIO
        .attempt(
          ExecutionTime
            .forCron(cron)
            .nextExecution(timeNow)
            .orElseThrow(() => new IllegalArgumentException(s"Cannot generate next run from provided cron => ${cron.asString()}"))
        )
        .refineToOrDie[IllegalArgumentException]
      durationInNanos = timeNow.until(timeNext, ChronoUnit.NANOS)
      duration        = Duration.fromNanos(durationInNanos)
    } yield duration

  /** @param cron
    * @param zoneId
    * @return
    *   Effect which will sleep in non-blocking way for duration between current time and next run
    */
  def sleepForCron(
      cron: Cron,
      zoneId: ZoneId = TimeZone.getDefault.toZoneId
  ): IO[IllegalArgumentException, Unit] =
    getNextRunDuration(cron, zoneId).flatMap(duration => ZIO.sleep(duration))

  /** @param effect
    * @param cron
    * @param maxRecurs
    * @param zoneId
    * @return
    *   This will either return number of times effect ran for cron or will never return anything and keep running forever based
    *   on value of param maxRecurs
    */
  def repeatEffectForCron[R, E >: Throwable, A](
      effect: ZIO[R, E, A],
      cron: Cron,
      maxRecurs: Int = 0,
      zoneId: ZoneId = TimeZone.getDefault.toZoneId
  ): ZIO[R, E, Long] =
    if (maxRecurs != 0)
      (sleepForCron(cron, zoneId) *> effect).repeat(Schedule.recurs(maxRecurs))
    else
      (sleepForCron(cron, zoneId) *> effect).repeat(Schedule.forever)

  /** @param tasks
    * @param maxRecurs
    * @param zoneId
    * @return
    *   This will either return number of times effects ran for cron or will never return anything and keep running all effects
    *   forever based on value of param maxRecurs
    */
  def repeatEffectsForCron[R, E >: Throwable, A](
      tasks: List[(ZIO[R, E, A], Cron)],
      maxRecurs: Int = 0,
      zoneId: ZoneId = TimeZone.getDefault.toZoneId
  ): ZIO[R, E, List[Long]] =
    ZIO.foreachPar(tasks)(input => repeatEffectForCron(input._1, input._2, maxRecurs, zoneId))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy