Repository: pallets-eco/croniter

Issue:
support question mark
many  of crontab implemenation support question mark on DayOfWeek and DayOfMonth field:

- https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm
- https://en.wikipedia.org/wiki/Cron
- https://techdocs.broadcom.com/us/en/symantec-security-software/endpoint-security-and-management/cloud-workload-protection-for-storage/1-0/Scan_Configuration_7/about-special-characters-in-cron-expressions-v123769576-d4995e65374.html

if croniter supports question mark as well, it will be bettter to cover more cases.

Hints from issue comments:
@kiorky: If you want such a feature, i'm not against, but you ll have to submit a PR for :-).@kiorky: released as 2.0.6@yyscamper: thanks for @JabberWocky-22

Solution patch:
diff --git a/src/croniter/croniter.py b/src/croniter/croniter.py
index 50fecf6..12cf3b3 100644
--- a/src/croniter/croniter.py
+++ b/src/croniter/croniter.py
@@ -665,6 +665,18 @@ class croniter(object):
             for expanderid, expander in EXPANDERS.items():
                 expr = expander(cls).expand(efl, i, expr, hash_id=hash_id)
 
+            if "?" in expr:
+                if expr != "?":
+                    raise CroniterBadCronError(
+                        "[{0}] is not acceptable.  Question mark can not "
+                        "used with other characters".format(expr_format))
+                if i not in [2, 4]:
+                    raise CroniterBadCronError(
+                        "[{0}] is not acceptable.  Question mark can only used "
+                        "in day_of_month or day_of_week".format(expr_format))
+                # currently just trade `?` as `*`
+                expr = "*"
+
             e_list = expr.split(',')
             res = []
 


Test patch:
diff --git a/src/croniter/tests/test_croniter.py b/src/croniter/tests/test_croniter.py
index 2e03c2d..58a8a51 100755
--- a/src/croniter/tests/test_croniter.py
+++ b/src/croniter/tests/test_croniter.py
@@ -1456,6 +1456,21 @@ class CroniterTest(base.TestCase):
         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_issue_47(self):
         base = datetime(2021, 3, 30, 4, 0)
         itr = croniter('0 6 30 3 *', base)