diff --git a/NCrontab.Signed/NCrontab.Signed.csproj b/NCrontab.Signed/NCrontab.Signed.csproj index 1942a7b..ee3eee8 100644 --- a/NCrontab.Signed/NCrontab.Signed.csproj +++ b/NCrontab.Signed/NCrontab.Signed.csproj @@ -31,6 +31,8 @@ + + diff --git a/NCrontab.Tests/CrontabScheduleTests.cs b/NCrontab.Tests/CrontabScheduleTests.cs index a015529..34776fd 100644 --- a/NCrontab.Tests/CrontabScheduleTests.cs +++ b/NCrontab.Tests/CrontabScheduleTests.cs @@ -111,7 +111,7 @@ public void Formatting(string format, string expression, bool includingSeconds) // Second tests - [TestCase("01/01/2003 00:00:00", "45 * * * * *", "01/01/2003 00:00:45" , true)] + [TestCase("01/01/2003 00:00:00", "45 * * * * *", "01/01/2003 00:00:45", true)] [TestCase("01/01/2003 00:00:00", "45-47,48,49 * * * * *", "01/01/2003 00:00:45", true)] [TestCase("01/01/2003 00:00:45", "45-47,48,49 * * * * *", "01/01/2003 00:00:46", true)] @@ -120,14 +120,14 @@ public void Formatting(string format, string expression, bool includingSeconds) [TestCase("01/01/2003 00:00:48", "45-47,48,49 * * * * *", "01/01/2003 00:00:49", true)] [TestCase("01/01/2003 00:00:49", "45-47,48,49 * * * * *", "01/01/2003 00:01:45", true)] - [TestCase("01/01/2003 00:00:00", "2/5 * * * * *", "01/01/2003 00:00:02" , true)] - [TestCase("01/01/2003 00:00:02", "2/5 * * * * *", "01/01/2003 00:00:07" , true)] - [TestCase("01/01/2003 00:00:50", "2/5 * * * * *", "01/01/2003 00:00:52" , true)] - [TestCase("01/01/2003 00:00:52", "2/5 * * * * *", "01/01/2003 00:00:57" , true)] - [TestCase("01/01/2003 00:00:57", "2/5 * * * * *", "01/01/2003 00:01:02" , true)] + [TestCase("01/01/2003 00:00:00", "2/5 * * * * *", "01/01/2003 00:00:02", true)] + [TestCase("01/01/2003 00:00:02", "2/5 * * * * *", "01/01/2003 00:00:07", true)] + [TestCase("01/01/2003 00:00:50", "2/5 * * * * *", "01/01/2003 00:00:52", true)] + [TestCase("01/01/2003 00:00:52", "2/5 * * * * *", "01/01/2003 00:00:57", true)] + [TestCase("01/01/2003 00:00:57", "2/5 * * * * *", "01/01/2003 00:01:02", true)] // See: https://github.com/atifaziz/NCrontab/issues/90 - [TestCase("24/02/2021 09:50:35", "* 0-1 10 * * *", "24/02/2021 10:00:00" , true)] + [TestCase("24/02/2021 09:50:35", "* 0-1 10 * * *", "24/02/2021 10:00:00", true)] // Minute tests @@ -247,11 +247,11 @@ public void Formatting(string format, string expression, bool includingSeconds) // Day of week tests - [TestCase("26/06/2003 10:00:00", "30 6 * * 0", "29/06/2003 06:30:00", false)] + [TestCase("26/06/2003 10:00:00", "30 6 * * 0", "29/06/2003 06:30:00", false)] [TestCase("26/06/2003 10:00:00", "30 6 * * sunday", "29/06/2003 06:30:00", false)] [TestCase("26/06/2003 10:00:00", "30 6 * * SUNDAY", "29/06/2003 06:30:00", false)] - [TestCase("19/06/2003 00:00:00", "1 12 * * 2", "24/06/2003 12:01:00", false)] - [TestCase("24/06/2003 12:01:00", "1 12 * * 2", "01/07/2003 12:01:00", false)] + [TestCase("19/06/2003 00:00:00", "1 12 * * 2", "24/06/2003 12:01:00", false)] + [TestCase("24/06/2003 12:01:00", "1 12 * * 2", "01/07/2003 12:01:00", false)] [TestCase("01/06/2003 14:55:00", "15 18 * * Mon", "02/06/2003 18:15:00", false)] [TestCase("02/06/2003 18:15:00", "15 18 * * Mon", "09/06/2003 18:15:00", false)] @@ -260,7 +260,7 @@ public void Formatting(string format, string expression, bool includingSeconds) [TestCase("23/06/2003 18:15:00", "15 18 * * Mon", "30/06/2003 18:15:00", false)] [TestCase("30/06/2003 18:15:00", "15 18 * * Mon", "07/07/2003 18:15:00", false)] - [TestCase("01/01/2003 00:00:00", "* * * * Mon", "06/01/2003 00:00:00", false)] + [TestCase("01/01/2003 00:00:00", "* * * * Mon", "06/01/2003 00:00:00", false)] [TestCase("01/01/2003 12:00:00", "45 16 1 * Mon", "01/09/2003 16:45:00", false)] [TestCase("01/09/2003 23:45:00", "45 16 1 * Mon", "01/12/2003 16:45:00", false)] @@ -282,9 +282,219 @@ public void Formatting(string format, string expression, bool includingSeconds) [TestCase("01/01/2000 12:00:00", "40 14/1 * * *", "01/01/2000 14:40:00", false)] [TestCase("01/01/2000 14:40:00", "40 14/1 * * *", "01/01/2000 15:40:00", false)] - public void Evaluations(string startTimeString, string cronExpression, string nextTimeString, bool includingSeconds) + public void Evaluations_Next(string startTimeString, string cronExpression, string nextTimeString, bool includingSeconds) { - CronCall(startTimeString, cronExpression, nextTimeString, new ParseOptions + CronCall_Next(startTimeString, cronExpression, nextTimeString, new ParseOptions + { + IncludingSeconds = includingSeconds + }); + } + + /// + /// Tests to see if the cron class can calculate the previous matching + /// time correctly in various circumstances. + /// + /// + + [TestCase("01/01/2003 00:01:00", "* * * * *", "01/01/2003 00:00:00", false)] + [TestCase("01/01/2003 00:02:00", "* * * * *", "01/01/2003 00:01:00", false)] + [TestCase("01/01/2003 00:03:00", "* * * * *", "01/01/2003 00:02:00", false)] + [TestCase("01/01/2003 01:00:00", "* * * * *", "01/01/2003 00:59:00", false)] + [TestCase("01/01/2003 02:00:00", "* * * * *", "01/01/2003 01:59:00", false)] + [TestCase("02/01/2003 00:00:00", "* * * * *", "01/01/2003 23:59:00", false)] + [TestCase("01/01/2004 00:00:00", "* * * * *", "31/12/2003 23:59:00", false)] + + [TestCase("01/03/2003 00:00:00", "* * * * *", "28/02/2003 23:59:00", false)] + [TestCase("29/02/2004 00:00:00", "* * * * *", "28/02/2004 23:59:00", false)] + + //// Second tests + + [TestCase("01/01/2003 00:01:00", "45 * * * * *", "01/01/2003 00:00:45", true)] + + [TestCase("01/01/2003 00:02:00", "45-47,48,49 * * * * *", "01/01/2003 00:01:49", true)] + [TestCase("01/01/2003 00:01:49", "45-47,48,49 * * * * *", "01/01/2003 00:01:48", true)] + [TestCase("01/01/2003 00:01:48", "45-47,48,49 * * * * *", "01/01/2003 00:01:47", true)] + [TestCase("01/01/2003 00:01:47", "45-47,48,49 * * * * *", "01/01/2003 00:01:46", true)] + [TestCase("01/01/2003 00:01:46", "45-47,48,49 * * * * *", "01/01/2003 00:01:45", true)] + [TestCase("01/01/2003 00:01:45", "45-47,48,49 * * * * *", "01/01/2003 00:00:49", true)] + + [TestCase("01/01/2003 00:02:00", "2/5 * * * * *", "01/01/2003 00:01:57", true)] + [TestCase("01/01/2003 00:01:57", "2/5 * * * * *", "01/01/2003 00:01:52", true)] + [TestCase("01/01/2003 00:01:10", "2/5 * * * * *", "01/01/2003 00:01:07", true)] + [TestCase("01/01/2003 00:01:07", "2/5 * * * * *", "01/01/2003 00:01:02", true)] + [TestCase("01/01/2003 00:01:02", "2/5 * * * * *", "01/01/2003 00:00:57", true)] + + //// See: https://github.com/atifaziz/NCrontab/issues/90 + [TestCase("24/02/2021 11:50:35", "* 0-1 10 * * *", "24/02/2021 10:01:59", true)] + + //// Minute tests + + [TestCase("01/01/2003 02:00:00", "45 * * * *", "01/01/2003 01:45:00", false)] + + [TestCase("01/01/2003 02:00:00", "45-47,48,49 * * * *", "01/01/2003 01:49:00", false)] + [TestCase("01/01/2003 01:49:00", "45-47,48,49 * * * *", "01/01/2003 01:48:00", false)] + [TestCase("01/01/2003 01:48:00", "45-47,48,49 * * * *", "01/01/2003 01:47:00", false)] + [TestCase("01/01/2003 01:47:00", "45-47,48,49 * * * *", "01/01/2003 01:46:00", false)] + [TestCase("01/01/2003 01:46:00", "45-47,48,49 * * * *", "01/01/2003 01:45:00", false)] + [TestCase("01/01/2003 01:45:00", "45-47,48,49 * * * *", "01/01/2003 00:49:00", false)] + + [TestCase("01/01/2003 02:00:00", "2/5 * * * *", "01/01/2003 01:57:00", false)] + [TestCase("01/01/2003 01:57:00", "2/5 * * * *", "01/01/2003 01:52:00", false)] + [TestCase("01/01/2003 01:10:00", "2/5 * * * *", "01/01/2003 01:07:00", false)] + [TestCase("01/01/2003 01:07:00", "2/5 * * * *", "01/01/2003 01:02:00", false)] + [TestCase("01/01/2003 01:02:00", "2/5 * * * *", "01/01/2003 00:57:00", false)] + + //// See: https://github.com/atifaziz/NCrontab/issues/90 + [TestCase("24/02/2021 11:50:35", "* * 10 * * *", "24/02/2021 10:59:59", true)] + [TestCase("24/02/2021 10:50:35", "* 55 * * * *", "24/02/2021 09:55:59", true)] + + [TestCase("01/01/2003 01:00:30", "3 45 * * * *", "01/01/2003 00:45:03", true)] + + [TestCase("01/01/2003 02:00:30", "6 45-47,48,49 * * * *", "01/01/2003 01:49:06", true)] + [TestCase("01/01/2003 01:49:03", "6 45-47,48,49 * * * *", "01/01/2003 01:48:06", true)] + [TestCase("01/01/2003 01:48:03", "6 45-47,48,49 * * * *", "01/01/2003 01:47:06", true)] + [TestCase("01/01/2003 01:47:03", "6 45-47,48,49 * * * *", "01/01/2003 01:46:06", true)] + [TestCase("01/01/2003 01:46:03", "6 45-47,48,49 * * * *", "01/01/2003 01:45:06", true)] + [TestCase("01/01/2003 01:45:03", "6 45-47,48,49 * * * *", "01/01/2003 00:49:06", true)] + + [TestCase("01/01/2003 02:00:30", "9 2/5 * * * *", "01/01/2003 01:57:09", true)] + [TestCase("01/01/2003 01:57:03", "9 2/5 * * * *", "01/01/2003 01:52:09", true)] + [TestCase("01/01/2003 01:10:30", "9 2/5 * * * *", "01/01/2003 01:07:09", true)] + [TestCase("01/01/2003 01:07:03", "9 2/5 * * * *", "01/01/2003 01:02:09", true)] + [TestCase("01/01/2003 01:02:03", "9 2/5 * * * *", "01/01/2003 00:57:09", true)] + + //// Hour tests + + [TestCase("20/12/2003 12:00:00", " * 3/4 * * *", "20/12/2003 11:59:00", false)] + [TestCase("20/12/2003 23:30:00", " * 3 * * *", "20/12/2003 03:59:00", false)] + [TestCase("20/12/2003 22:45:00", "30 3 * * *", "20/12/2003 03:30:00", false)] + + //// Day of month tests + + [TestCase("01/02/2003 00:15:00", "30 * 1 * *", "01/01/2003 23:30:00", false)] + [TestCase("01/01/2003 23:30:00", "30 * 1 * *", "01/01/2003 22:30:00", false)] + + [TestCase("25/01/2003 00:00:00", "10 * 22 * *", "22/01/2003 23:10:00", false)] + [TestCase("25/01/2003 00:00:00", "30 23 19 * *", "19/01/2003 23:30:00", false)] + [TestCase("25/01/2003 00:00:00", "30 23 21 * *", "21/01/2003 23:30:00", false)] + [TestCase("25/01/2003 00:59:00", " * * 21 * *", "21/01/2003 23:59:00", false)] + [TestCase("31/07/2003 00:00:00", " * * 30,31 * *", "30/07/2003 23:59:00", false)] + + //// Test month rollovers for months with 28,29,30 and 31 days + + [TestCase("01/03/2002 00:00:00", "* * * 2 *", "28/02/2002 23:59:00", false)] + [TestCase("01/03/2004 00:00:00", "* * * 2 *", "29/02/2004 23:59:00", false)] + [TestCase("01/04/2002 00:00:00", "* * * 3 *", "31/03/2002 23:59:00", false)] + [TestCase("01/05/2002 00:00:00", "* * * 4 *", "30/04/2002 23:59:00", false)] + + //// Test month 30,31 days + + //January + [TestCase("01/02/2000 00:00:00", "0 0 15,30,31 * *", "31/01/2000 00:00:00", false)] + [TestCase("31/01/2000 00:00:00", "0 0 15,30,31 * *", "30/01/2000 00:00:00", false)] + [TestCase("30/01/2000 00:00:00", "0 0 15,30,31 * *", "15/01/2000 00:00:00", false)] + [TestCase("15/01/2000 00:00:00", "0 0 15,30,31 * *", "31/12/1999 00:00:00", false)] + + //February + [TestCase("15/03/2000 00:00:00", "0 0 15,30,31 * *", "15/02/2000 00:00:00", false)] + + //March + [TestCase("15/04/2000 00:00:00", "0 0 15,30,31 * *", "31/03/2000 00:00:00", false)] + [TestCase("31/03/2000 00:00:00", "0 0 15,30,31 * *", "30/03/2000 00:00:00", false)] + [TestCase("30/03/2000 00:00:00", "0 0 15,30,31 * *", "15/03/2000 00:00:00", false)] + + //April + [TestCase("15/05/2000 00:00:00", "0 0 15,30,31 * *", "30/04/2000 00:00:00", false)] + [TestCase("30/04/2000 00:00:00", "0 0 15,30,31 * *", "15/04/2000 00:00:00", false)] + + //May + [TestCase("15/06/2000 00:00:00", "0 0 15,30,31 * *", "31/05/2000 00:00:00", false)] + [TestCase("31/05/2000 00:00:00", "0 0 15,30,31 * *", "30/05/2000 00:00:00", false)] + [TestCase("30/05/2000 00:00:00", "0 0 15,30,31 * *", "15/05/2000 00:00:00", false)] + + //June + [TestCase("15/07/2000 00:00:00", "0 0 15,30,31 * *", "30/06/2000 00:00:00", false)] + [TestCase("30/06/2000 00:00:00", "0 0 15,30,31 * *", "15/06/2000 00:00:00", false)] + + //July + [TestCase("15/08/2000 00:00:00", "0 0 15,30,31 * *", "31/07/2000 00:00:00", false)] + [TestCase("31/07/2000 00:00:00", "0 0 15,30,31 * *", "30/07/2000 00:00:00", false)] + [TestCase("30/07/2000 00:00:00", "0 0 15,30,31 * *", "15/07/2000 00:00:00", false)] + + //August + [TestCase("15/09/2000 00:00:00", "0 0 15,30,31 * *", "31/08/2000 00:00:00", false)] + [TestCase("31/08/2000 00:00:00", "0 0 15,30,31 * *", "30/08/2000 00:00:00", false)] + [TestCase("30/08/2000 00:00:00", "0 0 15,30,31 * *", "15/08/2000 00:00:00", false)] + + //September + [TestCase("15/10/2000 00:00:00", "0 0 15,30,31 * *", "30/09/2000 00:00:00", false)] + [TestCase("30/09/2000 00:00:00", "0 0 15,30,31 * *", "15/09/2000 00:00:00", false)] + + //October + [TestCase("15/11/2000 00:00:00", "0 0 15,30,31 * *", "31/10/2000 00:00:00", false)] + [TestCase("31/10/2000 00:00:00", "0 0 15,30,31 * *", "30/10/2000 00:00:00", false)] + [TestCase("30/10/2000 00:00:00", "0 0 15,30,31 * *", "15/10/2000 00:00:00", false)] + + //November + [TestCase("15/12/2000 00:00:00", "0 0 15,30,31 * *", "30/11/2000 00:00:00", false)] + [TestCase("30/11/2000 00:00:00", "0 0 15,30,31 * *", "15/11/2000 00:00:00", false)] + + //December + [TestCase("15/01/2001 00:00:00", "0 0 15,30,31 * *", "31/12/2000 00:00:00", false)] + [TestCase("31/12/2000 00:00:00", "0 0 15,30,31 * *", "30/12/2000 00:00:00", false)] + [TestCase("30/12/2000 00:00:00", "0 0 15,30,31 * *", "15/12/2000 00:00:00", false)] + + //// Other month tests (including year rollover) + + [TestCase("01/01/2003 05:00:00", "10 * * 6 *", "30/06/2002 23:10:00", false)] + [TestCase("15/02/2003 00:00:00", " 1 2 3 * *", "03/02/2003 02:01:00", false)] + [TestCase("01/01/2002 05:00:00", "10 * * February,April-Jun *", "30/06/2001 23:10:00", false)] + [TestCase("01/12/2003 00:00:00", "0 12 1 6 *", "01/06/2003 12:00:00", false)] + [TestCase("11/02/1990 14:23:00", "* 12 1 6 *", "01/06/1989 12:59:00", false)] + [TestCase("11/09/1988 14:23:00", "* 12 1 6 *", "01/06/1988 12:59:00", false)] + [TestCase("11/09/1988 14:23:00", "* 2,4-8,15 * 6 *", "30/06/1988 15:59:00", false)] + [TestCase("11/09/1988 14:23:00", "20 * * january,FeB,Mar,april,May,JuNE,July,Augu,SEPT-October,Nov,DECEM *", "11/09/1988 14:20:00", false)] + + //// Day of week tests + + [TestCase("01/07/2003 10:00:00", "30 6 * * 0", "29/06/2003 06:30:00", false)] + [TestCase("01/07/2003 10:00:00", "30 6 * * sunday", "29/06/2003 06:30:00", false)] + [TestCase("01/07/2003 10:00:00", "30 6 * * SUNDAY", "29/06/2003 06:30:00", false)] + [TestCase("29/06/2003 00:00:00", "1 12 * * 2", "24/06/2003 12:01:00", false)] + [TestCase("08/07/2003 12:01:00", "1 12 * * 2", "01/07/2003 12:01:00", false)] + + [TestCase("03/06/2003 14:55:00", "15 18 * * Mon", "02/06/2003 18:15:00", false)] + [TestCase("16/06/2003 18:15:00", "15 18 * * Mon", "09/06/2003 18:15:00", false)] + [TestCase("23/06/2003 18:15:00", "15 18 * * Mon", "16/06/2003 18:15:00", false)] + [TestCase("30/06/2003 18:15:00", "15 18 * * Mon", "23/06/2003 18:15:00", false)] + [TestCase("07/07/2003 18:15:00", "15 18 * * Mon", "30/06/2003 18:15:00", false)] + [TestCase("14/07/2003 18:15:00", "15 18 * * Mon", "07/07/2003 18:15:00", false)] + + [TestCase("11/01/2003 00:00:00", "* * * * Mon", "06/01/2003 23:59:00", false)] + [TestCase("01/12/2003 12:00:00", "45 16 1 * Mon", "01/09/2003 16:45:00", false)] + [TestCase("01/09/2003 09:45:00", "45 16 1 * Mon", "01/07/2002 16:45:00", false)] + + //// Leap year tests + + [TestCase("01/10/2000 12:00:00", "1 12 29 2 *", "29/02/2000 12:01:00", false)] + [TestCase("29/02/2000 12:01:00", "1 12 29 2 *", "29/02/1996 12:01:00", false)] + [TestCase("29/02/2008 12:01:00", "1 12 29 2 *", "29/02/2004 12:01:00", false)] + + //// Non-leap year tests + + [TestCase("01/04/2000 12:00:00", "1 12 28 2 *", "28/02/2000 12:01:00", false)] + [TestCase("28/02/2005 12:01:00", "1 12 28 2 *", "28/02/2004 12:01:00", false)] + [TestCase("28/02/2004 12:01:00", "1 12 28 2 *", "28/02/2003 12:01:00", false)] + [TestCase("28/02/2003 12:01:00", "1 12 28 2 *", "28/02/2002 12:01:00", false)] + [TestCase("28/02/2002 12:01:00", "1 12 28 2 *", "28/02/2001 12:01:00", false)] + [TestCase("29/02/2000 12:01:00", "1 12 28 2 *", "28/02/2000 12:01:00", false)] + + [TestCase("01/01/2000 15:00:00", "40 14/1 * * *", "01/01/2000 14:40:00", false)] + [TestCase("01/01/2000 16:40:00", "40 14/1 * * *", "01/01/2000 15:40:00", false)] + + public void Evaluations_Prev(string startTimeString, string cronExpression, string nextTimeString, bool includingSeconds) + { + CronCall_Prev(startTimeString, cronExpression, nextTimeString, new ParseOptions { IncludingSeconds = includingSeconds }); @@ -304,14 +514,37 @@ public void Evaluations(string startTimeString, string cronExpression, string ne [TestCase(" * * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 12:00:00", true )] [TestCase("10 30 12 * * Mon", "01/01/2003 00:00:00", "06/01/2003 12:00:10", true )] - public void FiniteOccurrences(string cronExpression, string startTimeString, string endTimeString, bool includingSeconds) + public void FiniteOccurrences_Next(string cronExpression, string startTimeString, string endTimeString, bool includingSeconds) { - CronFinite(cronExpression, startTimeString, endTimeString, new ParseOptions + CronFinite_Next(cronExpression, startTimeString, endTimeString, new ParseOptions { IncludingSeconds = includingSeconds }); } + [TestCase(" * * * * * ", "01/01/2003 00:00:00", "01/01/2003 00:00:00", false)] + [TestCase(" * * * * * ", "01/01/2003 00:00:00", "31/12/2002 23:59:59", false)] + [TestCase(" * * * * Mon", "01/01/2003 00:00:00", "31/12/2002 23:59:59", false)] + [TestCase(" * * * * Mon", "02/01/2003 00:00:00", "01/01/2003 00:00:00", false)] + [TestCase(" * * * * Mon", "02/01/2003 12:00:00", "01/01/2003 00:00:00", false)] + [TestCase("30 12 * * Mon", "06/01/2003 12:00:00", "01/01/2003 00:00:00", false)] + + [TestCase(" * * * * * * ", "01/01/2003 00:00:00", "01/01/2003 00:00:00", true)] + [TestCase(" * * * * * * ", "01/01/2003 00:00:00", "31/12/2002 23:59:59", true)] + [TestCase(" * * * * * Mon", "01/01/2003 00:00:00", "31/12/2002 23:59:59", true)] + [TestCase(" * * * * * Mon", "02/01/2003 00:00:00", "01/01/2003 00:00:00", true)] + [TestCase(" * * * * * Mon", "02/01/2003 12:00:00", "01/01/2003 00:00:00", true)] + [TestCase("10 30 12 * * Mon", "06/01/2003 12:00:10", "01/01/2003 00:00:00", true)] + + public void FiniteOccurrences_Prev(string cronExpression, string startTimeString, string endTimeString, bool includingSeconds) + { + CronFinite_Prev(cronExpression, startTimeString, endTimeString, new ParseOptions + { + IncludingSeconds = includingSeconds + }); + } + + // // Test to check we don't loop indefinitely looking for a February // 31st because no such date would ever exist! @@ -325,9 +558,25 @@ public void FiniteOccurrences(string cronExpression, string startTimeString, str #endif [TestCase("* * 31 Feb *", false)] [TestCase("* * * 31 Feb *", true)] - public void DontLoopIndefinitely(string expression, bool includingSeconds) + public void DontLoopIndefinitely_Next(string expression, bool includingSeconds) { - CronFinite(expression, "01/01/2001 00:00:00", "01/01/2010 00:00:00", new ParseOptions + CronFinite_Next(expression, "01/01/2001 00:00:00", "01/01/2010 00:00:00", new ParseOptions + { + IncludingSeconds = includingSeconds + }); + } + + [Category("Performance")] +#if NETCOREAPP1_0 + [Ignore("Timeout attribute missing from NUnit for .NET Core.")] +#else + [Timeout(1000)] +#endif + [TestCase("* * 31 Feb *", false)] + [TestCase("* * * 31 Feb *", true)] + public void DontLoopIndefinitely_Prev(string expression, bool includingSeconds) + { + CronFinite_Prev(expression, "01/01/2010 00:00:00", "01/01/2001 00:00:00", new ParseOptions { IncludingSeconds = includingSeconds }); @@ -409,6 +658,14 @@ public void GetNextOccurrences_NextOccurrenceInvalidTime_ShouldStopAtLastValidTi Assert.AreEqual(new DateTime(9988, 2, 29), occurrences.Last()); } + [Test] + public void GetPrevOccurrences_PrevOccurrenceInvalidTime_ShouldStopAtLastValidTime() + { + var schedule = CrontabSchedule.Parse("0 0 29 Feb Mon"); + var occurrences = schedule.GetPrevOccurrences(new DateTime(20, 1, 1), DateTime.MinValue); + Assert.AreEqual(new DateTime(16, 2, 29), occurrences.Last()); + } + // Instead of using strings and parsing as date, // consider NUnit's TestCaseData: // https://github.com/nunit/docs/wiki/TestCaseData @@ -427,8 +684,26 @@ public void GetNextOccurrence(string expression, string startDate, string endDat Assert.AreEqual(expected, occurrence); } + // Instead of using strings and parsing as date, + // consider NUnit's TestCaseData: + // https://github.com/nunit/docs/wiki/TestCaseData + + [TestCase("0 0 29 Feb Mon", "2017-12-31", "2017-01-01", "2017-01-01")] + [TestCase("0 0 29 Feb Mon", "9009-12-31", "9000-01-01", "9008-02-29")] + public void GetPrevOccurrence(string expression, string startDate, string endDate, string expectedValue) + { + var schedule = CrontabSchedule.Parse(expression); + var start = Time(startDate); + var end = Time(endDate); + var expected = Time(expectedValue); + + var occurrence = schedule.GetPrevOccurrence(start, end); + + Assert.AreEqual(expected, occurrence); + } + - static void CronCall(string startTimeString, string cronExpression, string nextTimeString, ParseOptions options) + static void CronCall_Next(string startTimeString, string cronExpression, string nextTimeString, ParseOptions options) { var schedule = CrontabSchedule.Parse(cronExpression, options); var next = schedule.GetNextOccurrence(Time(startTimeString)); @@ -437,7 +712,16 @@ static void CronCall(string startTimeString, string cronExpression, string nextT "Occurrence of <{0}> after <{1}>.", cronExpression, startTimeString); } - static void CronFinite(string cronExpression, string startTimeString, string endTimeString, ParseOptions options) + static void CronCall_Prev(string startTimeString, string cronExpression, string nextTimeString, ParseOptions options) + { + var schedule = CrontabSchedule.Parse(cronExpression, options); + var next = schedule.GetPrevOccurrence(Time(startTimeString)); + + Assert.AreEqual(nextTimeString, TimeString(next), + "Occurrence of <{0}> before <{1}>.", cronExpression, startTimeString); + } + + static void CronFinite_Next(string cronExpression, string startTimeString, string endTimeString, ParseOptions options) { var schedule = CrontabSchedule.Parse(cronExpression, options); var occurrence = schedule.GetNextOccurrence(Time(startTimeString), Time(endTimeString)); @@ -447,6 +731,16 @@ static void CronFinite(string cronExpression, string startTimeString, string end cronExpression, startTimeString, endTimeString); } + static void CronFinite_Prev(string cronExpression, string startTimeString, string endTimeString, ParseOptions options) + { + var schedule = CrontabSchedule.Parse(cronExpression, options); + var occurrence = schedule.GetPrevOccurrence(Time(startTimeString), Time(endTimeString)); + + Assert.AreEqual(endTimeString, TimeString(occurrence), + "Occurrence of <{0}> after <{1}> did not terminate with <{2}>.", + cronExpression, startTimeString, endTimeString); + } + static string TimeString(DateTime time) => time.ToString(TimeFormat, CultureInfo.InvariantCulture); static DateTime Time(string str) => DateTime.ParseExact(str, TimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None); } diff --git a/NCrontab/CrontabField.Iterator.cs b/NCrontab/CrontabField.Iterator.cs new file mode 100644 index 0000000..f9df31c --- /dev/null +++ b/NCrontab/CrontabField.Iterator.cs @@ -0,0 +1,52 @@ +using NCrontab.Utils; + +namespace NCrontab +{ + public sealed partial class CrontabField + { + internal sealed class Iterator + { + readonly CrontabField _crontabField; + readonly bool _forwardMoving; + + public Iterator(CrontabField crontabField, bool forwardMoving) + { + _crontabField = crontabField; + _forwardMoving = forwardMoving; + } + + /// + /// Returns the first valid value (according to filter) beginning at . + /// + /// + /// + public int Next(int start) + { + if (BeyondLowerBoundValueSet(start)) + return LowerBoundValueSet; + + var startIndex = _crontabField.ValueToIndex(start); + var lastIndex = _crontabField.ValueToIndex(UpperBoundValueSet); + + var incrementor = new Incrementor(_forwardMoving); + for (var i = startIndex; incrementor.BeforeOrEqual(i, lastIndex); i = incrementor.Increment(i)) + { + if (_crontabField._bits[i]) + return _crontabField.IndexToValue(i); + } + + return nil; + } + + public int LowerBound => _forwardMoving ? _crontabField.GetFirst() : _crontabField.GetLast(); + + public int LowerBoundValueSet => _forwardMoving ? _crontabField._minValueSet : _crontabField._maxValueSet; + public int UpperBoundValueSet => _forwardMoving ? _crontabField._maxValueSet : _crontabField._minValueSet; + + public bool BeyondLowerBoundValueSet(int start) => _forwardMoving ? + start < _crontabField._minValueSet : + start > _crontabField._maxValueSet; + + } + } +} diff --git a/NCrontab/CrontabField.cs b/NCrontab/CrontabField.cs index 2f2105a..7f46390 100644 --- a/NCrontab/CrontabField.cs +++ b/NCrontab/CrontabField.cs @@ -20,6 +20,7 @@ namespace NCrontab { + using NCrontab.Utils; #region Imports using System; @@ -37,6 +38,8 @@ namespace NCrontab public sealed partial class CrontabField : ICrontabField { + internal const int nil = -1; + readonly BitArray _bits; /* readonly */ int _minValueSet; /* readonly */ int _maxValueSet; @@ -111,36 +114,32 @@ public static CrontabField DaysOfWeek(string expression) => _impl = impl ?? throw new ArgumentNullException(nameof(impl)); _bits = new BitArray(impl.ValueCount); _minValueSet = int.MaxValue; - _maxValueSet = -1; + _maxValueSet = nil; } /// /// Gets the first value of the field or -1. /// - public int GetFirst() => _minValueSet < int.MaxValue ? _minValueSet : -1; + public int GetFirst() => _minValueSet < int.MaxValue ? _minValueSet : nil; + + /// + /// Gets the last value of the field or -1. + /// + public int GetLast() => _maxValueSet >= 0 ? _maxValueSet : nil; /// /// Gets the next value of the field that occurs after the given /// start value or -1 if there is no next value available. /// - public int Next(int start) - { - if (start < _minValueSet) - return _minValueSet; - - var startIndex = ValueToIndex(start); - var lastIndex = ValueToIndex(_maxValueSet); + public int Next(int start) => new Iterator(this, true).Next(start); - for (var i = startIndex; i <= lastIndex; i++) - { - if (_bits[i]) - return IndexToValue(i); - } - - return -1; - } + /// + /// Gets the previous value of the field that occurs before the given + /// start value or -1 if there is no previous value available. + /// + public int Prev(int start) => new Iterator(this, false).Next(start); int IndexToValue(int index) => index + _impl.MinValue; int ValueToIndex(int value) => value - _impl.MinValue; diff --git a/NCrontab/CrontabFieldImpl.cs b/NCrontab/CrontabFieldImpl.cs index 18973e6..7020b6a 100644 --- a/NCrontab/CrontabFieldImpl.cs +++ b/NCrontab/CrontabFieldImpl.cs @@ -90,7 +90,7 @@ public void Format(ICrontabField field, TextWriter writer, bool noNames) var next = field.GetFirst(); var count = 0; - while (next != -1) + while (next != CrontabField.nil) { var first = next; int last; @@ -219,7 +219,7 @@ T InternalParse(string str, CrontabFieldAccumulator acc, T success, Func(string expression, ParseOptions? options, Func GetNextOccurrences(DateTime baseTime, DateTime endTime) { - for (var occurrence = TryGetNextOccurrence(baseTime, endTime); + for (var occurrence = TryGetOccurrence(baseTime, endTime, true); occurrence != null && occurrence < endTime; - occurrence = TryGetNextOccurrence(occurrence.Value, endTime)) + occurrence = TryGetOccurrence(occurrence.Value, endTime, true)) + { + yield return occurrence.Value; + } + } + + public IEnumerable GetPrevOccurrences(DateTime baseTime, DateTime startTime) + { + for (var occurrence = TryGetOccurrence(baseTime, startTime, false); + occurrence != null && occurrence > startTime; + occurrence = TryGetOccurrence(occurrence.Value, startTime, false)) { yield return occurrence.Value; } @@ -195,6 +206,10 @@ public IEnumerable GetNextOccurrences(DateTime baseTime, DateTime endT public DateTime GetNextOccurrence(DateTime baseTime) => GetNextOccurrence(baseTime, DateTime.MaxValue); + + public DateTime GetPrevOccurrence(DateTime baseTime) => + GetPrevOccurrence(baseTime, DateTime.MinValue); + /// /// Gets the next occurrence of this schedule starting with a base /// time and up to an end time limit. @@ -211,180 +226,239 @@ public DateTime GetNextOccurrence(DateTime baseTime) => /// public DateTime GetNextOccurrence(DateTime baseTime, DateTime endTime) => - TryGetNextOccurrence(baseTime, endTime) ?? endTime; + TryGetOccurrence(baseTime, endTime, true) ?? endTime; + + /// + /// Gets the previous occurrence of this schedule starting with a base + /// time and up to an end time limit. + /// + /// + /// This method does not return the value of + /// itself if it falls on the schedule. For example, if + /// is midnight and the schedule was created from the expression * * * * * + /// (meaning every minute) then the previous occurrence of the schedule + /// will be at one minute before midnight and not midnight itself. + /// The method returns the previous occurrence before + /// . Also, is + /// exclusive. + /// + public DateTime GetPrevOccurrence(DateTime baseTime, DateTime startTime) => + TryGetOccurrence(baseTime, startTime, false) ?? startTime; - DateTime? TryGetNextOccurrence(DateTime baseTime, DateTime endTime) + DateTime? TryGetOccurrence(DateTime baseTime, DateTime limitTime, bool isNext) { - const int nil = -1; - - var baseYear = baseTime.Year; - var baseMonth = baseTime.Month; - var baseDay = baseTime.Day; - var baseHour = baseTime.Hour; - var baseMinute = baseTime.Minute; - var baseSecond = baseTime.Second; - - var endYear = endTime.Year; - var endMonth = endTime.Month; - var endDay = endTime.Day; - - var year = baseYear; - var month = baseMonth; - var day = baseDay; - var hour = baseHour; - var minute = baseMinute; - var second = baseSecond + 1; + var runningTime = new DateTimeComponents(baseTime); + var incrementor = new Incrementor(isNext); + + runningTime.Second = incrementor.Increment(runningTime.Second); // // Second // - var seconds = _seconds ?? SecondZero; - second = seconds.Next(second); + //Per expression filter advance (if necessary) to next second + var secondsIter = new CrontabField.Iterator(_seconds ?? SecondZero, isNext); + runningTime.Second = secondsIter.Next(runningTime.Second); - if (second == nil) - { - second = seconds.GetFirst(); - minute++; - } + AdjustForSecondInvalid(runningTime, incrementor, secondsIter); // // Minute // - minute = _minutes.Next(minute); + //Per expression filter advance (if necessary) to next minute + var minutesIter = new CrontabField.Iterator(_minutes, isNext); + runningTime.Minute = minutesIter.Next(runningTime.Minute); - if (minute == nil) - { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour++; - } - else if (minute > baseMinute) - { - second = seconds.GetFirst(); - } + AdjustForMinuteInvalidOrRollover(baseTime, runningTime, incrementor, secondsIter, minutesIter); // // Hour // - hour = _hours.Next(hour); - - if (hour == nil) - { - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day++; - } - else if (hour > baseHour) + //Per expression filter advance (if necessary) to next hour + var hoursIter = new CrontabField.Iterator(_hours, isNext); + runningTime.Hour = hoursIter.Next(runningTime.Hour); + + //Possible iterations will occur when advancing the day causes an invalid date (depending on which) + //month is being considered. Note: Not all months have the same number of days, unlike the other + //date/time elements which always have the same range. + var invalidDayOfMonth = false; + var daysIter = new CrontabField.Iterator(_days, isNext); + var monthsIter = new CrontabField.Iterator(_months, isNext); + do { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - } + //If anything but a repeat iteration of this do-while loop that is moving forward in time. + //When moving backward in time, we need to consider an hour adjustment to the last hour of the previous day + if (!invalidDayOfMonth || !isNext) + { + AdjustForHourInvalidOrRollover(baseTime, runningTime, incrementor, secondsIter, minutesIter, hoursIter); + + // + // Day + // + + //Per expression filter advance (if necessary) to next day + runningTime.Day = daysIter.Next(runningTime.Day); + } + + AdjustForDayInvalidOrRollover(baseTime, runningTime, incrementor, secondsIter, minutesIter, hoursIter, daysIter); + + // + // Month + // + + //Per expression filter advance (if necessary) to next month + runningTime.Month = monthsIter.Next(runningTime.Month); + + AdjustForMonthInvalidOrRollover(baseTime, isNext, runningTime, incrementor, secondsIter, minutesIter, hoursIter, invalidDayOfMonth, daysIter, monthsIter); + + //Reset variable for this iteration and assume valid day until tested down below. + invalidDayOfMonth = false; + + // + // Stop processing when year goes beyond the upper bound for the datetime or calendar + // object. Otherwise we would get an exception. + // + + var upperBoundYear = isNext ? Calendar.MaxSupportedDateTime.Year : Calendar.MinSupportedDateTime.Year; + if (incrementor.After(runningTime.Year, upperBoundYear)) + return null; + + // + // The day field in a cron expression spans the entire range of days + // in a month, which is from 1 to 31. However, the number of days in + // a month tend to be variable depending on the month (and the year + // in case of February). So a check is needed here to see if the + // date is a border case. If the day happens to be beyond 28 + // (meaning that we're dealing with the suspicious range of 29-31) + // and the date part has changed then we need to determine whether + // the day still makes sense for the given year and month. If the + // day is beyond the last possible value, then the day/month part + // for the schedule is re-evaluated. So an expression like "0 0 + // 15,31 * *" will yield the following sequence starting on midnight + // of Jan 1, 2000: + // + // Jan 15, Jan 31, Feb 15, Mar 15, Apr 15, Apr 31, ... + // + + var dateChanged = runningTime.Day != baseTime.Day || runningTime.Month != baseTime.Month || runningTime.Year != baseTime.Year; + + if (runningTime.Day > 28 && dateChanged && runningTime.Day > Calendar.GetDaysInMonth(runningTime.Year, runningTime.Month)) + { + if (incrementor.AfterOrEqual(runningTime.Year, limitTime.Year) && + incrementor.AfterOrEqual(runningTime.Month, limitTime.Month) && + incrementor.AfterOrEqual(runningTime.Day, limitTime.Day)) + return limitTime; + + if (isNext) + runningTime.Day = CrontabField.nil; + else + runningTime.Hour = CrontabField.nil; + + invalidDayOfMonth = true; + } + + } while (invalidDayOfMonth); + + var resultantTime = new DateTime(runningTime.Year, runningTime.Month, runningTime.Day, runningTime.Hour, runningTime.Minute, runningTime.Second, 0, baseTime.Kind); + + if (incrementor.AfterOrEqual(resultantTime, limitTime)) + return limitTime; // - // Day + // Day of week // - day = _days.Next(day); + if (_daysOfWeek.Contains((int)resultantTime.DayOfWeek)) + return resultantTime; - RetryDayMonth: + var resultantUpperBoundMinuteOfDay = new DateTime(runningTime.Year, runningTime.Month, runningTime.Day, isNext ? 23 : 0, isNext ? 59 : 0, isNext ? 59 : 0, 0, baseTime.Kind); + return TryGetOccurrence(resultantUpperBoundMinuteOfDay, limitTime, isNext); + } - if (day == nil) + private static void AdjustForMonthInvalidOrRollover(DateTime baseTime, bool isNext, DateTimeComponents runningTime, Incrementor incrementor, CrontabField.Iterator secondsIter, CrontabField.Iterator minutesIter, CrontabField.Iterator hoursIter, bool invalidDayOfMonth, CrontabField.Iterator daysIter, CrontabField.Iterator monthsIter) + { + if (runningTime.Month == CrontabField.nil) { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day = _days.GetFirst(); - month++; + runningTime.Second = secondsIter.LowerBound; + runningTime.Minute = minutesIter.LowerBound; + runningTime.Hour = hoursIter.LowerBound; + runningTime.Day = daysIter.LowerBound; + runningTime.Month = monthsIter.LowerBound; + runningTime.Year = incrementor.Increment(runningTime.Year); } - else if (day > baseDay) + else if (incrementor.After(runningTime.Month, baseTime.Month)) { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - } + runningTime.Second = secondsIter.LowerBound; + runningTime.Minute = minutesIter.LowerBound; + runningTime.Hour = hoursIter.LowerBound; - // - // Month - // - - month = _months.Next(month); + if (isNext || !invalidDayOfMonth) + runningTime.Day = daysIter.LowerBound; + } + } - if (month == nil) + private static void AdjustForDayInvalidOrRollover(DateTime baseTime, DateTimeComponents runningTime, Incrementor incrementor, CrontabField.Iterator secondsIter, CrontabField.Iterator minutesIter, CrontabField.Iterator hoursIter, CrontabField.Iterator daysIter) + { + if (runningTime.Day == CrontabField.nil) { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day = _days.GetFirst(); - month = _months.GetFirst(); - year++; + runningTime.Second = secondsIter.LowerBound; + runningTime.Minute = minutesIter.LowerBound; + runningTime.Hour = hoursIter.LowerBound; + runningTime.Day = daysIter.LowerBound; + runningTime.Month = incrementor.Increment(runningTime.Month); } - else if (month > baseMonth) + else if (incrementor.After(runningTime.Day, baseTime.Day)) { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day = _days.GetFirst(); + runningTime.Second = secondsIter.LowerBound; + runningTime.Minute = minutesIter.LowerBound; + runningTime.Hour = hoursIter.LowerBound; } + } - // - // Stop processing when year is too large for the datetime or calendar - // object. Otherwise we would get an exception. - // - - if (year > Calendar.MaxSupportedDateTime.Year) - return null; - - // - // The day field in a cron expression spans the entire range of days - // in a month, which is from 1 to 31. However, the number of days in - // a month tend to be variable depending on the month (and the year - // in case of February). So a check is needed here to see if the - // date is a border case. If the day happens to be beyond 28 - // (meaning that we're dealing with the suspicious range of 29-31) - // and the date part has changed then we need to determine whether - // the day still makes sense for the given year and month. If the - // day is beyond the last possible value, then the day/month part - // for the schedule is re-evaluated. So an expression like "0 0 - // 15,31 * *" will yield the following sequence starting on midnight - // of Jan 1, 2000: - // - // Jan 15, Jan 31, Feb 15, Mar 15, Apr 15, Apr 31, ... - // - - var dateChanged = day != baseDay || month != baseMonth || year != baseYear; - - if (day > 28 && dateChanged && day > Calendar.GetDaysInMonth(year, month)) + private static void AdjustForHourInvalidOrRollover(DateTime baseTime, DateTimeComponents runningTime, Incrementor incrementor, CrontabField.Iterator secondsIter, CrontabField.Iterator minutesIter, CrontabField.Iterator hoursIter) + { + if (runningTime.Hour == CrontabField.nil) { - if (year >= endYear && month >= endMonth && day >= endDay) - return endTime; - - day = nil; - goto RetryDayMonth; + runningTime.Minute = minutesIter.LowerBound; + runningTime.Hour = hoursIter.LowerBound; + runningTime.Day = incrementor.Increment(runningTime.Day); } + else if (incrementor.After(runningTime.Hour, baseTime.Hour)) + { + runningTime.Second = secondsIter.LowerBound; + runningTime.Minute = minutesIter.LowerBound; + } + } - var nextTime = new DateTime(year, month, day, hour, minute, second, 0, baseTime.Kind); - - if (nextTime >= endTime) - return endTime; - - // - // Day of week - // - - if (_daysOfWeek.Contains((int)nextTime.DayOfWeek)) - return nextTime; + private static void AdjustForMinuteInvalidOrRollover(DateTime baseTime, DateTimeComponents runningTime, Incrementor incrementor, CrontabField.Iterator secondsIter, CrontabField.Iterator minutesIter) + { + if (runningTime.Minute == CrontabField.nil) + { + runningTime.Second = secondsIter.LowerBound; + runningTime.Minute = minutesIter.LowerBound; + runningTime.Hour = incrementor.Increment(runningTime.Hour); + } + else if (incrementor.After(runningTime.Minute, baseTime.Minute)) + { + runningTime.Second = secondsIter.LowerBound; + } + } - return TryGetNextOccurrence(new DateTime(year, month, day, 23, 59, 59, 0, baseTime.Kind), endTime); + private static void AdjustForSecondInvalid(DateTimeComponents runningTime, Incrementor incrementor, CrontabField.Iterator secondsIter) + { + if (runningTime.Second == CrontabField.nil) + { + runningTime.Second = secondsIter.LowerBound; + runningTime.Minute = incrementor.Increment(runningTime.Minute); + } } /// /// Returns a string in crontab expression (expanded) that represents /// this schedule. /// - public override string ToString() { using var writer = new StringWriter(CultureInfo.InvariantCulture); diff --git a/NCrontab/ICrontabField.cs b/NCrontab/ICrontabField.cs index 0693c4c..3f3cc1e 100644 --- a/NCrontab/ICrontabField.cs +++ b/NCrontab/ICrontabField.cs @@ -25,6 +25,10 @@ public interface ICrontabField int GetFirst(); #pragma warning disable CA1716 // Identifiers should not match keywords (by design) int Next(int start); +#pragma warning restore CA1716 // Identifiers should not match keywords + int GetLast(); +#pragma warning disable CA1716 // Identifiers should not match keywords (by design) + int Prev(int start); #pragma warning restore CA1716 // Identifiers should not match keywords bool Contains(int value); } diff --git a/NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt b/NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt index 2f3239a..7fe3589 100644 --- a/NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt +++ b/NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt @@ -9,7 +9,9 @@ NCrontab.CrontabField.Contains(int value) -> bool NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void NCrontab.CrontabField.GetFirst() -> int +NCrontab.CrontabField.GetLast() -> int NCrontab.CrontabField.Next(int start) -> int +NCrontab.CrontabField.Prev(int start) -> int NCrontab.CrontabField.ToString(string? format) -> string! NCrontab.CrontabFieldKind NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind @@ -22,6 +24,9 @@ NCrontab.CrontabSchedule NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime) -> System.DateTime +NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime, System.DateTime startTime) -> System.DateTime +NCrontab.CrontabSchedule.GetPrevOccurrences(System.DateTime baseTime, System.DateTime startTime) -> System.Collections.Generic.IEnumerable! NCrontab.CrontabSchedule.ParseOptions NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void @@ -30,7 +35,9 @@ NCrontab.ExceptionProvider NCrontab.ICrontabField NCrontab.ICrontabField.Contains(int value) -> bool NCrontab.ICrontabField.GetFirst() -> int +NCrontab.ICrontabField.GetLast() -> int NCrontab.ICrontabField.Next(int start) -> int +NCrontab.ICrontabField.Prev(int start) -> int override NCrontab.CrontabField.ToString() -> string! override NCrontab.CrontabSchedule.ToString() -> string! static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField! diff --git a/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt b/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt index 2997735..5bbd003 100644 --- a/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt +++ b/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt @@ -8,7 +8,9 @@ NCrontab.CrontabField.Contains(int value) -> bool NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void NCrontab.CrontabField.GetFirst() -> int +NCrontab.CrontabField.GetLast() -> int NCrontab.CrontabField.Next(int start) -> int +NCrontab.CrontabField.Prev(int start) -> int NCrontab.CrontabField.ToString(string? format) -> string! NCrontab.CrontabFieldKind NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind @@ -21,6 +23,9 @@ NCrontab.CrontabSchedule NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime) -> System.DateTime +NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime, System.DateTime startTime) -> System.DateTime +NCrontab.CrontabSchedule.GetPrevOccurrences(System.DateTime baseTime, System.DateTime startTime) -> System.Collections.Generic.IEnumerable! NCrontab.CrontabSchedule.ParseOptions NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void @@ -29,7 +34,9 @@ NCrontab.ExceptionProvider NCrontab.ICrontabField NCrontab.ICrontabField.Contains(int value) -> bool NCrontab.ICrontabField.GetFirst() -> int +NCrontab.ICrontabField.GetLast() -> int NCrontab.ICrontabField.Next(int start) -> int +NCrontab.ICrontabField.Prev(int start) -> int override NCrontab.CrontabField.ToString() -> string! override NCrontab.CrontabSchedule.ToString() -> string! static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField! diff --git a/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt index 2997735..5bbd003 100644 --- a/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt +++ b/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -8,7 +8,9 @@ NCrontab.CrontabField.Contains(int value) -> bool NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void NCrontab.CrontabField.GetFirst() -> int +NCrontab.CrontabField.GetLast() -> int NCrontab.CrontabField.Next(int start) -> int +NCrontab.CrontabField.Prev(int start) -> int NCrontab.CrontabField.ToString(string? format) -> string! NCrontab.CrontabFieldKind NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind @@ -21,6 +23,9 @@ NCrontab.CrontabSchedule NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime) -> System.DateTime +NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime, System.DateTime startTime) -> System.DateTime +NCrontab.CrontabSchedule.GetPrevOccurrences(System.DateTime baseTime, System.DateTime startTime) -> System.Collections.Generic.IEnumerable! NCrontab.CrontabSchedule.ParseOptions NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void @@ -29,7 +34,9 @@ NCrontab.ExceptionProvider NCrontab.ICrontabField NCrontab.ICrontabField.Contains(int value) -> bool NCrontab.ICrontabField.GetFirst() -> int +NCrontab.ICrontabField.GetLast() -> int NCrontab.ICrontabField.Next(int start) -> int +NCrontab.ICrontabField.Prev(int start) -> int override NCrontab.CrontabField.ToString() -> string! override NCrontab.CrontabSchedule.ToString() -> string! static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField! diff --git a/NCrontab/Utils/DateTimeComponents.cs b/NCrontab/Utils/DateTimeComponents.cs new file mode 100644 index 0000000..3c26774 --- /dev/null +++ b/NCrontab/Utils/DateTimeComponents.cs @@ -0,0 +1,29 @@ +using System; + +namespace NCrontab.Utils +{ + /// + /// Used when heavy manipulation of the individual elements of a DateTime are needed. These elements may even + /// represent an invalid DateTime during or after manipulation. Resolution is only down to the second. + /// + internal sealed class DateTimeComponents + { + public DateTimeComponents(DateTime dateTime) + { + Year = dateTime.Year; + Month = dateTime.Month; + Day = dateTime.Day; + Hour = dateTime.Hour; + Minute = dateTime.Minute; + Second = dateTime.Second; + } + + public int Year { get; set; } + public int Month { get; set; } + public int Day { get; set; } + + public int Hour { get; set; } + public int Minute { get; set; } + public int Second { get; set; } + } +} diff --git a/NCrontab/Utils/Incrementor.cs b/NCrontab/Utils/Incrementor.cs new file mode 100644 index 0000000..fc356b6 --- /dev/null +++ b/NCrontab/Utils/Incrementor.cs @@ -0,0 +1,20 @@ +using System; + +namespace NCrontab.Utils +{ + internal sealed class Incrementor + { + readonly bool _forwardMoving; + + public Incrementor(bool forwardMoving) + { + _forwardMoving = forwardMoving; + } + + public bool BeforeOrEqual(int value, int limit) => _forwardMoving ? value <= limit : value >= limit; + public bool After(int value, int limit) => _forwardMoving ? value > limit : value < limit; + public bool AfterOrEqual(int value, int limit) => _forwardMoving ? value >= limit : value <= limit; + public bool AfterOrEqual(DateTime value, DateTime limit) => _forwardMoving ? value >= limit : value <= limit; + public int Increment(int value) => _forwardMoving ? value + 1 : value - 1; + } +}