Giter VIP home page Giter VIP logo

calev's Introduction

calev

CI Scaladex Scala Steward badge

Small Scala library for parsing systemd.time like calendar event expressions. It is available for Scala (JVM and ScalaJS) 2.12, 2.13 and 3.0. The core module has no dependencies.

What are calendar events?

It serves the same purpose as cron expressions, but uses a different syntax: a "normal" timestamp where each part is a pattern. A pattern is a list of values, a range or * meaning every value. Some examples:

Expression Meaning
*-*-* 12:15:00 every day at 12:15
2020-1,5,9-* 10:00:00 every day on Jan, May and Sept of 2020 at 10:00
Mon *-*-* 09:00:00 every monday at 9:00
Mon..Fri *-*-1/7 15:00:00 on 1.,8.,15. etc of every month at 15:00 but not on weekends

The 1/7 means value 1 and all multiples of 7 added to it. A range with repetition, like 1..12/2 means 1 and all multiples of 2 addet to it within the range 1..12.

For more information see

man systemd.time

or

https://man.cx/systemd.time#heading7

Limitations

This library has some limitations when parsing calendar events compared to systemd:

  • The ~ in the date part for refering last days of a month is not supported.
  • No parts except weekdays may be absent. Date and time parts must all be specified, except seconds are optional.

Modules

  • The core module has zero dependencies and implements the parser and generator for calendar events. It is also published for ScalaJS. With sbt, use:
    libraryDependencies += "com.github.eikek" %% "calev-core" % "0.7.1"
  • The fs2 module contains utilities to work with FS2 streams. These were taken, thankfully and slightly modified to exchange cron expressions for calendar events, from the fs2-cron library. It is also published for ScalaJS. With sbt, use
    libraryDependencies += "com.github.eikek" %% "calev-fs2" % "0.7.1"
  • The doobie module contains Meta, Read and Write instances for CalEvent to use with doobie.
    libraryDependencies += "com.github.eikek" %% "calev-doobie" % "0.7.1"
  • The circe module defines a json decoder and encoder for CalEvent instances to use with circe. It is also published for ScalaJS.
    libraryDependencies += "com.github.eikek" %% "calev-circe" % "0.7.1"
  • The jackson module defines CalevModule for Jackson
    libraryDependencies += "com.github.eikek" %% "calev-jackson" % "0.7.1"
  • The akka module allows to use calendar events with Akka Scheduler and Akka Timers.
    libraryDependencies += "com.github.eikek" %% "calev-akka" % "0.7.1"

Note that the fs2 module is also available via fs2-cron library.

Examples

Core

Calendar events can be read from a string:

import com.github.eikek.calev._

CalEvent.parse("Mon..Fri *-*-* 6,14:0:0")
// res0: Either[String, CalEvent] = Right(
//   value = CalEvent(
//     weekday = List(
//       values = Vector(Range(range = WeekdayRange(start = Mon, end = Fri)))
//     ),
//     date = DateEvent(year = All, month = All, day = All),
//     time = TimeEvent(
//       hour = List(
//         values = Vector(
//           Single(value = 6, rep = None),
//           Single(value = 14, rep = None)
//         )
//       ),
//       minute = List(values = Vector(Single(value = 0, rep = None))),
//       seconds = List(values = Vector(Single(value = 0, rep = None)))
//     ),
//     zone = None
//   )
// )

CalEvent.parse("Mon *-*-* 6,88:0:0")
// res1: Either[String, CalEvent] = Left(
//   value = "Value 88 not in range [0,23]"
// )

There is an unsafe way that throws exceptions:

CalEvent.unsafe("*-*-* 0/2:0:0")
// res2: CalEvent = CalEvent(
//   weekday = All,
//   date = DateEvent(year = All, month = All, day = All),
//   time = TimeEvent(
//     hour = List(values = Vector(Single(value = 0, rep = Some(value = 2)))),
//     minute = List(values = Vector(Single(value = 0, rep = None))),
//     seconds = List(values = Vector(Single(value = 0, rep = None)))
//   ),
//   zone = None
// )

There is a tiny dsl for more conveniently defining events in code:

import com.github.eikek.calev.Dsl._

val ce = CalEvent(AllWeekdays, DateEvent.All, time(0 #/ 2, 0.c, 0.c))
// ce: CalEvent = CalEvent(
//   weekday = All,
//   date = DateEvent(year = All, month = All, day = All),
//   time = TimeEvent(
//     hour = List(values = List(Single(value = 0, rep = Some(value = 2)))),
//     minute = List(values = List(Single(value = 0, rep = None))),
//     seconds = List(values = List(Single(value = 0, rep = None)))
//   ),
//   zone = None
// )
ce.asString
// res3: String = "*-*-* 00/2:00:00"

Once there is a calendar event, the times it will elapse next can be generated:

import java.time._

ce.asString
// res4: String = "*-*-* 00/2:00:00"
val now = LocalDateTime.now
// now: LocalDateTime = 2023-09-03T01:42:02.057451594
ce.nextElapse(now)
// res5: Option[LocalDateTime] = Some(value = 2023-09-03T02:00)
ce.nextElapses(now, 5)
// res6: List[LocalDateTime] = List(
//   2023-09-03T02:00,
//   2023-09-03T04:00,
//   2023-09-03T06:00,
//   2023-09-03T08:00,
//   2023-09-03T10:00
// )

If an event is in the past, the nextElapsed returns a None:

CalEvent.unsafe("1900-01-* 12,14:0:0").nextElapse(LocalDateTime.now)
// res7: Option[LocalDateTime] = None

FS2

The fs2 utilities allow to schedule things based on calendar events. This is the same as fs2-cron provides, only adopted to use calendar events instead of cron expressions. The example is also from there.

import cats.effect.IO
import _root_.fs2.Stream
import com.github.eikek.calev.fs2.Scheduler
import java.time.LocalTime

val everyTwoSeconds = CalEvent.unsafe("*-*-* *:*:0/2")
// everyTwoSeconds: CalEvent = CalEvent(
//   weekday = All,
//   date = DateEvent(year = All, month = All, day = All),
//   time = TimeEvent(
//     hour = All,
//     minute = All,
//     seconds = List(values = Vector(Single(value = 0, rep = Some(value = 2))))
//   ),
//   zone = None
// )
val scheduler = Scheduler.systemDefault[IO]
// scheduler: Scheduler[IO] = com.github.eikek.calev.fs2.Scheduler$$anon$1@357f0cc9

val printTime = Stream.eval(IO(println(LocalTime.now)))
// printTime: Stream[IO, Unit] = Stream(..)

val task = scheduler.awakeEvery(everyTwoSeconds) >> printTime
// task: Stream[[x]IO[x], Unit] = Stream(..)

import cats.effect.unsafe.implicits._
task.take(3).compile.drain.unsafeRunSync()
// 01:42:04.006338192
// 01:42:06.000327762
// 01:42:08.000910107

Doobie

When using doobie, this module contains instances to write and read calendar event expressions through SQL.

import com.github.eikek.calev._
import com.github.eikek.calev.doobie.CalevDoobieMeta._
import _root_.doobie._
import _root_.doobie.implicits._

case class Record(event: CalEvent)

val r = Record(CalEvent.unsafe("Mon *-*-* 0/2:15"))
// r: Record = Record(
//   event = CalEvent(
//     weekday = List(values = Vector(Single(day = Mon))),
//     date = DateEvent(year = All, month = All, day = All),
//     time = TimeEvent(
//       hour = List(values = Vector(Single(value = 0, rep = Some(value = 2)))),
//       minute = List(values = Vector(Single(value = 15, rep = None))),
//       seconds = List(values = List(Single(value = 0, rep = None)))
//     ),
//     zone = None
//   )
// )

val insert =
  sql"INSERT INTO mytable (event) VALUES (${r.event})".update.run
// insert: ConnectionIO[Int] = Suspend(
//   a = Uncancelable(
//     body = cats.effect.kernel.MonadCancel$$Lambda$2205/0x000000080187f010@16bed816
//   )
// )

val select =
  sql"SELECT event FROM mytable WHERE id = 1".query[Record].unique
// select: ConnectionIO[Record] = Suspend(
//   a = Uncancelable(
//     body = cats.effect.kernel.MonadCancel$$Lambda$2205/0x000000080187f010@4cae7d5
//   )
// )

Circe

The defined encoders/decoders can be put in scope to use calendar event expressions in json.

import com.github.eikek.calev._
import com.github.eikek.calev.circe.CalevCirceCodec._
import io.circe._
import io.circe.generic.semiauto._
import io.circe.syntax._

case class Meeting(name: String, event: CalEvent)
object Meeting {
  implicit val jsonDecoder = deriveDecoder[Meeting]
  implicit val jsonEncoder = deriveEncoder[Meeting]
}

val meeting = Meeting("trash can", CalEvent.unsafe("Mon..Fri *-*-* 14,18:0"))
// meeting: Meeting = Meeting(
//   name = "trash can",
//   event = CalEvent(
//     weekday = List(
//       values = Vector(Range(range = WeekdayRange(start = Mon, end = Fri)))
//     ),
//     date = DateEvent(year = All, month = All, day = All),
//     time = TimeEvent(
//       hour = List(
//         values = Vector(
//           Single(value = 14, rep = None),
//           Single(value = 18, rep = None)
//         )
//       ),
//       minute = List(values = Vector(Single(value = 0, rep = None))),
//       seconds = List(values = List(Single(value = 0, rep = None)))
//     ),
//     zone = None
//   )
// )
val json = meeting.asJson.noSpaces
// json: String = "{\"name\":\"trash can\",\"event\":\"Mon..Fri *-*-* 14,18:00:00\"}"
val read = for {
  parsed <- parser.parse(json)
  value <- parsed.as[Meeting]
} yield value
// read: Either[Error, Meeting] = Right(
//   value = Meeting(
//     name = "trash can",
//     event = CalEvent(
//       weekday = List(
//         values = Vector(Range(range = WeekdayRange(start = Mon, end = Fri)))
//       ),
//       date = DateEvent(year = All, month = All, day = All),
//       time = TimeEvent(
//         hour = List(
//           values = Vector(
//             Single(value = 14, rep = None),
//             Single(value = 18, rep = None)
//           )
//         ),
//         minute = List(values = Vector(Single(value = 0, rep = None))),
//         seconds = List(values = Vector(Single(value = 0, rep = None)))
//       ),
//       zone = None
//     )
//   )
// )

Jackson

Add CalevModule to use calendar event expressions in json:

import com.github.eikek.calev._
import com.fasterxml.jackson.core.`type`.TypeReference
import com.fasterxml.jackson.databind.json.JsonMapper
import com.github.eikek.calev.jackson.CalevModule

val jackson = JsonMapper
  .builder()
  .addModule(new CalevModule())
  .build()
// jackson: JsonMapper = com.fasterxml.jackson.databind.json.JsonMapper@4c821dc7

val myEvent    = CalEvent.unsafe("Mon *-*-* 05:00/10:00")
// myEvent: CalEvent = CalEvent(
//   weekday = List(values = Vector(Single(day = Mon))),
//   date = DateEvent(year = All, month = All, day = All),
//   time = TimeEvent(
//     hour = List(values = Vector(Single(value = 5, rep = None))),
//     minute = List(values = Vector(Single(value = 0, rep = Some(value = 10)))),
//     seconds = List(values = Vector(Single(value = 0, rep = None)))
//   ),
//   zone = None
// )

val eventSerialized = jackson.writeValueAsString(myEvent)
// eventSerialized: String = "\"Mon *-*-* 05:00/10:00\""
val eventDeserialized = jackson.readValue(eventSerialized, new TypeReference[CalEvent] {})
// eventDeserialized: CalEvent = CalEvent(
//   weekday = List(values = Vector(Single(day = Mon))),
//   date = DateEvent(year = All, month = All, day = All),
//   time = TimeEvent(
//     hour = List(values = Vector(Single(value = 5, rep = None))),
//     minute = List(values = Vector(Single(value = 0, rep = Some(value = 10)))),
//     seconds = List(values = Vector(Single(value = 0, rep = None)))
//   ),
//   zone = None
// )

Akka

Akka Timers

When building actor behavior, use CalevBehaviors.withCalevTimers to get access to CalevTimerScheduler.

Use CalevTimerScheduler to start single Akka Timer for the upcoming event according to given calendar event definition.

import com.github.eikek.calev.CalEvent
import java.time._
import com.github.eikek.calev.akka._
import com.github.eikek.calev.akka.dsl.CalevBehaviors
import _root_.akka.actor.typed._
import _root_.akka.actor.typed.scaladsl.Behaviors._

sealed trait Message
case class Tick(timestamp: ZonedDateTime) extends Message
case class Ping()                         extends Message

// every day, every full minute
def calEvent   = CalEvent.unsafe("*-*-* *:0/1:0")  

CalevBehaviors.withCalevTimers[Message]() { scheduler =>
  scheduler.startSingleTimer(calEvent, Tick)
    receiveMessage[Message] {
      case tick: Tick =>
        println(
          s"Tick scheduled at ${tick.timestamp.toLocalTime} received at: ${LocalTime.now}"
        )
        same
      case ping: Ping =>
        println("Ping received")
        same
    }
}
// res9: Behavior[Message] = Deferred(TimerSchedulerImpl.scala:29)

Use CalevBehaviors.withCalendarEvent to schedule messages according to the given calendar event definition.

CalevBehaviors.withCalendarEvent(calEvent)(
  Tick,
  receiveMessage[Message] {
    case tick: Tick =>
      println(
        s"Tick scheduled at ${tick.timestamp.toLocalTime} received at: ${LocalTime.now}"
      )
      same
    case ping: Ping =>
      println("Ping received")
      same
  }
)
// res10: Behavior[Message] = Deferred(InterceptorImpl.scala:29-30)

Testing

See CalevBehaviorsTest

Akka Scheduler

Schedule the sending of a message to the given target Actor at the time of the upcoming event according to the given calendar event definition.

def behavior(tickReceiver: ActorRef[Tick]): Behavior[Message] = 
  setup { actorCtx =>
    actorCtx.scheduleOnceWithCalendarEvent(calEvent, tickReceiver, Tick)
    same
  }

Schedule the running of a Runnable at the time of the upcoming event according to the given calendar event definition.

implicit val system: ActorSystem[_] = ActorSystem(empty, "my-system")
// system: ActorSystem[_] = akka://my-system
import system.executionContext

calevScheduler().scheduleOnceWithCalendarEvent(calEvent, () => {
  println(
      s"Called at: ${LocalTime.now}"
  )
})
// res11: Option[<none>.<root>.akka.actor.Cancellable] = Some(
//   value = akka.actor.LightArrayRevolverScheduler$TaskHolder@432e242d
// )
system.terminate()

calev's People

Contributors

eikek avatar eikek-scala-steward[bot] avatar mergify[bot] avatar pawelkaczor avatar renovate-bot avatar renovate[bot] avatar scala-steward avatar sheryv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

calev's Issues

Bug in readme

The readme shows a bug when generating events where there aren't any.

If an event is in the past, the nextElapsed returns a None:

CalEvent.unsafe("1900-01-* 12,14:0:0").nextElapse(LocalDateTime.now)
// res7: Option[LocalDateTime] = Some(value = 2021-01-06T12:00)

Introduce Calendar API

A Calendar instance contains calendar events (instances of CalEvent) each identified by a name (String).

Calendar interface

case class ScheduledEvent(scheduledAt: ZonedDateTime, eventName: String, calendarEvent: CalEvent)

trait Calendar {
  def calendarEvents: Map[String, CalEvent]

  def upcomingEvent(ref: ZonedDateTme): Option[ScheduledEvent]

}

There could be different types of the calendar: DailyCalendar, WeeklyCalendar, MonthlyCalendar. The implementation of upcomingEvent method could be optimized for each calendar type.

CalEvent.parse does not parse zone offsets

val cal = CalEvent.unsafe("Mon --* 15:00:00")
val calWithZone = cal.copy(zone = Some(ZoneOffset.UTC))

calWithZone.asString produces: Mon --* 15:00:00 Z

It should produce: Mon --* 15:00:00 UTC

Inconsistent output dates between calev-core and systemd cli

Value of next elapse calculated by this library is different from the output of systemd-analyze calendar command for the same input values. Example:

  • systemd
$ systemd-analyze calendar --iterations=1 "*-2-* 10:00:00"
  Original form: *-2-* 10:00:00
Normalized form: *-02-* 10:00:00
    Next elapse: Sat 2025-02-01 10:00:00 CET
       (in UTC): Sat 2025-02-01 09:00:00 UTC
       From now: 10 months 13 days left

$ date
Wed 20 Mar 2024 10:01:36 AM CET
  • main method from calev-core
$ sbt core/run --ref "2024-03-20T10:01:00" "*-2-* 10:00:00"
Normalized:  *-02-* 10:00:00
Reference:   2024-03-20T10:01
Next Elapses:
- 2025-02-21T10:00

systemd returns 2025-02-01 and calev: 2025-02-21. Is this difference intentional or is it a bug?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.