Skip to content

Commit 0364a59

Browse files
authored
Expose dedicated cron parsing function (#164)
Closes #162
1 parent d9e6c67 commit 0364a59

File tree

7 files changed

+89
-62
lines changed

7 files changed

+89
-62
lines changed

api/cardiologist.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public final class io/github/kevincianfarini/cardiologist/PulseBackpressureStrat
2626
}
2727

2828
public final class io/github/kevincianfarini/cardiologist/PulseSchedule {
29+
public static final field Companion Lio/github/kevincianfarini/cardiologist/PulseSchedule$Companion;
2930
public fun <init> (Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)V
3031
public fun equals (Ljava/lang/Object;)Z
3132
public final fun getAtHours ()Ljava/util/List;
@@ -38,6 +39,10 @@ public final class io/github/kevincianfarini/cardiologist/PulseSchedule {
3839
public fun toString ()Ljava/lang/String;
3940
}
4041

42+
public final class io/github/kevincianfarini/cardiologist/PulseSchedule$Companion {
43+
public final fun parseCron (Ljava/lang/String;)Lio/github/kevincianfarini/cardiologist/PulseSchedule;
44+
}
45+
4146
public final class io/github/kevincianfarini/cardiologist/PulseScheduleBuilder {
4247
public final fun atHours (I[I)V
4348
public final fun atHours (Lkotlin/ranges/IntRange;[Lkotlin/ranges/IntRange;)V

api/cardiologist.klib.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ final class io.github.kevincianfarini.cardiologist/PulseSchedule { // io.github.
3838
final fun equals(kotlin/Any?): kotlin/Boolean // io.github.kevincianfarini.cardiologist/PulseSchedule.equals|equals(kotlin.Any?){}[0]
3939
final fun hashCode(): kotlin/Int // io.github.kevincianfarini.cardiologist/PulseSchedule.hashCode|hashCode(){}[0]
4040
final fun toString(): kotlin/String // io.github.kevincianfarini.cardiologist/PulseSchedule.toString|toString(){}[0]
41+
42+
final object Companion { // io.github.kevincianfarini.cardiologist/PulseSchedule.Companion|null[0]
43+
final fun parseCron(kotlin/String): io.github.kevincianfarini.cardiologist/PulseSchedule // io.github.kevincianfarini.cardiologist/PulseSchedule.Companion.parseCron|parseCron(kotlin.String){}[0]
44+
}
4145
}
4246

4347
final class io.github.kevincianfarini.cardiologist/PulseScheduleBuilder { // io.github.kevincianfarini.cardiologist/PulseScheduleBuilder|null[0]

src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/PulseSchedule.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.kevincianfarini.cardiologist
22

33
import dev.drewhamilton.poko.Poko
4+
import io.github.kevincianfarini.cardiologist.impl.parseCronExpression
45
import kotlinx.datetime.DayOfWeek
56
import kotlinx.datetime.Month
67
import kotlinx.datetime.isoDayNumber
@@ -56,6 +57,32 @@ public class PulseSchedule internal constructor(
5657
// Don't check if onDaysOfWeek is empty because an empty set is equivalent to the wildcard `*` value in cron
5758
// expressions.
5859
}
60+
61+
public companion object {
62+
/**
63+
* Parse a cron [expression] into a [PulseSchedule].
64+
*
65+
* This function supports the standard cron specification, which includes:
66+
*
67+
* - Five fields specifying the minute, hour, day of month, month, and day of week.
68+
* - `*` denoting a wildcard value.
69+
* - List of values using commas, like `1,5,7`.
70+
* - Ranges of values, like `5-10`.
71+
* - Mixed range and list values, like `5-10,20-25`.
72+
* - The integers 0-59 for the minute field.
73+
* - The integers 0-23 for the hour field.
74+
* - The integers 1-31 for the day of week field.
75+
* - The integers 1-12 for the month field.
76+
* - The integers 0-6 or strings SUN-SAT for the day of week field.
77+
*
78+
* Functionality not articulated above is considered non-standard and is therefore explicitly not supported.
79+
*
80+
* @throws IllegalArgumentException Malformed cron expression or unsupported cron feature.
81+
*/
82+
public fun parseCron(expression: String): PulseSchedule {
83+
return expression.parseCronExpression()
84+
}
85+
}
5986
}
6087

6188
public class PulseScheduleBuilder internal constructor() {

src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/impl/cron.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ private fun String.parseDaysOfWeekExpressionOrNull(): Set<DayOfWeek>? {
7676
4 -> DayOfWeek.THURSDAY
7777
5 -> DayOfWeek.FRIDAY
7878
6 -> DayOfWeek.SATURDAY
79-
else -> error("Invalid day of week integer $dayOfWeekInt.")
79+
else -> {
80+
throw IllegalArgumentException("Invalid day of week integer $dayOfWeekInt.")
81+
}
8082
}
8183
add(result)
8284
}

src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/schedule.kt

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,7 @@ public fun Clock.schedulePulse(
137137
/**
138138
* Schedule a [Pulse] whose beats occur in accordance with the provided [cronExpression] for a specific [timeZone].
139139
*
140-
* This function supports the standard cron specification, which includes:
141-
*
142-
* - Five fields specifying the minute, hour, day of month, month, and day of week.
143-
* - `*` denoting a wildcard value.
144-
* - List of values using commas, like `1,5,7`.
145-
* - Ranges of values, like `5-10`.
146-
* - Mixed range and list values, like `5-10,20-25`.
147-
* - The integers 0-59 for the minute field.
148-
* - The integers 0-23 for the hour field.
149-
* - The integers 1-31 for the day of week field.
150-
* - The integers 1-12 for the month field.
151-
* - The integers 0-6 or strings SUN-SAT for the day of week field.
152-
*
153-
* Functionality not articulated above is considered non-standard and is therefore explicitly not supported.
140+
* See [PulseSchedule.parseCron] for supported cron expression features.
154141
*
155142
* Scheduling pulses is done in local time and is therefore subject to daylight savings time adjustments. Local time
156143
* conversion is sometimes ambiguous, and therefore it's recommended to schedule pulses in a fixed UTC offset timezone.
@@ -160,6 +147,13 @@ public fun Clock.schedulePulse(
160147
* @param timeZone The TimeZone to schedule pulses in.
161148
* @throws IllegalArgumentException if [cronExpression] is not valid.
162149
*/
150+
@Deprecated(
151+
message = "Parse cron expression separately from scheduling a pulse",
152+
replaceWith = ReplaceWith(
153+
"this.schedulePulse(PulseSchedule.parseCron(cronExpression), timeZone)",
154+
"io.github.kevincianfarini.cardiologist.PulseSchedule",
155+
),
156+
)
163157
@ExperimentalTime
164158
public fun Clock.schedulePulse(
165159
cronExpression: String,

src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/impl/ParseCronTest.kt renamed to src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseScheduleParseCronTest.kt

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
package io.github.kevincianfarini.cardiologist.impl
1+
package io.github.kevincianfarini.cardiologist
22

3-
import io.github.kevincianfarini.cardiologist.PulseSchedule
43
import kotlinx.datetime.DayOfWeek
54
import kotlinx.datetime.Month
65
import kotlin.test.Test
76
import kotlin.test.assertEquals
87
import kotlin.test.assertFails
8+
import kotlin.test.assertFailsWith
99

10-
class ParseCronTest {
10+
class PulseScheduleParseCronTest {
1111

1212
@Test
1313
fun incorrect_number_of_segments_errors() {
14-
val e = assertFails { "".parseCronExpression() }
14+
val e = assertFailsWith<IllegalArgumentException> {
15+
PulseSchedule.parseCron("")
16+
}
1517
assertEquals(
1618
expected = "'' is not a valid cron expression.",
1719
actual = e.message
@@ -29,7 +31,7 @@ class ParseCronTest {
2931
onDaysOfWeek = emptySet(),
3032
inMonths = Month.entries.toSet(),
3133
),
32-
actual = "* * * * *".parseCronExpression()
34+
actual = PulseSchedule.parseCron("* * * * *")
3335
)
3436
}
3537

@@ -44,7 +46,7 @@ class ParseCronTest {
4446
onDaysOfWeek = emptySet(),
4547
inMonths = Month.entries.toSet(),
4648
),
47-
actual = "* * \t * * *".parseCronExpression()
49+
actual = PulseSchedule.parseCron("* * \t * * *")
4850
)
4951
}
5052

@@ -59,7 +61,7 @@ class ParseCronTest {
5961
onDaysOfWeek = emptySet(),
6062
inMonths = Month.entries.toSet(),
6163
),
62-
actual = "0 * * * *".parseCronExpression()
64+
actual = PulseSchedule.parseCron("0 * * * *")
6365
)
6466
}
6567

@@ -74,7 +76,7 @@ class ParseCronTest {
7476
onDaysOfWeek = emptySet(),
7577
inMonths = Month.entries.toSet(),
7678
),
77-
actual = "0,5 * * * *".parseCronExpression()
79+
actual = PulseSchedule.parseCron("0,5 * * * *")
7880
)
7981
}
8082

@@ -89,7 +91,7 @@ class ParseCronTest {
8991
onDaysOfWeek = emptySet(),
9092
inMonths = Month.entries.toSet(),
9193
),
92-
actual = "0-5 * * * *".parseCronExpression()
94+
actual = PulseSchedule.parseCron("0-5 * * * *")
9395
)
9496
}
9597

@@ -104,7 +106,7 @@ class ParseCronTest {
104106
onDaysOfWeek = emptySet(),
105107
inMonths = Month.entries.toSet(),
106108
),
107-
actual = "0-5,10,15 * * * *".parseCronExpression()
109+
actual = PulseSchedule.parseCron("0-5,10,15 * * * *")
108110
)
109111
}
110112

@@ -119,14 +121,14 @@ class ParseCronTest {
119121
onDaysOfWeek = emptySet(),
120122
inMonths = Month.entries.toSet(),
121123
),
122-
actual = "0-5,10-15 * * * *".parseCronExpression()
124+
actual = PulseSchedule.parseCron("0-5,10-15 * * * *")
123125
)
124126
}
125127

126128
@Test
127129
fun minutes_double_comma_invalid() {
128-
val e = assertFails {
129-
"0,,1 * * * *".parseCronExpression()
130+
val e = assertFailsWith<IllegalArgumentException> {
131+
PulseSchedule.parseCron("0,,1 * * * *")
130132
}
131133
assertEquals(
132134
expected = "Cron expression is malformed: <<0,,1>> * * * *",
@@ -136,8 +138,8 @@ class ParseCronTest {
136138

137139
@Test
138140
fun minutes_double_range_invalid() {
139-
val e = assertFails {
140-
"0--1 * * * *".parseCronExpression()
141+
val e = assertFailsWith<IllegalArgumentException> {
142+
PulseSchedule.parseCron("0--1 * * * *")
141143
}
142144
assertEquals(
143145
expected = "Cron expression is malformed: <<0--1>> * * * *",
@@ -147,8 +149,8 @@ class ParseCronTest {
147149

148150
@Test
149151
fun minutes_range_no_end_invalid() {
150-
val e = assertFails {
151-
"0- * * * *".parseCronExpression()
152+
val e = assertFailsWith<IllegalArgumentException> {
153+
PulseSchedule.parseCron("0- * * * *")
152154
}
153155
assertEquals(
154156
expected = "Cron expression is malformed: <<0->> * * * *",
@@ -158,8 +160,8 @@ class ParseCronTest {
158160

159161
@Test
160162
fun minutes_range_no_start_invalid() {
161-
val e = assertFails {
162-
"-1 * * * *".parseCronExpression()
163+
val e = assertFailsWith<IllegalArgumentException> {
164+
PulseSchedule.parseCron("-1 * * * *")
163165
}
164166
assertEquals(
165167
expected = "Cron expression is malformed: <<-1>> * * * *",
@@ -169,8 +171,8 @@ class ParseCronTest {
169171

170172
@Test
171173
fun minutes_range_empty_invalid() {
172-
val e = assertFails {
173-
"5-3 * * * *".parseCronExpression()
174+
val e = assertFailsWith<IllegalArgumentException> {
175+
PulseSchedule.parseCron("5-3 * * * *")
174176
}
175177
assertEquals(
176178
expected = "Cron expression is malformed: <<5-3>> * * * *",
@@ -180,8 +182,8 @@ class ParseCronTest {
180182

181183
@Test
182184
fun minutes_invalid_value() {
183-
val e = assertFails {
184-
"75 * * * *".parseCronExpression()
185+
val e = assertFailsWith<IllegalArgumentException> {
186+
PulseSchedule.parseCron("75 * * * *")
185187
}
186188
assertEquals(
187189
expected = "Cron expression is malformed: <<75>> * * * *",
@@ -192,8 +194,8 @@ class ParseCronTest {
192194

193195
@Test
194196
fun hours_invalid_value() {
195-
val e = assertFails {
196-
"* 24 * * *".parseCronExpression()
197+
val e = assertFailsWith<IllegalArgumentException> {
198+
PulseSchedule.parseCron("* 24 * * *")
197199
}
198200
assertEquals(
199201
expected = "Cron expression is malformed: * <<24>> * * *",
@@ -203,8 +205,8 @@ class ParseCronTest {
203205

204206
@Test
205207
fun day_of_month_invalid_value() {
206-
val e = assertFails {
207-
"* * 0 * *".parseCronExpression()
208+
val e = assertFailsWith<IllegalArgumentException> {
209+
PulseSchedule.parseCron("* * 0 * *")
208210
}
209211
assertEquals(
210212
expected = "Cron expression is malformed: * * <<0>> * *",
@@ -214,8 +216,8 @@ class ParseCronTest {
214216

215217
@Test
216218
fun month_invalid_int_value() {
217-
val e = assertFails {
218-
"* * * 13 *".parseCronExpression()
219+
val e = assertFailsWith<IllegalArgumentException> {
220+
PulseSchedule.parseCron("* * * 13 *")
219221
}
220222
assertEquals(
221223
expected = "Cron expression is malformed: * * * <<13>> *",
@@ -225,8 +227,8 @@ class ParseCronTest {
225227

226228
@Test
227229
fun day_of_week_invalid_int_value() {
228-
val e = assertFails {
229-
"* * * * 7".parseCronExpression()
230+
val e = assertFailsWith<IllegalArgumentException> {
231+
PulseSchedule.parseCron("* * * * 7")
230232
}
231233
assertEquals(
232234
expected = "Cron expression is malformed: * * * * <<7>>",
@@ -261,7 +263,7 @@ class ParseCronTest {
261263
onDaysOfWeek = emptySet(),
262264
inMonths = setOf(enum),
263265
),
264-
actual = "* * * $string *".parseCronExpression()
266+
actual = PulseSchedule.parseCron("* * * $string *")
265267
)
266268
}
267269
}
@@ -288,7 +290,7 @@ class ParseCronTest {
288290
onDaysOfWeek = setOf(enum),
289291
inMonths = Month.entries.toSet()
290292
),
291-
actual = "* * * * $string".parseCronExpression()
293+
actual = PulseSchedule.parseCron("* * * * $string")
292294
)
293295
}
294296
}
@@ -304,14 +306,14 @@ class ParseCronTest {
304306
onDaysOfWeek = DayOfWeek.entries.toSet(),
305307
inMonths = Month.entries.toSet()
306308
),
307-
actual = "* * * * SUN-SAT".parseCronExpression()
309+
actual = PulseSchedule.parseCron("* * * * SUN-SAT")
308310
)
309311
}
310312

311313
@Test
312314
fun invalid_month_string() {
313-
val e = assertFails {
314-
"* * * WRONG *".parseCronExpression()
315+
val e = assertFailsWith<IllegalArgumentException> {
316+
PulseSchedule.parseCron("* * * WRONG *")
315317
}
316318
assertEquals(
317319
expected = "Cron expression is malformed: * * * <<WRONG>> *",
@@ -321,8 +323,8 @@ class ParseCronTest {
321323

322324
@Test
323325
fun invalid_day_of_week_string() {
324-
val e = assertFails {
325-
"* * * * WRONG".parseCronExpression()
326+
val e = assertFailsWith<IllegalArgumentException> {
327+
PulseSchedule.parseCron("* * * * WRONG")
326328
}
327329
assertEquals(
328330
expected = "Cron expression is malformed: * * * * <<WRONG>>",
@@ -332,8 +334,8 @@ class ParseCronTest {
332334

333335
@Test
334336
fun multiple_incorrect_values_yield_many_errors() {
335-
val e = assertFails {
336-
"60 24 32 13 7".parseCronExpression()
337+
val e = assertFailsWith<IllegalArgumentException> {
338+
PulseSchedule.parseCron("60 24 32 13 7")
337339
}
338340
assertEquals(
339341
expected = "Cron expression is malformed: <<60>> <<24>> <<32>> <<13>> <<7>>",

src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseTests.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,6 @@ class PulseTests {
257257
)
258258
}
259259

260-
@Test
261-
fun schedulePulse_with_cron_expression_simple() = runTest {
262-
val pulse = testClock.schedulePulse("* * * * *").take(1)
263-
val duration = testTimeSource.measureTime { pulse.beat { } }
264-
assertEquals(expected = 1.minutes, actual = duration)
265-
}
266-
267260
@Test
268261
fun schedulePulse_dsl_simple() = runTest {
269262
val pulse = testClock.schedulePulse { atSeconds(0) }.take(1)

0 commit comments

Comments
 (0)