Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions CronCraft.Test/CronHelperTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,98 @@ 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("")]
[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")]
[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()
{
Expand Down
23 changes: 14 additions & 9 deletions CronCraft/Extensions/CronHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)}";
Expand Down Expand Up @@ -384,18 +384,20 @@ private static string FormatTime(
string hour,
string minute,
TimeZoneInfo? timeZone,
CronSettings settings,
Dictionary<string, string> 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<string, string> phrases)
{
DateTime utcTime = new DateTime(2000, 1, 1, hour, minute, 0, DateTimeKind.Utc);
Expand All @@ -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);
}

Expand Down Expand Up @@ -520,6 +524,7 @@ private static bool TryDescribeTimePattern(
string hour,
string minute,
TimeZoneInfo? timeZone,
CronSettings settings,
Dictionary<string, string> phrases,
out string description)
{
Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
Expand All @@ -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)}";
Expand Down
15 changes: 15 additions & 0 deletions CronCraft/Models/CronSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
/// </summary>
public class CronSettings
{
private string? _timeFormat;

/// <summary>
/// Language used for output formatting.
/// Supported values: "en", "es", "fr", "de", "pt", "it", "nl", "zh", and "ja".
Expand All @@ -26,6 +28,19 @@ public class CronSettings
/// </summary>
public string? TimeZone { get; set; }

/// <summary>
/// .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.
/// </summary>
public string TimeFormat
{
get => _timeFormat ?? "hh:mm tt";
set => _timeFormat = string.IsNullOrWhiteSpace(value) ? null : value;
}

internal bool HasTimeFormatOverride => !string.IsNullOrWhiteSpace(_timeFormat);

/// <summary>
/// Custom mapping for day names when DayNameFormat is "custom".
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion CronCraft/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: 24-hour output (default: "hh:mm tt")
};

// Without timezone
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: 24-hour output (default: "hh:mm tt")
};

// Without timezone
Expand Down
Loading