{"id":18005546,"url":"https://github.com/eikek/calev","last_synced_at":"2025-03-26T10:32:08.344Z","repository":{"id":37846121,"uuid":"244999717","full_name":"eikek/calev","owner":"eikek","description":"Work with systemd.time like calendar events in Scala","archived":false,"fork":false,"pushed_at":"2024-10-23T21:39:52.000Z","size":741,"stargazers_count":11,"open_issues_count":7,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-24T10:09:57.394Z","etag":null,"topics":["calendar-events","fs2","functional-programming","scala","scala-library"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eikek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-03-04T20:41:53.000Z","updated_at":"2024-10-23T21:39:55.000Z","dependencies_parsed_at":"2024-01-14T15:25:20.832Z","dependency_job_id":"f6d8de77-2dba-4805-b4f0-541c5ea5e5c3","html_url":"https://github.com/eikek/calev","commit_stats":{"total_commits":536,"total_committers":9,"mean_commits":59.55555555555556,"dds":0.6082089552238805,"last_synced_commit":"8c92e80a2d879bddcc3e183257e5ef58d8f8f0e7"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikek%2Fcalev","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikek%2Fcalev/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikek%2Fcalev/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikek%2Fcalev/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eikek","download_url":"https://codeload.github.com/eikek/calev/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245637100,"owners_count":20648092,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["calendar-events","fs2","functional-programming","scala","scala-library"],"created_at":"2024-10-30T00:20:06.365Z","updated_at":"2025-03-26T10:32:07.871Z","avatar_url":"https://github.com/eikek.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# calev\n[![CI](https://github.com/eikek/calev/actions/workflows/ci.yml/badge.svg)](https://github.com/eikek/calev/actions/workflows/ci.yml)\n[![Scaladex](https://index.scala-lang.org/eikek/calev/latest.svg?color=blue)](https://index.scala-lang.org/eikek/calev/calev-core)\n[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat\u0026logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org)\n\nSmall Scala library for parsing systemd.time like calendar event\nexpressions. It is available for Scala (JVM and ScalaJS) 2.12, 2.13\nand 3.0. The core module has no dependencies.\n\n## What are calendar events?\n\nIt serves the same purpose as cron expressions, but uses a different\nsyntax: a \"normal\" timestamp where each part is a pattern. A pattern\nis a list of values, a range or `*` meaning every value. Some\nexamples:\n\n| Expression                  | Meaning                                                      |\n|-----------------------------|--------------------------------------------------------------|\n| `*-*-* 12:15:00`            | every day at 12:15                                           |\n| `2020-1,5,9-* 10:00:00`     | every day on Jan, May and Sept of 2020 at 10:00              |\n| `Mon *-*-* 09:00:00`        | every monday at 9:00                                         |\n| `Mon..Fri *-*-1/7 15:00:00` | on 1.,8.,15. etc of every month at 15:00 but not on weekends |\n\nThe `1/7` means value `1` and all multiples of `7` added to it. A\nrange with repetition, like `1..12/2` means `1` and all multiples of\n`2` addet to it within the range `1..12`.\n\nFor more information see\n\n```shell\nman systemd.time\n```\n\nor\n\n\u003chttps://man.cx/systemd.time#heading7\u003e\n\n\n## Limitations\n\nThis library has some limitations when parsing calendar events\ncompared to systemd:\n\n- The `~` in the date part for refering last days of a month is not\n  supported.\n- No parts except weekdays may be absent. Date and time parts must all\n  be specified, except seconds are optional.\n\n## Modules\n\n- The *core* module has zero dependencies and implements the parser\n  and generator for calendar events. It is also published for ScalaJS.\n  With sbt, use:\n  ```sbt\n  libraryDependencies += \"com.github.eikek\" %% \"calev-core\" % \"0.7.2\"\n  ```\n- The *fs2* module contains utilities to work with\n  [FS2](https://github.com/functional-streams-for-scala/fs2) streams.\n  These were taken, thankfully and slightly modified to exchange cron expressions\n  for calendar events, from the\n  [fs2-cron](https://github.com/fthomas/fs2-cron) library.  It is also published\n  for ScalaJS. With sbt, use\n  ```sbt\n  libraryDependencies += \"com.github.eikek\" %% \"calev-fs2\" % \"0.7.2\"\n  ```\n- The *doobie* module contains `Meta`, `Read` and `Write` instances\n  for `CalEvent` to use with\n  [doobie](https://github.com/tpolecat/doobie).\n  ```sbt\n  libraryDependencies += \"com.github.eikek\" %% \"calev-doobie\" % \"0.7.2\"\n  ```\n- The *circe* module defines a json decoder and encoder for `CalEvent`\n  instances to use with [circe](https://github.com/circe/circe).  It is also\n  published for ScalaJS.\n  ```sbt\n  libraryDependencies += \"com.github.eikek\" %% \"calev-circe\" % \"0.7.2\"\n  ```\n- The *jackson* module defines `CalevModule` for [Jackson](https://github.com/FasterXML/jackson)\n  ```sbt\n  libraryDependencies += \"com.github.eikek\" %% \"calev-jackson\" % \"0.7.2\"\n  ```\n- The *akka* module allows to use calendar events with [Akka Scheduler](https://doc.akka.io/docs/akka/current/scheduler.html)\n  and [Akka Timers](https://doc.akka.io/docs/akka/current/typed/interaction-patterns.html#typed-scheduling). \n  ```sbt\n  libraryDependencies += \"com.github.eikek\" %% \"calev-akka\" % \"0.7.2\"\n  ```\n\nNote that the fs2 module is also available via\n[fs2-cron](https://github.com/fthomas/fs2-cron) library.\n\n## Examples\n\n### Core\n\nCalendar events can be read from a string:\n\n```scala\nimport com.github.eikek.calev._\n\nCalEvent.parse(\"Mon..Fri *-*-* 6,14:0:0\")\n// res0: Either[String, CalEvent] = Right(\n//   value = CalEvent(\n//     weekday = List(\n//       values = Vector(Range(range = WeekdayRange(start = Mon, end = Fri)))\n//     ),\n//     date = DateEvent(year = All, month = All, day = All),\n//     time = TimeEvent(\n//       hour = List(\n//         values = Vector(\n//           Single(value = 6, rep = None),\n//           Single(value = 14, rep = None)\n//         )\n//       ),\n//       minute = List(values = Vector(Single(value = 0, rep = None))),\n//       seconds = List(values = Vector(Single(value = 0, rep = None)))\n//     ),\n//     zone = None\n//   )\n// )\n\nCalEvent.parse(\"Mon *-*-* 6,88:0:0\")\n// res1: Either[String, CalEvent] = Left(\n//   value = \"Value 88 not in range [0,23]\"\n// )\n```\n\nThere is an `unsafe` way that throws exceptions:\n\n```scala\nCalEvent.unsafe(\"*-*-* 0/2:0:0\")\n// res2: CalEvent = CalEvent(\n//   weekday = All,\n//   date = DateEvent(year = All, month = All, day = All),\n//   time = TimeEvent(\n//     hour = List(values = Vector(Single(value = 0, rep = Some(value = 2)))),\n//     minute = List(values = Vector(Single(value = 0, rep = None))),\n//     seconds = List(values = Vector(Single(value = 0, rep = None)))\n//   ),\n//   zone = None\n// )\n```\n\nThere is a tiny dsl for more conveniently defining events in code:\n\n```scala\nimport com.github.eikek.calev.Dsl._\n\nval ce = CalEvent(AllWeekdays, DateEvent.All, time(0 #/ 2, 0.c, 0.c))\n// ce: CalEvent = CalEvent(\n//   weekday = All,\n//   date = DateEvent(year = All, month = All, day = All),\n//   time = TimeEvent(\n//     hour = List(values = List(Single(value = 0, rep = Some(value = 2)))),\n//     minute = List(values = List(Single(value = 0, rep = None))),\n//     seconds = List(values = List(Single(value = 0, rep = None)))\n//   ),\n//   zone = None\n// )\nce.asString\n// res3: String = \"*-*-* 00/2:00:00\"\n```\n\nOnce there is a calendar event, the times it will elapse next can be\ngenerated:\n\n```scala\nimport java.time._\n\nce.asString\n// res4: String = \"*-*-* 00/2:00:00\"\nval now = LocalDateTime.now\n// now: LocalDateTime = 2024-05-26T10:08:08.053769386\nce.nextElapse(now)\n// res5: Option[LocalDateTime] = Some(value = 2024-05-26T12:00)\nce.nextElapses(now, 5)\n// res6: List[LocalDateTime] = List(\n//   2024-05-26T12:00,\n//   2024-05-26T14:00,\n//   2024-05-26T16:00,\n//   2024-05-26T18:00,\n//   2024-05-26T20:00\n// )\n```\n\nIf an event is in the past, the `nextElapsed` returns a `None`:\n\n```scala\nCalEvent.unsafe(\"1900-01-* 12,14:0:0\").nextElapse(LocalDateTime.now)\n// res7: Option[LocalDateTime] = None\n```\n\n\n### FS2\n\nThe fs2 utilities allow to schedule things based on calendar events.\nThis is the same as [fs2-cron](https://github.com/fthomas/fs2-cron)\nprovides, only adopted to use calendar events instead of cron\nexpressions. The example is also from there.\n\n```scala\nimport cats.effect.IO\nimport _root_.fs2.Stream\nimport com.github.eikek.calev.fs2.Scheduler\nimport java.time.LocalTime\n\nval everyTwoSeconds = CalEvent.unsafe(\"*-*-* *:*:0/2\")\n// everyTwoSeconds: CalEvent = CalEvent(\n//   weekday = All,\n//   date = DateEvent(year = All, month = All, day = All),\n//   time = TimeEvent(\n//     hour = All,\n//     minute = All,\n//     seconds = List(values = Vector(Single(value = 0, rep = Some(value = 2))))\n//   ),\n//   zone = None\n// )\nval scheduler = Scheduler.systemDefault[IO]\n// scheduler: Scheduler[IO] = com.github.eikek.calev.fs2.Scheduler$$anon$1@2899745e\n\nval printTime = Stream.eval(IO(println(LocalTime.now)))\n// printTime: Stream[IO, Unit] = Stream(..)\n\nval task = scheduler.awakeEvery(everyTwoSeconds) \u003e\u003e printTime\n// task: Stream[[x]IO[x], Unit] = Stream(..)\n\nimport cats.effect.unsafe.implicits._\ntask.take(3).compile.drain.unsafeRunSync()\n// 10:08:10.006032431\n// 10:08:12.000529963\n// 10:08:14.000333048\n```\n\n\n### Doobie\n\nWhen using doobie, this module contains instances to write and read\ncalendar event expressions through SQL.\n\n```scala\nimport com.github.eikek.calev._\nimport com.github.eikek.calev.doobie.CalevDoobieMeta._\nimport _root_.doobie._\nimport _root_.doobie.implicits._\n\ncase class Record(event: CalEvent)\n\nval r = Record(CalEvent.unsafe(\"Mon *-*-* 0/2:15\"))\n// r: Record = Record(\n//   event = CalEvent(\n//     weekday = List(values = Vector(Single(day = Mon))),\n//     date = DateEvent(year = All, month = All, day = All),\n//     time = TimeEvent(\n//       hour = List(values = Vector(Single(value = 0, rep = Some(value = 2)))),\n//       minute = List(values = Vector(Single(value = 15, rep = None))),\n//       seconds = List(values = List(Single(value = 0, rep = None)))\n//     ),\n//     zone = None\n//   )\n// )\n\nval insert =\n  sql\"INSERT INTO mytable (event) VALUES (${r.event})\".update.run\n// insert: ConnectionIO[Int] = Suspend(\n//   a = Uncancelable(\n//     body = cats.effect.kernel.MonadCancel$$Lambda$2310/0x00000008018c29c0@18f7a872\n//   )\n// )\n\nval select =\n  sql\"SELECT event FROM mytable WHERE id = 1\".query[Record].unique\n// select: ConnectionIO[Record] = Suspend(\n//   a = Uncancelable(\n//     body = cats.effect.kernel.MonadCancel$$Lambda$2310/0x00000008018c29c0@61defefb\n//   )\n// )\n```\n\n\n### Circe\n\nThe defined encoders/decoders can be put in scope to use calendar\nevent expressions in json.\n\n```scala\nimport com.github.eikek.calev._\nimport com.github.eikek.calev.circe.CalevCirceCodec._\nimport io.circe._\nimport io.circe.generic.semiauto._\nimport io.circe.syntax._\n\ncase class Meeting(name: String, event: CalEvent)\nobject Meeting {\n  implicit val jsonDecoder = deriveDecoder[Meeting]\n  implicit val jsonEncoder = deriveEncoder[Meeting]\n}\n\nval meeting = Meeting(\"trash can\", CalEvent.unsafe(\"Mon..Fri *-*-* 14,18:0\"))\n// meeting: Meeting = Meeting(\n//   name = \"trash can\",\n//   event = CalEvent(\n//     weekday = List(\n//       values = Vector(Range(range = WeekdayRange(start = Mon, end = Fri)))\n//     ),\n//     date = DateEvent(year = All, month = All, day = All),\n//     time = TimeEvent(\n//       hour = List(\n//         values = Vector(\n//           Single(value = 14, rep = None),\n//           Single(value = 18, rep = None)\n//         )\n//       ),\n//       minute = List(values = Vector(Single(value = 0, rep = None))),\n//       seconds = List(values = List(Single(value = 0, rep = None)))\n//     ),\n//     zone = None\n//   )\n// )\nval json = meeting.asJson.noSpaces\n// json: String = \"{\\\"name\\\":\\\"trash can\\\",\\\"event\\\":\\\"Mon..Fri *-*-* 14,18:00:00\\\"}\"\nval read = for {\n  parsed \u003c- parser.parse(json)\n  value \u003c- parsed.as[Meeting]\n} yield value\n// read: Either[Error, Meeting] = Right(\n//   value = Meeting(\n//     name = \"trash can\",\n//     event = CalEvent(\n//       weekday = List(\n//         values = Vector(Range(range = WeekdayRange(start = Mon, end = Fri)))\n//       ),\n//       date = DateEvent(year = All, month = All, day = All),\n//       time = TimeEvent(\n//         hour = List(\n//           values = Vector(\n//             Single(value = 14, rep = None),\n//             Single(value = 18, rep = None)\n//           )\n//         ),\n//         minute = List(values = Vector(Single(value = 0, rep = None))),\n//         seconds = List(values = Vector(Single(value = 0, rep = None)))\n//       ),\n//       zone = None\n//     )\n//   )\n// )\n```\n\n\n### Jackson\n\nAdd `CalevModule` to use calendar event expressions in json: \n\n```scala\nimport com.github.eikek.calev._\nimport com.fasterxml.jackson.core.`type`.TypeReference\nimport com.fasterxml.jackson.databind.json.JsonMapper\nimport com.github.eikek.calev.jackson.CalevModule\n\nval jackson = JsonMapper\n  .builder()\n  .addModule(new CalevModule())\n  .build()\n// jackson: JsonMapper = com.fasterxml.jackson.databind.json.JsonMapper@648100de\n\nval myEvent    = CalEvent.unsafe(\"Mon *-*-* 05:00/10:00\")\n// myEvent: CalEvent = CalEvent(\n//   weekday = List(values = Vector(Single(day = Mon))),\n//   date = DateEvent(year = All, month = All, day = All),\n//   time = TimeEvent(\n//     hour = List(values = Vector(Single(value = 5, rep = None))),\n//     minute = List(values = Vector(Single(value = 0, rep = Some(value = 10)))),\n//     seconds = List(values = Vector(Single(value = 0, rep = None)))\n//   ),\n//   zone = None\n// )\n\nval eventSerialized = jackson.writeValueAsString(myEvent)\n// eventSerialized: String = \"\\\"Mon *-*-* 05:00/10:00\\\"\"\nval eventDeserialized = jackson.readValue(eventSerialized, new TypeReference[CalEvent] {})\n// eventDeserialized: CalEvent = CalEvent(\n//   weekday = List(values = Vector(Single(day = Mon))),\n//   date = DateEvent(year = All, month = All, day = All),\n//   time = TimeEvent(\n//     hour = List(values = Vector(Single(value = 5, rep = None))),\n//     minute = List(values = Vector(Single(value = 0, rep = Some(value = 10)))),\n//     seconds = List(values = Vector(Single(value = 0, rep = None)))\n//   ),\n//   zone = None\n// )\n```\n\n\n### Akka\n\n#### Akka Timers\n\nWhen building actor behavior, use ```CalevBehaviors.withCalevTimers``` \nto get access to ```CalevTimerScheduler```.\n\nUse ```CalevTimerScheduler``` to start single Akka Timer \nfor the upcoming event according to given calendar event definition.\n\n```scala\nimport com.github.eikek.calev.CalEvent\nimport java.time._\nimport com.github.eikek.calev.akka._\nimport com.github.eikek.calev.akka.dsl.CalevBehaviors\nimport _root_.akka.actor.typed._\nimport _root_.akka.actor.typed.scaladsl.Behaviors._\n\nsealed trait Message\ncase class Tick(timestamp: ZonedDateTime) extends Message\ncase class Ping()                         extends Message\n\n// every day, every full minute\ndef calEvent   = CalEvent.unsafe(\"*-*-* *:0/1:0\") \n\nCalevBehaviors.withCalevTimers[Message]() { scheduler =\u003e\n  scheduler.startSingleTimer(calEvent, Tick)\n    receiveMessage[Message] {\n      case tick: Tick =\u003e\n        println(\n          s\"Tick scheduled at ${tick.timestamp.toLocalTime} received at: ${LocalTime.now}\"\n        )\n        same\n      case ping: Ping =\u003e\n        println(\"Ping received\")\n        same\n    }\n}\n// res9: Behavior[Message] = Deferred(TimerSchedulerImpl.scala:29)\n```\n\nUse ```CalevBehaviors.withCalendarEvent``` to schedule messages according \nto the given calendar event definition.   \n\n```scala\nCalevBehaviors.withCalendarEvent(calEvent)(\n  Tick,\n  receiveMessage[Message] {\n    case tick: Tick =\u003e\n      println(\n        s\"Tick scheduled at ${tick.timestamp.toLocalTime} received at: ${LocalTime.now}\"\n      )\n      same\n    case ping: Ping =\u003e\n      println(\"Ping received\")\n      same\n  }\n)\n// res10: Behavior[Message] = Deferred(InterceptorImpl.scala:29-30)\n```\n\n#### Testing\n\nSee [CalevBehaviorsTest](https://github.com/eikek/calev/blob/master/modules/akka/src/test/scala/com/github/eikek/calev/akka/dsl/CalevBehaviorsTest.scala)\n\n#### Akka Scheduler \n\nSchedule the sending of a message to the given target Actor at the time of \nthe upcoming event according to the given calendar event definition.\n\n```scala\ndef behavior(tickReceiver: ActorRef[Tick]): Behavior[Message] = \n  setup { actorCtx =\u003e\n    actorCtx.scheduleOnceWithCalendarEvent(calEvent, tickReceiver, Tick)\n    same\n  }\n```\n\nSchedule the running of a ```Runnable``` at the time of the upcoming \nevent according to the given calendar event definition.\n\n```scala\nimplicit val system: ActorSystem[_] = ActorSystem(empty, \"my-system\")\n// system: ActorSystem[_] = akka://my-system\nimport system.executionContext\n\ncalevScheduler().scheduleOnceWithCalendarEvent(calEvent, () =\u003e {\n  println(\n      s\"Called at: ${LocalTime.now}\"\n  )\n})\n// res11: Option[\u003cnone\u003e.\u003croot\u003e.akka.actor.Cancellable] = Some(\n//   value = akka.actor.LightArrayRevolverScheduler$TaskHolder@44929971\n// )\nsystem.terminate()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feikek%2Fcalev","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feikek%2Fcalev","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feikek%2Fcalev/lists"}