diff --git a/CronCraft.Test/CronHelperTest.cs b/CronCraft.Test/CronHelperTest.cs index 66c8ffd..099e6ea 100644 --- a/CronCraft.Test/CronHelperTest.cs +++ b/CronCraft.Test/CronHelperTest.cs @@ -182,6 +182,98 @@ 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); + } + + [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")] + [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); + } + + [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 3540346..8272551 100644 --- a/CronCraft/Extensions/CronHelper.cs +++ b/CronCraft/Extensions/CronHelper.cs @@ -44,8 +44,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "Every {0} in {1}", ["EveryXMonthsOnDay"] = "Every {0} months on the {1}", ["TimeFormat"] = "12" }); @@ -61,8 +65,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "Cada {0} en {1}", ["EveryXMonthsOnDay"] = "Cada {0} meses el día {1}", ["TimeFormat"] = "12" }); @@ -78,8 +86,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "Chaque {0} en {1}", ["EveryXMonthsOnDay"] = "Tous les {0} mois le {1}", ["TimeFormat"] = "12" }); @@ -94,8 +106,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "Jeden {0} im {1}", ["EveryXMonthsOnDay"] = "Alle {0} Monate am {1}", ["TimeFormat"] = "24" }); @@ -109,8 +125,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "Em {0} em {1}", ["EveryXMonthsOnDay"] = "A cada {0} meses no dia {1}", ["TimeFormat"] = "24" }); @@ -124,8 +144,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "Ogni {0} in {1}", ["EveryXMonthsOnDay"] = "Ogni {0} mesi il giorno {1}", ["TimeFormat"] = "24" }); @@ -139,8 +163,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "Elke {0} in {1}", ["EveryXMonthsOnDay"] = "Elke {0} maanden op de {1}", ["TimeFormat"] = "24" }); @@ -154,8 +182,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "{1}每{0}", ["EveryXMonthsOnDay"] = "每 {0} 个月的第 {1} 天", ["TimeFormat"] = "24" }); @@ -169,8 +201,12 @@ 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}", + ["EveryDayOfWeekInMonth"] = "{1}の毎週{0}", ["EveryXMonthsOnDay"] = "{0}か月ごとの{1}日", ["TimeFormat"] = "24" }); @@ -269,10 +305,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("EveryDayOfWeekInMonth", JoinDays(dayOfWeek, daysMap), monthName)} {Phrase("AtTime", time)}"; + return $"{Phrase("EveryDayOfWeek", JoinDays(dayOfWeek, daysMap))} {Phrase("AtTime", time)}"; + } if (dayOfMonth != "*" && (dayOfWeek == "*" || dayOfWeek == "?")) { @@ -280,6 +329,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 +456,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,