├── .gitignore ├── src ├── shakespeare.gleam └── shakespeare │ └── actors │ ├── key_value.gleam │ ├── periodic.gleam │ └── scheduled.gleam ├── test ├── shakespeare_test.gleam └── shakespeare │ └── actors │ ├── periodic_test.gleam │ └── scheduled_test.gleam ├── gleam.toml ├── README.md ├── .github └── workflows │ └── test.yml ├── LICENSE ├── CHANGELOG.md └── manifest.toml /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | -------------------------------------------------------------------------------- /src/shakespeare.gleam: -------------------------------------------------------------------------------- 1 | /// A thunk. 2 | pub type Thunk = 3 | fn() -> Nil 4 | -------------------------------------------------------------------------------- /test/shakespeare_test.gleam: -------------------------------------------------------------------------------- 1 | import startest 2 | 3 | pub fn main() { 4 | startest.run(startest.default_config()) 5 | } 6 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "shakespeare" 2 | version = "0.1.3" 3 | description = "General-purpose OTP actors." 4 | licenses = ["MIT"] 5 | repository = { type = "github", user = "maxdeviant", repo = "shakespeare" } 6 | 7 | [dependencies] 8 | birl = "~> 1.6" 9 | gleam_erlang = "~> 0.25" 10 | gleam_otp = "~> 0.10" 11 | gleam_stdlib = "~> 0.34 or ~> 1.0" 12 | 13 | [dev-dependencies] 14 | startest = ">= 0.2.1 and < 1.0.0" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shakespeare 2 | 3 | [![Package Version](https://img.shields.io/hexpm/v/shakespeare)](https://hex.pm/packages/shakespeare) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/shakespeare/) 5 | ![Erlang-compatible](https://img.shields.io/badge/target-erlang-b83998) 6 | 7 | 🎭 General-purpose OTP actors. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | gleam add shakespeare 13 | ``` 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build_and_test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: erlef/setup-beam@v1.15.4 15 | with: 16 | otp-version: "26.0.2" 17 | gleam-version: "1.0.0" 18 | rebar3-version: "3" 19 | - run: gleam format --check src test 20 | - run: gleam deps download 21 | - run: gleam test 22 | -------------------------------------------------------------------------------- /test/shakespeare/actors/periodic_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/erlang/process 2 | import shakespeare/actors/key_value 3 | import shakespeare/actors/periodic.{Ms} 4 | import startest/expect 5 | 6 | pub fn periodic_actor_test() { 7 | let count_key = "count" 8 | let assert Ok(counter) = key_value.start() 9 | key_value.set(counter, count_key, 0) 10 | 11 | let increment = fn() { 12 | let assert Ok(count) = key_value.get(counter, count_key) 13 | key_value.set(counter, count_key, count + 1) 14 | } 15 | 16 | periodic.start(do: increment, every: Ms(10)) 17 | |> expect.to_be_ok 18 | 19 | process.sleep(50) 20 | 21 | key_value.get(counter, count_key) 22 | |> expect.to_equal(Ok(5)) 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Marshall Bowers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.3] - 2024-12-06 11 | 12 | ### Fixed 13 | 14 | - Fixed a crash when starting the `ScheduledActor` with a `Weekly` schedule set for earlier that same day. 15 | 16 | ## [0.1.2] - 2024-04-27 17 | 18 | ### Added 19 | 20 | - Added `Weekly` schedule for `ScheduledActor`. 21 | 22 | ## [0.1.1] - 2024-04-14 23 | 24 | ### Fixed 25 | 26 | - Fixed a bug where `PeriodicActor` would spawn two copies of itself. 27 | 28 | ## [0.1.0] - 2024-04-11 29 | 30 | - Initial release. 31 | 32 | [unreleased]: https://github.com/maxdeviant/shakespeare/compare/v0.1.3...HEAD 33 | [0.1.3]: https://github.com/maxdeviant/shakespeare/compare/v0.1.2...v0.1.3 34 | [0.1.2]: https://github.com/maxdeviant/shakespeare/compare/v0.1.1...v0.1.2 35 | [0.1.1]: https://github.com/maxdeviant/shakespeare/compare/v0.1.0...v0.1.1 36 | [0.1.0]: https://github.com/maxdeviant/shakespeare/compare/a1b5ab4...v0.1.0 37 | -------------------------------------------------------------------------------- /src/shakespeare/actors/key_value.gleam: -------------------------------------------------------------------------------- 1 | //// An actor that provides a simple key-value store. 2 | 3 | import gleam/dict.{type Dict} 4 | import gleam/erlang/process.{type Subject} 5 | import gleam/otp/actor 6 | import gleam/result 7 | 8 | /// An actor that serves as an in-memory key-value store. 9 | pub opaque type KeyValueActor(a) { 10 | KeyValueActor(subject: Subject(Message(a))) 11 | } 12 | 13 | /// Starts a new `KeyValueActor`. 14 | pub fn start() -> Result(KeyValueActor(a), actor.StartError) { 15 | actor.start(dict.new(), handle_message) 16 | |> result.map(KeyValueActor) 17 | } 18 | 19 | /// Sets the value associated with the given key. 20 | /// 21 | /// If value already exists for the given key it will be overwritten. 22 | pub fn set(actor: KeyValueActor(a), key: String, value: a) { 23 | process.send(actor.subject, Set(key, value)) 24 | } 25 | 26 | /// Gets the value for the given key. 27 | pub fn get(actor: KeyValueActor(a), key: String) -> Result(a, Nil) { 28 | process.try_call(actor.subject, Get(key, _), 10) 29 | |> result.map_error(fn(_) { Nil }) 30 | |> result.flatten 31 | } 32 | 33 | type Message(a) { 34 | Set(key: String, value: a) 35 | Get(name: String, reply_with: Subject(Result(a, Nil))) 36 | } 37 | 38 | fn handle_message( 39 | message: Message(a), 40 | state: Dict(String, a), 41 | ) -> actor.Next(Message(a), Dict(String, a)) { 42 | case message { 43 | Set(key, value) -> { 44 | actor.continue( 45 | state 46 | |> dict.insert(key, value), 47 | ) 48 | } 49 | Get(key, client) -> { 50 | let value = 51 | state 52 | |> dict.get(key) 53 | 54 | process.send(client, value) 55 | actor.continue(state) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/shakespeare/actors/periodic.gleam: -------------------------------------------------------------------------------- 1 | //// An actor that runs periodically. 2 | 3 | import gleam/erlang/process.{type Subject} 4 | import gleam/function.{identity} 5 | import gleam/otp/actor 6 | import gleam/result 7 | import shakespeare.{type Thunk} 8 | 9 | /// An interval of time. 10 | pub type Interval { 11 | /// An interval in milliseconds. 12 | Ms(Int) 13 | } 14 | 15 | /// An actor that performs a given action periodically. 16 | pub opaque type PeriodicActor { 17 | PeriodicActor(subject: Subject(Message)) 18 | } 19 | 20 | /// Starts a new `PeriodicActor` that executes the given function on the 21 | /// specified interval. 22 | pub fn start( 23 | do do_work: Thunk, 24 | every interval: Interval, 25 | ) -> Result(PeriodicActor, actor.StartError) { 26 | actor.start_spec(actor.Spec( 27 | init: fn() { init(interval, do_work) }, 28 | loop: loop, 29 | init_timeout: 100, 30 | )) 31 | |> result.map(PeriodicActor) 32 | } 33 | 34 | type Message { 35 | Run 36 | } 37 | 38 | type State { 39 | State(self: Subject(Message), interval: Interval, do_work: Thunk) 40 | } 41 | 42 | fn init(interval: Interval, do_work: Thunk) { 43 | let subject = process.new_subject() 44 | let state = State(subject, interval, do_work) 45 | 46 | let selector = 47 | process.new_selector() 48 | |> process.selecting(subject, identity) 49 | 50 | enqueue_first_run(state) 51 | actor.Ready(state, selector) 52 | } 53 | 54 | fn loop(message: Message, state: State) -> actor.Next(Message, State) { 55 | case message { 56 | Run -> { 57 | state.do_work() 58 | 59 | enqueue_next_run(state) 60 | actor.continue(state) 61 | } 62 | } 63 | } 64 | 65 | /// Enqueues the first run, to run immediately by the actor. 66 | fn enqueue_first_run(state: State) -> Nil { 67 | process.send(state.self, Run) 68 | } 69 | 70 | /// Enqueues the next run, to run after the actor's configured interval. 71 | fn enqueue_next_run(state: State) -> Nil { 72 | let Ms(interval) = state.interval 73 | 74 | process.send_after(state.self, interval, Run) 75 | Nil 76 | } 77 | -------------------------------------------------------------------------------- /test/shakespeare/actors/scheduled_test.gleam: -------------------------------------------------------------------------------- 1 | import birl.{Fri, Sun} 2 | import shakespeare/actors/scheduled.{Daily, Hourly, Weekly} 3 | import startest/expect 4 | 5 | pub fn scheduled_next_occurrence_at_hourly_test() { 6 | let assert Ok(now) = birl.parse("2024-04-07T15:01:51.248Z") 7 | scheduled.next_occurrence_at(now, Hourly(30, 0)) 8 | |> birl.to_iso8601 9 | |> expect.to_equal("2024-04-07T15:30:00.000Z") 10 | 11 | let assert Ok(now) = birl.parse("2024-04-07T15:31:51.248Z") 12 | scheduled.next_occurrence_at(now, Hourly(30, 0)) 13 | |> birl.to_iso8601 14 | |> expect.to_equal("2024-04-07T16:30:00.000Z") 15 | } 16 | 17 | pub fn scheduled_next_occurrence_at_daily_test() { 18 | let assert Ok(now) = birl.parse("2024-04-07T15:59:51.248Z") 19 | scheduled.next_occurrence_at(now, Daily(16, 0, 0)) 20 | |> birl.to_iso8601 21 | |> expect.to_equal("2024-04-07T16:00:00.000Z") 22 | 23 | let assert Ok(now) = birl.parse("2024-04-07T16:01:51.248Z") 24 | scheduled.next_occurrence_at(now, Daily(16, 0, 0)) 25 | |> birl.to_iso8601 26 | |> expect.to_equal("2024-04-08T16:00:00.000Z") 27 | } 28 | 29 | pub fn scheduled_next_occurrence_at_weekly_test() { 30 | let assert Ok(now) = birl.parse("2024-04-22T19:37:40.916Z") 31 | scheduled.next_occurrence_at(now, Weekly(Fri, 18, 0, 0)) 32 | |> birl.to_iso8601 33 | |> expect.to_equal("2024-04-26T18:00:00.000Z") 34 | 35 | let assert Ok(now) = birl.parse("2024-04-26T16:37:40.916Z") 36 | scheduled.next_occurrence_at(now, Weekly(Fri, 18, 0, 0)) 37 | |> birl.to_iso8601 38 | |> expect.to_equal("2024-04-26T18:00:00.000Z") 39 | 40 | let assert Ok(now) = birl.parse("2024-04-27T19:37:40.916Z") 41 | scheduled.next_occurrence_at(now, Weekly(Sun, 13, 0, 0)) 42 | |> birl.to_iso8601 43 | |> expect.to_equal("2024-04-28T13:00:00.000Z") 44 | 45 | let assert Ok(now) = birl.parse("2024-04-27T19:37:40.916Z") 46 | scheduled.next_occurrence_at(now, Weekly(Fri, 18, 0, 0)) 47 | |> birl.to_iso8601 48 | |> expect.to_equal("2024-05-03T18:00:00.000Z") 49 | 50 | let assert Ok(now) = birl.parse("2024-12-06T22:29:00.065Z") 51 | scheduled.next_occurrence_at(now, Weekly(Fri, 22, 30, 0)) 52 | |> birl.to_iso8601 53 | |> expect.to_equal("2024-12-06T22:30:00.000Z") 54 | 55 | let assert Ok(now) = birl.parse("2024-12-06T23:00:00.065Z") 56 | scheduled.next_occurrence_at(now, Weekly(Fri, 22, 30, 0)) 57 | |> birl.to_iso8601 58 | |> expect.to_equal("2024-12-13T22:30:00.000Z") 59 | } 60 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 | { name = "bigben", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "bigben", source = "hex", outer_checksum = "8E5A98FA6E981EEEF016C40F1CDFADA095927CAF6CAAA0C7E295EED02FC95947" }, 7 | { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, 8 | { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 9 | { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 10 | { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, 11 | { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, 12 | { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 13 | { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" }, 14 | { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, 15 | { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, 16 | { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, 17 | { name = "glint", version = "1.0.0-rc1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "91EC8EFA3E8FBCB842C219AA02E8072204CEE02597B4C36C2ED78AD86DD1B2EC" }, 18 | { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 19 | { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, 20 | { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, 21 | { name = "startest", version = "0.2.1", build_tools = ["gleam"], requirements = ["argv", "bigben", "birl", "exception", "gleam_community_ansi", "gleam_erlang", "gleam_javascript", "gleam_stdlib", "glint", "simplifile", "tom"], otp_app = "startest", source = "hex", outer_checksum = "D87A0C48712EE53878BBC5058FD4E74CD2B766779E6E7F72FC69D472C820C820" }, 22 | { name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" }, 23 | { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, 24 | ] 25 | 26 | [requirements] 27 | birl = { version = "~> 1.6" } 28 | gleam_erlang = { version = "~> 0.25" } 29 | gleam_otp = { version = "~> 0.10" } 30 | gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } 31 | startest = { version = ">= 0.2.1 and < 1.0.0" } 32 | -------------------------------------------------------------------------------- /src/shakespeare/actors/scheduled.gleam: -------------------------------------------------------------------------------- 1 | //// An actor that runs on a schedule. 2 | 3 | import birl.{type Time, type Weekday} 4 | import birl/duration 5 | import gleam/erlang/process.{type Subject} 6 | import gleam/int 7 | import gleam/order 8 | import gleam/otp/actor 9 | import gleam/result 10 | import shakespeare.{type Thunk} 11 | 12 | /// A schedule for a `ScheduledActor`. 13 | pub type Schedule { 14 | /// The actor runs hourly, at the specified minute and second. 15 | Hourly(minute: Int, second: Int) 16 | /// The actor runs daily, at the specified hour, minute, and second. 17 | Daily(hour: Int, minute: Int, second: Int) 18 | /// The actor runs weekly, at the specified day, hour, minute, and second. 19 | Weekly(day: Weekday, hour: Int, minute: Int, second: Int) 20 | } 21 | 22 | /// An actor that performs a given action on a schedule. 23 | pub opaque type ScheduledActor { 24 | ScheduledActor(subject: Subject(Message)) 25 | } 26 | 27 | /// Starts a new `ScheduledActor` that executes the given function on the 28 | /// specified schedule. 29 | pub fn start( 30 | do do_work: Thunk, 31 | runs schedule: Schedule, 32 | ) -> Result(ScheduledActor, actor.StartError) { 33 | actor.start_spec(actor.Spec( 34 | init: fn() { init(schedule, do_work) }, 35 | loop: loop, 36 | init_timeout: 100, 37 | )) 38 | |> result.map(ScheduledActor) 39 | } 40 | 41 | type Message { 42 | Run 43 | } 44 | 45 | type State { 46 | State(self: Subject(Message), schedule: Schedule, do_work: Thunk) 47 | } 48 | 49 | fn init(occurrence: Schedule, do_work: Thunk) { 50 | let subject = process.new_subject() 51 | let state = State(subject, occurrence, do_work) 52 | 53 | let selector = 54 | process.new_selector() 55 | |> process.selecting(subject, fn(x) { x }) 56 | 57 | enqueue_next_run(state) 58 | actor.Ready(state, selector) 59 | } 60 | 61 | fn loop(message: Message, state: State) -> actor.Next(Message, State) { 62 | case message { 63 | Run -> { 64 | state.do_work() 65 | 66 | enqueue_next_run(state) 67 | actor.continue(state) 68 | } 69 | } 70 | } 71 | 72 | fn enqueue_next_run(state: State) -> Nil { 73 | let now = birl.utc_now() 74 | 75 | let ms_until_next_occurrence = 76 | next_occurrence_at(now, state.schedule) 77 | |> birl.difference(now) 78 | |> duration_to_milliseconds 79 | 80 | process.send_after(state.self, ms_until_next_occurrence, Run) 81 | Nil 82 | } 83 | 84 | /// Returns the time of the next occurrence for the given schedule. 85 | pub fn next_occurrence_at(now: Time, schedule: Schedule) -> Time { 86 | case schedule { 87 | Hourly(minute, second) -> { 88 | let occurrence_this_hour = 89 | now 90 | |> birl.set_time_of_day(birl.TimeOfDay( 91 | birl.get_time_of_day(now).hour, 92 | minute, 93 | second, 94 | 0, 95 | )) 96 | 97 | case birl.compare(now, occurrence_this_hour) { 98 | order.Lt | order.Eq -> occurrence_this_hour 99 | order.Gt -> { 100 | let in_one_hour = 101 | now 102 | |> birl.add(duration.hours(1)) 103 | let hour = birl.get_time_of_day(in_one_hour).hour 104 | 105 | in_one_hour 106 | |> birl.set_time_of_day(birl.TimeOfDay(hour, minute, second, 0)) 107 | } 108 | } 109 | } 110 | Daily(hour, minute, second) -> { 111 | let occurrence_this_day = 112 | now 113 | |> birl.set_time_of_day(birl.TimeOfDay(hour, minute, second, 0)) 114 | 115 | case birl.compare(now, occurrence_this_day) { 116 | order.Lt | order.Eq -> occurrence_this_day 117 | order.Gt -> { 118 | now 119 | |> birl.add(duration.days(1)) 120 | |> birl.set_time_of_day(birl.TimeOfDay(hour, minute, second, 0)) 121 | } 122 | } 123 | } 124 | Weekly(day, hour, minute, second) -> { 125 | let current_day = birl.weekday(now) 126 | let day_diff = weekday_to_int(day) - weekday_to_int(current_day) 127 | 128 | case int.compare(day_diff, 0) { 129 | order.Eq -> { 130 | let occurrence_this_day = 131 | now 132 | |> birl.set_time_of_day(birl.TimeOfDay(hour, minute, second, 0)) 133 | 134 | case birl.compare(now, occurrence_this_day) { 135 | order.Lt | order.Eq -> occurrence_this_day 136 | order.Gt -> { 137 | now 138 | |> birl.add(duration.days(7)) 139 | |> birl.set_time_of_day(birl.TimeOfDay(hour, minute, second, 0)) 140 | } 141 | } 142 | } 143 | order.Gt -> { 144 | now 145 | |> birl.add(duration.days(day_diff)) 146 | |> birl.set_time_of_day(birl.TimeOfDay(hour, minute, second, 0)) 147 | } 148 | order.Lt -> { 149 | now 150 | |> birl.add(duration.days(7 + day_diff)) 151 | |> birl.set_time_of_day(birl.TimeOfDay(hour, minute, second, 0)) 152 | } 153 | } 154 | } 155 | } 156 | } 157 | 158 | fn duration_to_milliseconds(duration: duration.Duration) -> Int { 159 | let duration.Duration(microseconds) = duration 160 | microseconds / 1000 161 | } 162 | 163 | fn weekday_to_int(value: Weekday) -> Int { 164 | case value { 165 | birl.Sun -> 0 166 | birl.Mon -> 1 167 | birl.Tue -> 2 168 | birl.Wed -> 3 169 | birl.Thu -> 4 170 | birl.Fri -> 5 171 | birl.Sat -> 6 172 | } 173 | } 174 | --------------------------------------------------------------------------------