From 5998b0c381f67a4b38ccc6aee5f8797e8673b9ef Mon Sep 17 00:00:00 2001 From: Teesofttech Date: Sat, 27 Jun 2026 09:59:09 +0100 Subject: [PATCH 1/2] Add configurable time format --- CronCraft.Test/CronHelperTest.cs | 79 ++++++++++++++++++++++++++++++ CronCraft/Extensions/CronHelper.cs | 23 +++++---- CronCraft/Models/CronSettings.cs | 15 ++++++ CronCraft/README.md | 3 +- README.md | 3 +- 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/CronCraft.Test/CronHelperTest.cs b/CronCraft.Test/CronHelperTest.cs index 099e6ea..c8d6520 100644 --- a/CronCraft.Test/CronHelperTest.cs +++ b/CronCraft.Test/CronHelperTest.cs @@ -144,6 +144,85 @@ public void Test_EveryDayAtSpecificTime() Assert.AreEqual("Every day at 02:30 PM", result); } + [TestMethod] + public void Test_DefaultTimeFormat_Is12Hour() + { + var settings = new CronSettings(); + + Assert.AreEqual("hh:mm tt", settings.TimeFormat); + } + + [DataTestMethod] + [DataRow("0 0 * * *", "Every day at 00:00")] + [DataRow("30 14 * * *", "Every day at 14:30")] + [DataRow("59 23 * * *", "Every day at 23:59")] + public void Test_CustomTimeFormat_24Hour(string cron, string expected) + { + var settings = new CronSettings { TimeFormat = "HH:mm" }; + + string result = CronHelper.ToHumanReadable(cron, settings); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void Test_CustomTimeFormat_12Hour() + { + var settings = new CronSettings { TimeFormat = "hh:mm tt" }; + + string result = CronHelper.ToHumanReadable("30 14 * * *", settings); + + Assert.AreEqual("Every day at 02:30 PM", result); + } + + [TestMethod] + public void Test_CustomTimeFormat_OverridesLanguageDefault() + { + var settings = new CronSettings + { + Language = "de", + TimeFormat = "hh:mm tt" + }; + + string result = CronHelper.ToHumanReadable("30 14 * * *", settings); + + Assert.AreEqual("Jeden Tag um 02:30 PM Uhr", result); + } + + [TestMethod] + public void Test_LanguageDefaultTimeFormat_IsPreservedWithoutOverride() + { + var settings = new CronSettings { Language = "de" }; + + string result = CronHelper.ToHumanReadable("30 14 * * *", settings); + + Assert.AreEqual("Jeden Tag um 14:30 Uhr", result); + } + + [TestMethod] + public void Test_CustomTimeFormat_AppliesToTimeRanges() + { + var settings = CreateCustomSettings(); + settings.TimeFormat = "HH:mm"; + + string result = CronHelper.ToHumanReadable("0 8-17 * * 1-5", settings); + + Assert.AreEqual( + "Every hour from 08:00 to 17:00 on Monday through Friday", + result); + } + + [TestMethod] + public void Test_CustomTimeFormat_AppliesToTimeLists() + { + var settings = CreateCustomSettings(); + settings.TimeFormat = "HH:mm"; + + string result = CronHelper.ToHumanReadable("0 8,12,17 * * *", settings); + + Assert.AreEqual("Every day at 08:00, 12:00 and 17:00", result); + } + [TestMethod] public void Test_EveryMonAtTime() { diff --git a/CronCraft/Extensions/CronHelper.cs b/CronCraft/Extensions/CronHelper.cs index 8272551..e5d11fe 100644 --- a/CronCraft/Extensions/CronHelper.cs +++ b/CronCraft/Extensions/CronHelper.cs @@ -231,7 +231,7 @@ private static string BuildHumanReadable( string month = parts[3]; string dayOfWeek = parts[4]; - string time = FormatTime(hour, minute, timeZone, phrases); + string time = FormatTime(hour, minute, timeZone, settings, phrases); string Phrase(string key, params object[] args) => phrases.TryGetValue(key, out var value) @@ -274,7 +274,7 @@ string Phrase(string key, params object[] args) => // --- Range and list patterns --- - if (TryDescribeTimePattern(hour, minute, timeZone, phrases, out var timePattern)) + if (TryDescribeTimePattern(hour, minute, timeZone, settings, phrases, out var timePattern)) { if (dayOfWeek != "*" && dayOfWeek != "?") return $"{timePattern} on {JoinDays(dayOfWeek, daysMap)}"; @@ -384,18 +384,20 @@ private static string FormatTime( string hour, string minute, TimeZoneInfo? timeZone, + CronSettings settings, Dictionary phrases) { int h = int.TryParse(hour.Replace("*/", "0"), out var hParsed) ? hParsed : 0; int m = int.TryParse(minute.Replace("*/", "0"), out var mParsed) ? mParsed : 0; - return FormatTime(h, m, timeZone, phrases); + return FormatTime(h, m, timeZone, settings, phrases); } private static string FormatTime( int hour, int minute, TimeZoneInfo? timeZone, + CronSettings settings, Dictionary phrases) { DateTime utcTime = new DateTime(2000, 1, 1, hour, minute, 0, DateTimeKind.Utc); @@ -404,9 +406,11 @@ private static string FormatTime( ? TimeZoneInfo.ConvertTimeFromUtc(utcTime, timeZone) : utcTime; - var format = phrases.TryGetValue("TimeFormat", out var timeFormat) && timeFormat == "24" - ? "HH:mm" - : "hh:mm tt"; + var format = settings.HasTimeFormatOverride + ? settings.TimeFormat + : phrases.TryGetValue("TimeFormat", out var timeFormat) && timeFormat == "24" + ? "HH:mm" + : "hh:mm tt"; return localTime.ToString(format, System.Globalization.CultureInfo.InvariantCulture); } @@ -520,6 +524,7 @@ private static bool TryDescribeTimePattern( string hour, string minute, TimeZoneInfo? timeZone, + CronSettings settings, Dictionary phrases, out string description) { @@ -533,7 +538,7 @@ private static bool TryDescribeTimePattern( int.TryParse(bounds[1], out var endHour)) { description = - $"Every hour from {FormatTime(startHour, fixedMinute, timeZone, phrases)} to {FormatTime(endHour, fixedMinute, timeZone, phrases)}"; + $"Every hour from {FormatTime(startHour, fixedMinute, timeZone, settings, phrases)} to {FormatTime(endHour, fixedMinute, timeZone, settings, phrases)}"; return true; } } @@ -546,7 +551,7 @@ private static bool TryDescribeTimePattern( int.TryParse(bounds[1], out var endMinute)) { description = - $"Every minute from {FormatTime(fixedHour, startMinute, timeZone, phrases)} to {FormatTime(fixedHour, endMinute, timeZone, phrases)}"; + $"Every minute from {FormatTime(fixedHour, startMinute, timeZone, settings, phrases)} to {FormatTime(fixedHour, endMinute, timeZone, settings, phrases)}"; return true; } } @@ -558,7 +563,7 @@ private static bool TryDescribeTimePattern( var times = ( from parsedHour in hours from parsedMinute in minutes - select FormatTime(parsedHour, parsedMinute, timeZone, phrases)) + select FormatTime(parsedHour, parsedMinute, timeZone, settings, phrases)) .ToList(); description = $"At {JoinDescriptions(times)}"; diff --git a/CronCraft/Models/CronSettings.cs b/CronCraft/Models/CronSettings.cs index dcfe2e7..414c4be 100644 --- a/CronCraft/Models/CronSettings.cs +++ b/CronCraft/Models/CronSettings.cs @@ -5,6 +5,8 @@ /// public class CronSettings { + private string? _timeFormat; + /// /// Language used for output formatting. /// Supported values: "en", "es", "fr", "de", "pt", "it", "nl", "zh", and "ja". @@ -26,6 +28,19 @@ public class CronSettings /// public string? TimeZone { get; set; } + /// + /// .NET time format string used for rendered times. + /// Default is "hh:mm tt" for backwards-compatible 12-hour output. + /// Examples: "hh:mm tt" for 12-hour time, "HH:mm" for 24-hour time. + /// + public string TimeFormat + { + get => _timeFormat ?? "hh:mm tt"; + set => _timeFormat = value; + } + + internal bool HasTimeFormatOverride => !string.IsNullOrWhiteSpace(_timeFormat); + /// /// Custom mapping for day names when DayNameFormat is "custom". /// diff --git a/CronCraft/README.md b/CronCraft/README.md index bda5b40..39d5c48 100644 --- a/CronCraft/README.md +++ b/CronCraft/README.md @@ -45,7 +45,8 @@ string cronExpression = "0 0 * * *"; // Every day at midnight var settings = new CronSettings { Language = "en", - DayNameFormat = "short" + DayNameFormat = "short", + TimeFormat = "HH:mm" // Optional: use "hh:mm tt" for 12-hour output }; // Without timezone diff --git a/README.md b/README.md index d1c6bbf..f23d954 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ string cronExpression = "0 0 * * *"; // Every day at midnight var settings = new CronSettings { Language = "en", - DayNameFormat = "short" + DayNameFormat = "short", + TimeFormat = "HH:mm" // Optional: use "hh:mm tt" for 12-hour output }; // Without timezone From 14649b3a5461ac261d8524bca3fce98fc682e254 Mon Sep 17 00:00:00 2001 From: Teesofttech Date: Sat, 27 Jun 2026 10:18:51 +0100 Subject: [PATCH 2/2] Normalize blank time formats --- CronCraft.Test/CronHelperTest.cs | 13 +++++++++++++ CronCraft/Models/CronSettings.cs | 2 +- CronCraft/README.md | 2 +- README.md | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CronCraft.Test/CronHelperTest.cs b/CronCraft.Test/CronHelperTest.cs index c8d6520..4c2ac12 100644 --- a/CronCraft.Test/CronHelperTest.cs +++ b/CronCraft.Test/CronHelperTest.cs @@ -152,6 +152,19 @@ public void Test_DefaultTimeFormat_Is12Hour() Assert.AreEqual("hh:mm tt", settings.TimeFormat); } + [DataTestMethod] + [DataRow("")] + [DataRow(" ")] + public void Test_BlankTimeFormat_UsesDefault(string timeFormat) + { + var settings = new CronSettings { TimeFormat = timeFormat }; + + string result = CronHelper.ToHumanReadable("30 14 * * *", settings); + + Assert.AreEqual("hh:mm tt", settings.TimeFormat); + Assert.AreEqual("Every day at 02:30 PM", result); + } + [DataTestMethod] [DataRow("0 0 * * *", "Every day at 00:00")] [DataRow("30 14 * * *", "Every day at 14:30")] diff --git a/CronCraft/Models/CronSettings.cs b/CronCraft/Models/CronSettings.cs index 414c4be..6dbaff1 100644 --- a/CronCraft/Models/CronSettings.cs +++ b/CronCraft/Models/CronSettings.cs @@ -36,7 +36,7 @@ public class CronSettings public string TimeFormat { get => _timeFormat ?? "hh:mm tt"; - set => _timeFormat = value; + set => _timeFormat = string.IsNullOrWhiteSpace(value) ? null : value; } internal bool HasTimeFormatOverride => !string.IsNullOrWhiteSpace(_timeFormat); diff --git a/CronCraft/README.md b/CronCraft/README.md index 39d5c48..d1dfa3c 100644 --- a/CronCraft/README.md +++ b/CronCraft/README.md @@ -46,7 +46,7 @@ var settings = new CronSettings { Language = "en", DayNameFormat = "short", - TimeFormat = "HH:mm" // Optional: use "hh:mm tt" for 12-hour output + TimeFormat = "HH:mm" // Optional: 24-hour output (default: "hh:mm tt") }; // Without timezone diff --git a/README.md b/README.md index f23d954..5d03988 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ var settings = new CronSettings { Language = "en", DayNameFormat = "short", - TimeFormat = "HH:mm" // Optional: use "hh:mm tt" for 12-hour output + TimeFormat = "HH:mm" // Optional: 24-hour output (default: "hh:mm tt") }; // Without timezone