calendarspec: add support for scheduling timers at the end of the month
authorDouglas Christman <DouglasChristman@gmail.com>
Tue, 22 Nov 2016 15:05:10 +0000 (10:05 -0500)
committerDouglas Christman <DouglasChristman@gmail.com>
Wed, 23 Nov 2016 17:37:43 +0000 (12:37 -0500)
"*-*~1"       => The last day of every month
"*-02~3..5"   => The third, fourth, and fifth last days in February
"Mon 05~07/1" => The last Monday in May

Resolves #3861

TODO
man/systemd.time.xml
src/basic/calendarspec.c
src/basic/calendarspec.h
src/test/test-calendarspec.c

diff --git a/TODO b/TODO
index 7cc8c9d..93f0500 100644 (file)
--- a/TODO
+++ b/TODO
@@ -585,7 +585,6 @@ Features:
   - timer units should get the ability to trigger when:
     o CLOCK_REALTIME makes jumps (TFD_TIMER_CANCEL_ON_SET)
     o DST changes
-  - Support 2012-02~4 as syntax for specifying the fourth to last day of the month.
   - Modulate timer frequency based on battery state
 
 * add libsystemd-password or so to query passwords during boot using the password agent logic
index 47229b4..fb13ea5 100644 (file)
     are matched.  Each component may also contain a range of values
     separated by <literal>..</literal>.</para>
 
+    <para>A date specification may use <literal>~</literal> to indicate the
+    last day(s) in a month. For example, <literal>*-02~03</literal> means
+    "the third last day in February," and <literal>Mon *-05~07/1</literal>
+    means "the last Monday in May."</para>
+
     <para>The seconds component may contain decimal fractions both in
     the value and the repetition. All fractions are rounded to 6
     decimal places.</para>
index 2fc5ceb..359bb16 100644 (file)
@@ -137,6 +137,9 @@ int calendar_spec_normalize(CalendarSpec *c) {
         if (c->weekdays_bits <= 0 || c->weekdays_bits >= BITS_WEEKDAYS)
                 c->weekdays_bits = -1;
 
+        if (c->end_of_month && !c->day)
+                c->end_of_month = false;
+
         fix_year(c->year);
 
         sort_chain(&c->year);
@@ -149,18 +152,27 @@ int calendar_spec_normalize(CalendarSpec *c) {
         return 0;
 }
 
-_pure_ static bool chain_valid(CalendarComponent *c, int from, int to) {
+_pure_ static bool chain_valid(CalendarComponent *c, int from, int to, bool eom) {
         if (!c)
                 return true;
 
         if (c->value < from || c->value > to)
                 return false;
 
-        if (c->value + c->repeat > to)
+        /*
+         * c->repeat must be short enough so at least one repetition may
+         * occur before the end of the interval.  For dates scheduled
+         * relative to the end of the month (eom), c->value corresponds
+         * to the Nth last day of the month.
+         */
+        if (eom && c->value - c->repeat < from)
+                return false;
+
+        if (!eom && c->value + c->repeat > to)
                 return false;
 
         if (c->next)
-                return chain_valid(c->next, from, to);
+                return chain_valid(c->next, from, to, eom);
 
         return true;
 }
@@ -171,22 +183,22 @@ _pure_ bool calendar_spec_valid(CalendarSpec *c) {
         if (c->weekdays_bits > BITS_WEEKDAYS)
                 return false;
 
-        if (!chain_valid(c->year, MIN_YEAR, MAX_YEAR))
+        if (!chain_valid(c->year, MIN_YEAR, MAX_YEAR, false))
                 return false;
 
-        if (!chain_valid(c->month, 1, 12))
+        if (!chain_valid(c->month, 1, 12, false))
                 return false;
 
-        if (!chain_valid(c->day, 1, 31))
+        if (!chain_valid(c->day, 1, 31, c->end_of_month))
                 return false;
 
-        if (!chain_valid(c->hour, 0, 23))
+        if (!chain_valid(c->hour, 0, 23, false))
                 return false;
 
-        if (!chain_valid(c->minute, 0, 59))
+        if (!chain_valid(c->minute, 0, 59, false))
                 return false;
 
-        if (!chain_valid(c->microsecond, 0, 60*USEC_PER_SEC-1))
+        if (!chain_valid(c->microsecond, 0, 60*USEC_PER_SEC-1, false))
                 return false;
 
         return true;
@@ -293,7 +305,7 @@ int calendar_spec_to_string(const CalendarSpec *c, char **p) {
         format_chain(f, 4, c->year, false);
         fputc('-', f);
         format_chain(f, 2, c->month, false);
-        fputc('-', f);
+        fputc(c->end_of_month ? '~' : '-', f);
         format_chain(f, 2, c->day, false);
         fputc(' ', f);
         format_chain(f, 2, c->hour, false);
@@ -542,7 +554,7 @@ static int prepend_component(const char **p, bool usec, CalendarComponent **c) {
                 }
         }
 
-        if (*e != 0 && *e != ' ' && *e != ',' && *e != '-' && *e != ':')
+        if (*e != 0 && *e != ' ' && *e != ',' && *e != '-' && *e != '~' && *e != ':')
                 return -EINVAL;
 
         cc = new0(CalendarComponent, 1);
@@ -622,7 +634,9 @@ static int parse_date(const char **p, CalendarSpec *c) {
                 return 0;
         }
 
-        if (*t != '-') {
+        if (*t == '~')
+                c->end_of_month = true;
+        else if (*t != '-') {
                 free_chain(first);
                 return -EINVAL;
         }
@@ -640,9 +654,12 @@ static int parse_date(const char **p, CalendarSpec *c) {
                 c->month = first;
                 c->day = second;
                 return 0;
-        }
+        } else if (c->end_of_month)
+                return -EINVAL;
 
-        if (*t != '-') {
+        if (*t == '~')
+                c->end_of_month = true;
+        else if (*t != '-') {
                 free_chain(first);
                 free_chain(second);
                 return -EINVAL;
@@ -656,7 +673,7 @@ static int parse_date(const char **p, CalendarSpec *c) {
                 return r;
         }
 
-        /* Got tree parts, hence it is year, month and day */
+        /* Got three parts, hence it is year, month and day */
         if (*t == ' ' || *t == 0) {
                 *p = t + strspn(t, " ");
                 c->year = first;
@@ -967,9 +984,11 @@ fail:
         return r;
 }
 
-static int find_matching_component(const CalendarComponent *c, int *val) {
-        const CalendarComponent *n;
-        int d = -1;
+static int find_matching_component(const CalendarSpec *spec, const CalendarComponent *c,
+                                   struct tm *tm, int *val) {
+        const CalendarComponent *n, *p = c;
+        struct tm t;
+        int v, d = -1;
         bool d_set = false;
         int r;
 
@@ -981,17 +1000,30 @@ static int find_matching_component(const CalendarComponent *c, int *val) {
         while (c) {
                 n = c->next;
 
-                if (c->value >= *val) {
+                if (spec->end_of_month && p == spec->day) {
+                        t = *tm;
+                        t.tm_mon++;
+                        t.tm_mday = 1 - c->value;
+
+                        if (mktime_or_timegm(&t, spec->utc) == (time_t) -1 ||
+                            t.tm_mon != tm->tm_mon)
+                                v = -1;
+                        else
+                                v = t.tm_mday;
+                } else
+                        v = c->value;
+
+                if (v >= *val) {
 
-                        if (!d_set || c->value < d) {
-                                d = c->value;
+                        if (!d_set || v < d) {
+                                d = v;
                                 d_set = true;
                         }
 
                 } else if (c->repeat > 0) {
                         int k;
 
-                        k = c->value + c->repeat * ((*val - c->value + c->repeat -1) / c->repeat);
+                        k = v + c->repeat * ((*val - v + c->repeat -1) / c->repeat);
 
                         if (!d_set || k < d) {
                                 d = k;
@@ -1069,7 +1101,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                 c.tm_isdst = spec->dst;
 
                 c.tm_year += 1900;
-                r = find_matching_component(spec->year, &c.tm_year);
+                r = find_matching_component(spec, spec->year, &c, &c.tm_year);
                 c.tm_year -= 1900;
 
                 if (r > 0) {
@@ -1083,7 +1115,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                         return -ENOENT;
 
                 c.tm_mon += 1;
-                r = find_matching_component(spec->month, &c.tm_mon);
+                r = find_matching_component(spec, spec->month, &c, &c.tm_mon);
                 c.tm_mon -= 1;
 
                 if (r > 0) {
@@ -1098,7 +1130,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                         continue;
                 }
 
-                r = find_matching_component(spec->day, &c.tm_mday);
+                r = find_matching_component(spec, spec->day, &c, &c.tm_mday);
                 if (r > 0)
                         c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0;
                 if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
@@ -1114,7 +1146,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                         continue;
                 }
 
-                r = find_matching_component(spec->hour, &c.tm_hour);
+                r = find_matching_component(spec, spec->hour, &c, &c.tm_hour);
                 if (r > 0)
                         c.tm_min = c.tm_sec = tm_usec = 0;
                 if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
@@ -1123,7 +1155,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                         continue;
                 }
 
-                r = find_matching_component(spec->minute, &c.tm_min);
+                r = find_matching_component(spec, spec->minute, &c, &c.tm_min);
                 if (r > 0)
                         c.tm_sec = tm_usec = 0;
                 if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
@@ -1133,7 +1165,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                 }
 
                 c.tm_sec = c.tm_sec * USEC_PER_SEC + tm_usec;
-                r = find_matching_component(spec->microsecond, &c.tm_sec);
+                r = find_matching_component(spec, spec->microsecond, &c, &c.tm_sec);
                 tm_usec = c.tm_sec % USEC_PER_SEC;
                 c.tm_sec /= USEC_PER_SEC;
 
index c608722..78af274 100644 (file)
@@ -36,6 +36,7 @@ typedef struct CalendarComponent {
 
 typedef struct CalendarSpec {
         int weekdays_bits;
+        bool end_of_month;
         bool utc;
         int dst;
 
index 18e46f2..76f5819 100644 (file)
@@ -176,6 +176,9 @@ int main(int argc, char* argv[]) {
         test_one("1..3-1..3 1..3:1..3", "*-01,02,03-01,02,03 01,02,03:01,02,03:00");
         test_one("00:00:1.125..2.125", "*-*-* 00:00:01.125000,02.125000");
         test_one("00:00:1.0..3.8", "*-*-* 00:00:01,02,03");
+        test_one("*-*~1 Utc", "*-*~01 00:00:00 UTC");
+        test_one("*-*~05,3 ", "*-*~03,05 00:00:00");
+        test_one("*-*~* 00:00:00", "*-*-* 00:00:00");
 
         test_next("2016-03-27 03:17:00", "", 12345, 1459048620000000);
         test_next("2016-03-27 03:17:00", "CET", 12345, 1459041420000000);
@@ -191,6 +194,9 @@ int main(int argc, char* argv[]) {
         test_next("2015-11-13 09:11:23.42/1.77", "EET", 1447398683419999, 1447398683420000);
         test_next("Sun 16:00:00", "CET", 1456041600123456, 1456066800000000);
         test_next("*-04-31", "", 12345, -1);
+        test_next("2016-02~01 UTC", "", 12345, 1456704000000000);
+        test_next("Mon 2017-05~01..07 UTC", "", 12345, 1496016000000000);
+        test_next("Mon 2017-05~07/1 UTC", "", 12345, 1496016000000000);
 
         assert_se(calendar_spec_from_string("test", &c) < 0);
         assert_se(calendar_spec_from_string("", &c) < 0);
@@ -200,6 +206,8 @@ int main(int argc, char* argv[]) {
         assert_se(calendar_spec_from_string("2000-03-05 00:00.1:00", &c) < 0);
         assert_se(calendar_spec_from_string("00:00:00/0.00000001", &c) < 0);
         assert_se(calendar_spec_from_string("00:00:00.0..00.9", &c) < 0);
+        assert_se(calendar_spec_from_string("2016~11-22", &c) < 0);
+        assert_se(calendar_spec_from_string("*-*~5/5", &c) < 0);
 
         test_timestamp();
         test_hourly_bug_4031();