From f7a334ecfd070d321b9da68ecf021b52e0dc9ada Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sun, 21 Jun 2026 00:30:02 +0300 Subject: [PATCH] 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. --- .../util/CronExpressionFormatter.kt | 280 +++++++++--------- 1 file changed, 147 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/util/CronExpressionFormatter.kt b/app/src/main/java/com/m57/hermescontrol/util/CronExpressionFormatter.kt index 30ce9c94..35916de6 100644 --- a/app/src/main/java/com/m57/hermescontrol/util/CronExpressionFormatter.kt +++ b/app/src/main/java/com/m57/hermescontrol/util/CronExpressionFormatter.kt @@ -3,6 +3,25 @@ package com.m57.hermescontrol.util import com.m57.hermescontrol.BuildConfig object CronExpressionFormatter { + private val dowNames = + listOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") + private val monthNames = + listOf( + "", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ) + fun cronToHumanReadable(schedule: String): String { val clean = schedule.trim().replace("\\s+".toRegex(), " ") val parts = clean.split(" ") @@ -17,143 +36,14 @@ object CronExpressionFormatter { 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" - } + parseSimpleShortcuts(minutePart, hourPart, domPart, monthPart, dowPart)?.let { return it } - // 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") - val monthNames = - listOf( - "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "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) - } - - // 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" - } - - // 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" - if (domPart != "*" && monthPart != "*" && dowPart == "*") { - val doms = parseField(domPart, 1, 31) - val months = parseField(monthPart, 1, 12) - if (doms.size == 1 && months.size == 1 && hoursList.size == 1 && minutesList.size == 1) { - val timeStr = formatTime(hoursList[0], minutesList[0]) - val monthStr = monthNames[months[0]] - val domStr = formatOrdinal(doms[0]) - return if (timeStr == "midnight") { - "On $monthStr $domStr at midnight" - } else { - "On $monthStr $domStr at $timeStr" - } - } - } - - if (domPart != "*" && monthPart == "*" && dowPart == "*") { - val doms = parseField(domPart, 1, 31) - if (doms.size == 1 && hoursList.size == 1 && minutesList.size == 1) { - val timeStr = formatTime(hoursList[0], minutesList[0]) - val domStr = formatOrdinal(doms[0]) - return "On the $domStr of every month at $timeStr" - } - } - - // Days of week cases: - // "30 8 * * 1-5" -> "At 08:30 Monday through Friday" - // "0 9 * * 1" -> "Every Monday at 09:00" - if (dowPart != "*" && domPart == "*" && monthPart == "*") { - if (hoursList.size == 1 && minutesList.size == 1) { - val timeStr = formatTime(hoursList[0], minutesList[0]) - if (dowPart.contains("-")) { - val bounds = dowPart.split("-") - if (bounds.size == 2) { - val start = bounds[0].toIntOrNull() ?: -1 - val end = bounds[1].toIntOrNull() ?: -1 - if (start in 0..7 && end in 0..7) { - val startName = dowNames[start] - val endName = dowNames[end] - return "At $timeStr $startName through $endName" - } - } - } - val dows = parseField(dowPart, 0, 7) - if (dows.size == 1) { - val dowName = dowNames[dows[0]] - return "Every $dowName at $timeStr" - } - } - } - - // General case: - // e.g. "0 9,21 * * *" -> "At 09:00 and 21:00 every day" - if (domPart == "*" && monthPart == "*" && dowPart == "*") { - if (hoursList.isNotEmpty() && minutesList.size == 1) { - val timeStrings = hoursList.map { formatTime(it, minutesList[0]) } - val timeDesc = - if (timeStrings.size > 1) { - timeStrings.dropLast(1).joinToString(", ") + " and " + timeStrings.last() - } else { - timeStrings[0] - } - return "At $timeDesc every day" - } - } + 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 @@ -165,6 +55,130 @@ object CronExpressionFormatter { } } + 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, + minutesList: List, + ): String? { + if (domPart != "*" && monthPart != "*" && dowPart == "*") { + val doms = parseField(domPart, 1, 31) + val months = parseField(monthPart, 1, 12) + if (doms.size == 1 && months.size == 1 && hoursList.size == 1 && minutesList.size == 1) { + val timeStr = formatTime(hoursList[0], minutesList[0]) + val monthStr = monthNames[months[0]] + val domStr = formatOrdinal(doms[0]) + return if (timeStr == "midnight") { + "On $monthStr $domStr at midnight" + } else { + "On $monthStr $domStr at $timeStr" + } + } + } + if (domPart != "*" && monthPart == "*" && dowPart == "*") { + val doms = parseField(domPart, 1, 31) + if (doms.size == 1 && hoursList.size == 1 && minutesList.size == 1) { + val timeStr = formatTime(hoursList[0], minutesList[0]) + val domStr = formatOrdinal(doms[0]) + return "On the $domStr of every month at $timeStr" + } + } + return null + } + + private fun parseDaysOfWeek( + dowPart: String, + domPart: String, + monthPart: String, + hoursList: List, + minutesList: List, + ): String? { + if (dowPart != "*" && domPart == "*" && monthPart == "*") { + if (hoursList.size == 1 && minutesList.size == 1) { + val timeStr = formatTime(hoursList[0], minutesList[0]) + if (dowPart.contains("-")) { + val bounds = dowPart.split("-") + if (bounds.size == 2) { + val start = bounds[0].toIntOrNull() ?: -1 + val end = bounds[1].toIntOrNull() ?: -1 + if (start in 0..7 && end in 0..7) { + val startName = dowNames[start] + val endName = dowNames[end] + return "At $timeStr $startName through $endName" + } + } + } + val dows = parseField(dowPart, 0, 7) + if (dows.size == 1) { + val dowName = dowNames[dows[0]] + return "Every $dowName at $timeStr" + } + } + } + return null + } + + private fun parseGeneralTime( + domPart: String, + monthPart: String, + dowPart: String, + hoursList: List, + minutesList: List, + ): String? { + if (domPart == "*" && monthPart == "*" && dowPart == "*") { + if (hoursList.isNotEmpty() && minutesList.size == 1) { + val timeStrings = hoursList.map { formatTime(it, minutesList[0]) } + val timeDesc = + if (timeStrings.size > 1) { + timeStrings.dropLast(1).joinToString(", ") + " and " + timeStrings.last() + } else { + timeStrings[0] + } + return "At $timeDesc every day" + } + } + return null + } + + 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( field: String, min: Int,