Skip to content

Commit 9924511

Browse files
authored
Merge pull request systemd#4703 from dobyrch/calendar-offset
calendarspec: add support for scheduling timers at the end of the month
2 parents 331d6a2 + 8ea8035 commit 9924511

File tree

5 files changed

+85
-29
lines changed

5 files changed

+85
-29
lines changed

TODO

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,6 @@ Features:
585585
- timer units should get the ability to trigger when:
586586
o CLOCK_REALTIME makes jumps (TFD_TIMER_CANCEL_ON_SET)
587587
o DST changes
588-
- Support 2012-02~4 as syntax for specifying the fourth to last day of the month.
589588
- Modulate timer frequency based on battery state
590589

591590
* add libsystemd-password or so to query passwords during boot using the password agent logic

man/systemd.time.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@
223223
are matched. Each component may also contain a range of values
224224
separated by <literal>..</literal>.</para>
225225

226+
<para>A date specification may use <literal>~</literal> to indicate the
227+
last day(s) in a month. For example, <literal>*-02~03</literal> means
228+
"the third last day in February," and <literal>Mon *-05~07/1</literal>
229+
means "the last Monday in May."</para>
230+
226231
<para>The seconds component may contain decimal fractions both in
227232
the value and the repetition. All fractions are rounded to 6
228233
decimal places.</para>

src/basic/calendarspec.c

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
/* Longest valid date/time range is 1970..2199 */
3636
#define MAX_RANGE_LEN 230
37+
#define MIN_YEAR 1970
38+
#define MAX_YEAR 2199
3739
#define BITS_WEEKDAYS 127
3840

3941
static void free_chain(CalendarComponent *c) {
@@ -135,6 +137,9 @@ int calendar_spec_normalize(CalendarSpec *c) {
135137
if (c->weekdays_bits <= 0 || c->weekdays_bits >= BITS_WEEKDAYS)
136138
c->weekdays_bits = -1;
137139

140+
if (c->end_of_month && !c->day)
141+
c->end_of_month = false;
142+
138143
fix_year(c->year);
139144

140145
sort_chain(&c->year);
@@ -147,18 +152,27 @@ int calendar_spec_normalize(CalendarSpec *c) {
147152
return 0;
148153
}
149154

150-
_pure_ static bool chain_valid(CalendarComponent *c, int from, int to) {
155+
_pure_ static bool chain_valid(CalendarComponent *c, int from, int to, bool eom) {
151156
if (!c)
152157
return true;
153158

154159
if (c->value < from || c->value > to)
155160
return false;
156161

157-
if (c->value + c->repeat > to)
162+
/*
163+
* c->repeat must be short enough so at least one repetition may
164+
* occur before the end of the interval. For dates scheduled
165+
* relative to the end of the month (eom), c->value corresponds
166+
* to the Nth last day of the month.
167+
*/
168+
if (eom && c->value - c->repeat < from)
169+
return false;
170+
171+
if (!eom && c->value + c->repeat > to)
158172
return false;
159173

160174
if (c->next)
161-
return chain_valid(c->next, from, to);
175+
return chain_valid(c->next, from, to, eom);
162176

163177
return true;
164178
}
@@ -169,22 +183,22 @@ _pure_ bool calendar_spec_valid(CalendarSpec *c) {
169183
if (c->weekdays_bits > BITS_WEEKDAYS)
170184
return false;
171185

172-
if (!chain_valid(c->year, 1970, 2199))
186+
if (!chain_valid(c->year, MIN_YEAR, MAX_YEAR, false))
173187
return false;
174188

175-
if (!chain_valid(c->month, 1, 12))
189+
if (!chain_valid(c->month, 1, 12, false))
176190
return false;
177191

178-
if (!chain_valid(c->day, 1, 31))
192+
if (!chain_valid(c->day, 1, 31, c->end_of_month))
179193
return false;
180194

181-
if (!chain_valid(c->hour, 0, 23))
195+
if (!chain_valid(c->hour, 0, 23, false))
182196
return false;
183197

184-
if (!chain_valid(c->minute, 0, 59))
198+
if (!chain_valid(c->minute, 0, 59, false))
185199
return false;
186200

187-
if (!chain_valid(c->microsecond, 0, 60*USEC_PER_SEC-1))
201+
if (!chain_valid(c->microsecond, 0, 60*USEC_PER_SEC-1, false))
188202
return false;
189203

190204
return true;
@@ -291,7 +305,7 @@ int calendar_spec_to_string(const CalendarSpec *c, char **p) {
291305
format_chain(f, 4, c->year, false);
292306
fputc('-', f);
293307
format_chain(f, 2, c->month, false);
294-
fputc('-', f);
308+
fputc(c->end_of_month ? '~' : '-', f);
295309
format_chain(f, 2, c->day, false);
296310
fputc(' ', f);
297311
format_chain(f, 2, c->hour, false);
@@ -540,7 +554,7 @@ static int prepend_component(const char **p, bool usec, CalendarComponent **c) {
540554
}
541555
}
542556

543-
if (*e != 0 && *e != ' ' && *e != ',' && *e != '-' && *e != ':')
557+
if (*e != 0 && *e != ' ' && *e != ',' && *e != '-' && *e != '~' && *e != ':')
544558
return -EINVAL;
545559

546560
cc = new0(CalendarComponent, 1);
@@ -620,7 +634,9 @@ static int parse_date(const char **p, CalendarSpec *c) {
620634
return 0;
621635
}
622636

623-
if (*t != '-') {
637+
if (*t == '~')
638+
c->end_of_month = true;
639+
else if (*t != '-') {
624640
free_chain(first);
625641
return -EINVAL;
626642
}
@@ -638,9 +654,12 @@ static int parse_date(const char **p, CalendarSpec *c) {
638654
c->month = first;
639655
c->day = second;
640656
return 0;
641-
}
657+
} else if (c->end_of_month)
658+
return -EINVAL;
642659

643-
if (*t != '-') {
660+
if (*t == '~')
661+
c->end_of_month = true;
662+
else if (*t != '-') {
644663
free_chain(first);
645664
free_chain(second);
646665
return -EINVAL;
@@ -654,7 +673,7 @@ static int parse_date(const char **p, CalendarSpec *c) {
654673
return r;
655674
}
656675

657-
/* Got tree parts, hence it is year, month and day */
676+
/* Got three parts, hence it is year, month and day */
658677
if (*t == ' ' || *t == 0) {
659678
*p = t + strspn(t, " ");
660679
c->year = first;
@@ -965,9 +984,11 @@ int calendar_spec_from_string(const char *p, CalendarSpec **spec) {
965984
return r;
966985
}
967986

968-
static int find_matching_component(const CalendarComponent *c, int *val) {
969-
const CalendarComponent *n;
970-
int d = -1;
987+
static int find_matching_component(const CalendarSpec *spec, const CalendarComponent *c,
988+
struct tm *tm, int *val) {
989+
const CalendarComponent *n, *p = c;
990+
struct tm t;
991+
int v, d = -1;
971992
bool d_set = false;
972993
int r;
973994

@@ -979,17 +1000,30 @@ static int find_matching_component(const CalendarComponent *c, int *val) {
9791000
while (c) {
9801001
n = c->next;
9811002

982-
if (c->value >= *val) {
1003+
if (spec->end_of_month && p == spec->day) {
1004+
t = *tm;
1005+
t.tm_mon++;
1006+
t.tm_mday = 1 - c->value;
1007+
1008+
if (mktime_or_timegm(&t, spec->utc) == (time_t) -1 ||
1009+
t.tm_mon != tm->tm_mon)
1010+
v = -1;
1011+
else
1012+
v = t.tm_mday;
1013+
} else
1014+
v = c->value;
1015+
1016+
if (v >= *val) {
9831017

984-
if (!d_set || c->value < d) {
985-
d = c->value;
1018+
if (!d_set || v < d) {
1019+
d = v;
9861020
d_set = true;
9871021
}
9881022

9891023
} else if (c->repeat > 0) {
9901024
int k;
9911025

992-
k = c->value + c->repeat * ((*val - c->value + c->repeat -1) / c->repeat);
1026+
k = v + c->repeat * ((*val - v + c->repeat -1) / c->repeat);
9931027

9941028
if (!d_set || k < d) {
9951029
d = k;
@@ -1017,6 +1051,14 @@ static bool tm_out_of_bounds(const struct tm *tm, bool utc) {
10171051
if (mktime_or_timegm(&t, utc) == (time_t) -1)
10181052
return true;
10191053

1054+
/*
1055+
* Set an upper bound on the year so impossible dates like "*-02-31"
1056+
* don't cause find_next() to loop forever. tm_year contains years
1057+
* since 1900, so adjust it accordingly.
1058+
*/
1059+
if (tm->tm_year + 1900 > MAX_YEAR)
1060+
return true;
1061+
10201062
/* Did any normalization take place? If so, it was out of bounds before */
10211063
return
10221064
t.tm_year != tm->tm_year ||
@@ -1059,7 +1101,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
10591101
c.tm_isdst = spec->dst;
10601102

10611103
c.tm_year += 1900;
1062-
r = find_matching_component(spec->year, &c.tm_year);
1104+
r = find_matching_component(spec, spec->year, &c, &c.tm_year);
10631105
c.tm_year -= 1900;
10641106

10651107
if (r > 0) {
@@ -1073,7 +1115,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
10731115
return -ENOENT;
10741116

10751117
c.tm_mon += 1;
1076-
r = find_matching_component(spec->month, &c.tm_mon);
1118+
r = find_matching_component(spec, spec->month, &c, &c.tm_mon);
10771119
c.tm_mon -= 1;
10781120

10791121
if (r > 0) {
@@ -1088,7 +1130,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
10881130
continue;
10891131
}
10901132

1091-
r = find_matching_component(spec->day, &c.tm_mday);
1133+
r = find_matching_component(spec, spec->day, &c, &c.tm_mday);
10921134
if (r > 0)
10931135
c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0;
10941136
if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
@@ -1104,7 +1146,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
11041146
continue;
11051147
}
11061148

1107-
r = find_matching_component(spec->hour, &c.tm_hour);
1149+
r = find_matching_component(spec, spec->hour, &c, &c.tm_hour);
11081150
if (r > 0)
11091151
c.tm_min = c.tm_sec = tm_usec = 0;
11101152
if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
@@ -1113,7 +1155,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
11131155
continue;
11141156
}
11151157

1116-
r = find_matching_component(spec->minute, &c.tm_min);
1158+
r = find_matching_component(spec, spec->minute, &c, &c.tm_min);
11171159
if (r > 0)
11181160
c.tm_sec = tm_usec = 0;
11191161
if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
@@ -1123,7 +1165,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
11231165
}
11241166

11251167
c.tm_sec = c.tm_sec * USEC_PER_SEC + tm_usec;
1126-
r = find_matching_component(spec->microsecond, &c.tm_sec);
1168+
r = find_matching_component(spec, spec->microsecond, &c, &c.tm_sec);
11271169
tm_usec = c.tm_sec % USEC_PER_SEC;
11281170
c.tm_sec /= USEC_PER_SEC;
11291171

src/basic/calendarspec.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ typedef struct CalendarComponent {
3636

3737
typedef struct CalendarSpec {
3838
int weekdays_bits;
39+
bool end_of_month;
3940
bool utc;
4041
int dst;
4142

src/test/test-calendarspec.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ int main(int argc, char* argv[]) {
176176
test_one("1..3-1..3 1..3:1..3", "*-01,02,03-01,02,03 01,02,03:01,02,03:00");
177177
test_one("00:00:1.125..2.125", "*-*-* 00:00:01.125000,02.125000");
178178
test_one("00:00:1.0..3.8", "*-*-* 00:00:01,02,03");
179+
test_one("*-*~1 Utc", "*-*~01 00:00:00 UTC");
180+
test_one("*-*~05,3 ", "*-*~03,05 00:00:00");
181+
test_one("*-*~* 00:00:00", "*-*-* 00:00:00");
179182

180183
test_next("2016-03-27 03:17:00", "", 12345, 1459048620000000);
181184
test_next("2016-03-27 03:17:00", "CET", 12345, 1459041420000000);
@@ -190,6 +193,10 @@ int main(int argc, char* argv[]) {
190193
test_next("2015-11-13 09:11:23.42/1.77", "EET", 1447398683420000, 1447398685190000);
191194
test_next("2015-11-13 09:11:23.42/1.77", "EET", 1447398683419999, 1447398683420000);
192195
test_next("Sun 16:00:00", "CET", 1456041600123456, 1456066800000000);
196+
test_next("*-04-31", "", 12345, -1);
197+
test_next("2016-02~01 UTC", "", 12345, 1456704000000000);
198+
test_next("Mon 2017-05~01..07 UTC", "", 12345, 1496016000000000);
199+
test_next("Mon 2017-05~07/1 UTC", "", 12345, 1496016000000000);
193200

194201
assert_se(calendar_spec_from_string("test", &c) < 0);
195202
assert_se(calendar_spec_from_string("", &c) < 0);
@@ -199,6 +206,8 @@ int main(int argc, char* argv[]) {
199206
assert_se(calendar_spec_from_string("2000-03-05 00:00.1:00", &c) < 0);
200207
assert_se(calendar_spec_from_string("00:00:00/0.00000001", &c) < 0);
201208
assert_se(calendar_spec_from_string("00:00:00.0..00.9", &c) < 0);
209+
assert_se(calendar_spec_from_string("2016~11-22", &c) < 0);
210+
assert_se(calendar_spec_from_string("*-*~5/5", &c) < 0);
202211

203212
test_timestamp();
204213
test_hourly_bug_4031();

0 commit comments

Comments
 (0)