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:
1 file changed
+93
-79
@@ -3,55 +3,9 @@ package com.m57.hermescontrol.util
|
||||
import com.m57.hermescontrol.BuildConfig
|
||||
|
||||
object CronExpressionFormatter {
|
||||
fun cronToHumanReadable(schedule: String): String {
|
||||
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 =
|
||||
private val dowNames =
|
||||
listOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
|
||||
val monthNames =
|
||||
private val monthNames =
|
||||
listOf(
|
||||
"",
|
||||
"January",
|
||||
@@ -68,28 +22,67 @@ object CronExpressionFormatter {
|
||||
"December",
|
||||
)
|
||||
|
||||
// Format time: "09:00", "midnight", etc.
|
||||
fun formatTime(
|
||||
h: Int,
|
||||
m: Int,
|
||||
): String {
|
||||
if (h == 0 && m == 0) return "midnight"
|
||||
return String.format("%02d:%02d", h, m)
|
||||
fun cronToHumanReadable(schedule: String): String {
|
||||
val clean = schedule.trim().replace("\\s+".toRegex(), " ")
|
||||
val parts = clean.split(" ")
|
||||
if (parts.size != 5) {
|
||||
return schedule
|
||||
}
|
||||
|
||||
// Helper for ordinals: 1st, 2nd, 3rd, 4th...
|
||||
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"
|
||||
try {
|
||||
val minutePart = parts[0]
|
||||
val hourPart = parts[1]
|
||||
val domPart = parts[2]
|
||||
val monthPart = parts[3]
|
||||
val dowPart = parts[4]
|
||||
|
||||
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:
|
||||
// "0 12 1 * *" -> "On the 1st of every month at 12:00"
|
||||
// "0 0 1 1 *" -> "On January 1st at midnight"
|
||||
private fun parseSimpleShortcuts(
|
||||
minutePart: String,
|
||||
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 == "*") {
|
||||
val doms = parseField(domPart, 1, 31)
|
||||
val months = parseField(monthPart, 1, 12)
|
||||
@@ -104,7 +97,6 @@ object CronExpressionFormatter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (domPart != "*" && monthPart == "*" && dowPart == "*") {
|
||||
val doms = parseField(domPart, 1, 31)
|
||||
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 null
|
||||
}
|
||||
|
||||
// Days of week cases:
|
||||
// "30 8 * * 1-5" -> "At 08:30 Monday through Friday"
|
||||
// "0 9 * * 1" -> "Every Monday at 09:00"
|
||||
private fun parseDaysOfWeek(
|
||||
dowPart: String,
|
||||
domPart: String,
|
||||
monthPart: String,
|
||||
hoursList: List<Int>,
|
||||
minutesList: List<Int>,
|
||||
): String? {
|
||||
if (dowPart != "*" && domPart == "*" && monthPart == "*") {
|
||||
if (hoursList.size == 1 && minutesList.size == 1) {
|
||||
val timeStr = formatTime(hoursList[0], minutesList[0])
|
||||
@@ -139,9 +137,16 @@ object CronExpressionFormatter {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// General case:
|
||||
// e.g. "0 9,21 * * *" -> "At 09:00 and 21:00 every day"
|
||||
private fun parseGeneralTime(
|
||||
domPart: String,
|
||||
monthPart: String,
|
||||
dowPart: String,
|
||||
hoursList: List<Int>,
|
||||
minutesList: List<Int>,
|
||||
): String? {
|
||||
if (domPart == "*" && monthPart == "*" && dowPart == "*") {
|
||||
if (hoursList.isNotEmpty() && minutesList.size == 1) {
|
||||
val timeStrings = hoursList.map { formatTime(it, minutesList[0]) }
|
||||
@@ -154,15 +159,24 @@ object CronExpressionFormatter {
|
||||
return "At $timeDesc every day"
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return schedule
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
android.util.Log.w("CronExpressionFormatter", "Failed to parse cron: $schedule", e)
|
||||
}
|
||||
return "Raw: $schedule"
|
||||
private fun formatTime(
|
||||
h: Int,
|
||||
m: Int,
|
||||
): String {
|
||||
if (h == 0 && m == 0) return "midnight"
|
||||
return String.format("%02d:%02d", h, m)
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
Reference in new issue
Block a user