Refactor CronExpressionFormatter to improve maintainability (#192)

Extract logic from a single long method `cronToHumanReadable` into distinct parser helpers for shortcuts, day of month, days of week, and general time handling. Move repetitive static properties and small format helpers to the object scope.
This commit is contained in:
M57 authored and GitHub committed 2026-06-21 00:30:02 +03:00
1 parent 5e90a4d1a7
commit f7a334ecfd
1 file changed
+93 -79
@@ -3,55 +3,9 @@ package com.m57.hermescontrol.util
import com.m57.hermescontrol.BuildConfig import com.m57.hermescontrol.BuildConfig
object CronExpressionFormatter { object CronExpressionFormatter {
fun cronToHumanReadable(schedule: String): String { private val dowNames =
val clean = schedule.trim().replace("\\s+".toRegex(), " ")
val parts = clean.split(" ")
if (parts.size != 5) {
return schedule
}
try {
val minutePart = parts[0]
val hourPart = parts[1]
val domPart = parts[2]
val monthPart = parts[3]
val dowPart = parts[4]
// 1. Check for common simple shortcuts
// Every X minutes: */X * * * *
if (minutePart.startsWith("*/") &&
hourPart == "*" &&
domPart == "*" &&
monthPart == "*" &&
dowPart == "*"
) {
val step = minutePart.substring(2).toIntOrNull()
if (step != null) return "Every $step minutes"
}
// Every X hours: 0 */X * * *
if (minutePart == "0" &&
hourPart.startsWith("*/") &&
domPart == "*" &&
monthPart == "*" &&
dowPart == "*"
) {
val step = hourPart.substring(2).toIntOrNull()
if (step != null) return "Every $step hours"
}
// Midnight: 0 0 * * *
if (minutePart == "0" && hourPart == "0" && domPart == "*" && monthPart == "*" && dowPart == "*") {
return "Every day at midnight"
}
// At time every day: 0 H * * * or 0 H,H2 * * *
val hoursList = parseField(hourPart, 0, 23)
val minutesList = parseField(minutePart, 0, 59)
val dowNames =
listOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") listOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
val monthNames = private val monthNames =
listOf( listOf(
"", "",
"January", "January",
@@ -68,28 +22,67 @@ object CronExpressionFormatter {
"December", "December",
) )
// Format time: "09:00", "midnight", etc. fun cronToHumanReadable(schedule: String): String {
fun formatTime( val clean = schedule.trim().replace("\\s+".toRegex(), " ")
h: Int, val parts = clean.split(" ")
m: Int, if (parts.size != 5) {
): String { return schedule
if (h == 0 && m == 0) return "midnight"
return String.format("%02d:%02d", h, m)
} }
// Helper for ordinals: 1st, 2nd, 3rd, 4th... try {
fun formatOrdinal(d: Int): String = val minutePart = parts[0]
when { val hourPart = parts[1]
d % 100 in 11..13 -> "${d}th" val domPart = parts[2]
d % 10 == 1 -> "${d}st" val monthPart = parts[3]
d % 10 == 2 -> "${d}nd" val dowPart = parts[4]
d % 10 == 3 -> "${d}rd"
else -> "${d}th" parseSimpleShortcuts(minutePart, hourPart, domPart, monthPart, dowPart)?.let { return it }
val hoursList = parseField(hourPart, 0, 23)
val minutesList = parseField(minutePart, 0, 59)
parseDayOfMonthAndMonth(domPart, monthPart, dowPart, hoursList, minutesList)?.let { return it }
parseDaysOfWeek(dowPart, domPart, monthPart, hoursList, minutesList)?.let { return it }
parseGeneralTime(domPart, monthPart, dowPart, hoursList, minutesList)?.let { return it }
// Fallback
return schedule
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
android.util.Log.w("CronExpressionFormatter", "Failed to parse cron: $schedule", e)
}
return "Raw: $schedule"
}
} }
// Day of Month / Month specific cases: private fun parseSimpleShortcuts(
// "0 12 1 * *" -> "On the 1st of every month at 12:00" minutePart: String,
// "0 0 1 1 *" -> "On January 1st at midnight" hourPart: String,
domPart: String,
monthPart: String,
dowPart: String,
): String? {
if (minutePart.startsWith("*/") && hourPart == "*" && domPart == "*" && monthPart == "*" && dowPart == "*") {
val step = minutePart.substring(2).toIntOrNull()
if (step != null) return "Every $step minutes"
}
if (minutePart == "0" && hourPart.startsWith("*/") && domPart == "*" && monthPart == "*" && dowPart == "*") {
val step = hourPart.substring(2).toIntOrNull()
if (step != null) return "Every $step hours"
}
if (minutePart == "0" && hourPart == "0" && domPart == "*" && monthPart == "*" && dowPart == "*") {
return "Every day at midnight"
}
return null
}
private fun parseDayOfMonthAndMonth(
domPart: String,
monthPart: String,
dowPart: String,
hoursList: List<Int>,
minutesList: List<Int>,
): String? {
if (domPart != "*" && monthPart != "*" && dowPart == "*") { if (domPart != "*" && monthPart != "*" && dowPart == "*") {
val doms = parseField(domPart, 1, 31) val doms = parseField(domPart, 1, 31)
val months = parseField(monthPart, 1, 12) val months = parseField(monthPart, 1, 12)
@@ -104,7 +97,6 @@ object CronExpressionFormatter {
} }
} }
} }
if (domPart != "*" && monthPart == "*" && dowPart == "*") { if (domPart != "*" && monthPart == "*" && dowPart == "*") {
val doms = parseField(domPart, 1, 31) val doms = parseField(domPart, 1, 31)
if (doms.size == 1 && hoursList.size == 1 && minutesList.size == 1) { if (doms.size == 1 && hoursList.size == 1 && minutesList.size == 1) {
@@ -113,10 +105,16 @@ object CronExpressionFormatter {
return "On the $domStr of every month at $timeStr" return "On the $domStr of every month at $timeStr"
} }
} }
return null
}
// Days of week cases: private fun parseDaysOfWeek(
// "30 8 * * 1-5" -> "At 08:30 Monday through Friday" dowPart: String,
// "0 9 * * 1" -> "Every Monday at 09:00" domPart: String,
monthPart: String,
hoursList: List<Int>,
minutesList: List<Int>,
): String? {
if (dowPart != "*" && domPart == "*" && monthPart == "*") { if (dowPart != "*" && domPart == "*" && monthPart == "*") {
if (hoursList.size == 1 && minutesList.size == 1) { if (hoursList.size == 1 && minutesList.size == 1) {
val timeStr = formatTime(hoursList[0], minutesList[0]) val timeStr = formatTime(hoursList[0], minutesList[0])
@@ -139,9 +137,16 @@ object CronExpressionFormatter {
} }
} }
} }
return null
}
// General case: private fun parseGeneralTime(
// e.g. "0 9,21 * * *" -> "At 09:00 and 21:00 every day" domPart: String,
monthPart: String,
dowPart: String,
hoursList: List<Int>,
minutesList: List<Int>,
): String? {
if (domPart == "*" && monthPart == "*" && dowPart == "*") { if (domPart == "*" && monthPart == "*" && dowPart == "*") {
if (hoursList.isNotEmpty() && minutesList.size == 1) { if (hoursList.isNotEmpty() && minutesList.size == 1) {
val timeStrings = hoursList.map { formatTime(it, minutesList[0]) } val timeStrings = hoursList.map { formatTime(it, minutesList[0]) }
@@ -154,15 +159,24 @@ object CronExpressionFormatter {
return "At $timeDesc every day" return "At $timeDesc every day"
} }
} }
return null
}
// Fallback private fun formatTime(
return schedule h: Int,
} catch (e: Exception) { m: Int,
if (BuildConfig.DEBUG) { ): String {
android.util.Log.w("CronExpressionFormatter", "Failed to parse cron: $schedule", e) if (h == 0 && m == 0) return "midnight"
} return String.format("%02d:%02d", h, m)
return "Raw: $schedule"
} }
private fun formatOrdinal(d: Int): String =
when {
d % 100 in 11..13 -> "${d}th"
d % 10 == 1 -> "${d}st"
d % 10 == 2 -> "${d}nd"
d % 10 == 3 -> "${d}rd"
else -> "${d}th"
} }
private fun parseField( private fun parseField(