Repository: kevincianfarini/cardiologist (kotlin)

Issue:
Consider better `toString()` for `PulseSchedule`
This is hard to read:

`PulseSchedule(atSeconds=[0], atMinutes=[0], atHours=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], onDaysOfMonth=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], inMonths=[JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER], onDaysOfWeek=[])`

I think collapsing ranges greater than 2 in numbers would help (e.g., "3-8", "MARCH-JULY", etc.). It would transform the above into

`PulseSchedule(atSeconds=[0], atMinutes=[0], atHours=[0-23], onDaysOfMonth=[1-31], inMonths=[JANUARY-DECEMBER], onDaysOfWeek=[])`

Pull request description:
Closes #165

Patch:
diff --git a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/PulseSchedule.kt b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/PulseSchedule.kt
index b73d227..b5a2d82 100644
--- a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/PulseSchedule.kt
+++ b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/PulseSchedule.kt
@@ -11,7 +11,7 @@ import kotlinx.datetime.number
  * A [Pulse] schedule that can be used with [schedulePulse] to define complex schedules.
  */
 @Poko
-public class PulseSchedule internal constructor(
+public class PulseSchedule private constructor(
     public val atSeconds: List<Int>,
     public val atMinutes: List<Int>,
     public val atHours: List<Int>,
@@ -58,6 +58,57 @@ public class PulseSchedule internal constructor(
         // expressions.
     }
 
+    override fun toString(): String = buildString {
+        append("PulseSchedule{atSeconds=[")
+        appendRanges(atSeconds) { it }
+        append("], atMinutes=[")
+        appendRanges(atMinutes) { it }
+        append("], atHours=[")
+        appendRanges(atHours) { it }
+        append("], onDaysOfMonth=[")
+        appendRanges(onDaysOfMonth) { it }
+        append("], inMonths=[")
+        appendRanges(inMonths, Enum<*>::ordinal)
+        if (onDaysOfWeek.isNotEmpty()) {
+            append("], onDaysOfWeek=[")
+            appendRanges(onDaysOfWeek, Enum<*>::ordinal)
+        }
+        append("]}")
+    }
+
+    private fun <T : Any> StringBuilder.appendRanges(elements: Collection<T>, value: (T) -> Int) {
+        val iterator = elements.iterator()
+
+        // All collections are guaranteed to be non-empty.
+        val first = iterator.next()
+        var startValue = value(first)
+        append(first)
+
+        var last = first
+        var lastValue = startValue
+        while (iterator.hasNext()) {
+            val current = iterator.next()
+            val currentValue = value(current)
+            if (currentValue != lastValue + 1) {
+                if (lastValue > startValue) {
+                    append('-')
+                    append(last)
+                }
+                append(", ")
+                append(current)
+
+                startValue = currentValue
+            }
+            last = current
+            lastValue = currentValue
+        }
+
+        if (lastValue > startValue) {
+            append('-')
+            append(last)
+        }
+    }
+
     public companion object {
         /**
          * Parse a cron [expression] into a [PulseSchedule].
@@ -332,4 +383,4 @@ private fun MutableSet<DayOfWeek>.addAllDaysOfWeek(range: ClosedRange<DayOfWeek>
  */
 public fun buildPulseSchedule(builder: PulseScheduleBuilder.() -> Unit): PulseSchedule {
     return PulseScheduleBuilder().apply(builder).build()
-}
\ No newline at end of file
+}


Test patch:
diff --git a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseScheduleTest.kt b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseScheduleTest.kt
index 4a88444..e09c1a5 100644
--- a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseScheduleTest.kt
+++ b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseScheduleTest.kt
@@ -4,6 +4,7 @@ import kotlinx.datetime.Month
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFails
+import kotlinx.datetime.DayOfWeek
 
 class PulseScheduleTest {
 
@@ -168,4 +169,46 @@ class PulseScheduleTest {
             actual = e.message,
         )
     }
-}
\ No newline at end of file
+
+    @Test
+    fun toStrings() {
+        // Minimal elements.
+        assertEquals(
+            "PulseSchedule{atSeconds=[0], atMinutes=[0], atHours=[0], onDaysOfMonth=[1], inMonths=[JANUARY]}",
+            PulseSchedule(
+                atSeconds = setOf(0),
+                atMinutes = setOf(0),
+                atHours = setOf(0),
+                onDaysOfMonth = setOf(1),
+                inMonths = setOf(Month.JANUARY),
+                onDaysOfWeek = emptySet(),
+            ).toString(),
+        )
+
+        // Multi-element ranges, two-element ranges, individual elements.
+        assertEquals(
+            "PulseSchedule{atSeconds=[0-2, 4-5, 7], atMinutes=[0, 2-3, 5-7], atHours=[0-2, 4-5, 7], onDaysOfMonth=[1, 3-4, 6-8], inMonths=[JANUARY, MARCH-APRIL, JUNE-AUGUST], onDaysOfWeek=[MONDAY-WEDNESDAY, FRIDAY]}",
+            PulseSchedule(
+                atSeconds = setOf(0, 1, 2, 4, 5, 7),
+                atMinutes = setOf(0, 2, 3, 5, 6, 7),
+                atHours = setOf(0, 1, 2, 4, 5, 7),
+                onDaysOfMonth = setOf(1, 3, 4, 6, 7, 8),
+                inMonths = setOf(Month.JANUARY, Month.MARCH, Month.APRIL, Month.JUNE, Month.JULY, Month.AUGUST),
+                onDaysOfWeek = setOf(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY),
+            ).toString(),
+        )
+
+        // Full element ranges.
+        assertEquals(
+            "PulseSchedule{atSeconds=[0-59], atMinutes=[0-59], atHours=[0-23], onDaysOfMonth=[1-31], inMonths=[JANUARY-DECEMBER], onDaysOfWeek=[MONDAY-SUNDAY]}",
+            PulseSchedule(
+                atSeconds = (0..59).toSet(),
+                atMinutes = (0..59).toSet(),
+                atHours = (0..23).toSet(),
+                onDaysOfMonth = (1..31).toSet(),
+                inMonths = Month.entries.toSet(),
+                onDaysOfWeek = DayOfWeek.entries.toSet(),
+            ).toString(),
+        )
+    }
+}