From 1c3c0d4c6ec5ed95d59e49d7eec76502d8d2b5d9 Mon Sep 17 00:00:00 2001 From: Teesofttech Date: Sat, 27 Jun 2026 09:02:49 +0100 Subject: [PATCH 1/2] Fix specific month descriptions --- CronCraft.Test/CronHelperTest.cs | 59 +++++++++++++++++ CronCraft/Extensions/CronHelper.cs | 102 +++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/CronCraft.Test/CronHelperTest.cs b/CronCraft.Test/CronHelperTest.cs index 66c8ffd..c2bcace 100644 --- a/CronCraft.Test/CronHelperTest.cs +++ b/CronCraft.Test/CronHelperTest.cs @@ -182,6 +182,65 @@ public void Test_Every6MonthsOn27th() Assert.AreEqual("Every 6 months on the 27th at 04:00 AM", result); } + [TestMethod] + public void Test_SpecificMonthAndDay() + { + var settings = CreateCustomSettings(); + + string result = CronHelper.ToHumanReadable("0 9 1 3 *", settings); + + Assert.AreEqual("Every year on March 1st at 09:00 AM", result); + } + + [TestMethod] + public void Test_SpecificMonthAndDay_December() + { + var settings = CreateCustomSettings(); + + string result = CronHelper.ToHumanReadable("0 0 25 12 *", settings); + + Assert.AreEqual("Every year on December 25th at 12:00 AM", result); + } + + [TestMethod] + public void Test_SpecificMonthWithWildcardDay() + { + var settings = CreateCustomSettings(); + + string result = CronHelper.ToHumanReadable("0 9 * 6 *", settings); + + Assert.AreEqual("Every day in June at 09:00 AM", result); + } + + [TestMethod] + public void Test_MonthStepWithWildcardDay() + { + var settings = CreateCustomSettings(); + + string result = CronHelper.ToHumanReadable("0 9 * */3 *", settings); + + Assert.AreEqual("Every 3 months at 09:00 AM", result); + } + + [DataTestMethod] + [DataRow("en", "Every year on March 1st at 09:00 AM")] + [DataRow("es", "Cada año el día 1st de Marzo a las 09:00 AM")] + [DataRow("fr", "Chaque année le 1st en Mars à 09:00 AM")] + [DataRow("de", "Jedes Jahr am 1. im März um 09:00 Uhr")] + [DataRow("pt", "Todo ano no dia 1º de Março às 09:00")] + [DataRow("it", "Ogni anno il giorno 1 di Marzo alle 09:00")] + [DataRow("nl", "Elk jaar op 1e Maart om 09:00")] + [DataRow("zh", "每年三月第 1 天 09:00")] + [DataRow("ja", "毎年3月1日 09:00に")] + public void Test_SpecificMonthName_IsLocalized(string language, string expected) + { + var settings = new CronSettings { Language = language }; + + string result = CronHelper.ToHumanReadable("0 9 1 3 *", settings); + + Assert.AreEqual(expected, result); + } + [TestMethod] public void Test_SpecificDayAndWeek() { diff --git a/CronCraft/Extensions/CronHelper.cs b/CronCraft/Extensions/CronHelper.cs index 3540346..73d6cc0 100644 --- a/CronCraft/Extensions/CronHelper.cs +++ b/CronCraft/Extensions/CronHelper.cs @@ -44,6 +44,9 @@ private static string ToHumanReadableEnglish(string cron, CronSettings settings, ["EveryXHours"] = "Every {0} hours", ["EveryXHoursOn"] = "Every {0} hours on {1}", ["EveryMonthOnDay"] = "Every month on the {0}", + ["EveryYearOnMonthDay"] = "Every year on {0} {1}", + ["EveryDayInMonth"] = "Every day in {0}", + ["EveryXMonths"] = "Every {0} months", ["OnDayAndWeek"] = "On {0} and {1}", ["EveryDayOfWeek"] = "Every {0}", ["EveryXMonthsOnDay"] = "Every {0} months on the {1}", @@ -61,6 +64,9 @@ private static string ToHumanReadableSpanish(string cron, CronSettings settings, ["EveryXHours"] = "Cada {0} horas", ["EveryXHoursOn"] = "Cada {0} horas los {1}", ["EveryMonthOnDay"] = "Cada mes el día {0}", + ["EveryYearOnMonthDay"] = "Cada año el día {1} de {0}", + ["EveryDayInMonth"] = "Cada día en {0}", + ["EveryXMonths"] = "Cada {0} meses", ["OnDayAndWeek"] = "El {0} y los {1}", ["EveryDayOfWeek"] = "Cada {0}", ["EveryXMonthsOnDay"] = "Cada {0} meses el día {1}", @@ -78,6 +84,9 @@ private static string ToHumanReadableFrench(string cron, CronSettings settings, ["EveryXHours"] = "Toutes les {0} heures", ["EveryXHoursOn"] = "Toutes les {0} heures le {1}", ["EveryMonthOnDay"] = "Chaque mois le {0}", + ["EveryYearOnMonthDay"] = "Chaque année le {1} en {0}", + ["EveryDayInMonth"] = "Chaque jour en {0}", + ["EveryXMonths"] = "Tous les {0} mois", ["OnDayAndWeek"] = "Le {0} et le {1}", ["EveryDayOfWeek"] = "Chaque {0}", ["EveryXMonthsOnDay"] = "Tous les {0} mois le {1}", @@ -94,6 +103,9 @@ private static string ToHumanReadableGerman(string cron, CronSettings settings, ["EveryXHours"] = "Alle {0} Stunden", ["EveryXHoursOn"] = "Alle {0} Stunden am {1}", ["EveryMonthOnDay"] = "Jeden Monat am {0}", + ["EveryYearOnMonthDay"] = "Jedes Jahr am {1} im {0}", + ["EveryDayInMonth"] = "Jeden Tag im {0}", + ["EveryXMonths"] = "Alle {0} Monate", ["OnDayAndWeek"] = "Am {0} und am {1}", ["EveryDayOfWeek"] = "Jeden {0}", ["EveryXMonthsOnDay"] = "Alle {0} Monate am {1}", @@ -109,6 +121,9 @@ private static string ToHumanReadablePortuguese(string cron, CronSettings settin ["EveryXHours"] = "A cada {0} horas", ["EveryXHoursOn"] = "A cada {0} horas em {1}", ["EveryMonthOnDay"] = "Todo mês no dia {0}", + ["EveryYearOnMonthDay"] = "Todo ano no dia {1} de {0}", + ["EveryDayInMonth"] = "Todos os dias em {0}", + ["EveryXMonths"] = "A cada {0} meses", ["OnDayAndWeek"] = "No dia {0} e em {1}", ["EveryDayOfWeek"] = "Em {0}", ["EveryXMonthsOnDay"] = "A cada {0} meses no dia {1}", @@ -124,6 +139,9 @@ private static string ToHumanReadableItalian(string cron, CronSettings settings, ["EveryXHours"] = "Ogni {0} ore", ["EveryXHoursOn"] = "Ogni {0} ore di {1}", ["EveryMonthOnDay"] = "Ogni mese il giorno {0}", + ["EveryYearOnMonthDay"] = "Ogni anno il giorno {1} di {0}", + ["EveryDayInMonth"] = "Ogni giorno in {0}", + ["EveryXMonths"] = "Ogni {0} mesi", ["OnDayAndWeek"] = "Il giorno {0} e di {1}", ["EveryDayOfWeek"] = "Ogni {0}", ["EveryXMonthsOnDay"] = "Ogni {0} mesi il giorno {1}", @@ -139,6 +157,9 @@ private static string ToHumanReadableDutch(string cron, CronSettings settings, T ["EveryXHours"] = "Elke {0} uur", ["EveryXHoursOn"] = "Elke {0} uur op {1}", ["EveryMonthOnDay"] = "Elke maand op de {0}", + ["EveryYearOnMonthDay"] = "Elk jaar op {1} {0}", + ["EveryDayInMonth"] = "Elke dag in {0}", + ["EveryXMonths"] = "Elke {0} maanden", ["OnDayAndWeek"] = "Op de {0} en op {1}", ["EveryDayOfWeek"] = "Elke {0}", ["EveryXMonthsOnDay"] = "Elke {0} maanden op de {1}", @@ -154,6 +175,9 @@ private static string ToHumanReadableChinese(string cron, CronSettings settings, ["EveryXHours"] = "每 {0} 小时", ["EveryXHoursOn"] = "每 {0} 小时({1})", ["EveryMonthOnDay"] = "每月第 {0} 天", + ["EveryYearOnMonthDay"] = "每年{0}第 {1} 天", + ["EveryDayInMonth"] = "{0}每天", + ["EveryXMonths"] = "每 {0} 个月", ["OnDayAndWeek"] = "每月第 {0} 天和{1}", ["EveryDayOfWeek"] = "每{0}", ["EveryXMonthsOnDay"] = "每 {0} 个月的第 {1} 天", @@ -169,6 +193,9 @@ private static string ToHumanReadableJapanese(string cron, CronSettings settings ["EveryXHours"] = "{0}時間ごと", ["EveryXHoursOn"] = "{1}に{0}時間ごと", ["EveryMonthOnDay"] = "毎月{0}日", + ["EveryYearOnMonthDay"] = "毎年{0}{1}日", + ["EveryDayInMonth"] = "{0}は毎日", + ["EveryXMonths"] = "{0}か月ごと", ["OnDayAndWeek"] = "毎月{0}日と{1}", ["EveryDayOfWeek"] = "毎週{0}", ["EveryXMonthsOnDay"] = "{0}か月ごとの{1}日", @@ -269,10 +296,23 @@ string Phrase(string key, params object[] args) => } if ((dayOfWeek == "*" || dayOfWeek == "?") && dayOfMonth == "*") + { + if (month.StartsWith("*/")) + return $"{Phrase("EveryXMonths", month.Replace("*/", ""))} {Phrase("AtTime", time)}"; + + if (TryGetMonthName(month, settings.Language, out var monthName)) + return $"{Phrase("EveryDayInMonth", monthName)} {Phrase("AtTime", time)}"; + return Phrase("EveryDay") + " " + Phrase("AtTime", time); + } if (dayOfWeek != "*" && dayOfMonth == "*") + { + if (TryGetMonthName(month, settings.Language, out var monthName)) + return $"{Phrase("EveryDayOfWeek", JoinDays(dayOfWeek, daysMap))} in {monthName} {Phrase("AtTime", time)}"; + return $"{Phrase("EveryDayOfWeek", JoinDays(dayOfWeek, daysMap))} {Phrase("AtTime", time)}"; + } if (dayOfMonth != "*" && (dayOfWeek == "*" || dayOfWeek == "?")) { @@ -280,6 +320,8 @@ string Phrase(string key, params object[] args) => return month.StartsWith("*/") ? $"{Phrase("EveryXMonthsOnDay", month.Replace("*/", ""), ordinal)} {Phrase("AtTime", time)}" + : TryGetMonthName(month, settings.Language, out var monthName) + ? $"{Phrase("EveryYearOnMonthDay", monthName, ordinal)} {Phrase("AtTime", time)}" : $"{Phrase("EveryMonthOnDay", ordinal)} {Phrase("AtTime", time)}"; } @@ -405,6 +447,66 @@ private static string DescribeOrdinals(string expression, string language = "en" return JoinDescriptions(descriptions); } + private static bool TryGetMonthName(string month, string language, out string monthName) + { + monthName = string.Empty; + + if (!int.TryParse(month, out var monthNumber) || monthNumber is < 1 or > 12) + return false; + + var names = language.ToLowerInvariant() switch + { + "es" => new string[] + { + "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", + "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre" + }, + "fr" => new string[] + { + "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", + "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" + }, + "de" => new string[] + { + "Januar", "Februar", "März", "April", "Mai", "Juni", + "Juli", "August", "September", "Oktober", "November", "Dezember" + }, + "pt" => new string[] + { + "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", + "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" + }, + "it" => new string[] + { + "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", + "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre" + }, + "nl" => new string[] + { + "Januari", "Februari", "Maart", "April", "Mei", "Juni", + "Juli", "Augustus", "September", "Oktober", "November", "December" + }, + "zh" => new string[] + { + "一月", "二月", "三月", "四月", "五月", "六月", + "七月", "八月", "九月", "十月", "十一月", "十二月" + }, + "ja" => new string[] + { + "1月", "2月", "3月", "4月", "5月", "6月", + "7月", "8月", "9月", "10月", "11月", "12月" + }, + _ => new string[] + { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + } + }; + + monthName = names[monthNumber - 1]; + return true; + } + private static bool TryDescribeTimePattern( string hour, string minute, From d1eada10b62b4b0c45a0d110112ce12741c57da0 Mon Sep 17 00:00:00 2001 From: Teesofttech Date: Sat, 27 Jun 2026 09:42:26 +0100 Subject: [PATCH 2/2] Localize weekday month descriptions --- CronCraft.Test/CronHelperTest.cs | 33 ++++++++++++++++++++++++++++++ CronCraft/Extensions/CronHelper.cs | 11 +++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CronCraft.Test/CronHelperTest.cs b/CronCraft.Test/CronHelperTest.cs index c2bcace..099e6ea 100644 --- a/CronCraft.Test/CronHelperTest.cs +++ b/CronCraft.Test/CronHelperTest.cs @@ -222,6 +222,16 @@ public void Test_MonthStepWithWildcardDay() Assert.AreEqual("Every 3 months at 09:00 AM", result); } + [TestMethod] + public void Test_SpecificMonthWithDayOfWeek() + { + var settings = new CronSettings { Language = "en", DayNameFormat = "short" }; + + string result = CronHelper.ToHumanReadable("0 9 * 6 1", settings); + + Assert.AreEqual("Every Mon in June at 09:00 AM", result); + } + [DataTestMethod] [DataRow("en", "Every year on March 1st at 09:00 AM")] [DataRow("es", "Cada año el día 1st de Marzo a las 09:00 AM")] @@ -241,6 +251,29 @@ public void Test_SpecificMonthName_IsLocalized(string language, string expected) Assert.AreEqual(expected, result); } + [DataTestMethod] + [DataRow("en", "Every Mon in June at 09:00 AM")] + [DataRow("es", "Cada Lun en Junio a las 09:00 AM")] + [DataRow("fr", "Chaque Lun en Juin à 09:00 AM")] + [DataRow("de", "Jeden Mo im Juni um 09:00 Uhr")] + [DataRow("pt", "Em Seg em Junho às 09:00")] + [DataRow("it", "Ogni Lun in Giugno alle 09:00")] + [DataRow("nl", "Elke Ma in Juni om 09:00")] + [DataRow("zh", "六月每周一 09:00")] + [DataRow("ja", "6月の毎週月曜 09:00に")] + public void Test_SpecificMonthWithDayOfWeek_IsLocalized(string language, string expected) + { + var settings = new CronSettings + { + Language = language, + DayNameFormat = "short" + }; + + string result = CronHelper.ToHumanReadable("0 9 * 6 1", settings); + + Assert.AreEqual(expected, result); + } + [TestMethod] public void Test_SpecificDayAndWeek() { diff --git a/CronCraft/Extensions/CronHelper.cs b/CronCraft/Extensions/CronHelper.cs index 73d6cc0..8272551 100644 --- a/CronCraft/Extensions/CronHelper.cs +++ b/CronCraft/Extensions/CronHelper.cs @@ -49,6 +49,7 @@ private static string ToHumanReadableEnglish(string cron, CronSettings settings, ["EveryXMonths"] = "Every {0} months", ["OnDayAndWeek"] = "On {0} and {1}", ["EveryDayOfWeek"] = "Every {0}", + ["EveryDayOfWeekInMonth"] = "Every {0} in {1}", ["EveryXMonthsOnDay"] = "Every {0} months on the {1}", ["TimeFormat"] = "12" }); @@ -69,6 +70,7 @@ private static string ToHumanReadableSpanish(string cron, CronSettings settings, ["EveryXMonths"] = "Cada {0} meses", ["OnDayAndWeek"] = "El {0} y los {1}", ["EveryDayOfWeek"] = "Cada {0}", + ["EveryDayOfWeekInMonth"] = "Cada {0} en {1}", ["EveryXMonthsOnDay"] = "Cada {0} meses el día {1}", ["TimeFormat"] = "12" }); @@ -89,6 +91,7 @@ private static string ToHumanReadableFrench(string cron, CronSettings settings, ["EveryXMonths"] = "Tous les {0} mois", ["OnDayAndWeek"] = "Le {0} et le {1}", ["EveryDayOfWeek"] = "Chaque {0}", + ["EveryDayOfWeekInMonth"] = "Chaque {0} en {1}", ["EveryXMonthsOnDay"] = "Tous les {0} mois le {1}", ["TimeFormat"] = "12" }); @@ -108,6 +111,7 @@ private static string ToHumanReadableGerman(string cron, CronSettings settings, ["EveryXMonths"] = "Alle {0} Monate", ["OnDayAndWeek"] = "Am {0} und am {1}", ["EveryDayOfWeek"] = "Jeden {0}", + ["EveryDayOfWeekInMonth"] = "Jeden {0} im {1}", ["EveryXMonthsOnDay"] = "Alle {0} Monate am {1}", ["TimeFormat"] = "24" }); @@ -126,6 +130,7 @@ private static string ToHumanReadablePortuguese(string cron, CronSettings settin ["EveryXMonths"] = "A cada {0} meses", ["OnDayAndWeek"] = "No dia {0} e em {1}", ["EveryDayOfWeek"] = "Em {0}", + ["EveryDayOfWeekInMonth"] = "Em {0} em {1}", ["EveryXMonthsOnDay"] = "A cada {0} meses no dia {1}", ["TimeFormat"] = "24" }); @@ -144,6 +149,7 @@ private static string ToHumanReadableItalian(string cron, CronSettings settings, ["EveryXMonths"] = "Ogni {0} mesi", ["OnDayAndWeek"] = "Il giorno {0} e di {1}", ["EveryDayOfWeek"] = "Ogni {0}", + ["EveryDayOfWeekInMonth"] = "Ogni {0} in {1}", ["EveryXMonthsOnDay"] = "Ogni {0} mesi il giorno {1}", ["TimeFormat"] = "24" }); @@ -162,6 +168,7 @@ private static string ToHumanReadableDutch(string cron, CronSettings settings, T ["EveryXMonths"] = "Elke {0} maanden", ["OnDayAndWeek"] = "Op de {0} en op {1}", ["EveryDayOfWeek"] = "Elke {0}", + ["EveryDayOfWeekInMonth"] = "Elke {0} in {1}", ["EveryXMonthsOnDay"] = "Elke {0} maanden op de {1}", ["TimeFormat"] = "24" }); @@ -180,6 +187,7 @@ private static string ToHumanReadableChinese(string cron, CronSettings settings, ["EveryXMonths"] = "每 {0} 个月", ["OnDayAndWeek"] = "每月第 {0} 天和{1}", ["EveryDayOfWeek"] = "每{0}", + ["EveryDayOfWeekInMonth"] = "{1}每{0}", ["EveryXMonthsOnDay"] = "每 {0} 个月的第 {1} 天", ["TimeFormat"] = "24" }); @@ -198,6 +206,7 @@ private static string ToHumanReadableJapanese(string cron, CronSettings settings ["EveryXMonths"] = "{0}か月ごと", ["OnDayAndWeek"] = "毎月{0}日と{1}", ["EveryDayOfWeek"] = "毎週{0}", + ["EveryDayOfWeekInMonth"] = "{1}の毎週{0}", ["EveryXMonthsOnDay"] = "{0}か月ごとの{1}日", ["TimeFormat"] = "24" }); @@ -309,7 +318,7 @@ string Phrase(string key, params object[] args) => if (dayOfWeek != "*" && dayOfMonth == "*") { if (TryGetMonthName(month, settings.Language, out var monthName)) - return $"{Phrase("EveryDayOfWeek", JoinDays(dayOfWeek, daysMap))} in {monthName} {Phrase("AtTime", time)}"; + return $"{Phrase("EveryDayOfWeekInMonth", JoinDays(dayOfWeek, daysMap), monthName)} {Phrase("AtTime", time)}"; return $"{Phrase("EveryDayOfWeek", JoinDays(dayOfWeek, daysMap))} {Phrase("AtTime", time)}"; }