Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/eikek/calev
Work with systemd.time like calendar events in Scala
https://github.com/eikek/calev
calendar-events fs2 functional-programming scala scala-library
Last synced: 8 days ago
JSON representation
Work with systemd.time like calendar events in Scala
- Host: GitHub
- URL: https://github.com/eikek/calev
- Owner: eikek
- License: mit
- Created: 2020-03-04T20:41:53.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2024-10-23T21:39:52.000Z (14 days ago)
- Last Synced: 2024-10-24T10:09:57.394Z (13 days ago)
- Topics: calendar-events, fs2, functional-programming, scala, scala-library
- Language: Scala
- Homepage:
- Size: 724 KB
- Stars: 11
- Watchers: 2
- Forks: 4
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# calev
[![CI](https://github.com/eikek/calev/actions/workflows/ci.yml/badge.svg)](https://github.com/eikek/calev/actions/workflows/ci.yml)
[![Scaladex](https://index.scala-lang.org/eikek/calev/latest.svg?color=blue)](https://index.scala-lang.org/eikek/calev/calev-core)
[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org)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
```shell
man systemd.time
```or
## 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:
```sbt
libraryDependencies += "com.github.eikek" %% "calev-core" % "0.7.2"
```
- The *fs2* module contains utilities to work with
[FS2](https://github.com/functional-streams-for-scala/fs2) streams.
These were taken, thankfully and slightly modified to exchange cron expressions
for calendar events, from the
[fs2-cron](https://github.com/fthomas/fs2-cron) library. It is also published
for ScalaJS. With sbt, use
```sbt
libraryDependencies += "com.github.eikek" %% "calev-fs2" % "0.7.2"
```
- The *doobie* module contains `Meta`, `Read` and `Write` instances
for `CalEvent` to use with
[doobie](https://github.com/tpolecat/doobie).
```sbt
libraryDependencies += "com.github.eikek" %% "calev-doobie" % "0.7.2"
```
- The *circe* module defines a json decoder and encoder for `CalEvent`
instances to use with [circe](https://github.com/circe/circe). It is also
published for ScalaJS.
```sbt
libraryDependencies += "com.github.eikek" %% "calev-circe" % "0.7.2"
```
- The *jackson* module defines `CalevModule` for [Jackson](https://github.com/FasterXML/jackson)
```sbt
libraryDependencies += "com.github.eikek" %% "calev-jackson" % "0.7.2"
```
- The *akka* module allows to use calendar events with [Akka Scheduler](https://doc.akka.io/docs/akka/current/scheduler.html)
and [Akka Timers](https://doc.akka.io/docs/akka/current/typed/interaction-patterns.html#typed-scheduling).
```sbt
libraryDependencies += "com.github.eikek" %% "calev-akka" % "0.7.2"
```Note that the fs2 module is also available via
[fs2-cron](https://github.com/fthomas/fs2-cron) library.## Examples
### Core
Calendar events can be read from a string:
```scala
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:
```scala
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:
```scala
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:```scala
import java.time._ce.asString
// res4: String = "*-*-* 00/2:00:00"
val now = LocalDateTime.now
// now: LocalDateTime = 2024-05-26T10:08:08.053769386
ce.nextElapse(now)
// res5: Option[LocalDateTime] = Some(value = 2024-05-26T12:00)
ce.nextElapses(now, 5)
// res6: List[LocalDateTime] = List(
// 2024-05-26T12:00,
// 2024-05-26T14:00,
// 2024-05-26T16:00,
// 2024-05-26T18:00,
// 2024-05-26T20:00
// )
```If an event is in the past, the `nextElapsed` returns a `None`:
```scala
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](https://github.com/fthomas/fs2-cron)
provides, only adopted to use calendar events instead of cron
expressions. The example is also from there.```scala
import cats.effect.IO
import _root_.fs2.Stream
import com.github.eikek.calev.fs2.Scheduler
import java.time.LocalTimeval 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@2899745eval 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()
// 10:08:10.006032431
// 10:08:12.000529963
// 10:08:14.000333048
```### Doobie
When using doobie, this module contains instances to write and read
calendar event expressions through SQL.```scala
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$2310/0x00000008018c29c0@18f7a872
// )
// )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$2310/0x00000008018c29c0@61defefb
// )
// )
```### Circe
The defined encoders/decoders can be put in scope to use calendar
event expressions in json.```scala
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:
```scala
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.CalevModuleval jackson = JsonMapper
.builder()
.addModule(new CalevModule())
.build()
// jackson: JsonMapper = com.fasterxml.jackson.databind.json.JsonMapper@648100deval 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.```scala
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.```scala
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](https://github.com/eikek/calev/blob/master/modules/akka/src/test/scala/com/github/eikek/calev/akka/dsl/CalevBehaviorsTest.scala)
#### 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.```scala
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.```scala
implicit val system: ActorSystem[_] = ActorSystem(empty, "my-system")
// system: ActorSystem[_] = akka://my-system
import system.executionContextcalevScheduler().scheduleOnceWithCalendarEvent(calEvent, () => {
println(
s"Called at: ${LocalTime.now}"
)
})
// res11: Option[..akka.actor.Cancellable] = Some(
// value = akka.actor.LightArrayRevolverScheduler$TaskHolder@44929971
// )
system.terminate()
```