Skip to content

Commit 5f71275

Browse files
Add PulseSchedule and PulseScheduleBuilder (#94)
Closes #93
1 parent 4a96e2c commit 5f71275

File tree

5 files changed

+525
-0
lines changed

5 files changed

+525
-0
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.dokka)
77
alias(libs.plugins.kmpmt)
88
alias(libs.plugins.kotlin.multiplatform)
9+
alias(libs.plugins.poko)
910
alias(libs.plugins.publish)
1011
}
1112

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ kmpmt = "0.1.1"
44
kotlin = "2.1.20"
55
kotlinx-coroutines = "1.10.2"
66
kotlinx-datetime = "0.6.2"
7+
poko = "0.18.6"
78
publish = "0.30.0"
89

910
[libraries]
@@ -16,4 +17,5 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
1617
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
1718
kmpmt = { id = "com.jakewharton.kmp-missing-targets", version.ref = "kmpmt" }
1819
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
20+
poko = { id = "dev.drewhamilton.poko", version.ref = "poko" }
1921
publish = { id = "com.vanniktech.maven.publish", version.ref = "publish" }
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package io.github.kevincianfarini.cardiologist
2+
3+
import dev.drewhamilton.poko.Poko
4+
import kotlinx.datetime.DayOfWeek
5+
import kotlinx.datetime.Month
6+
7+
/**
8+
* A [Pulse] schedule that can be used with [schedulePulse] to define complex schedules.
9+
*
10+
* @constructor Creates a new PulseSchedule. Second values must be in range 0..59, minute values must be in range 0..59,
11+
* hour values must be in range 0..23, and day of month values must be in range 1..31. This constructor also
12+
* requires that seconds, minutes, hours, days of month, and months cannot be empty sets.
13+
* @throws IllegalArgumentException if the constructor is called with any of our bounds value or an improperly empty set.
14+
*/
15+
@Poko
16+
public class PulseSchedule(
17+
public val atSeconds: Set<Int>,
18+
public val atMinutes: Set<Int>,
19+
public val atHours: Set<Int>,
20+
public val onDaysOfMonth: Set<Int>,
21+
public val inMonths: Set<Month>,
22+
public val onDaysOfWeek: Set<DayOfWeek>,
23+
) {
24+
init {
25+
require(!atSeconds.any { it !in 0..59 }) { "Seconds has an out of bound value: $atSeconds" }
26+
require(atSeconds.isNotEmpty()) { "Seconds cannot be empty!" }
27+
require(!atMinutes.any { it !in 0..59 }) { "Minutes has an out of bound value: $atMinutes" }
28+
require(atMinutes.isNotEmpty()) { "Minutes cannot be empty!" }
29+
require(!atHours.any { it !in 0..23 }) { "Hours has an out of bound value: $atHours" }
30+
require(atHours.isNotEmpty()) { "Hours cannot be empty!" }
31+
require(!onDaysOfMonth.any { it !in 1..31 }) { "Days of month has an out of bound value: $onDaysOfMonth" }
32+
require(onDaysOfMonth.isNotEmpty()) { "Days of month cannot be empty!" }
33+
require(inMonths.isNotEmpty()) { "Months cannot be empty!" }
34+
// Don't check if onDaysOfWeek is empty because an empty set is equivalent to the wildcard `*` value in cron
35+
// expressions.
36+
}
37+
}
38+
39+
public class PulseScheduleBuilder internal constructor() {
40+
41+
private var _atSeconds: Set<Int>? = null
42+
private var _atMinutes: Set<Int>? = null
43+
private var _atHours: Set<Int> ? = null
44+
private var _onDaysOfMonth: Set<Int>? = null
45+
private var _inMonths: Set<Month>? = null
46+
private var _onDaysOfWeek: Set<DayOfWeek>? = null
47+
48+
public fun atSeconds(value: Int, vararg values: Int) {
49+
_atSeconds = buildSet {
50+
_atSeconds?.let(this::addAll)
51+
add(value)
52+
values.forEach(this::add)
53+
}
54+
}
55+
56+
public fun atMinutes(value: Int, vararg values: Int) {
57+
_atMinutes = buildSet {
58+
_atMinutes?.let(this::addAll)
59+
add(value)
60+
values.forEach(this::add)
61+
}
62+
}
63+
64+
public fun atHours(value: Int, vararg values: Int) {
65+
_atHours = buildSet {
66+
_atHours?.let(this::addAll)
67+
add(value)
68+
values.forEach(this::add)
69+
}
70+
}
71+
72+
public fun onDaysOfMonth(value: Int, vararg values: Int) {
73+
_onDaysOfMonth = buildSet {
74+
_onDaysOfMonth?.let(this::addAll)
75+
add(value)
76+
values.forEach(this::add)
77+
}
78+
}
79+
80+
public fun inMonths(value: Month, vararg values: Month) {
81+
_inMonths = buildSet {
82+
_inMonths?.let(this::addAll)
83+
add(value)
84+
values.forEach(this::add)
85+
}
86+
}
87+
88+
public fun onDaysOfWeek(value: DayOfWeek, vararg values: DayOfWeek) {
89+
_onDaysOfWeek = buildSet {
90+
_onDaysOfWeek?.let(this::addAll)
91+
add(value)
92+
values.forEach(this::add)
93+
}
94+
}
95+
96+
internal fun build(): PulseSchedule = PulseSchedule(
97+
atSeconds = _atSeconds ?: (0..59).toSet(),
98+
atMinutes = _atMinutes ?: (0..59).toSet(),
99+
atHours = _atHours ?: (0..23).toSet(),
100+
onDaysOfMonth = _onDaysOfMonth ?: (1..31).toSet(),
101+
inMonths = _inMonths ?: Month.entries.toSet(),
102+
onDaysOfWeek = _onDaysOfWeek ?: emptySet(),
103+
)
104+
}
105+
106+
/**
107+
* Build a complex [PulseSchedule] with a DSL.
108+
*
109+
* For example, the following code will build a [schedule][PulseSchedule] that, when used with [schedulePulse], will
110+
* beat once per minute at the 0th second on Mondays and Fridays.
111+
*
112+
* ```kt
113+
* val schedule = buildPulseSchedule {
114+
* atSeconds(0)
115+
* onDaysOfWeek(DayOfWeek.MONDAY, DayOfWeek.FRIDAY)
116+
* }
117+
* ```
118+
*
119+
* Multiple successive calls to the same function on [PulseScheduleBuilder] are additive. This is useful for building
120+
* schedules imperatively. For example, the following will create a schedule that, when used with [schedulePulse], will
121+
* beat on three random seconds of each minute.
122+
*
123+
* ```kt
124+
* val schedule = buildPulseSchedule {
125+
* val random = Random.Default
126+
* repeat(3) {
127+
* atSeconds(random.nextInt(0..59))
128+
* }
129+
* }
130+
* ```
131+
*
132+
* The valid values for each function are:
133+
*
134+
* - `atSeconds`: 0..59
135+
* - `atMinutes`: 0..59
136+
* - `atHours`: 0..23
137+
* - `onDaysOfMonth`: 1..31
138+
*
139+
* @throws IllegalArgumentException if the [builder] lambda completes with any values outside the above range.
140+
*/
141+
public fun buildPulseSchedule(builder: PulseScheduleBuilder.() -> Unit): PulseSchedule {
142+
return PulseScheduleBuilder().apply(builder).build()
143+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package io.github.kevincianfarini.cardiologist
2+
3+
import kotlinx.datetime.DayOfWeek
4+
import kotlinx.datetime.Month
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
8+
class PulseScheduleBuilderTest {
9+
10+
@Test
11+
fun empty_builder_produces_default_value() = assertEquals(
12+
expected = PulseSchedule(
13+
atSeconds = (0..59).toSet(),
14+
atMinutes = (0..59).toSet(),
15+
atHours = (0..23).toSet(),
16+
onDaysOfMonth = (1..31).toSet(),
17+
inMonths = Month.entries.toSet(),
18+
onDaysOfWeek = emptySet(),
19+
),
20+
actual = buildPulseSchedule { },
21+
)
22+
23+
@Test
24+
fun simple_seconds() = assertEquals(
25+
expected = PulseSchedule(
26+
atSeconds = setOf(0, 30),
27+
atMinutes = (0..59).toSet(),
28+
atHours = (0..23).toSet(),
29+
onDaysOfMonth = (1..31).toSet(),
30+
inMonths = Month.entries.toSet(),
31+
onDaysOfWeek = emptySet(),
32+
),
33+
actual = buildPulseSchedule {
34+
atSeconds(0, 30)
35+
},
36+
)
37+
38+
@Test
39+
fun multiple_seconds_calls_are_additive() = assertEquals(
40+
expected = PulseSchedule(
41+
atSeconds = setOf(0, 15, 30, 45),
42+
atMinutes = (0..59).toSet(),
43+
atHours = (0..23).toSet(),
44+
onDaysOfMonth = (1..31).toSet(),
45+
inMonths = Month.entries.toSet(),
46+
onDaysOfWeek = emptySet(),
47+
),
48+
actual = buildPulseSchedule {
49+
atSeconds(0, 30)
50+
atSeconds(15, 45)
51+
},
52+
)
53+
54+
@Test
55+
fun simple_minutes() = assertEquals(
56+
expected = PulseSchedule(
57+
atSeconds = (0..59).toSet(),
58+
atMinutes = setOf(0, 30),
59+
atHours = (0..23).toSet(),
60+
onDaysOfMonth = (1..31).toSet(),
61+
inMonths = Month.entries.toSet(),
62+
onDaysOfWeek = emptySet(),
63+
),
64+
actual = buildPulseSchedule {
65+
atMinutes(0, 30)
66+
},
67+
)
68+
69+
@Test
70+
fun multiple_minutes_calls_are_additive() = assertEquals(
71+
expected = PulseSchedule(
72+
atSeconds = (0..59).toSet(),
73+
atMinutes = setOf(0, 15, 30, 45),
74+
atHours = (0..23).toSet(),
75+
onDaysOfMonth = (1..31).toSet(),
76+
inMonths = Month.entries.toSet(),
77+
onDaysOfWeek = emptySet(),
78+
),
79+
actual = buildPulseSchedule {
80+
atMinutes(0, 30)
81+
atMinutes(15, 45)
82+
},
83+
)
84+
85+
@Test
86+
fun simple_hours() = assertEquals(
87+
expected = PulseSchedule(
88+
atSeconds = (0..59).toSet(),
89+
atMinutes = (0..59).toSet(),
90+
atHours = setOf(0, 12),
91+
onDaysOfMonth = (1..31).toSet(),
92+
inMonths = Month.entries.toSet(),
93+
onDaysOfWeek = emptySet(),
94+
),
95+
actual = buildPulseSchedule {
96+
atHours(0, 12)
97+
},
98+
)
99+
100+
@Test
101+
fun multiple_hours_are_additive() = assertEquals(
102+
expected = PulseSchedule(
103+
atSeconds = (0..59).toSet(),
104+
atMinutes = (0..59).toSet(),
105+
atHours = setOf(0, 6, 12, 18),
106+
onDaysOfMonth = (1..31).toSet(),
107+
inMonths = Month.entries.toSet(),
108+
onDaysOfWeek = emptySet(),
109+
),
110+
actual = buildPulseSchedule {
111+
atHours(0, 12)
112+
atHours(6, 18)
113+
},
114+
)
115+
116+
@Test
117+
fun simple_days_of_month() = assertEquals(
118+
expected = PulseSchedule(
119+
atSeconds = (0..59).toSet(),
120+
atMinutes = (0..59).toSet(),
121+
atHours = (0..23).toSet(),
122+
onDaysOfMonth = setOf(1, 15),
123+
inMonths = Month.entries.toSet(),
124+
onDaysOfWeek = emptySet(),
125+
),
126+
actual = buildPulseSchedule {
127+
onDaysOfMonth(1, 15)
128+
},
129+
)
130+
131+
@Test
132+
fun multiple_days_of_month_calls_are_additive() = assertEquals(
133+
expected = PulseSchedule(
134+
atSeconds = (0..59).toSet(),
135+
atMinutes = (0..59).toSet(),
136+
atHours = (0..23).toSet(),
137+
onDaysOfMonth = setOf(1, 7, 15, 21),
138+
inMonths = Month.entries.toSet(),
139+
onDaysOfWeek = emptySet(),
140+
),
141+
actual = buildPulseSchedule {
142+
onDaysOfMonth(1, 15)
143+
onDaysOfMonth(7, 21)
144+
},
145+
)
146+
147+
@Test
148+
fun simple_months() = assertEquals(
149+
expected = PulseSchedule(
150+
atSeconds = (0..59).toSet(),
151+
atMinutes = (0..59).toSet(),
152+
atHours = (0..23).toSet(),
153+
onDaysOfMonth = (1..31).toSet(),
154+
inMonths = setOf(Month.OCTOBER, Month.DECEMBER),
155+
onDaysOfWeek = emptySet(),
156+
),
157+
actual = buildPulseSchedule {
158+
inMonths(Month.OCTOBER, Month.DECEMBER)
159+
},
160+
)
161+
162+
@Test
163+
fun multiple_months_calls_are_additive() = assertEquals(
164+
expected = PulseSchedule(
165+
atSeconds = (0..59).toSet(),
166+
atMinutes = (0..59).toSet(),
167+
atHours = (0..23).toSet(),
168+
onDaysOfMonth = (1..31).toSet(),
169+
inMonths = setOf(Month.OCTOBER, Month.DECEMBER, Month.MAY),
170+
onDaysOfWeek = emptySet(),
171+
),
172+
actual = buildPulseSchedule {
173+
inMonths(Month.OCTOBER, Month.DECEMBER)
174+
inMonths(Month.MAY)
175+
},
176+
)
177+
178+
@Test
179+
fun simple_day_of_week() = assertEquals(
180+
expected = PulseSchedule(
181+
atSeconds = (0..59).toSet(),
182+
atMinutes = (0..59).toSet(),
183+
atHours = (0..23).toSet(),
184+
onDaysOfMonth = (1..31).toSet(),
185+
inMonths = Month.entries.toSet(),
186+
onDaysOfWeek = setOf(DayOfWeek.MONDAY, DayOfWeek.FRIDAY),
187+
),
188+
actual = buildPulseSchedule {
189+
onDaysOfWeek(DayOfWeek.MONDAY, DayOfWeek.FRIDAY)
190+
},
191+
)
192+
193+
@Test
194+
fun multiple_day_of_week_calls_are_additive() = assertEquals(
195+
expected = PulseSchedule(
196+
atSeconds = (0..59).toSet(),
197+
atMinutes = (0..59).toSet(),
198+
atHours = (0..23).toSet(),
199+
onDaysOfMonth = (1..31).toSet(),
200+
inMonths = Month.entries.toSet(),
201+
onDaysOfWeek = setOf(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY),
202+
),
203+
actual = buildPulseSchedule {
204+
onDaysOfWeek(DayOfWeek.MONDAY, DayOfWeek.FRIDAY)
205+
onDaysOfWeek(DayOfWeek.WEDNESDAY)
206+
},
207+
)
208+
}

0 commit comments

Comments
 (0)