Skip to content

Instantly share code, notes, and snippets.

@jdegoes
Last active December 8, 2020 16:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jdegoes/c751b21bd789ebb1b37d9c2af2f93d00 to your computer and use it in GitHub Desktop.
Save jdegoes/c751b21bd789ebb1b37d9c2af2f93d00 to your computer and use it in GitHub Desktop.
package zio.cron
import java.time.{ Month => JMonth }
import java.time.{ DayOfWeek => JDayOfWeek }
import zio.Schedule
// 0 16 1,15 * * echo Timesheets Due > /dev/console
final case class CronSchedule(
minute: CronSchedule.Minute,
hour: CronSchedule.Hour,
dayOfMonth: CronSchedule.DayOfMonth,
month: CronSchedule.Month,
dayOfWeek: CronSchedule.DayOfWeek
) {
final def render: String =
s"${minute.render} ${hour.render} ${dayOfMonth.render} ${month.render} ${dayOfWeek.render}"
final def toSchedule: Schedule[Any, Any, Long] =
minute.toSchedule *> hour.toSchedule *> dayOfMonth.toSchedule *> month.toSchedule *> dayOfWeek.toSchedule
}
object CronSchedule {
sealed trait Minute { self =>
import Minute._
override final def equals(that: scala.Any): Boolean =
that match {
case that: Minute => self.toList.toSet == that.toList.toSet
case _ => false
}
final def filter(f: Int => Boolean): Minute = List(toList.filter(f))
final def filterNot(f: Int => Boolean): Minute = filter(v => !f(v))
final def render: String =
self match {
case Any => "*"
case List(value) => value.mkString(",")
case Range(value) =>
if (value.step != 1) s"${value.start}-${value.end}/${value.step}"
else s"${value.start}-${value.end}"
}
final def simplify: Minute = {
lazy val elements = self.toList
if (self == Any || (Any.toList.toSet -- elements.toSet).isEmpty) Any
else if (elements.isEmpty) self
else {
val min = elements.min
val max = elements.max
val range = min to max
if ((range.toSet -- elements.toSet).isEmpty) Range(min to max)
else self
}
}
final def toList: scala.List[Int] =
self match {
case Any => (0 to 59).toList
case List(value) => value.filter(i => i >= 0 && i <= 59).toList.sorted
case Range(value) => value.filter(i => i >= 0 && i <= 59).toList.sorted
}
final def toSchedule: Schedule[Any, Any, Long] =
(toList.foldLeft[Schedule[Any, Any, Any]](Schedule.recurs(0)) {
case (acc, minute) => acc || Schedule.minuteOfHour(minute)
}) *> Schedule.count
}
object Minute {
object Any extends Minute
final case class List(value: Iterable[Int]) extends Minute
final case class Range(value: scala.Range) extends Minute
def any: Any = Any
def list(value: Int, values: Int*): Minute = List(Vector(value) ++ values.toVector)
def list(values: Iterable[Int]): Minute = List(values)
def range(range: scala.Range): Minute = Range(range)
def step(increment: Int): Minute = Range((0 to 59) by increment)
}
sealed trait Hour { self =>
import Hour._
final def filter(f: Int => Boolean): Hour = List(toList.filter(f))
final def filterNot(f: Int => Boolean): Hour = filter(v => !f(v))
final def render: String =
self match {
case Any => "*"
case List(value) => value.mkString(",")
case Range(value) =>
if (value.step != 1) s"${value.start}-${value.end}/${value.step}"
else s"${value.start}-${value.end}"
}
final def simplify: Hour = {
lazy val elements = self.toList
if (self == Any || (Any.toList.toSet -- elements.toSet).isEmpty) Any
else if (elements.isEmpty) self
else {
val min = elements.min
val max = elements.max
val range = min to max
if ((range.toSet -- elements.toSet).isEmpty) Range(min to max)
else self
}
}
final def toList: scala.List[Int] =
self match {
case Any => (0 to 23).toList
case List(value) => value.filter(i => i >= 0 && i <= 23).toList.sorted
case Range(value) => value.filter(i => i >= 0 && i <= 23).toList.sorted
}
final def toSchedule: Schedule[Any, Any, Long] =
(toList.foldLeft[Schedule[Any, Any, Any]](Schedule.recurs(0)) {
case (acc, hour) => acc || Schedule.hourOfDay(hour)
}) *> Schedule.count
}
object Hour {
object Any extends Hour
final case class List(value: Iterable[Int]) extends Hour
final case class Range(value: scala.Range) extends Hour
}
sealed trait DayOfMonth { self =>
import DayOfMonth._
final def filter(f: Int => Boolean): DayOfMonth = List(toList.filter(f))
final def filterNot(f: Int => Boolean): DayOfMonth = filter(v => !f(v))
final def render: String =
self match {
case Any => "*"
case List(value) => value.mkString(",")
case Range(value) =>
if (value.step != 1) s"${value.start}-${value.end}/${value.step}"
else s"${value.start}-${value.end}"
}
final def simplify: DayOfMonth = {
lazy val elements = self.toList
if (self == Any || (Any.toList.toSet -- elements.toSet).isEmpty) Any
else if (elements.isEmpty) self
else {
val min = elements.min
val max = elements.max
val range = min to max
if ((range.toSet -- elements.toSet).isEmpty) Range(min to max)
else self
}
}
final def toList: scala.List[Int] =
self match {
case Any => (1 to 31).toList
case List(value) => value.filter(i => i >= 1 && i <= 31).toList.sorted
case Range(value) => value.filter(i => i >= 1 && i <= 31).toList.sorted
}
final def toSchedule: Schedule[Any, Any, Long] =
(toList.foldLeft[Schedule[Any, Any, Any]](Schedule.recurs(0)) {
case (acc, day) => acc || Schedule.dayOfMonth(day)
}) *> Schedule.count
}
object DayOfMonth {
object Any extends DayOfMonth
final case class List(value: Iterable[Int]) extends DayOfMonth
final case class Range(value: scala.Range) extends DayOfMonth
}
sealed trait Month { self =>
import Month._
final def filter(f: JMonth => Boolean): Month = List(toList.filter(f))
final def filterNot(f: JMonth => Boolean): Month = filter(v => !f(v))
final def render: String =
self match {
case Any => "*"
case List(value) => value.mkString(",")
case Range(start, end, step) =>
if (step != 1) s"${start}-${end}/${step}"
else s"${start}-${end}"
}
final def simplify: Month = {
lazy val elements = self.toList
if (self == Any || (Any.toList.toSet -- elements.toSet).isEmpty) Any
else if (elements.isEmpty) self
else {
val min = elements.map(_.getValue()).min
val max = elements.map(_.getValue()).max
val range = (min to max).map(JMonth.of(_))
if ((range.toSet -- elements.toSet).isEmpty) Range(JMonth.of(min), JMonth.of(max))
else self
}
}
final def toList: scala.List[JMonth] =
self match {
case Any => JMonth.values.toList
case List(value) => value.toList.sortBy(_.getValue())
case Range(start, end, increment) =>
scala.Range(start.getValue(), end.getValue(), increment).toList.map(JMonth.of(_))
}
final def toSchedule: Schedule[Any, Any, Long] =
(toList.foldLeft[Schedule[Any, Any, Any]](Schedule.recurs(0)) {
case (acc, month) => acc || ??? // FIXME: Add Schedule.monthOfYear
}) *> Schedule.count
}
object Month {
object Any extends Month
final case class List(value: Iterable[JMonth]) extends Month
final case class Range(start: JMonth, end: JMonth, increment: Int = 1) extends Month
}
sealed trait DayOfWeek { self =>
import DayOfWeek._
final def filter(f: JDayOfWeek => Boolean): DayOfWeek = List(toList.filter(f))
final def filterNot(f: JDayOfWeek => Boolean): DayOfWeek = filter(v => !f(v))
final def render: String =
self match {
case Any => "*"
case List(value) => value.mkString(",")
case Range(start, end, step) =>
if (step != 1) s"${start}-${end}/${step}"
else s"${start}-${end}"
}
final def toList: scala.List[JDayOfWeek] =
self match {
case Any => JDayOfWeek.values().toList
case List(value) => value.toList.sortBy(_.getValue())
case Range(start, end, increment) =>
scala.Range(start.getValue(), end.getValue(), increment).map(JDayOfWeek.of(_)).toList
}
final def toSchedule: Schedule[Any, Any, Long] =
(toList.foldLeft[Schedule[Any, Any, Any]](Schedule.recurs(0)) {
case (acc, dayOfWeek) => acc || Schedule.dayOfWeek(dayOfWeek.getValue())
}) *> Schedule.count
}
object DayOfWeek {
object Any extends DayOfWeek
final case class List(value: Iterable[JDayOfWeek]) extends DayOfWeek
final case class Range(start: JDayOfWeek, end: JDayOfWeek, increment: Int = 1) extends DayOfWeek
}
// 0 16 1,15 * *
def fromString(string: String): Either[String, CronSchedule] =
???
}
final case class CrontabEntry(schedule: CronSchedule, process: String) {
def render: String = schedule.render + " " + process
}
object CrontabEntry {
// 0 16 1,15 * * echo Timesheets Due > /dev/console
def fromString(string: String): Either[String, CrontabEntry] =
???
}
final case class CrontabFile(entries: Vector[CrontabEntry]) {
def render: String = entries.map(_.render).mkString("\n")
}
object CrontabFile {
def fromString(string: String): Either[::[String], CrontabFile] = {
val list: List[Either[String, CrontabEntry]] =
string.split("""(\r?\n)+""").toList.map(_.trim).filterNot(_ == "").map(CrontabEntry.fromString(_))
def collect[A, B](value: List[Either[A, B]]): Either[::[A], List[B]] =
value match {
case Nil => Right(Nil)
case head :: tail =>
val tail2 = collect(tail)
tail2 match {
case Left(as) => head.left.toOption.fold(Left(as))(a => Left(::(a, as)))
case Right(bs) => head.fold[Either[::[A], List[B]]](a => Left(::(a, Nil)), b => Right(b :: bs))
}
}
collect(list).map(list => CrontabFile(list.toVector))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment