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
82 changes: 82 additions & 0 deletions CronCraft.Test/CronHelperTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,86 @@ public void Test_InvalidCronExpressionException_ContainsOffendingExpression()

Assert.AreEqual(badCron, ex.CronExpression);
}

// --- Quartz special characters: L, W, # ---

[TestMethod]
public void Test_L_LastDayOfMonth()
{
var settings = new CronSettings { Language = "en" };
string result = CronHelper.ToHumanReadable("0 0 L * ?", settings);
Assert.AreEqual("Last day of the month at 12:00 AM", result);
}

[TestMethod]
public void Test_LW_LastWeekdayOfMonth()
{
var settings = new CronSettings { Language = "en" };
string result = CronHelper.ToHumanReadable("0 0 LW * ?", settings);
Assert.AreEqual("Last weekday of the month at 12:00 AM", result);
}

[TestMethod]
public void Test_W_NearestWeekday()
{
var settings = new CronSettings { Language = "en" };
string result = CronHelper.ToHumanReadable("0 0 15W * ?", settings);
Assert.AreEqual("Nearest weekday to the 15th of the month at 12:00 AM", result);
}

[TestMethod]
public void Test_W_NearestWeekday_1st()
{
var settings = new CronSettings { Language = "en" };
string result = CronHelper.ToHumanReadable("0 9 1W * ?", settings);
Assert.AreEqual("Nearest weekday to the 1st of the month at 09:00 AM", result);
}

[TestMethod]
public void Test_Hash_NthWeekdayOfMonth_FirstMonday()
{
var settings = new CronSettings { Language = "en", DayNameFormat = "full" };
string result = CronHelper.ToHumanReadable("0 0 ? * 2#1", settings);
Assert.AreEqual("Every first Monday of the month at 12:00 AM", result);
}

[TestMethod]
public void Test_Hash_NthWeekdayOfMonth_ThirdFriday()
{
var settings = new CronSettings { Language = "en", DayNameFormat = "full" };
string result = CronHelper.ToHumanReadable("0 0 ? * 6#3", settings);
Assert.AreEqual("Every third Friday of the month at 12:00 AM", result);
}

[TestMethod]
public void Test_Hash_NthWeekdayOfMonth_SecondWednesday()
{
var settings = new CronSettings { Language = "en", DayNameFormat = "full" };
string result = CronHelper.ToHumanReadable("0 10 ? * 4#2", settings);
Assert.AreEqual("Every second Wednesday of the month at 10:00 AM", result);
}

[TestMethod]
public void Test_L_LastDayOfWeek_LastMonday()
{
var settings = new CronSettings { Language = "en", DayNameFormat = "full" };
string result = CronHelper.ToHumanReadable("0 0 ? * 2L", settings);
Assert.AreEqual("Last Monday of the month at 12:00 AM", result);
}

[TestMethod]
public void Test_L_LastDayOfWeek_LastFriday()
{
var settings = new CronSettings { Language = "en", DayNameFormat = "full" };
string result = CronHelper.ToHumanReadable("0 17 ? * 6L", settings);
Assert.AreEqual("Last Friday of the month at 05:00 PM", result);
}

[TestMethod]
public void Test_Hash_QuartzSixPart_FirstTuesday()
{
var settings = new CronSettings { Language = "en", DayNameFormat = "full" };
string result = CronHelper.ToHumanReadable("0 0 9 ? * 3#1", settings);
Assert.AreEqual("Every first Tuesday of the month at 09:00 AM", result);
}
}
57 changes: 57 additions & 0 deletions CronCraft/Extensions/CronHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,42 @@ string Phrase(string key, params object[] args) =>
? string.Format(value, args)
: string.Format(key, args);

// --- Quartz special characters: L, W, # ---

// day-of-week: 2#1 → "Every first Monday of the month"
// Quartz day-of-week is 1-based (1=Sun…7=Sat); daysMap is 0-based (0=Sun…6=Sat)
if (dayOfWeek.Contains('#'))
{
var nthResult = DescribeNthWeekday(dayOfWeek, daysMap);
return $"{nthResult} {Phrase("AtTime", time)}";
}

// day-of-week: 2L → "Last Monday of the month" (Quartz 1-based, convert to 0-based)
if (dayOfWeek.EndsWith('L') && dayOfWeek != "L")
{
var quartzDay = int.TryParse(dayOfWeek.TrimEnd('L'), out var qd) ? qd : -1;
var standardDay = (quartzDay - 1).ToString();
var dayName = daysMap.TryGetValue(standardDay, out var n) ? n : $"Day {standardDay}";
return $"Last {dayName} of the month {Phrase("AtTime", time)}";
}
Comment on lines +110 to +117

// day-of-month: LW → "Last weekday of the month"
if (dayOfMonth.Equals("LW", StringComparison.OrdinalIgnoreCase))
return $"Last weekday of the month {Phrase("AtTime", time)}";

// day-of-month: L → "Last day of the month"
if (dayOfMonth == "L")
return $"Last day of the month {Phrase("AtTime", time)}";

// day-of-month: 15W → "Nearest weekday to the 15th"
if (dayOfMonth.EndsWith('W'))
{
var baseDay = Ordinal(dayOfMonth.TrimEnd('W'));
return $"Nearest weekday to the {baseDay} of the month {Phrase("AtTime", time)}";
}
Comment on lines +119 to +132

// --- Standard patterns ---

if (minute.StartsWith("*/") && hour == "*")
return Phrase("EveryXMinutes", minute.Replace("*/", ""));

Expand Down Expand Up @@ -130,6 +166,26 @@ string Phrase(string key, params object[] args) =>
return cron;
}

// "2#1" → "Every first Monday of the month" (Quartz day 2 = Mon in 1-based, so subtract 1)
// "6#3" → "Every third Friday of the month"
private static string DescribeNthWeekday(string dayOfWeek, Dictionary<string, string> daysMap)
{
var hashParts = dayOfWeek.Split('#');
// Quartz uses 1-based day-of-week; daysMap is 0-based
var standardDay = int.TryParse(hashParts[0], out var qd) ? (qd - 1).ToString() : hashParts[0];
var dayName = daysMap.TryGetValue(standardDay, out var n) ? n : $"Day {standardDay}";
string occurrence = hashParts[1] switch
{
"1" => "first",
"2" => "second",
"3" => "third",
"4" => "fourth",
"5" => "fifth",
_ => $"{hashParts[1]}th"
};
Comment on lines +177 to +185
return $"Every {occurrence} {dayName} of the month";
}

private static string ConvertQuartzToCronos(string quartzCron)
{
string[] parts = quartzCron.Split(' ', StringSplitOptions.RemoveEmptyEntries);
Expand All @@ -140,6 +196,7 @@ private static string ConvertQuartzToCronos(string quartzCron)
string hour = parts[2];
string day = parts[3];
string month = parts[4];
// Preserve L/#-suffixed values; only replace a bare "?" with "*"
string dayOfWeek = parts[5] == "?" ? "*" : parts[5];

return $"{minute} {hour} {day} {month} {dayOfWeek}";
Expand Down
Loading