Files
ezra-environment/home/venv/lib/python3.12/site-packages/croniter/tests/test_croniter.py
2026-04-03 22:42:06 +00:00

3178 lines
111 KiB
Python
Executable File

#!/usr/bin/env python
import unittest
import zoneinfo
from datetime import datetime, timedelta
from functools import partial
from time import sleep
import dateutil.tz
import pytz
from croniter import (
CroniterBadCronError,
CroniterBadDateError,
CroniterNotAlphaError,
CroniterUnsupportedSyntaxError,
croniter,
datetime_to_timestamp,
)
from croniter.croniter import VALID_LEN_EXPRESSION
from croniter.tests import base
class CroniterTest(base.TestCase):
def test_second_sec(self):
base = datetime(2012, 4, 6, 13, 26, 10)
itr = croniter("* * * * * 15,25", base)
n = itr.get_next(datetime)
self.assertEqual(15, n.second)
n = itr.get_next(datetime)
self.assertEqual(25, n.second)
n = itr.get_next(datetime)
self.assertEqual(15, n.second)
self.assertEqual(27, n.minute)
def test_second(self):
base = datetime(2012, 4, 6, 13, 26, 10)
itr = croniter("*/1 * * * * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(base.year, n1.year)
self.assertEqual(base.month, n1.month)
self.assertEqual(base.day, n1.day)
self.assertEqual(base.hour, n1.hour)
self.assertEqual(base.minute, n1.minute)
self.assertEqual(base.second + 1, n1.second)
def test_second_repeat(self):
base = datetime(2012, 4, 6, 13, 26, 36)
itr = croniter("* * * * * */15", base)
n1 = itr.get_next(datetime)
n2 = itr.get_next(datetime)
n3 = itr.get_next(datetime)
self.assertEqual(base.year, n1.year)
self.assertEqual(base.month, n1.month)
self.assertEqual(base.day, n1.day)
self.assertEqual(base.hour, n1.hour)
self.assertEqual(base.minute, n1.minute)
self.assertEqual(45, n1.second)
self.assertEqual(base.year, n2.year)
self.assertEqual(base.month, n2.month)
self.assertEqual(base.day, n2.day)
self.assertEqual(base.hour, n2.hour)
self.assertEqual(base.minute + 1, n2.minute)
self.assertEqual(0, n2.second)
self.assertEqual(base.year, n3.year)
self.assertEqual(base.month, n3.month)
self.assertEqual(base.day, n3.day)
self.assertEqual(base.hour, n3.hour)
self.assertEqual(base.minute + 1, n3.minute)
self.assertEqual(15, n3.second)
def test_minute(self):
# minute asterisk
base = datetime(2010, 1, 23, 12, 18)
itr = croniter("*/1 * * * *", base)
n1 = itr.get_next(datetime) # 19
self.assertEqual(base.year, n1.year)
self.assertEqual(base.month, n1.month)
self.assertEqual(base.day, n1.day)
self.assertEqual(base.hour, n1.hour)
self.assertEqual(base.minute, n1.minute - 1)
for i in range(39): # ~ 58
itr.get_next()
n2 = itr.get_next(datetime)
self.assertEqual(n2.minute, 59)
n3 = itr.get_next(datetime)
self.assertEqual(n3.minute, 0)
self.assertEqual(n3.hour, 13)
itr = croniter("*/5 * * * *", base)
n4 = itr.get_next(datetime)
self.assertEqual(n4.minute, 20)
for i in range(6):
itr.get_next()
n5 = itr.get_next(datetime)
self.assertEqual(n5.minute, 55)
n6 = itr.get_next(datetime)
self.assertEqual(n6.minute, 0)
self.assertEqual(n6.hour, 13)
def test_hour(self):
base = datetime(2010, 1, 24, 12, 2)
itr = croniter("0 */3 * * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.hour, 15)
self.assertEqual(n1.minute, 0)
for i in range(2):
itr.get_next()
n2 = itr.get_next(datetime)
self.assertEqual(n2.hour, 0)
self.assertEqual(n2.day, 25)
def test_day(self):
base = datetime(2010, 2, 24, 12, 9)
itr = croniter("0 0 */3 * *", base)
n1 = itr.get_next(datetime)
# 1 4 7 10 13 16 19 22 25 28
self.assertEqual(n1.day, 25)
n2 = itr.get_next(datetime)
self.assertEqual(n2.day, 28)
n3 = itr.get_next(datetime)
self.assertEqual(n3.day, 1)
self.assertEqual(n3.month, 3)
# test leap year
base = datetime(1996, 2, 27)
itr = croniter("0 0 * * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.day, 28)
self.assertEqual(n1.month, 2)
n2 = itr.get_next(datetime)
self.assertEqual(n2.day, 29)
self.assertEqual(n2.month, 2)
base2 = datetime(2000, 2, 27)
itr2 = croniter("0 0 * * *", base2)
n3 = itr2.get_next(datetime)
self.assertEqual(n3.day, 28)
self.assertEqual(n3.month, 2)
n4 = itr2.get_next(datetime)
self.assertEqual(n4.day, 29)
self.assertEqual(n4.month, 2)
def test_day2(self):
base3 = datetime(2024, 2, 28)
itr2 = croniter("* * 29 2 *", base3)
n3 = itr2.get_prev(datetime)
self.assertEqual(n3.year, 2020)
self.assertEqual(n3.month, 2)
self.assertEqual(n3.day, 29)
def test_weekday(self):
base = datetime(2010, 2, 25)
itr = croniter("0 0 * * sat", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.isoweekday(), 6)
self.assertEqual(n1.day, 27)
n2 = itr.get_next(datetime)
self.assertEqual(n2.isoweekday(), 6)
self.assertEqual(n2.day, 6)
self.assertEqual(n2.month, 3)
base = datetime(2010, 1, 25)
itr = croniter("0 0 1 * wed", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.month, 1)
self.assertEqual(n1.day, 27)
self.assertEqual(n1.year, 2010)
n2 = itr.get_next(datetime)
self.assertEqual(n2.month, 2)
self.assertEqual(n2.day, 1)
self.assertEqual(n2.year, 2010)
n3 = itr.get_next(datetime)
self.assertEqual(n3.month, 2)
self.assertEqual(n3.day, 3)
self.assertEqual(n3.year, 2010)
def test_nth_weekday(self):
base = datetime(2010, 2, 25)
itr = croniter("0 0 * * sat#1", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.isoweekday(), 6)
self.assertEqual(n1.day, 6)
self.assertEqual(n1.month, 3)
n2 = itr.get_next(datetime)
self.assertEqual(n2.isoweekday(), 6)
self.assertEqual(n2.day, 3)
self.assertEqual(n2.month, 4)
base = datetime(2010, 1, 25)
itr = croniter("0 0 * * wed#5", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.month, 3)
self.assertEqual(n1.day, 31)
self.assertEqual(n1.year, 2010)
n2 = itr.get_next(datetime)
self.assertEqual(n2.month, 6)
self.assertEqual(n2.day, 30)
self.assertEqual(n2.year, 2010)
n3 = itr.get_next(datetime)
self.assertEqual(n3.month, 9)
self.assertEqual(n3.day, 29)
self.assertEqual(n3.year, 2010)
def test_weekday_day_and(self):
base = datetime(2010, 1, 25)
itr = croniter("0 0 1 * mon", base, day_or=False)
n1 = itr.get_next(datetime)
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 1)
self.assertEqual(n1.year, 2010)
n2 = itr.get_next(datetime)
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, 1)
self.assertEqual(n2.year, 2010)
n3 = itr.get_next(datetime)
self.assertEqual(n3.month, 11)
self.assertEqual(n3.day, 1)
self.assertEqual(n3.year, 2010)
def test_dom_dow_vixie_cron_bug(self):
expr = "0 16 */2 * sat"
# UNION OF "every odd-numbered day" and "every Saturday"
itr = croniter(expr, start_time=datetime(2023, 5, 2), ret_type=datetime)
self.assertEqual(itr.get_next(), datetime(2023, 5, 3, 16, 0, 0)) # Wed May 3 2023
self.assertEqual(itr.get_next(), datetime(2023, 5, 5, 16, 0, 0)) # Fri May 5 2023
self.assertEqual(itr.get_next(), datetime(2023, 5, 6, 16, 0, 0)) # Sat May 6 2023
self.assertEqual(itr.get_next(), datetime(2023, 5, 7, 16, 0, 0)) # Sun May 7 2023
# INTERSECTION OF "every odd-numbered day" and "every Saturday"
itr = croniter(
expr, start_time=datetime(2023, 5, 2), ret_type=datetime, implement_cron_bug=True
)
self.assertEqual(itr.get_next(), datetime(2023, 5, 13, 16, 0, 0)) # Sat May 13 2023
self.assertEqual(itr.get_next(), datetime(2023, 5, 27, 16, 0, 0)) # Sat May 27 2023
self.assertEqual(itr.get_next(), datetime(2023, 6, 3, 16, 0, 0)) # Sat June 3 2023
self.assertEqual(itr.get_next(), datetime(2023, 6, 17, 16, 0, 0)) # Sun June 17 2023
def test_month(self):
base = datetime(2010, 1, 25)
itr = croniter("0 0 1 * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 1)
n2 = itr.get_next(datetime)
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, 1)
for i in range(8):
itr.get_next()
n3 = itr.get_next(datetime)
self.assertEqual(n3.month, 12)
self.assertEqual(n3.year, 2010)
n4 = itr.get_next(datetime)
self.assertEqual(n4.month, 1)
self.assertEqual(n4.year, 2011)
def test_last_day_of_month(self):
base = datetime(2015, 9, 4)
itr = croniter("0 0 l * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.month, 9)
self.assertEqual(n1.day, 30)
n2 = itr.get_next(datetime)
self.assertEqual(n2.month, 10)
self.assertEqual(n2.day, 31)
n3 = itr.get_next(datetime)
self.assertEqual(n3.month, 11)
self.assertEqual(n3.day, 30)
n4 = itr.get_next(datetime)
self.assertEqual(n4.month, 12)
self.assertEqual(n4.day, 31)
def test_range_with_uppercase_last_day_of_month(self):
base = datetime(2015, 9, 4)
itr = croniter("0 0 29-L * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.month, 9)
self.assertEqual(n1.day, 29)
n2 = itr.get_next(datetime)
self.assertEqual(n2.month, 9)
self.assertEqual(n2.day, 30)
def test_prev_last_day_of_month(self):
base = datetime(2009, 12, 31, hour=20)
itr = croniter("0 0 l * *", base)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 12)
self.assertEqual(n1.day, 31)
base = datetime(2009, 12, 31)
itr = croniter("0 0 l * *", base)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 11)
self.assertEqual(n1.day, 30)
base = datetime(2010, 1, 5)
itr = croniter("0 0 l * *", base)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 12)
self.assertEqual(n1.day, 31)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 11)
self.assertEqual(n1.day, 30)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 10)
self.assertEqual(n1.day, 31)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 9)
self.assertEqual(n1.day, 30)
base = datetime(2010, 1, 31, minute=2)
itr = croniter("* * l * *", base)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 1)
self.assertEqual(n1.day, 31)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 1)
self.assertEqual(n1.day, 31)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 12)
self.assertEqual(n1.day, 31)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.month, 12)
self.assertEqual(n1.day, 31)
def test_error(self):
itr = croniter("* * * * *")
self.assertRaises(TypeError, itr.get_next, str)
self.assertRaises(ValueError, croniter, "* * * *")
self.assertRaises(ValueError, croniter, "-90 * * * *")
self.assertRaises(ValueError, croniter, "a * * * *")
self.assertRaises(ValueError, croniter, "* * * janu-jun *")
self.assertRaises(ValueError, croniter, "1-1_0 * * * *")
self.assertRaises(ValueError, croniter, "0-10/error * * * *")
self.assertRaises(ValueError, croniter, "0-10/ * * * *")
self.assertRaises(CroniterBadCronError, croniter, "0-1& * * * *", datetime.now())
self.assertRaises(ValueError, croniter, "* * 5-100 * *")
def test_sunday_to_thursday_with_alpha_conversion(self):
base = datetime(2010, 8, 25, 15, 56) # wednesday
itr = croniter("30 22 * * sun-thu", base)
next = itr.get_next(datetime)
self.assertEqual(base.year, next.year)
self.assertEqual(base.month, next.month)
self.assertEqual(base.day, next.day)
self.assertEqual(22, next.hour)
self.assertEqual(30, next.minute)
def test_optimize_cron_expressions(self):
"""Non-optimal cron expressions that can be simplified."""
wildcard = ["*"]
m, h, d, mon, dow, s = range(6)
# Test each field individually
self.assertEqual(croniter("0-59 0 1 1 0").expanded[m], wildcard)
self.assertEqual(croniter("0 0-23 1 1 0").expanded[h], wildcard)
self.assertEqual(croniter("0 0 1-31 1 0").expanded[dow], [0])
self.assertEqual(croniter("0 0 1-31 1 *").expanded[d], wildcard)
self.assertEqual(croniter("0 0 1 1-12 0").expanded[mon], wildcard)
self.assertEqual(croniter("0 0 1 1 0-6").expanded[dow], [0, 1, 2, 3, 4, 5, 6])
self.assertEqual(croniter("0 0 * 1 0-6").expanded[dow], wildcard)
self.assertEqual(croniter("0 0 1 1 0-6").expanded[dow], [0, 1, 2, 3, 4, 5, 6])
self.assertEqual(croniter("0 0 1 1 0-6,sat#3").expanded[dow], [0, 1, 2, 3, 4, 5, 6])
self.assertEqual(croniter("0 0 * 1 0-6").expanded[dow], wildcard)
self.assertEqual(croniter("0 0 * 1 0-6,sat#3").expanded[dow], wildcard)
self.assertEqual(croniter("0 0 1 1 0 0-59").expanded[s], wildcard)
# Real life examples
self.assertEqual(croniter("30 1-12,0,10-23 15-21 * fri").expanded[h], wildcard)
self.assertEqual(croniter("30 1-23,0 15-21 * fri").expanded[h], wildcard)
def test_block_dup_ranges(self):
"""Ensure that duplicate/overlapping ranges are squashed"""
m, h, d, mon, dow, s = range(6)
self.assertEqual(croniter("* 5,5,1-6 * * *").expanded[h], [1, 2, 3, 4, 5, 6])
self.assertEqual(croniter("* * * * 2-3,4-5,3,3,3").expanded[dow], [2, 3, 4, 5])
self.assertEqual(croniter("* * * * * 1,5,*/20,20,15").expanded[s], [0, 1, 5, 15, 20, 40])
self.assertEqual(croniter("* 4,1-4,5,4 * * *").expanded[h], [1, 2, 3, 4, 5])
# Real life example
self.assertEqual(
croniter("59 23 * 1 wed,fri,mon-thu,tue,tue").expanded[dow], [1, 2, 3, 4, 5]
)
def test_prev_minute(self):
base = datetime(2010, 8, 25, 15, 56)
itr = croniter("*/1 * * * *", base)
prev = itr.get_prev(datetime)
self.assertEqual(base.year, prev.year)
self.assertEqual(base.month, prev.month)
self.assertEqual(base.day, prev.day)
self.assertEqual(base.hour, prev.hour)
self.assertEqual(base.minute, prev.minute + 1)
base = datetime(2010, 8, 25, 15, 0)
itr = croniter("*/1 * * * *", base)
prev = itr.get_prev(datetime)
self.assertEqual(base.year, prev.year)
self.assertEqual(base.month, prev.month)
self.assertEqual(base.day, prev.day)
self.assertEqual(base.hour, prev.hour + 1)
self.assertEqual(59, prev.minute)
base = datetime(2010, 8, 25, 0, 0)
itr = croniter("*/1 * * * *", base)
prev = itr.get_prev(datetime)
self.assertEqual(base.year, prev.year)
self.assertEqual(base.month, prev.month)
self.assertEqual(base.day, prev.day + 1)
self.assertEqual(23, prev.hour)
self.assertEqual(59, prev.minute)
def test_prev_day_of_month_with_crossing(self):
"""
Test getting previous occurrence that crosses into previous month.
"""
base = datetime(2012, 3, 15, 0, 0)
itr = croniter("0 0 22 * *", base)
prev = itr.get_prev(datetime)
self.assertEqual(prev.year, 2012)
self.assertEqual(prev.month, 2)
self.assertEqual(prev.day, 22)
self.assertEqual(prev.hour, 0)
self.assertEqual(prev.minute, 0)
def test_prev_weekday(self):
base = datetime(2010, 8, 25, 15, 56)
itr = croniter("0 0 * * sat,sun", base)
prev1 = itr.get_prev(datetime)
self.assertEqual(prev1.year, base.year)
self.assertEqual(prev1.month, base.month)
self.assertEqual(prev1.day, 22)
self.assertEqual(prev1.hour, 0)
self.assertEqual(prev1.minute, 0)
prev2 = itr.get_prev(datetime)
self.assertEqual(prev2.year, base.year)
self.assertEqual(prev2.month, base.month)
self.assertEqual(prev2.day, 21)
self.assertEqual(prev2.hour, 0)
self.assertEqual(prev2.minute, 0)
prev3 = itr.get_prev(datetime)
self.assertEqual(prev3.year, base.year)
self.assertEqual(prev3.month, base.month)
self.assertEqual(prev3.day, 15)
self.assertEqual(prev3.hour, 0)
self.assertEqual(prev3.minute, 0)
def test_prev_nth_weekday(self):
base = datetime(2010, 8, 25, 15, 56)
itr = croniter("0 0 * * sat#1,sun#2", base)
prev1 = itr.get_prev(datetime)
self.assertEqual(prev1.year, base.year)
self.assertEqual(prev1.month, base.month)
self.assertEqual(prev1.day, 8)
self.assertEqual(prev1.hour, 0)
self.assertEqual(prev1.minute, 0)
prev2 = itr.get_prev(datetime)
self.assertEqual(prev2.year, base.year)
self.assertEqual(prev2.month, base.month)
self.assertEqual(prev2.day, 7)
self.assertEqual(prev2.hour, 0)
self.assertEqual(prev2.minute, 0)
prev3 = itr.get_prev(datetime)
self.assertEqual(prev3.year, base.year)
self.assertEqual(prev3.month, 7)
self.assertEqual(prev3.day, 11)
self.assertEqual(prev3.hour, 0)
self.assertEqual(prev3.minute, 0)
def test_prev_weekday2(self):
base = datetime(2010, 8, 25, 15, 56)
itr = croniter("10 0 * * 0", base)
prev = itr.get_prev(datetime)
self.assertEqual(prev.day, 22)
self.assertEqual(prev.hour, 0)
self.assertEqual(prev.minute, 10)
def test_iso_weekday(self):
base = datetime(2010, 2, 25)
itr = croniter("0 0 * * 6", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.isoweekday(), 6)
self.assertEqual(n1.day, 27)
n2 = itr.get_next(datetime)
self.assertEqual(n2.isoweekday(), 6)
self.assertEqual(n2.day, 6)
self.assertEqual(n2.month, 3)
def test_bug1(self):
base = datetime(2012, 2, 24)
itr = croniter("5 0 */2 * *", base)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 5)
self.assertEqual(n1.month, 2)
# month starts from 1, 3 .... then 21, 23
# so correct is not 22 but 23
self.assertEqual(n1.day, 23)
def test_bug2(self):
base = datetime(2012, 1, 1, 0, 0)
iter = croniter("0 * * 3 *", base)
n1 = iter.get_next(datetime)
self.assertEqual(n1.year, base.year)
self.assertEqual(n1.month, 3)
self.assertEqual(n1.day, base.day)
self.assertEqual(n1.hour, base.hour)
self.assertEqual(n1.minute, base.minute)
n2 = iter.get_next(datetime)
self.assertEqual(n2.year, base.year)
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, base.day)
self.assertEqual(n2.hour, base.hour + 1)
self.assertEqual(n2.minute, base.minute)
n3 = iter.get_next(datetime)
self.assertEqual(n3.year, base.year)
self.assertEqual(n3.month, 3)
self.assertEqual(n3.day, base.day)
self.assertEqual(n3.hour, base.hour + 2)
self.assertEqual(n3.minute, base.minute)
def test_bug3(self):
base = datetime(2013, 3, 1, 12, 17, 34, 257877)
c = croniter("00 03 16,30 * *", base)
n1 = c.get_next(datetime)
self.assertEqual(n1.month, 3)
self.assertEqual(n1.day, 16)
n2 = c.get_next(datetime)
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, 30)
n3 = c.get_next(datetime)
self.assertEqual(n3.month, 4)
self.assertEqual(n3.day, 16)
n4 = c.get_prev(datetime)
self.assertEqual(n4.month, 3)
self.assertEqual(n4.day, 30)
n5 = c.get_prev(datetime)
self.assertEqual(n5.month, 3)
self.assertEqual(n5.day, 16)
n6 = c.get_prev(datetime)
self.assertEqual(n6.month, 2)
self.assertEqual(n6.day, 16)
def test_bug34(self):
base = datetime(2012, 2, 24, 0, 0, 0)
itr = croniter("* * 31 2 *", base)
try:
itr.get_next(datetime)
except (CroniterBadDateError,) as ex:
self.assertEqual(f"{ex}", "failed to find next date")
def test_bug57(self):
base = datetime(2012, 2, 24, 0, 0, 0)
itr = croniter("0 4/6 * * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.hour, 4)
self.assertEqual(n1.minute, 0)
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 24)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.hour, 22)
self.assertEqual(n1.minute, 0)
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 23)
itr = croniter("0 0/6 * * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.hour, 6)
self.assertEqual(n1.minute, 0)
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 24)
n1 = itr.get_prev(datetime)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 0)
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 24)
def test_multiple_months(self):
base = datetime(2016, 3, 1, 0, 0, 0)
itr = croniter("0 0 1 3,6,9,12 *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.month, 6)
self.assertEqual(n1.day, 1)
self.assertEqual(n1.year, 2016)
base = datetime(2016, 2, 15, 0, 0, 0)
itr = croniter("0 0 1 3,6,9,12 *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.month, 3)
self.assertEqual(n1.day, 1)
self.assertEqual(n1.year, 2016)
base = datetime(2016, 12, 3, 10, 0, 0)
itr = croniter("0 0 1 3,6,9,12 *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.month, 3)
self.assertEqual(n1.day, 1)
self.assertEqual(n1.year, 2017)
# The result with this parameters was incorrect.
# self.assertEqual(p1.month, 12
# AssertionError: 9 != 12
base = datetime(2016, 3, 1, 0, 0, 0)
itr = croniter("0 0 1 3,6,9,12 *", base)
p1 = itr.get_prev(datetime)
self.assertEqual(p1.hour, 0)
self.assertEqual(p1.month, 12)
self.assertEqual(p1.day, 1)
self.assertEqual(p1.year, 2015)
# check my change resolves another hidden bug.
base = datetime(2016, 2, 1, 0, 0, 0)
itr = croniter("0 0 1,15,31 * *", base)
p1 = itr.get_prev(datetime)
self.assertEqual(p1.hour, 0)
self.assertEqual(p1.month, 1)
self.assertEqual(p1.day, 31)
self.assertEqual(p1.year, 2016)
base = datetime(2016, 6, 1, 0, 0, 0)
itr = croniter("0 0 1 3,6,9,12 *", base)
p1 = itr.get_prev(datetime)
self.assertEqual(p1.hour, 0)
self.assertEqual(p1.month, 3)
self.assertEqual(p1.day, 1)
self.assertEqual(p1.year, 2016)
base = datetime(2016, 3, 1, 0, 0, 0)
itr = croniter("0 0 1 1,3,6,9,12 *", base)
p1 = itr.get_prev(datetime)
self.assertEqual(p1.hour, 0)
self.assertEqual(p1.month, 1)
self.assertEqual(p1.day, 1)
self.assertEqual(p1.year, 2016)
base = datetime(2016, 3, 1, 0, 0, 0)
itr = croniter("0 0 1 1,3,6,9,12 *", base)
p1 = itr.get_prev(datetime)
self.assertEqual(p1.hour, 0)
self.assertEqual(p1.month, 1)
self.assertEqual(p1.day, 1)
self.assertEqual(p1.year, 2016)
def test_range_generator(self):
base = datetime(2013, 3, 4, 0, 0)
itr = croniter("1-9/2 0 1 * *", base)
n1 = itr.get_next(datetime)
n2 = itr.get_next(datetime)
n3 = itr.get_next(datetime)
n4 = itr.get_next(datetime)
n5 = itr.get_next(datetime)
self.assertEqual(n1.minute, 1)
self.assertEqual(n2.minute, 3)
self.assertEqual(n3.minute, 5)
self.assertEqual(n4.minute, 7)
self.assertEqual(n5.minute, 9)
def test_previous_hour(self):
base = datetime(2012, 6, 23, 17, 41)
itr = croniter("* 10 * * *", base)
prev1 = itr.get_prev(datetime)
self.assertEqual(prev1.year, base.year)
self.assertEqual(prev1.month, base.month)
self.assertEqual(prev1.day, base.day)
self.assertEqual(prev1.hour, 10)
self.assertEqual(prev1.minute, 59)
def test_previous_day(self):
base = datetime(2012, 6, 27, 0, 15)
itr = croniter("* * 26 * *", base)
prev1 = itr.get_prev(datetime)
self.assertEqual(prev1.year, base.year)
self.assertEqual(prev1.month, base.month)
self.assertEqual(prev1.day, 26)
self.assertEqual(prev1.hour, 23)
self.assertEqual(prev1.minute, 59)
def test_previous_month(self):
base = datetime(2012, 6, 18, 0, 15)
itr = croniter("* * * 5 *", base)
prev1 = itr.get_prev(datetime)
self.assertEqual(prev1.year, base.year)
self.assertEqual(prev1.month, 5)
self.assertEqual(prev1.day, 31)
self.assertEqual(prev1.hour, 23)
self.assertEqual(prev1.minute, 59)
def test_previous_dow(self):
base = datetime(2012, 5, 13, 18, 48)
itr = croniter("* * * * sat", base)
prev1 = itr.get_prev(datetime)
self.assertEqual(prev1.year, base.year)
self.assertEqual(prev1.month, base.month)
self.assertEqual(prev1.day, 12)
self.assertEqual(prev1.hour, 23)
self.assertEqual(prev1.minute, 59)
def test_get_current(self):
base = datetime(2012, 9, 25, 11, 24)
itr = croniter("* * * * *", base)
res = itr.get_current(datetime)
self.assertEqual(base.year, res.year)
self.assertEqual(base.month, res.month)
self.assertEqual(base.day, res.day)
self.assertEqual(base.hour, res.hour)
self.assertEqual(base.minute, res.minute)
def test_first_of_march(self):
"""Test not skipping first of March.
This fixes https://github.com/pallets-eco/croniter/issues/1
"""
it = croniter("0 0 */10 * *", datetime(2025, 2, 22))
self.assertEqual(it.get_next(datetime).isoformat(), "2025-03-01T00:00:00")
def test_timezone(self):
base = datetime(2013, 3, 4, 12, 15)
itr = croniter("* * * * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.tzinfo, None)
tokyo = zoneinfo.ZoneInfo("Asia/Tokyo")
start = datetime(2013, 3, 4, 12, 15, tzinfo=tokyo)
itr2 = croniter("* * * * *", start)
n2 = itr2.get_next(datetime)
self.assertEqual(n2.tzinfo.key, "Asia/Tokyo")
def test_timezone_pytz(self):
tokyo = pytz.timezone("Asia/Tokyo")
base = datetime(2013, 3, 4, 12, 15)
itr2 = croniter("* * * * *", tokyo.localize(base))
n2 = itr2.get_next(datetime)
self.assertEqual(n2.tzinfo.zone, "Asia/Tokyo")
def test_timezone_dateutil(self):
tokyo = dateutil.tz.gettz("Asia/Tokyo")
base = datetime(2013, 3, 4, 12, 15, tzinfo=tokyo)
itr = croniter("* * * * *", base)
n1 = itr.get_next(datetime)
self.assertEqual(n1.tzinfo.tzname(n1), "JST")
def test_init_no_start_time(self):
itr = croniter("* * * * *")
sleep(0.01)
itr2 = croniter("* * * * *")
# Greater does not exists in py26
self.assertTrue(itr2.cur > itr.cur)
def test_timezone_winter_time(self):
"""Test Athens jumps backwards: 2013-10-27 04:00 -> 03:00 (UTC+3 -> UTC+2)."""
tz = zoneinfo.ZoneInfo("Europe/Athens")
expected_schedule = [
"2013-10-27T02:30:00+03:00",
"2013-10-27T03:00:00+03:00",
"2013-10-27T03:30:00+03:00",
"2013-10-27T03:00:00+02:00",
"2013-10-27T03:30:00+02:00",
"2013-10-27T04:00:00+02:00",
"2013-10-27T04:30:00+02:00",
]
start = datetime(2013, 10, 27, 2, 0, 0, tzinfo=tz)
ct = croniter("*/30 * * * *", start)
schedule = [ct.get_next(datetime).isoformat() for _ in range(7)]
self.assertEqual(schedule, expected_schedule)
start = datetime(2013, 10, 27, 5, 0, 0, tzinfo=tz)
ct = croniter("*/30 * * * *", start)
schedule = [ct.get_prev(datetime).isoformat() for _ in range(7)]
self.assertEqual(schedule, list(reversed(expected_schedule)))
def test_timezone_winter_time_pytz(self):
"""Test Athens jumps backwards: 2013-10-27 04:00 -> 03:00 (UTC+3 -> UTC+2)."""
tz = pytz.timezone("Europe/Athens")
expected_schedule = [
"2013-10-27T02:30:00+03:00",
"2013-10-27T03:00:00+03:00",
"2013-10-27T03:30:00+03:00",
"2013-10-27T03:00:00+02:00",
"2013-10-27T03:30:00+02:00",
"2013-10-27T04:00:00+02:00",
"2013-10-27T04:30:00+02:00",
]
start = datetime(2013, 10, 27, 2, 0, 0)
ct = croniter("*/30 * * * *", tz.localize(start))
schedule = [ct.get_next(datetime).isoformat() for _ in range(7)]
self.assertEqual(schedule, expected_schedule)
start = datetime(2013, 10, 27, 5, 0, 0)
ct = croniter("*/30 * * * *", tz.localize(start))
schedule = [ct.get_prev(datetime).isoformat() for _ in range(7)]
self.assertEqual(schedule, list(reversed(expected_schedule)))
def test_timezone_summer_time(self):
"""Test Athens jumps forward: 2013-03-31 03:00 -> 04:00 (UTC+2 -> UTC+3)."""
tz = zoneinfo.ZoneInfo("Europe/Athens")
expected_schedule = [
"2013-03-31T01:30:00+02:00",
"2013-03-31T02:00:00+02:00",
"2013-03-31T02:30:00+02:00",
"2013-03-31T04:00:00+03:00",
"2013-03-31T04:30:00+03:00",
]
start = datetime(2013, 3, 31, 1, 0, 0, tzinfo=tz)
ct = croniter("*/30 * * * *", start)
schedule = [ct.get_next(datetime).isoformat() for _ in range(5)]
self.assertEqual(schedule, expected_schedule)
start = datetime(2013, 3, 31, 5, 0, 0, tzinfo=tz)
ct = croniter("*/30 * * * *", start)
schedule = [ct.get_prev(datetime).isoformat() for _ in range(5)]
self.assertEqual(schedule, list(reversed(expected_schedule)))
def test_timezone_summer_time_pytz(self):
"""Test Athens jumps forward: 2013-03-31 03:00 -> 04:00 (UTC+2 -> UTC+3)."""
tz = pytz.timezone("Europe/Athens")
expected_schedule = [
"2013-03-31T01:30:00+02:00",
"2013-03-31T02:00:00+02:00",
"2013-03-31T02:30:00+02:00",
"2013-03-31T04:00:00+03:00",
"2013-03-31T04:30:00+03:00",
]
start = datetime(2013, 3, 31, 1, 0, 0)
ct = croniter("*/30 * * * *", tz.localize(start))
schedule = [ct.get_next(datetime).isoformat() for _ in range(5)]
self.assertEqual(schedule, expected_schedule)
start = datetime(2013, 3, 31, 5, 0, 0)
ct = croniter("*/30 * * * *", tz.localize(start))
schedule = [ct.get_prev(datetime).isoformat() for _ in range(5)]
self.assertEqual(schedule, list(reversed(expected_schedule)))
def test_std_dst(self):
"""
DST tests
This fixes https://github.com/taichino/croniter/issues/82
"""
tz = zoneinfo.ZoneInfo("Europe/Warsaw")
# -> 2017-03-26 01:59+1:00 -> 03:00+2:00
local_date = datetime(2017, 3, 26, tzinfo=tz)
val = croniter("0 0 * * *", local_date).get_next(datetime)
self.assertEqual(val.isoformat(), "2017-03-27T00:00:00+02:00")
#
local_date = datetime(2017, 3, 26, 1, tzinfo=tz)
cr = croniter("0 * * * *", local_date)
val = cr.get_next(datetime)
self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")
val = cr.get_current(datetime)
self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")
# -> 2017-10-29 02:59+2:00 -> 02:00+1:00
local_date = datetime(2017, 10, 29, tzinfo=tz)
val = croniter("0 0 * * *", local_date).get_next(datetime)
self.assertEqual(val.isoformat(), "2017-10-30T00:00:00+01:00")
local_date = datetime(2017, 10, 29, 1, 59, tzinfo=tz)
cr = croniter("0 * * * *", local_date)
schedule = [cr.get_next(datetime).isoformat() for _ in range(4)]
expected_schedule = [
"2017-10-29T02:00:00+02:00",
"2017-10-29T02:00:00+01:00",
"2017-10-29T03:00:00+01:00",
"2017-10-29T04:00:00+01:00",
]
self.assertEqual(schedule, expected_schedule)
def test_std_dst_pytz(self):
"""
DST tests
This fixes https://github.com/taichino/croniter/issues/82
"""
tz = pytz.timezone("Europe/Warsaw")
# -> 2017-03-26 01:59+1:00 -> 03:00+2:00
local_date = tz.localize(datetime(2017, 3, 26))
val = croniter("0 0 * * *", local_date).get_next(datetime)
self.assertEqual(val.isoformat(), "2017-03-27T00:00:00+02:00")
#
local_date = tz.localize(datetime(2017, 3, 26, 1))
cr = croniter("0 * * * *", local_date)
val = cr.get_next(datetime)
self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")
val = cr.get_current(datetime)
self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")
# -> 2017-10-29 02:59+2:00 -> 02:00+1:00
local_date = tz.localize(datetime(2017, 10, 29))
val = croniter("0 0 * * *", local_date).get_next(datetime)
self.assertEqual(val.isoformat(), "2017-10-30T00:00:00+01:00")
local_date = tz.localize(datetime(2017, 10, 29, 1, 59))
cr = croniter("0 * * * *", local_date)
schedule = [cr.get_next(datetime).isoformat() for _ in range(4)]
expected_schedule = [
"2017-10-29T02:00:00+02:00",
"2017-10-29T02:00:00+01:00",
"2017-10-29T03:00:00+01:00",
"2017-10-29T04:00:00+01:00",
]
self.assertEqual(schedule, expected_schedule)
def test_std_dst2(self):
"""
DST tests
This fixes https://github.com/taichino/croniter/issues/87
São Paulo, Brazil: 18/02/2018 00:00 -> 17/02/2018 23:00
"""
tz = zoneinfo.ZoneInfo("America/Sao_Paulo")
local_dates = [
# 17-22: 00 -> 18-00:00
(datetime(2018, 2, 17, 21, 0, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(datetime(2018, 2, 17, 22, 0, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(datetime(2018, 2, 17, 23, 0, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
# 18-00: 00 -> 19-00:00
(datetime(2018, 2, 18, 0, 0, 0, tzinfo=tz), "2018-02-19 00:00:00-03:00"),
# 17-22: 00 -> 18-00:00
(datetime(2018, 2, 17, 21, 5, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(datetime(2018, 2, 17, 22, 5, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(datetime(2018, 2, 17, 23, 5, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
# 18-00: 00 -> 19-00:00
(datetime(2018, 2, 18, 0, 5, 0, tzinfo=tz), "2018-02-19 00:00:00-03:00"),
]
ret1 = [croniter("0 0 * * *", d[0]).get_next(datetime) for d in local_dates]
sret1 = [str(d) for d in ret1]
lret1 = [str(d[1]) for d in local_dates]
self.assertEqual(sret1, lret1)
def test_std_dst2_pytz(self):
"""
DST tests
This fixes https://github.com/taichino/croniter/issues/87
São Paulo, Brazil: 18/02/2018 00:00 -> 17/02/2018 23:00
"""
tz = pytz.timezone("America/Sao_Paulo")
local_dates = [
# 17-22: 00 -> 18-00:00
(tz.localize(datetime(2018, 2, 17, 21, 0, 0)), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(tz.localize(datetime(2018, 2, 17, 22, 0, 0)), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(tz.localize(datetime(2018, 2, 17, 23, 0, 0)), "2018-02-18 00:00:00-03:00"),
# 18-00: 00 -> 19-00:00
(tz.localize(datetime(2018, 2, 18, 0, 0, 0)), "2018-02-19 00:00:00-03:00"),
# 17-22: 00 -> 18-00:00
(tz.localize(datetime(2018, 2, 17, 21, 5, 0)), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(tz.localize(datetime(2018, 2, 17, 22, 5, 0)), "2018-02-18 00:00:00-03:00"),
# 17-23: 00 -> 18-00:00
(tz.localize(datetime(2018, 2, 17, 23, 5, 0)), "2018-02-18 00:00:00-03:00"),
# 18-00: 00 -> 19-00:00
(tz.localize(datetime(2018, 2, 18, 0, 5, 0)), "2018-02-19 00:00:00-03:00"),
]
ret1 = [croniter("0 0 * * *", d[0]).get_next(datetime) for d in local_dates]
sret1 = [str(d) for d in ret1]
lret1 = [str(d[1]) for d in local_dates]
self.assertEqual(sret1, lret1)
def test_std_dst3(self):
"""
DST tests
This fixes https://github.com/taichino/croniter/issues/90
Adelaide, Australia: 15/04/2020 00:00 -> 15/03/2020
"""
tz = zoneinfo.ZoneInfo("Australia/Adelaide")
schedule = croniter("0 0 24 * *", datetime(2020, 4, 15, tzinfo=tz))
val1 = schedule.get_prev(datetime)
self.assertEqual(val1.isoformat(), "2020-03-24T00:00:00+10:30")
val2 = schedule.get_next(datetime)
self.assertEqual(val2.isoformat(), "2020-04-24T00:00:00+09:30")
def test_std_dst3_pytz(self):
"""
DST tests
This fixes https://github.com/taichino/croniter/issues/90
Adelaide, Australia: 15/04/2020 00:00 -> 15/03/2020
"""
tz = pytz.timezone("Australia/Adelaide")
schedule = croniter("0 0 24 * *", tz.localize(datetime(2020, 4, 15)))
val1 = schedule.get_prev(datetime)
self.assertEqual(val1.isoformat(), "2020-03-24T00:00:00+10:30")
val2 = schedule.get_next(datetime)
self.assertEqual(val2.isoformat(), "2020-04-24T00:00:00+09:30")
def test_dst_daily(self) -> None:
"""
DST test for daily schedule
London jumps forward: 2025-03-30 01:00 -> 02:00 (UTC+0 -> UTC+1).
"""
london = dateutil.tz.gettz("Europe/London")
start = datetime(2025, 3, 30, tzinfo=london)
ct = croniter("7 0 * * *", start)
schedule = [ct.get_next(datetime).isoformat() for _ in range(3)]
expected_schedule = [
"2025-03-30T00:07:00+00:00",
"2025-03-31T00:07:00+01:00",
"2025-04-01T00:07:00+01:00",
]
self.assertEqual(schedule, expected_schedule)
def test_dst_hourly(self) -> None:
"""
DST test for hourly schedule
This fixes https://github.com/pallets-eco/croniter/issues/149
London jumps forward: 2025-03-30 01:00 -> 02:00 (UTC+0 -> UTC+1).
"""
london = dateutil.tz.gettz("Europe/London")
start = datetime(2025, 3, 30, tzinfo=london)
ct = croniter("7 * * * *", start)
schedule = [ct.get_next(datetime).isoformat() for _ in range(3)]
expected_schedule = [
"2025-03-30T00:07:00+00:00",
"2025-03-30T02:07:00+01:00",
"2025-03-30T03:07:00+01:00",
]
self.assertEqual(schedule, expected_schedule)
def test_error_alpha_cron(self):
self.assertRaises(CroniterNotAlphaError, croniter.expand, "* * * janu-jun *")
def test_error_bad_cron(self):
self.assertRaises(CroniterBadCronError, croniter.expand, "* * * *")
self.assertRaises(
CroniterBadCronError, croniter.expand, ("* " * (max(VALID_LEN_EXPRESSION) + 1)).strip()
)
def test_is_valid(self):
self.assertTrue(croniter.is_valid("0 * * * *"))
self.assertFalse(croniter.is_valid("0 * *"))
self.assertFalse(croniter.is_valid("* * * janu-jun *"))
self.assertTrue(croniter.is_valid("H 0 * * *", hash_id="abc"))
def test_is_valid_strict(self):
# Feb 31 - never exists
self.assertTrue(croniter.is_valid("0 0 31 2 *"))
self.assertFalse(croniter.is_valid("0 0 31 2 *", strict=True))
# Feb 30 - never exists
self.assertFalse(croniter.is_valid("0 0 30 2 *", strict=True))
# Apr 31 - never exists
self.assertFalse(croniter.is_valid("0 0 31 4 *", strict=True))
# Jun 31 - never exists
self.assertFalse(croniter.is_valid("0 0 31 6 *", strict=True))
# Feb 29 without year - valid (leap years exist)
self.assertTrue(croniter.is_valid("0 0 29 2 *", strict=True))
# Jan 31 - valid
self.assertTrue(croniter.is_valid("0 0 31 1 *", strict=True))
# Day 31 in months 1,2 - day 31 is reachable in Jan
self.assertTrue(croniter.is_valid("0 0 31 1,2 *", strict=True))
# Day 30 in months 2,4 - day 30 is reachable in Apr
self.assertTrue(croniter.is_valid("0 0 30 2,4 *", strict=True))
# Wildcard month - always valid
self.assertTrue(croniter.is_valid("0 0 31 * *", strict=True))
# Wildcard day - always valid
self.assertTrue(croniter.is_valid("0 0 * 2 *", strict=True))
# Last day of month - always valid
self.assertTrue(croniter.is_valid("0 0 l * *", strict=True))
# Normal expressions remain valid
self.assertTrue(croniter.is_valid("0 * * * *", strict=True))
self.assertTrue(croniter.is_valid("*/5 * * * *", strict=True))
# expand() also supports strict
with self.assertRaises(CroniterBadCronError):
croniter.expand("0 0 31 2 *", strict=True)
def test_is_valid_strict_with_year(self):
# Feb 29 in a leap year - valid
self.assertTrue(croniter.is_valid("0 0 29 2 * 0 2024", strict=True))
self.assertTrue(croniter.is_valid("0 0 29 2 * 0 2028", strict=True))
# Feb 29 in a non-leap year - invalid
self.assertFalse(croniter.is_valid("0 0 29 2 * 0 2023", strict=True))
self.assertFalse(croniter.is_valid("0 0 29 2 * 0 2025", strict=True))
# Feb 29 with mixed years (some leap, some not) - valid
self.assertTrue(croniter.is_valid("0 0 29 2 * 0 2023,2024", strict=True))
# Feb 29 with year range including a leap year - valid
self.assertTrue(croniter.is_valid("0 0 29 2 * 0 2023-2025", strict=True))
# Feb 29 with year range of all non-leap years - invalid
self.assertFalse(croniter.is_valid("0 0 29 2 * 0 2025-2027", strict=True))
# Feb 31 with any year - always invalid
self.assertFalse(croniter.is_valid("0 0 31 2 * 0 2024", strict=True))
# Feb 30 with leap year - still invalid (leap year only adds day 29)
self.assertFalse(croniter.is_valid("0 0 30 2 * 0 2024", strict=True))
# Wildcard year - Feb 29 valid (leap years exist)
self.assertTrue(croniter.is_valid("0 0 29 2 * 0 *", strict=True))
def test_is_valid_strict_year_parameter(self):
# 5-field expression with strict_year parameter
# Feb 29 with leap year param - valid
self.assertTrue(croniter.is_valid("0 0 29 2 *", strict=True, strict_year=2024))
self.assertTrue(croniter.is_valid("0 0 29 2 *", strict=True, strict_year=2000))
# Feb 29 with non-leap year param - invalid
self.assertFalse(croniter.is_valid("0 0 29 2 *", strict=True, strict_year=2023))
self.assertFalse(croniter.is_valid("0 0 29 2 *", strict=True, strict_year=1900))
# Feb 31 - invalid regardless of year
self.assertFalse(croniter.is_valid("0 0 31 2 *", strict=True, strict_year=2024))
# strict_year as list of years
self.assertTrue(croniter.is_valid("0 0 29 2 *", strict=True, strict_year=[2023, 2024]))
self.assertFalse(croniter.is_valid("0 0 29 2 *", strict=True, strict_year=[2023, 2025]))
# Non-Feb months ignore strict_year
self.assertTrue(croniter.is_valid("0 0 31 1 *", strict=True, strict_year=2023))
# strict_year without strict has no effect (backward compatible)
self.assertTrue(croniter.is_valid("0 0 31 2 *", strict_year=2024))
def test_nearest_weekday_basic(self):
# 15W: nearest weekday to the 15th
# Jan 2024: 15th is Monday -> fires on 15th
base = datetime(2024, 1, 1)
itr = croniter("0 9 15W * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 1, 15, 9, 0))
# Feb 2024: 15th is Thursday -> fires on 15th
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 2, 15, 9, 0))
# Mar 2024: 15th is Friday -> fires on 15th
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 3, 15, 9, 0))
def test_nearest_weekday_saturday(self):
# Jun 2024: 15th is Saturday -> fires on Friday 14th
base = datetime(2024, 6, 1)
itr = croniter("0 9 15W * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 6, 14, 9, 0))
def test_nearest_weekday_sunday(self):
# Sep 2024: 15th is Sunday -> fires on Monday 16th
base = datetime(2024, 9, 1)
itr = croniter("0 9 15W * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 9, 16, 9, 0))
def test_nearest_weekday_first_saturday(self):
# 1W: 1st is Saturday -> fires on Monday 3rd (no backward month crossing)
# Jun 2024: 1st is Saturday
base = datetime(2024, 5, 31)
itr = croniter("0 9 1W * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 6, 3, 9, 0))
def test_nearest_weekday_last_day_sunday(self):
# 31W in Mar 2025: 31st is Monday -> fires on 31st
base = datetime(2025, 3, 1)
itr = croniter("0 9 31W * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2025, 3, 31, 9, 0))
def test_nearest_weekday_end_of_month_boundary(self):
# 30W in Nov 2024: 30th is Saturday -> fires on Friday 29th
base = datetime(2024, 11, 1)
itr = croniter("0 9 30W * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 11, 29, 9, 0))
# 31W in a month with only 30 days (Nov): clamps to 30th (Sat) -> Fri 29th
base = datetime(2024, 11, 1)
itr = croniter("0 9 31W * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 11, 29, 9, 0))
def test_nearest_weekday_wn_format(self):
# W15 format (prefix) should work the same as 15W
base = datetime(2024, 1, 1)
itr = croniter("0 9 W15 * *", base)
n = itr.get_next(datetime)
self.assertEqual(n, datetime(2024, 1, 15, 9, 0))
def test_nearest_weekday_get_prev(self):
# Test get_prev with W
base = datetime(2024, 6, 30)
itr = croniter("0 9 15W * *", base)
# Jun 2024: 15th is Saturday -> nearest weekday is Friday 14th
n = itr.get_prev(datetime)
self.assertEqual(n, datetime(2024, 6, 14, 9, 0))
def test_nearest_weekday_is_valid(self):
self.assertTrue(croniter.is_valid("0 9 15W * *"))
self.assertTrue(croniter.is_valid("0 9 W15 * *"))
self.assertTrue(croniter.is_valid("0 9 1W * *"))
self.assertTrue(croniter.is_valid("0 9 31W * *"))
# W cannot be used with list or range
self.assertFalse(croniter.is_valid("0 9 15W,16 * *"))
self.assertFalse(croniter.is_valid("0 9 1,15W * *"))
# Out of range
self.assertFalse(croniter.is_valid("0 9 0W * *"))
self.assertFalse(croniter.is_valid("0 9 32W * *"))
def test_nearest_weekday_iteration(self):
# Test iteration across multiple months
base = datetime(2023, 12, 31)
itr = croniter("0 0 1W * *", base)
results = [itr.get_next(datetime) for _ in range(6)]
# Jan 2024: 1st is Mon -> 1st
self.assertEqual(results[0], datetime(2024, 1, 1, 0, 0))
# Feb 2024: 1st is Thu -> 1st
self.assertEqual(results[1], datetime(2024, 2, 1, 0, 0))
# Mar 2024: 1st is Fri -> 1st
self.assertEqual(results[2], datetime(2024, 3, 1, 0, 0))
# Apr 2024: 1st is Mon -> 1st
self.assertEqual(results[3], datetime(2024, 4, 1, 0, 0))
# May 2024: 1st is Wed -> 1st
self.assertEqual(results[4], datetime(2024, 5, 1, 0, 0))
# Jun 2024: 1st is Sat -> Mon 3rd
self.assertEqual(results[5], datetime(2024, 6, 3, 0, 0))
def test_exactly_the_same_minute(self):
base = datetime(2018, 3, 5, 12, 30, 50)
itr = croniter("30 7,12,17 * * *", base)
n1 = itr.get_prev(datetime)
self.assertEqual(12, n1.hour)
n2 = itr.get_prev(datetime)
self.assertEqual(7, n2.hour)
n3 = itr.get_next(datetime)
self.assertEqual(12, n3.hour)
def test_next_when_now_satisfies_cron(self):
ts_a = datetime(2018, 5, 21, 0, 3, 0)
ts_b = datetime(2018, 5, 21, 0, 4, 20)
test_cron = "4 * * * *"
next_a = croniter(test_cron, start_time=ts_a).get_next()
next_b = croniter(test_cron, start_time=ts_b).get_next()
self.assertTrue(next_b > next_a)
def test_milliseconds(self):
"""
https://github.com/taichino/croniter/issues/107
"""
_croniter = partial(croniter, "0 10 * * *", ret_type=datetime)
dt = datetime(2018, 1, 2, 10, 0, 0, 500)
self.assertEqual(_croniter(start_time=dt).get_prev(), datetime(2018, 1, 2, 10, 0))
self.assertEqual(_croniter(start_time=dt).get_next(), datetime(2018, 1, 3, 10, 0))
dt = datetime(2018, 1, 2, 10, 0, 1, 0)
self.assertEqual(_croniter(start_time=dt).get_prev(), datetime(2018, 1, 2, 10, 0))
self.assertEqual(_croniter(start_time=dt).get_next(), datetime(2018, 1, 3, 10, 0))
dt = datetime(2018, 1, 2, 9, 59, 59, 999999)
self.assertEqual(_croniter(start_time=dt).get_prev(), datetime(2018, 1, 1, 10, 0))
self.assertEqual(_croniter(start_time=dt).get_next(), datetime(2018, 1, 2, 10, 0))
def test_invalid_zerorepeat(self):
self.assertFalse(croniter.is_valid("*/0 * * * *"))
def test_weekday_range(self):
ret = []
# jan 14 is monday
dt = datetime(2019, 1, 14, 0, 0, 0, 0)
for i in range(10):
c = croniter("0 0 * * 2-4 *", start_time=dt)
dt = datetime.fromtimestamp(c.get_next(), dateutil.tz.tzutc()).replace(tzinfo=None)
ret.append(dt)
dt += timedelta(days=1)
sret = [str(r) for r in ret]
self.assertEqual(
sret,
[
"2019-01-15 00:00:00",
"2019-01-16 00:00:01",
"2019-01-17 00:00:02",
"2019-01-22 00:00:00",
"2019-01-23 00:00:01",
"2019-01-24 00:00:02",
"2019-01-29 00:00:00",
"2019-01-30 00:00:01",
"2019-01-31 00:00:02",
"2019-02-05 00:00:00",
],
)
ret = []
dt = datetime(2019, 1, 14, 0, 0, 0, 0)
for i in range(10):
c = croniter("0 0 * * 0-6 *", start_time=dt)
dt = datetime.fromtimestamp(c.get_next(), dateutil.tz.tzutc()).replace(tzinfo=None)
ret.append(dt)
dt += timedelta(days=1)
sret = [str(r) for r in ret]
self.assertEqual(
sret,
[
"2019-01-14 00:00:01",
"2019-01-15 00:00:02",
"2019-01-16 00:00:03",
"2019-01-17 00:00:04",
"2019-01-18 00:00:05",
"2019-01-19 00:00:06",
"2019-01-20 00:00:07",
"2019-01-21 00:00:08",
"2019-01-22 00:00:09",
"2019-01-23 00:00:10",
],
)
def test_issue_monsun_117(self):
ret = []
dt = datetime(2019, 1, 14, 0, 0, 0, 0)
for i in range(12):
# c = croniter("0 0 * * Mon-Sun *", start_time=dt)
c = croniter("0 0 * * Wed-Sun *", start_time=dt)
dt = datetime.fromtimestamp(c.get_next(), tz=dateutil.tz.tzutc()).replace(tzinfo=None)
ret.append(dt)
dt += timedelta(days=1)
sret = [str(r) for r in ret]
self.assertEqual(
sret,
[
"2019-01-16 00:00:00",
"2019-01-17 00:00:01",
"2019-01-18 00:00:02",
"2019-01-19 00:00:03",
"2019-01-20 00:00:04",
"2019-01-23 00:00:00",
"2019-01-24 00:00:01",
"2019-01-25 00:00:02",
"2019-01-26 00:00:03",
"2019-01-27 00:00:04",
"2019-01-30 00:00:00",
"2019-01-31 00:00:01",
],
)
def test_mixdow(self):
base = datetime(2018, 10, 1, 0, 0)
itr = croniter("1 1 7,14,21,L * *", base)
self.assertTrue(isinstance(itr.get_next(), float))
def test_match(self):
self.assertTrue(croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 0, 0)))
self.assertFalse(croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 1, 0, 0)))
self.assertTrue(croniter.match("0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 1, 0)))
self.assertFalse(croniter.match("0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 2, 0)))
self.assertTrue(croniter.match("31 * * * *", datetime(2019, 1, 14, 1, 31, 0, 0)))
self.assertTrue(
croniter.match("0 0 10 * wed", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=True)
)
self.assertTrue(
croniter.match("0 0 10 * fri", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=True)
)
self.assertTrue(
croniter.match("0 0 10 * fri", datetime(2020, 6, 12, 0, 0, 0, 0), day_or=True)
)
self.assertTrue(
croniter.match("0 0 10 * wed", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=False)
)
self.assertFalse(
croniter.match("0 0 10 * fri", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=False)
)
self.assertFalse(
croniter.match("0 0 10 * fri", datetime(2020, 6, 12, 0, 0, 0, 0), day_or=False)
)
def test_match_precision(self):
# Default precision for 5-field cron is 60 seconds
# so 59 seconds off still matches
self.assertTrue(croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 59, 0)))
# but 61 seconds off does not
self.assertFalse(croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 1, 1, 0)))
# With explicit precision_in_seconds=1, only exact second matches
self.assertTrue(
croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 0, 0), precision_in_seconds=1)
)
self.assertFalse(
croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 59, 0), precision_in_seconds=1)
)
# Default precision for 6-field cron is 1 second
self.assertTrue(croniter.match("0 0 * * * 0", datetime(2019, 1, 14, 0, 0, 0, 0)))
self.assertFalse(croniter.match("0 0 * * * 0", datetime(2019, 1, 14, 0, 0, 1, 0)))
# With explicit precision_in_seconds=60 on a 6-field cron, 59 seconds off matches
self.assertTrue(
croniter.match(
"0 0 * * * 0", datetime(2019, 1, 14, 0, 0, 59, 0), precision_in_seconds=60
)
)
def test_match_range_precision(self):
# With precision_in_seconds=1, match_range is strict
self.assertFalse(
croniter.match_range(
"0 0 * * *",
datetime(2019, 1, 14, 0, 0, 30, 0),
datetime(2019, 1, 14, 0, 0, 40, 0),
precision_in_seconds=1,
)
)
# With default precision (60s), same range matches
self.assertTrue(
croniter.match_range(
"0 0 * * *",
datetime(2019, 1, 14, 0, 0, 30, 0),
datetime(2019, 1, 14, 0, 0, 40, 0),
)
)
def test_match_handle_bad_cron(self):
# some cron expression can"t get prev value and should not raise exception
self.assertFalse(croniter.match("0 0 31 1 1#1", datetime(2020, 1, 31), day_or=False))
self.assertFalse(croniter.match("0 0 31 1 * 0 2024/2", datetime(2020, 1, 31)))
def test_match_range(self):
self.assertTrue(
croniter.match_range(
"0 0 * * *", datetime(2019, 1, 13, 0, 59, 0, 0), datetime(2019, 1, 14, 0, 1, 0, 0)
)
)
self.assertFalse(
croniter.match_range(
"0 0 * * *", datetime(2019, 1, 13, 0, 1, 0, 0), datetime(2019, 1, 13, 0, 59, 0, 0)
)
)
self.assertTrue(
croniter.match_range(
"0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 0, 0), datetime(2023, 5, 25, 0, 0, 2, 0)
)
)
self.assertFalse(
croniter.match_range(
"0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 2, 0), datetime(2023, 5, 25, 0, 0, 4, 0)
)
)
self.assertTrue(
croniter.match_range(
"0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 1, 0), datetime(2023, 5, 25, 0, 0, 4, 0)
)
)
self.assertTrue(
croniter.match_range(
"31 * * * *",
datetime(2019, 1, 14, 1, 30, 0, 0),
datetime(2019, 1, 14, 1, 31, 0, 0),
)
)
self.assertTrue(
croniter.match_range(
"0 0 10 * wed",
datetime(2020, 6, 9, 0, 0, 0, 0),
datetime(2020, 6, 11, 0, 0, 0, 0),
day_or=True,
)
)
self.assertTrue(
croniter.match_range(
"0 0 10 * fri",
datetime(2020, 6, 10, 0, 0, 0, 0),
datetime(2020, 6, 11, 0, 0, 0, 0),
day_or=True,
)
)
self.assertTrue(
croniter.match_range(
"0 0 10 * fri",
datetime(2020, 6, 11, 0, 0, 0, 0),
datetime(2020, 6, 12, 0, 0, 0, 0),
day_or=True,
)
)
self.assertTrue(
croniter.match_range(
"0 0 10 * wed",
datetime(2020, 6, 9, 0, 0, 0, 0),
datetime(2020, 6, 12, 0, 0, 0, 0),
day_or=False,
)
)
self.assertFalse(
croniter.match_range(
"0 0 10 * fri",
datetime(2020, 6, 8, 0, 0, 0, 0),
datetime(2020, 6, 9, 0, 0, 0, 0),
day_or=False,
)
)
self.assertFalse(
croniter.match_range(
"0 0 10 * fri",
datetime(2020, 6, 7, 0, 0, 0, 0),
datetime(2020, 6, 11, 0, 0, 0, 0),
day_or=False,
)
)
self.assertFalse(
croniter.match_range(
"2 4 1 * wed",
datetime(2019, 1, 1, 3, 2, 0, 0),
datetime(2019, 1, 1, 5, 2, 0, 0),
day_or=False,
)
)
def test_dst_issue90_st31ny(self):
"""Test DST gap with cron job every day at 02:01.
Paris jumps forward: 2020-03-29 02:00 -> 03:00 (UTC+1 -> UTC+2).
So 2020-03-29 02:01 does not exist in local time.
This fixes https://github.com/taichino/croniter/issues/90#issuecomment-605615205
"""
expected_schedule = [
"2020-03-28T02:01:00+01:00", # only checked for get_prev
"2020-03-29T03:00:00+02:00",
"2020-03-30T02:01:00+02:00",
"2020-03-31T02:01:00+02:00", # only checked for get_next
]
tz = zoneinfo.ZoneInfo("Europe/Paris")
now = datetime(2020, 3, 29, 1, 59, 55, tzinfo=tz)
it = croniter("1 2 * * *", now)
schedule = [it.get_next(datetime).isoformat() for _ in range(3)]
self.assertEqual(schedule, expected_schedule[1:])
schedule = [it.get_prev(datetime).isoformat() for _ in range(3)]
self.assertEqual(schedule, list(reversed(expected_schedule[:-1])))
def test_dst_issue90_st31ny_pytz(self):
"""Test DST gap with cron job every day at 02:01.
Paris jumps forward: 2020-03-29 02:00 -> 03:00 (UTC+1 -> UTC+2).
So 2020-03-29 02:01 does not exist in local time.
This fixes https://github.com/taichino/croniter/issues/90#issuecomment-605615205
"""
expected_schedule = [
"2020-03-28T02:01:00+01:00", # only checked for get_prev
"2020-03-29T03:00:00+02:00",
"2020-03-30T02:01:00+02:00",
"2020-03-31T02:01:00+02:00", # only checked for get_next
]
tz = pytz.timezone("Europe/Paris")
now = tz.localize(datetime(2020, 3, 29, 1, 59, 55))
it = croniter("1 2 * * *", now)
schedule = [it.get_next(datetime).isoformat() for _ in range(3)]
self.assertEqual(schedule, expected_schedule[1:])
schedule = [it.get_prev(datetime).isoformat() for _ in range(3)]
self.assertEqual(schedule, list(reversed(expected_schedule[:-1])))
def test_dst_iter(self):
"""Test Hebron jumps one hour forward on 2022-03-27 00:00 (UTC+2 -> UTC+3)."""
tz = zoneinfo.ZoneInfo("Asia/Hebron")
now = datetime(2022, 3, 25, 0, 0, 0, tzinfo=tz)
it = croniter("0 0 * * *", now)
ret = [
it.get_next(datetime).isoformat(),
it.get_next(datetime).isoformat(),
it.get_next(datetime).isoformat(),
]
self.assertEqual(
ret,
[
"2022-03-26T00:00:00+02:00",
"2022-03-27T01:00:00+03:00",
"2022-03-28T00:00:00+03:00",
],
)
def test_dst_iter_pytz(self):
"""Test Hebron jumps one hour forward on 2022-03-27 00:00 (UTC+2 -> UTC+3)."""
tz = pytz.timezone("Asia/Hebron")
now = tz.localize(datetime(2022, 3, 25, 0, 0, 0))
it = croniter("0 0 * * *", now)
ret = [
it.get_next(datetime).isoformat(),
it.get_next(datetime).isoformat(),
it.get_next(datetime).isoformat(),
]
self.assertEqual(
ret,
[
"2022-03-26T00:00:00+02:00",
"2022-03-27T01:00:00+03:00",
"2022-03-28T00:00:00+03:00",
],
)
def test_nth_wday_simple(self):
def f(y, m, w):
return croniter._get_nth_weekday_of_month(y, m, w)
sun, mon, tue, wed, thu, fri, sat = range(7)
self.assertEqual(f(2000, 1, mon), (3, 10, 17, 24, 31))
self.assertEqual(f(2000, 2, tue), (1, 8, 15, 22, 29)) # Leap year
self.assertEqual(f(2000, 3, wed), (1, 8, 15, 22, 29))
self.assertEqual(f(2000, 4, thu), (6, 13, 20, 27))
self.assertEqual(f(2000, 2, fri), (4, 11, 18, 25))
self.assertEqual(f(2000, 2, sat), (5, 12, 19, 26))
def test_nth_as_last_wday_simple(self):
def f(y, m, w):
return croniter._get_nth_weekday_of_month(y, m, w)[-1]
sun, mon, tue, wed, thu, fri, sat = range(7)
self.assertEqual(f(2000, 2, tue), 29)
self.assertEqual(f(2000, 2, sun), 27)
self.assertEqual(f(2000, 2, mon), 28)
self.assertEqual(f(2000, 2, wed), 23)
self.assertEqual(f(2000, 2, thu), 24)
self.assertEqual(f(2000, 2, fri), 25)
self.assertEqual(f(2000, 2, sat), 26)
def test_wdom_core_leap_year(self):
def f(y, m, w):
return croniter._get_nth_weekday_of_month(y, m, w)[-1]
sun, mon, tue, wed, thu, fri, sat = range(7)
self.assertEqual(f(2000, 2, tue), 29)
self.assertEqual(f(2000, 2, sun), 27)
self.assertEqual(f(2000, 2, mon), 28)
self.assertEqual(f(2000, 2, wed), 23)
self.assertEqual(f(2000, 2, thu), 24)
self.assertEqual(f(2000, 2, fri), 25)
self.assertEqual(f(2000, 2, sat), 26)
def test_lwom_friday(self):
it = croniter("0 0 * * L5", datetime(1987, 1, 15), ret_type=datetime)
items = [next(it) for i in range(12)]
self.assertListEqual(
items,
[
datetime(1987, 1, 30),
datetime(1987, 2, 27),
datetime(1987, 3, 27),
datetime(1987, 4, 24),
datetime(1987, 5, 29),
datetime(1987, 6, 26),
datetime(1987, 7, 31),
datetime(1987, 8, 28),
datetime(1987, 9, 25),
datetime(1987, 10, 30),
datetime(1987, 11, 27),
datetime(1987, 12, 25),
],
)
def test_lwom_friday_2hours(self):
# This works with +/- "days=1' in proc_day_of_week_last() and I don't know WHY?!?
it = croniter("0 1,5 * * L5", datetime(1987, 1, 15), ret_type=datetime)
items = [next(it) for i in range(12)]
self.assertListEqual(
items,
[
datetime(1987, 1, 30, 1),
datetime(1987, 1, 30, 5),
datetime(1987, 2, 27, 1),
datetime(1987, 2, 27, 5),
datetime(1987, 3, 27, 1),
datetime(1987, 3, 27, 5),
datetime(1987, 4, 24, 1),
datetime(1987, 4, 24, 5),
datetime(1987, 5, 29, 1),
datetime(1987, 5, 29, 5),
datetime(1987, 6, 26, 1),
datetime(1987, 6, 26, 5),
],
)
def test_lwom_friday_2xh_2xm(self):
it = croniter("0,30 1,5 * * L5", datetime(1987, 1, 15), ret_type=datetime)
items = [next(it) for i in range(12)]
self.assertListEqual(
items,
[
datetime(1987, 1, 30, 1, 0),
datetime(1987, 1, 30, 1, 30),
datetime(1987, 1, 30, 5, 0),
datetime(1987, 1, 30, 5, 30),
datetime(1987, 2, 27, 1, 0),
datetime(1987, 2, 27, 1, 30),
datetime(1987, 2, 27, 5, 0),
datetime(1987, 2, 27, 5, 30),
datetime(1987, 3, 27, 1, 0),
datetime(1987, 3, 27, 1, 30),
datetime(1987, 3, 27, 5, 0),
datetime(1987, 3, 27, 5, 30),
],
)
def test_lwom_saturday_rev(self):
it = croniter("0 0 * * L6", datetime(2017, 12, 31), ret_type=datetime, is_prev=True)
items = [next(it) for i in range(12)]
self.assertListEqual(
items,
[
datetime(2017, 12, 30),
datetime(2017, 11, 25),
datetime(2017, 10, 28),
datetime(2017, 9, 30),
datetime(2017, 8, 26),
datetime(2017, 7, 29),
datetime(2017, 6, 24),
datetime(2017, 5, 27),
datetime(2017, 4, 29),
datetime(2017, 3, 25),
datetime(2017, 2, 25),
datetime(2017, 1, 28),
],
)
def test_lwom_tue_thu(self):
it = croniter("0 0 * * L2,L4", datetime(2016, 6, 1), ret_type=datetime)
items = [next(it) for i in range(10)]
self.assertListEqual(
items,
[
datetime(2016, 6, 28),
datetime(2016, 6, 30),
datetime(2016, 7, 26),
datetime(2016, 7, 28),
datetime(2016, 8, 25), # last tuesday comes before the last thursday
datetime(2016, 8, 30),
datetime(2016, 9, 27),
datetime(2016, 9, 29),
datetime(2016, 10, 25),
datetime(2016, 10, 27),
],
)
def test_hash_mixup_all_fri_3rd_sat(self):
# It appears that it's not possible to MIX a literal dow with a `dow#n` format
cron_a = "0 0 * * 6#3"
cron_b = "0 0 * * 5"
cron_c = "0 0 * * 5,6#3"
start = datetime(2021, 3, 1)
expect_a = [datetime(2021, 3, 20)]
expect_b = [
datetime(2021, 3, 5),
datetime(2021, 3, 12),
datetime(2021, 3, 19),
datetime(2021, 3, 26),
]
expect_c = sorted(set(expect_a) & set(expect_b))
def getn(expr, n):
it = croniter(expr, start, ret_type=datetime)
return [next(it) for i in range(n)]
self.assertListEqual(getn(cron_a, 1), expect_a)
self.assertListEqual(getn(cron_b, 4), expect_b)
with self.assertRaises(CroniterUnsupportedSyntaxError):
self.assertListEqual(getn(cron_c, 5), expect_c)
def test_lwom_mixup_all_fri_last_sat(self):
# Based on the failure of test_hash_mixup_all_fri_3rd_sat, we should expect
# this to fail too as this implementation simply extends nth_weekday_of_month
cron_a = "0 0 * * L6"
cron_b = "0 0 * * 5"
cron_c = "0 0 * * 5,L6"
start = datetime(2021, 3, 1)
expect_a = [datetime(2021, 3, 27)]
expect_b = [
datetime(2021, 3, 5),
datetime(2021, 3, 12),
datetime(2021, 3, 19),
datetime(2021, 3, 26),
]
expect_c = sorted(set(expect_a) | set(expect_b))
def getn(expr, n):
it = croniter(expr, start, ret_type=datetime)
return [next(it) for i in range(n)]
self.assertListEqual(getn(cron_a, 1), expect_a)
self.assertListEqual(getn(cron_b, 4), expect_b)
with self.assertRaises(CroniterUnsupportedSyntaxError):
self.assertListEqual(getn(cron_c, 5), expect_c)
def test_lwom_mixup_firstlast_sat(self):
# First saturday, last saturday
start = datetime(2021, 3, 1)
cron_a = "0 0 * * 6#1"
cron_b = "0 0 * * L6"
cron_c = "0 0 * * L6,6#1"
expect_a = [datetime(2021, 3, 6), datetime(2021, 4, 3), datetime(2021, 5, 1)]
expect_b = [datetime(2021, 3, 27), datetime(2021, 4, 24), datetime(2021, 5, 29)]
expect_c = sorted(expect_a + expect_b)
def getn(expr, n):
it = croniter(expr, start, ret_type=datetime)
return [next(it) for i in range(n)]
self.assertListEqual(getn(cron_a, 3), expect_a)
self.assertListEqual(getn(cron_b, 3), expect_b)
self.assertListEqual(getn(cron_c, 6), expect_c)
def test_lwom_mixup_4th_and_last(self):
# 4th and last monday
start = datetime(2021, 11, 1)
cron_a = "0 0 * * 1#4"
cron_b = "0 0 * * L1"
cron_c = "0 0 * * 1#4,L1"
expect_a = [datetime(2021, 11, 22), datetime(2021, 12, 27), datetime(2022, 1, 24)]
expect_b = [datetime(2021, 11, 29), datetime(2021, 12, 27), datetime(2022, 1, 31)]
expect_c = sorted(set(expect_a) | set(expect_b))
def getn(expr, n):
it = croniter(expr, start, ret_type=datetime)
return [next(it) for i in range(n)]
self.assertListEqual(getn(cron_a, 3), expect_a)
self.assertListEqual(getn(cron_b, 3), expect_b)
self.assertListEqual(getn(cron_c, 5), expect_c)
def test_configure_second_location(self):
base = datetime(2010, 8, 25, 0)
itr = croniter("59 58 1 * * *", base, second_at_beginning=True)
n = itr.get_next(datetime)
self.assertEqual(n.year, base.year)
self.assertEqual(n.month, base.month)
self.assertEqual(n.day, base.day)
self.assertEqual(n.hour, 1)
self.assertEqual(n.minute, 58)
self.assertEqual(n.second, 59)
def test_nth_out_of_range(self):
with self.assertRaises(CroniterBadCronError):
croniter("0 0 * * 1#7")
with self.assertRaises(CroniterBadCronError):
croniter("0 0 * * 1#0")
def test_last_out_of_range(self):
with self.assertRaises(CroniterBadCronError):
croniter("0 0 * * L-1")
with self.assertRaises(CroniterBadCronError):
croniter("0 0 * * L8")
def test_question_mark(self):
base = datetime(2010, 8, 25, 15, 56)
itr = croniter("0 0 1 * ?", base)
n = itr.get_next(datetime)
self.assertEqual(n.year, base.year)
self.assertEqual(n.month, 9)
self.assertEqual(n.day, 1)
self.assertEqual(n.hour, 0)
self.assertEqual(n.minute, 0)
def test_invalid_question_mark(self):
self.assertRaises(CroniterBadCronError, croniter, "? * * * *")
self.assertRaises(CroniterBadCronError, croniter, "* ? * * *")
self.assertRaises(CroniterBadCronError, croniter, "* * ?,* * *")
def test_year(self):
itr1 = croniter("0 0 11 * * 0 2060", datetime(2050, 1, 1))
n1 = itr1.get_next(datetime)
self.assertEqual(n1.year, 2060)
self.assertEqual(n1.month, 1)
self.assertEqual(n1.day, 11)
n2 = itr1.get_next(datetime)
self.assertEqual(n2.year, 2060)
self.assertEqual(n2.month, 2)
self.assertEqual(n2.day, 11)
itr2 = croniter("0 0 11 * * 0 2050-2060", datetime(2055, 1, 30))
n3 = itr2.get_next(datetime)
self.assertEqual(n3.year, 2055)
self.assertEqual(n3.month, 2)
self.assertEqual(n3.day, 11)
itr3 = croniter("0 0 29 2 * 0 2025,2021-2023,2028", datetime(2020, 1, 1))
n4 = itr3.get_next(datetime)
self.assertEqual(n4.year, 2028)
self.assertEqual(n4.month, 2)
self.assertEqual(n4.day, 29)
itr4 = croniter("0 0 29 2 * 0 2025,*", datetime(2020, 1, 1))
n5 = itr4.get_next(datetime)
self.assertEqual(n5.year, 2020)
self.assertEqual(n5.month, 2)
self.assertEqual(n5.day, 29)
itr5 = croniter("0 0 29 2 * 0 2022/3", datetime(2020, 1, 1))
n6 = itr5.get_next(datetime)
self.assertEqual(n6.year, 2028)
self.assertEqual(n6.month, 2)
self.assertEqual(n6.day, 29)
itr6 = croniter("0 0 29 2 * 0 2023-2035/3", datetime(2020, 1, 1))
n7 = itr6.get_next(datetime)
self.assertEqual(n7.year, 2032)
self.assertEqual(n7.month, 2)
self.assertEqual(n7.day, 29)
def test_year_with_other_field(self):
itr1 = croniter("0 0 31 11-12 * 0 2023", datetime(2000, 1, 30))
n1 = itr1.get_next(datetime)
self.assertEqual(n1.year, 2023)
self.assertEqual(n1.month, 12)
self.assertEqual(n1.day, 31)
itr2 = croniter("0 0 31 1-2 * 0 2023-2025", datetime(2024, 12, 30))
n2 = itr2.get_next(datetime)
self.assertEqual(n2.year, 2025)
self.assertEqual(n2.month, 1)
self.assertEqual(n2.day, 31)
itr3 = croniter("0 0 1 1 1 0 2020-2030", datetime(2000, 1, 1), day_or=False)
n3 = itr3.get_next(datetime)
self.assertEqual(n3.year, 2024)
self.assertEqual(n3.month, 1)
self.assertEqual(n3.day, 1)
def test_year_get_prev(self):
itr1 = croniter("0 0 11 * * 0 2000", datetime(2010, 1, 1))
p1 = itr1.get_prev(datetime)
self.assertEqual(p1.year, 2000)
self.assertEqual(p1.month, 12)
self.assertEqual(p1.day, 11)
itr2 = croniter("0 0 11 * * 0 2000", datetime(2010, 1, 1))
p2 = itr2.get_prev(datetime)
self.assertEqual(p2.year, 2000)
self.assertEqual(p2.month, 12)
self.assertEqual(p2.day, 11)
itr2 = croniter("0 0 29 2 * 0 2010-2030", datetime(2020, 1, 1))
p2 = itr2.get_prev(datetime)
self.assertEqual(p2.year, 2016)
self.assertEqual(p2.month, 2)
self.assertEqual(p2.day, 29)
def test_year_match(self):
self.assertTrue(croniter.match("* * * * * * 2024", datetime(2024, 1, 1)))
self.assertTrue(
croniter.match(
"59 58 23 31 12 * 2024",
datetime(2024, 12, 31, 23, 58, 59),
second_at_beginning=True,
)
)
self.assertFalse(croniter.match("* * * * * * 2024-2026", datetime(2027, 1, 1)))
self.assertFalse(croniter.match("* * * * * * 2024/2", datetime(2025, 1, 1)))
def test_year_bad_date_error(self):
with self.assertRaises(CroniterBadDateError):
itr = croniter("* * * * * * 2020", datetime(2030, 1, 1))
itr.get_next()
with self.assertRaises(CroniterBadDateError):
itr = croniter("* * * * * * 2020", datetime(2000, 1, 1))
itr.get_prev()
with self.assertRaises(CroniterBadDateError):
itr = croniter("* * 29 2 * * 2021-2023", datetime(2000, 1, 1))
itr.get_next()
def test_year_with_second_at_beginning(self):
base = datetime(2050, 1, 1)
itr = croniter("59 58 23 31 12 * 2070", base, second_at_beginning=True)
n = itr.get_next(datetime)
self.assertEqual(n.year, 2070)
self.assertEqual(n.month, 12)
self.assertEqual(n.day, 31)
self.assertEqual(n.hour, 23)
self.assertEqual(n.minute, 58)
self.assertEqual(n.second, 59)
def test_invalid_year(self):
self.assertRaises(CroniterBadCronError, croniter, "0 0 1 * * 0 1000")
self.assertRaises(CroniterBadCronError, croniter, "0 0 1 * * 0 99999")
self.assertRaises(CroniterBadCronError, croniter, "0 0 1 * * 0 2070#3")
def test_issue_47(self):
base = datetime(2021, 3, 30, 4, 0)
itr = croniter("0 6 30 3 *", base)
prev1 = itr.get_prev(datetime)
self.assertEqual(prev1.year, base.year - 1)
self.assertEqual(prev1.month, 3)
self.assertEqual(prev1.day, 30)
self.assertEqual(prev1.hour, 6)
self.assertEqual(prev1.minute, 0)
maxDiff = None
def test_issue_142_dow(self):
ret = []
for i in range(1, 31):
ret.append(
(i, croniter("35 * 1-l/8 * *", datetime(2020, 1, i), ret_type=datetime).get_next())
)
i += 1
self.assertEqual(
ret,
[
(1, datetime(2020, 1, 1, 0, 35)),
(2, datetime(2020, 1, 9, 0, 35)),
(3, datetime(2020, 1, 9, 0, 35)),
(4, datetime(2020, 1, 9, 0, 35)),
(5, datetime(2020, 1, 9, 0, 35)),
(6, datetime(2020, 1, 9, 0, 35)),
(7, datetime(2020, 1, 9, 0, 35)),
(8, datetime(2020, 1, 9, 0, 35)),
(9, datetime(2020, 1, 9, 0, 35)),
(10, datetime(2020, 1, 17, 0, 35)),
(11, datetime(2020, 1, 17, 0, 35)),
(12, datetime(2020, 1, 17, 0, 35)),
(13, datetime(2020, 1, 17, 0, 35)),
(14, datetime(2020, 1, 17, 0, 35)),
(15, datetime(2020, 1, 17, 0, 35)),
(16, datetime(2020, 1, 17, 0, 35)),
(17, datetime(2020, 1, 17, 0, 35)),
(18, datetime(2020, 1, 25, 0, 35)),
(19, datetime(2020, 1, 25, 0, 35)),
(20, datetime(2020, 1, 25, 0, 35)),
(21, datetime(2020, 1, 25, 0, 35)),
(22, datetime(2020, 1, 25, 0, 35)),
(23, datetime(2020, 1, 25, 0, 35)),
(24, datetime(2020, 1, 25, 0, 35)),
(25, datetime(2020, 1, 25, 0, 35)),
(26, datetime(2020, 2, 1, 0, 35)),
(27, datetime(2020, 2, 1, 0, 35)),
(28, datetime(2020, 2, 1, 0, 35)),
(29, datetime(2020, 2, 1, 0, 35)),
(30, datetime(2020, 2, 1, 0, 35)),
],
)
def test_issue145_getnext(self):
# Example of quarterly event cron schedule
start = datetime(2020, 9, 24)
cron = "0 13 8 1,4,7,10 wed"
with self.assertRaises(CroniterBadDateError):
it = croniter(cron, start, day_or=False, max_years_between_matches=1)
it.get_next()
# New functionality (0.3.35) allowing croniter to find spare matches of cron
# patterns across multiple years
it = croniter(cron, start, day_or=False, max_years_between_matches=5)
self.assertEqual(it.get_next(datetime), datetime(2025, 1, 8, 13))
def test_explicit_year_forward(self):
start = datetime(2020, 9, 24)
cron = "0 13 8 1,4,7,10 wed"
# Expect exception because no explicit range was provided. Therefore, the
# caller should be made aware that an implicit limit was hit.
ccron = croniter(cron, start, day_or=False)
ccron._max_years_between_matches = 1
iterable = ccron.all_next()
with self.assertRaises(CroniterBadDateError):
next(iterable)
iterable = croniter(cron, start, day_or=False, max_years_between_matches=5).all_next(
datetime
)
n = next(iterable)
self.assertEqual(n, datetime(2025, 1, 8, 13))
# If the explicitly given lookahead isn't enough to reach the next date, that's fine.
# The caller specified the maximum gap, so no just stop iteration
iterable = croniter(cron, start, day_or=False, max_years_between_matches=2).all_next(
datetime
)
with self.assertRaises(StopIteration):
next(iterable)
def test_issue151(self):
"""."""
self.assertTrue(croniter.match("* * * * *", datetime(2019, 1, 14, 11, 0, 59, 999999)))
def test_overflow(self):
"""."""
self.assertRaises(CroniterBadCronError, croniter, "0-10000000 * * * *", datetime.now())
def test_issue156(self):
"""."""
dt = croniter("* * * * *,0", datetime(2019, 1, 14, 11, 0, 59, 999999)).get_next()
self.assertEqual(1547463660.0, dt)
self.assertRaises(CroniterBadCronError, croniter, "* * * * *,b")
dt = croniter("0 0 * * *,sat#3", datetime(2019, 1, 14, 11, 0, 59, 999999)).get_next()
self.assertEqual(1547856000.0, dt)
def test_confirm_sort(self):
m, h, d, mon, dow, s = range(6)
self.assertListEqual(croniter("0 8,22,10,23 1 1 0").expanded[h], [8, 10, 22, 23])
self.assertListEqual(croniter("0 0 25-L 1 0").expanded[d], [25, 26, 27, 28, 29, 30, 31])
self.assertListEqual(croniter("1 1 7,14,21,L * *").expanded[d], [7, 14, 21, "l"])
self.assertListEqual(croniter("0 0 * * *,sat#3").expanded[dow], ["*", 6])
def test_issue_k6(self):
self.assertRaises(CroniterBadCronError, croniter, "0 0 0 0 0")
self.assertRaises(CroniterBadCronError, croniter, "0 0 0 1 0")
def test_issue_k11(self):
now = datetime(2019, 1, 14, 11, 0, 59, tzinfo=zoneinfo.ZoneInfo("America/New_York"))
nextnow = croniter("* * * * * ").next(datetime, start_time=now)
nextnow2 = croniter("* * * * * ", now).next(datetime)
for nt in nextnow, nextnow2:
self.assertEqual(nt.tzinfo.key, "America/New_York")
self.assertEqual(int(croniter._datetime_to_timestamp(nt)), 1547481660)
def test_issue_k12(self):
tz = zoneinfo.ZoneInfo("Europe/Athens")
base = datetime(2010, 1, 23, 12, 18, tzinfo=tz)
itr = croniter("* * * * *")
itr.set_current(start_time=base)
n1 = itr.get_next() # 19
self.assertEqual(n1, datetime_to_timestamp(base) + 60)
def test_issue_k34(self):
# invalid cron, but should throw appropriate exception
self.assertRaises(CroniterBadCronError, croniter, "4 0 L/2 2 0")
def test_issue_k33(self):
y = 2018
# At 11:30 PM, between day 1 and 7 of the month, Monday through Friday, only in January
ret = []
for i in range(10):
cron = croniter("30 23 1-7 JAN MON-FRI#1", datetime(y + i, 1, 1), ret_type=datetime)
for j in range(7):
d = cron.get_next()
if d.year == y + i:
ret.append(d)
rets = [
datetime(2018, 1, 1, 23, 30),
datetime(2018, 1, 2, 23, 30),
datetime(2018, 1, 3, 23, 30),
datetime(2018, 1, 4, 23, 30),
datetime(2018, 1, 5, 23, 30),
datetime(2019, 1, 1, 23, 30),
datetime(2019, 1, 2, 23, 30),
datetime(2019, 1, 3, 23, 30),
datetime(2019, 1, 4, 23, 30),
datetime(2019, 1, 7, 23, 30),
datetime(2020, 1, 1, 23, 30),
datetime(2020, 1, 2, 23, 30),
datetime(2020, 1, 3, 23, 30),
datetime(2020, 1, 6, 23, 30),
datetime(2020, 1, 7, 23, 30),
datetime(2021, 1, 1, 23, 30),
datetime(2021, 1, 4, 23, 30),
datetime(2021, 1, 5, 23, 30),
datetime(2021, 1, 6, 23, 30),
datetime(2021, 1, 7, 23, 30),
datetime(2022, 1, 3, 23, 30),
datetime(2022, 1, 4, 23, 30),
datetime(2022, 1, 5, 23, 30),
datetime(2022, 1, 6, 23, 30),
datetime(2022, 1, 7, 23, 30),
datetime(2023, 1, 2, 23, 30),
datetime(2023, 1, 3, 23, 30),
datetime(2023, 1, 4, 23, 30),
datetime(2023, 1, 5, 23, 30),
datetime(2023, 1, 6, 23, 30),
datetime(2024, 1, 1, 23, 30),
datetime(2024, 1, 2, 23, 30),
datetime(2024, 1, 3, 23, 30),
datetime(2024, 1, 4, 23, 30),
datetime(2024, 1, 5, 23, 30),
datetime(2025, 1, 1, 23, 30),
datetime(2025, 1, 2, 23, 30),
datetime(2025, 1, 3, 23, 30),
datetime(2025, 1, 6, 23, 30),
datetime(2025, 1, 7, 23, 30),
datetime(2026, 1, 1, 23, 30),
datetime(2026, 1, 2, 23, 30),
datetime(2026, 1, 5, 23, 30),
datetime(2026, 1, 6, 23, 30),
datetime(2026, 1, 7, 23, 30),
datetime(2027, 1, 1, 23, 30),
datetime(2027, 1, 4, 23, 30),
datetime(2027, 1, 5, 23, 30),
datetime(2027, 1, 6, 23, 30),
datetime(2027, 1, 7, 23, 30),
]
self.assertEqual(ret, rets)
croniter.expand("30 6 1-7 MAY MON#1")
def test_bug_62_leap(self):
ret = croniter("15 22 29 2 *", datetime(2024, 2, 29)).get_prev(datetime)
self.assertEqual(ret, datetime(2020, 2, 29, 22, 15))
def test_get_prev_leap_year_feb29(self):
"""get_prev should not skip Feb 29 on leap years (issue #203)."""
expr = "0 0 29 * *"
for start in [datetime(2024, 3, 2), datetime(2024, 3, 15), datetime(2024, 3, 28)]:
ret = croniter(expr, start).get_prev(datetime)
self.assertEqual(ret, datetime(2024, 2, 29))
# Also verify from January crosses year boundary correctly
ret = croniter(expr, datetime(2024, 1, 15)).get_prev(datetime)
self.assertEqual(ret, datetime(2023, 12, 29))
def test_expand_from_start_time_minute(self):
seven_seconds_interval_pattern = "*/7 * * * *"
ret1 = croniter(
seven_seconds_interval_pattern,
start_time=datetime(2024, 7, 11, 10, 11),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret1, datetime(2024, 7, 11, 10, 18))
ret2 = croniter(
seven_seconds_interval_pattern,
start_time=datetime(2024, 7, 11, 10, 12),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret2, datetime(2024, 7, 11, 10, 19))
ret3 = croniter(
seven_seconds_interval_pattern,
start_time=datetime(2024, 7, 11, 10, 11),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret3, datetime(2024, 7, 11, 10, 4))
ret4 = croniter(
seven_seconds_interval_pattern,
start_time=datetime(2024, 7, 11, 10, 12),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret4, datetime(2024, 7, 11, 10, 5))
def test_expand_from_start_time_hour(self):
seven_hours_interval_pattern = "0 */7 * * *"
ret1 = croniter(
seven_hours_interval_pattern,
start_time=datetime(2024, 7, 11, 15, 0),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret1, datetime(2024, 7, 11, 22, 0))
ret2 = croniter(
seven_hours_interval_pattern,
start_time=datetime(2024, 7, 11, 16, 0),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret2, datetime(2024, 7, 11, 23, 0))
ret3 = croniter(
seven_hours_interval_pattern,
start_time=datetime(2024, 7, 11, 15, 0),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret3, datetime(2024, 7, 11, 8, 0))
ret4 = croniter(
seven_hours_interval_pattern,
start_time=datetime(2024, 7, 11, 16, 0),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret4, datetime(2024, 7, 11, 9, 0))
def test_expand_from_start_time_date(self):
five_days_interval_pattern = "0 0 */5 * *"
ret1 = croniter(
five_days_interval_pattern,
start_time=datetime(2024, 7, 12),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret1, datetime(2024, 7, 17))
ret2 = croniter(
five_days_interval_pattern,
start_time=datetime(2024, 7, 13),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret2, datetime(2024, 7, 18))
ret3 = croniter(
five_days_interval_pattern,
start_time=datetime(2024, 7, 12),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret3, datetime(2024, 7, 7))
ret4 = croniter(
five_days_interval_pattern,
start_time=datetime(2024, 7, 13),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret4, datetime(2024, 7, 8))
def test_expand_from_start_time_month(self):
three_monts_interval_pattern = "0 0 1 */3 *"
ret1 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 7, 1),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret1, datetime(2024, 10, 1))
ret2 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 8, 1),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret2, datetime(2024, 11, 1))
ret3 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 7, 1),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret3, datetime(2024, 4, 1))
ret4 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 8, 1),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret4, datetime(2024, 5, 1))
def test_expand_from_start_time_day_of_week(self):
three_monts_interval_pattern = "0 0 * * */2"
ret1 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 7, 10),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret1, datetime(2024, 7, 12))
ret2 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 7, 11),
expand_from_start_time=True,
).get_next(datetime)
self.assertEqual(ret2, datetime(2024, 7, 13))
ret3 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 7, 10),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret3, datetime(2024, 7, 8))
ret4 = croniter(
three_monts_interval_pattern,
start_time=datetime(2024, 7, 11),
expand_from_start_time=True,
).get_prev(datetime)
self.assertEqual(ret4, datetime(2024, 7, 9))
def test_get_next_fails_with_expand_from_start_time_true(self):
expanded_croniter = croniter("0 0 */5 * *", expand_from_start_time=True)
self.assertRaises(
ValueError, expanded_croniter.get_next, datetime, start_time=datetime(2024, 7, 12)
)
def test_get_next_update_current(self):
cron = croniter("* * * * * *")
cron.set_current(datetime(2024, 7, 12), force=True)
retn = [(cron.get_next(datetime), cron.get_current(datetime)) for a in range(3)]
self.assertEqual(
retn,
[
(datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0, 1)),
(datetime(2024, 7, 12, 0, 0, 2), datetime(2024, 7, 12, 0, 0, 2)),
(datetime(2024, 7, 12, 0, 0, 3), datetime(2024, 7, 12, 0, 0, 3)),
],
)
retns = (
cron.get_next(datetime, start_time=datetime(2024, 7, 12)),
cron.get_current(datetime),
)
self.assertEqual(retn[0], retns)
cron.set_current(datetime(2024, 7, 12), force=True)
retp = [(cron.get_prev(datetime), cron.get_current(datetime)) for a in range(3)]
self.assertEqual(
retp,
[
(datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 11, 23, 59, 59)),
(datetime(2024, 7, 11, 23, 59, 58), datetime(2024, 7, 11, 23, 59, 58)),
(datetime(2024, 7, 11, 23, 59, 57), datetime(2024, 7, 11, 23, 59, 57)),
],
)
retps = (
cron.get_prev(datetime, start_time=datetime(2024, 7, 12)),
cron.get_current(datetime),
)
self.assertEqual(retp[0], retps)
cron.set_current(datetime(2024, 7, 12), force=True)
r = cron.all_next(datetime)
retan = [(next(r), cron.get_current(datetime)) for a in range(3)]
r = cron.all_next(datetime, start_time=datetime(2024, 7, 12))
retans = [(next(r), cron.get_current(datetime)) for a in range(3)]
cron.set_current(datetime(2024, 7, 12), force=True)
r = cron.all_prev(datetime)
retap = [(next(r), cron.get_current(datetime)) for a in range(3)]
r = cron.all_prev(datetime, start_time=datetime(2024, 7, 12))
retaps = [(next(r), cron.get_current(datetime)) for a in range(3)]
self.assertEqual(retp, retap)
self.assertEqual(retp, retaps)
self.assertEqual(retn, retan)
self.assertEqual(retn, retans)
cron.set_current(datetime(2024, 7, 12), force=True)
uretn = [
(cron.get_next(datetime, update_current=False), cron.get_current(datetime))
for a in range(3)
]
self.assertEqual(
uretn,
[
(datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
(datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
(datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
],
)
cron.set_current(datetime(2024, 7, 12), force=True)
uretp = [
(cron.get_prev(datetime, update_current=False), cron.get_current(datetime))
for a in range(3)
]
self.assertEqual(
uretp,
[
(datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
(datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
(datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
],
)
cron.set_current(datetime(2024, 7, 12), force=True)
r = cron.all_next(datetime, update_current=False)
uretan = [(next(r), cron.get_current(datetime)) for a in range(3)]
cron.set_current(datetime(2024, 7, 12), force=True)
r = cron.all_prev(datetime, update_current=False)
uretap = [(next(r), cron.get_current(datetime)) for a in range(3)]
self.assertEqual(uretp, uretap)
self.assertEqual(uretn, uretan)
def test_issue_2038y(self):
base = datetime(2040, 1, 1, 0, 0)
itr = croniter("* * * * *", base)
try:
itr.get_next()
except OverflowError:
raise Exception("overflow not fixed!")
def test_revert_issue_90_aka_support_dow7(self):
self.assertTrue(croniter.is_valid("* * * * 1-7"))
self.assertTrue(croniter.is_valid("* * * * 7"))
def test_sunday_ranges_to(self):
self._test_sunday_ranges("0 0 * * Sun-Sun", list(range(2, 32)))
self._test_sunday_ranges("0 0 * * Mon-Sun", list(range(2, 32)))
self._test_sunday_ranges(
"0 0 * * Tue-Sun",
[
2,
3,
4,
5,
6,
7,
9,
10,
11,
12,
13,
14,
16,
17,
18,
19,
20,
21,
23,
24,
25,
26,
27,
28,
30,
31,
1,
2,
3,
4,
],
)
self._test_sunday_ranges(
"0 0 * * Wed-Sun",
[
3,
4,
5,
6,
7,
10,
11,
12,
13,
14,
17,
18,
19,
20,
21,
24,
25,
26,
27,
28,
31,
1,
2,
3,
4,
7,
8,
9,
10,
11,
],
)
self._test_sunday_ranges(
"0 0 * * Thu-Sun",
[
4,
5,
6,
7,
11,
12,
13,
14,
18,
19,
20,
21,
25,
26,
27,
28,
1,
2,
3,
4,
8,
9,
10,
11,
15,
16,
17,
18,
22,
23,
],
)
self._test_sunday_ranges(
"0 0 * * Fri-Sun",
[
5,
6,
7,
12,
13,
14,
19,
20,
21,
26,
27,
28,
2,
3,
4,
9,
10,
11,
16,
17,
18,
23,
24,
25,
1,
2,
3,
8,
9,
10,
],
)
self._test_sunday_ranges(
"0 0 * * Sat-Sun",
[
6,
7,
13,
14,
20,
21,
27,
28,
3,
4,
10,
11,
17,
18,
24,
25,
2,
3,
9,
10,
16,
17,
23,
24,
30,
31,
6,
7,
13,
14,
],
)
def test_sunday_ranges_from(self):
self._test_sunday_ranges(
"0 0 * * Sun-Mon",
[
7,
8,
14,
15,
21,
22,
28,
29,
4,
5,
11,
12,
18,
19,
25,
26,
3,
4,
10,
11,
17,
18,
24,
25,
31,
1,
7,
8,
14,
15,
],
)
self._test_sunday_ranges(
"0 0 * * Sun-Tue",
[
2,
7,
8,
9,
14,
15,
16,
21,
22,
23,
28,
29,
30,
4,
5,
6,
11,
12,
13,
18,
19,
20,
25,
26,
27,
3,
4,
5,
10,
11,
],
)
self._test_sunday_ranges(
"0 0 * * Sun-Wed",
[
2,
3,
7,
8,
9,
10,
14,
15,
16,
17,
21,
22,
23,
24,
28,
29,
30,
31,
4,
5,
6,
7,
11,
12,
13,
14,
18,
19,
20,
21,
],
)
self._test_sunday_ranges(
"0 0 * * Sun-Thu",
[
2,
3,
4,
7,
8,
9,
10,
11,
14,
15,
16,
17,
18,
21,
22,
23,
24,
25,
28,
29,
30,
31,
1,
4,
5,
6,
7,
8,
11,
12,
],
)
self._test_sunday_ranges(
"0 0 * * Sun-Fri",
[
2,
3,
4,
5,
7,
8,
9,
10,
11,
12,
14,
15,
16,
17,
18,
19,
21,
22,
23,
24,
25,
26,
28,
29,
30,
31,
1,
2,
4,
5,
],
)
self._test_sunday_ranges(
"0 0 * * Sun-Sat",
[
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
],
)
self._test_sunday_ranges(
"0 0 * * Thu-Tue/2",
[
2,
4,
6,
9,
11,
13,
16,
18,
20,
23,
25,
27,
30,
1,
3,
6,
8,
10,
13,
15,
17,
20,
22,
24,
27,
29,
2,
5,
7,
9,
],
)
self._test_sunday_ranges(
"0 0 * * Thu-Tue/3",
[
4,
7,
11,
14,
18,
21,
25,
28,
1,
4,
8,
11,
15,
18,
22,
25,
29,
3,
7,
10,
14,
17,
21,
24,
28,
31,
4,
7,
11,
14,
],
)
def test_mth_ranges_from(self):
self._test_mth_cron_ranges(
"0 0 1 Jan-Dec *",
[
"24 2",
"24 3",
"24 4",
"24 5",
"24 6",
"24 7",
"24 8",
"24 9",
"24 10",
"24 11",
"24 12",
"25 1",
"25 2",
"25 3",
"25 4",
"25 5",
],
)
self._test_mth_cron_ranges(
"0 0 1 Nov-Mar *",
[
"24 2",
"24 3",
"24 11",
"24 12",
"25 1",
"25 2",
"25 3",
"25 11",
"25 12",
"26 1",
"26 2",
"26 3",
"26 11",
"26 12",
"27 1",
"27 2",
],
)
self._test_mth_cron_ranges(
"0 0 1 Apr-Feb *",
[
"24 2",
"24 4",
"24 5",
"24 6",
"24 7",
"24 8",
"24 9",
"24 10",
"24 11",
"24 12",
"25 1",
"25 2",
"25 4",
"25 5",
"25 6",
"25 7",
],
)
self._test_mth_cron_ranges(
"0 0 1 Apr-Mar/3 *",
[
"24 4",
"24 7",
"24 10",
"25 1",
"25 4",
"25 7",
"25 10",
"26 1",
"26 4",
"26 7",
"26 10",
"27 1",
"27 4",
"27 7",
"27 10",
"28 1",
],
)
self._test_mth_cron_ranges(
"0 0 1 Apr-Mar/2 *",
[
"24 3",
"24 4",
"24 6",
"24 8",
"24 10",
"24 12",
"25 3",
"25 4",
"25 6",
"25 8",
"25 10",
"25 12",
"26 3",
"26 4",
"26 6",
"26 8",
],
)
self._test_mth_cron_ranges(
"0 0 1 Jan-Aug/2 *",
[
"24 3",
"24 5",
"24 7",
"25 1",
"25 3",
"25 5",
"25 7",
"26 1",
"26 3",
"26 5",
"26 7",
"27 1",
"27 3",
"27 5",
"27 7",
"28 1",
],
)
self._test_mth_cron_ranges(
"0 0 1 Jan-Aug/4 *",
[
"24 5",
"25 1",
"25 5",
"26 1",
"26 5",
"27 1",
"27 5",
"28 1",
"28 5",
"29 1",
"29 5",
"30 1",
"30 5",
"31 1",
"31 5",
"32 1",
],
)
def _test_cron_ranges(
self, expr, wanted, generator=None, loops=None, start=None, is_prev=None
):
rets = (generator or gen_x_results)(
expr, loops=loops or 10, start=start or datetime(2024, 1, 1), is_prev=is_prev
)
for ret in rets:
self.assertEqual(wanted, ret)
def _test_mth_cron_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
return self._test_cron_ranges(
expr,
wanted,
generator=gen_x_mth_results,
loops=loops or 16,
start=start,
is_prev=is_prev,
)
def _test_sunday_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
return self._test_cron_ranges(
expr,
wanted,
generator=gen_all_sunday_forms,
loops=loops or 30,
start=start,
is_prev=is_prev,
)
def gen_x_mth_results(expr, loops=None, start=None, is_prev=None):
start = start or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start)
n = cron.get_prev if is_prev else cron.get_next
return [[f"{str(a.year)[-2:]} {a.month}" for a in [n(datetime) for i in range(loops or 16)]]]
def gen_x_results(expr, loops=None, start=None, is_prev=None):
start = start or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start)
n = cron.get_prev if is_prev else cron.get_next
return [[a.isoformat() for a in [n(datetime) for i in range(loops or 30)]]]
def gen_all_sunday_forms(expr, loops=None, start=None, is_prev=None):
start = start or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start)
n = cron.get_prev if is_prev else cron.get_next
ret1 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
cron = croniter(expr.lower().replace("sun", "7"), start_time=start)
n = cron.get_prev if is_prev else cron.get_next
ret2 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
cron = croniter(expr.lower().replace("sun", "0"), start_time=start)
n = cron.get_prev if is_prev else cron.get_next
ret3 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
return ret1, ret2, ret3
if __name__ == "__main__":
unittest.main()