calendarspec: allow repetition values with ranges

"Every other hour from 9 until 5" can be written as
`9..17/2:00` instead of `9,11,13,15,17:00`
This commit is contained in:
Douglas Christman 2016-12-15 20:02:10 -05:00
parent ebc8968bc0
commit a2eb5ea79c
4 changed files with 120 additions and 89 deletions

View file

@ -217,11 +217,12 @@
<para>In the date and time specifications, any component may be <para>In the date and time specifications, any component may be
specified as <literal>*</literal> in which case any value will specified as <literal>*</literal> in which case any value will
match. Alternatively, each component can be specified as a list of match. Alternatively, each component can be specified as a list of
values separated by commas. Values may also be suffixed with values separated by commas. Values may be suffixed with
<literal>/</literal> and a repetition value, which indicates that <literal>/</literal> and a repetition value, which indicates that
the value itself and the value plus all multiples of the repetition value the value itself and the value plus all multiples of the repetition value
are matched. Each component may also contain a range of values are matched. Two values separated by <literal>..</literal> may be used
separated by <literal>..</literal>.</para> to indicate a range of values; ranges may also be followed with
<literal>/</literal> and a repetition value.</para>
<para>A date specification may use <literal>~</literal> to indicate the <para>A date specification may use <literal>~</literal> to indicate the
last day(s) in a month. For example, <literal>*-02~03</literal> means last day(s) in a month. For example, <literal>*-02~03</literal> means
@ -281,7 +282,7 @@ Wed..Sat,Tue 12-10-15 1:2:3 → Tue..Sat 2012-10-15 01:02:03
Sat,Sun 12-05 08:05:40 → Sat,Sun *-12-05 08:05:40 Sat,Sun 12-05 08:05:40 → Sat,Sun *-12-05 08:05:40
Sat,Sun 08:05:40 → Sat,Sun *-*-* 08:05:40 Sat,Sun 08:05:40 → Sat,Sun *-*-* 08:05:40
2003-03-05 05:40 → 2003-03-05 05:40:00 2003-03-05 05:40 → 2003-03-05 05:40:00
05:40:23.4200004/3.1700005 → 05:40:23.420000/3.170001 05:40:23.4200004/3.1700005 → *-*-* 05:40:23.420000/3.170001
2003-02..04-05 → 2003-02..04-05 00:00:00 2003-02..04-05 → 2003-02..04-05 00:00:00
2003-03-05 05:40 UTC → 2003-03-05 05:40:00 UTC 2003-03-05 05:40 UTC → 2003-03-05 05:40:00 UTC
2003-03-05 → 2003-03-05 00:00:00 2003-03-05 → 2003-03-05 00:00:00

View file

@ -33,11 +33,9 @@
#include "parse-util.h" #include "parse-util.h"
#include "string-util.h" #include "string-util.h"
/* Longest valid date/time range is 1970..2199 */ #define BITS_WEEKDAYS 127
#define MAX_RANGE_LEN 230 #define MIN_YEAR 1970
#define MIN_YEAR 1970 #define MAX_YEAR 2199
#define MAX_YEAR 2199
#define BITS_WEEKDAYS 127
static void free_chain(CalendarComponent *c) { static void free_chain(CalendarComponent *c) {
CalendarComponent *n; CalendarComponent *n;
@ -72,6 +70,11 @@ static int component_compare(const void *_a, const void *_b) {
if ((*a)->value > (*b)->value) if ((*a)->value > (*b)->value)
return 1; return 1;
if ((*a)->range_end < (*b)->range_end)
return -1;
if ((*a)->range_end > (*b)->range_end)
return 1;
if ((*a)->repeat < (*b)->repeat) if ((*a)->repeat < (*b)->repeat)
return -1; return -1;
if ((*a)->repeat > (*b)->repeat) if ((*a)->repeat > (*b)->repeat)
@ -80,15 +83,24 @@ static int component_compare(const void *_a, const void *_b) {
return 0; return 0;
} }
static void sort_chain(CalendarComponent **c) { static void normalize_chain(CalendarComponent **c) {
unsigned n = 0, k; unsigned n = 0, k;
CalendarComponent **b, *i, **j, *next; CalendarComponent **b, *i, **j, *next;
assert(c); assert(c);
for (i = *c; i; i = i->next) for (i = *c; i; i = i->next) {
n++; n++;
/*
* While we're counting the chain, also normalize `range_end`
* so the length of the range is a multiple of `repeat`
*/
if (i->range_end > i->value)
i->range_end -= (i->range_end - i->value) % i->repeat;
}
if (n <= 1) if (n <= 1)
return; return;
@ -125,9 +137,15 @@ static void fix_year(CalendarComponent *c) {
if (c->value >= 0 && c->value < 70) if (c->value >= 0 && c->value < 70)
c->value += 2000; c->value += 2000;
if (c->range_end >= 0 && c->range_end < 70)
c->range_end += 2000;
if (c->value >= 70 && c->value < 100) if (c->value >= 70 && c->value < 100)
c->value += 1900; c->value += 1900;
if (c->range_end >= 70 && c->range_end < 100)
c->range_end += 1900;
c = n; c = n;
} }
} }
@ -143,12 +161,12 @@ int calendar_spec_normalize(CalendarSpec *c) {
fix_year(c->year); fix_year(c->year);
sort_chain(&c->year); normalize_chain(&c->year);
sort_chain(&c->month); normalize_chain(&c->month);
sort_chain(&c->day); normalize_chain(&c->day);
sort_chain(&c->hour); normalize_chain(&c->hour);
sort_chain(&c->minute); normalize_chain(&c->minute);
sort_chain(&c->microsecond); normalize_chain(&c->microsecond);
return 0; return 0;
} }
@ -157,20 +175,32 @@ _pure_ static bool chain_valid(CalendarComponent *c, int from, int to, bool end_
if (!c) if (!c)
return true; return true;
/* Forbid dates more than 28 days from the end of the month */
if (end_of_month)
to -= 3;
if (c->value < from || c->value > to) if (c->value < from || c->value > to)
return false; return false;
/* /*
* c->repeat must be short enough so at least one repetition may * c->repeat must be short enough so at least one repetition may
* occur before the end of the interval. For dates scheduled * occur before the end of the interval. For dates scheduled
* relative to the end of the month, c->value corresponds to the * relative to the end of the month, c->value and c->range_end
* Nth last day of the month. * correspond to the Nth last day of the month.
*/ */
if (end_of_month && c->value - c->repeat < from) if (c->range_end >= 0) {
return false; if (c->range_end < from || c ->range_end > to)
return false;
if (!end_of_month && c->value + c->repeat > to) if (c->value + c->repeat > c->range_end)
return false; return false;
} else {
if (end_of_month && c->value - c->repeat < from)
return false;
if (!end_of_month && c->value + c->repeat > to)
return false;
}
if (c->next) if (c->next)
return chain_valid(c->next, from, to, end_of_month); return chain_valid(c->next, from, to, end_of_month);
@ -255,7 +285,6 @@ static void format_weekdays(FILE *f, const CalendarSpec *c) {
} }
static void format_chain(FILE *f, int space, const CalendarComponent *c, bool usec) { static void format_chain(FILE *f, int space, const CalendarComponent *c, bool usec) {
const CalendarComponent *n, *p;
int d = usec ? (int) USEC_PER_SEC : 1; int d = usec ? (int) USEC_PER_SEC : 1;
assert(f); assert(f);
@ -268,31 +297,20 @@ static void format_chain(FILE *f, int space, const CalendarComponent *c, bool us
assert(c->value >= 0); assert(c->value >= 0);
fprintf(f, "%0*i", space, c->value / d); fprintf(f, "%0*i", space, c->value / d);
if (c->value % d != 0) if (c->value % d > 0)
fprintf(f, ".%06i", c->value % d); fprintf(f, ".%06i", c->value % d);
if (c->repeat != 0) if (c->range_end > 0)
fprintf(f, "..%0*i", space, c->range_end / d);
if (c->range_end % d > 0)
fprintf(f, ".%06i", c->range_end % d);
if (c->repeat > 0 && !(c->range_end > 0 && c->repeat == d))
fprintf(f, "/%i", c->repeat / d); fprintf(f, "/%i", c->repeat / d);
if (c->repeat % d != 0) if (c->repeat % d > 0)
fprintf(f, ".%06i", c->repeat % d); fprintf(f, ".%06i", c->repeat % d);
p = c; if (c->next) {
for (;;) {
n = p->next;
if (!n || n->repeat || p->repeat)
break;
if (n->value - p->value != d)
break;
p = n;
}
if (p->value - c->value >= 2 * d) {
fputs("..", f);
format_chain(f, space, p, usec);
} else if (c->next) {
fputc(',', f); fputc(',', f);
format_chain(f, space, c->next, usec); format_chain(f, space, c->next, usec);
} }
@ -531,6 +549,7 @@ static int const_chain(int value, CalendarComponent **c) {
return -ENOMEM; return -ENOMEM;
cc->value = value; cc->value = value;
cc->range_end = -1;
cc->repeat = 0; cc->repeat = 0;
cc->next = *c; cc->next = *c;
@ -540,7 +559,7 @@ static int const_chain(int value, CalendarComponent **c) {
} }
static int prepend_component(const char **p, bool usec, CalendarComponent **c) { static int prepend_component(const char **p, bool usec, CalendarComponent **c) {
unsigned long i, value, range_end, range_inc, repeat = 0; unsigned long value, range_end = -1, repeat = 0;
CalendarComponent *cc; CalendarComponent *cc;
int r; int r;
const char *e; const char *e;
@ -554,6 +573,15 @@ static int prepend_component(const char **p, bool usec, CalendarComponent **c) {
if (r < 0) if (r < 0)
return r; return r;
if (e[0] == '.' && e[1] == '.') {
e += 2;
r = parse_component_decimal(&e, usec, &range_end);
if (r < 0)
return r;
repeat = usec ? USEC_PER_SEC : 1;
}
if (*e == '/') { if (*e == '/') {
e++; e++;
r = parse_component_decimal(&e, usec, &repeat); r = parse_component_decimal(&e, usec, &repeat);
@ -562,30 +590,6 @@ static int prepend_component(const char **p, bool usec, CalendarComponent **c) {
if (repeat == 0) if (repeat == 0)
return -ERANGE; return -ERANGE;
} else if (e[0] == '.' && e[1] == '.') {
e += 2;
r = parse_component_decimal(&e, usec, &range_end);
if (r < 0)
return r;
if (value >= range_end)
return -EINVAL;
range_inc = usec ? USEC_PER_SEC : 1;
/* Don't allow impossibly large ranges... */
if (range_end - value >= MAX_RANGE_LEN * range_inc)
return -EINVAL;
/* ...or ranges with only a single element */
if (range_end - value < range_inc)
return -EINVAL;
for (i = value; i <= range_end; i += range_inc) {
r = const_chain(i, c);
if (r < 0)
return r;
}
} }
if (*e != 0 && *e != ' ' && *e != ',' && *e != '-' && *e != '~' && *e != ':') if (*e != 0 && *e != ' ' && *e != ',' && *e != '-' && *e != '~' && *e != ':')
@ -596,6 +600,7 @@ static int prepend_component(const char **p, bool usec, CalendarComponent **c) {
return -ENOMEM; return -ENOMEM;
cc->value = value; cc->value = value;
cc->range_end = range_end;
cc->repeat = repeat; cc->repeat = repeat;
cc->next = *c; cc->next = *c;
@ -1014,11 +1019,24 @@ fail:
return r; return r;
} }
static int find_end_of_month(struct tm *tm, bool utc, int day)
{
struct tm t = *tm;
t.tm_mon++;
t.tm_mday = 1 - day;
if (mktime_or_timegm(&t, utc) == (time_t) -1 ||
t.tm_mon != tm->tm_mon)
return -1;
return t.tm_mday;
}
static int find_matching_component(const CalendarSpec *spec, const CalendarComponent *c, static int find_matching_component(const CalendarSpec *spec, const CalendarComponent *c,
struct tm *tm, int *val) { struct tm *tm, int *val) {
const CalendarComponent *n, *p = c; const CalendarComponent *n, *p = c;
struct tm t; int v, e, d = -1;
int v, d = -1;
bool d_set = false; bool d_set = false;
int r; int r;
@ -1030,18 +1048,16 @@ static int find_matching_component(const CalendarSpec *spec, const CalendarCompo
while (c) { while (c) {
n = c->next; n = c->next;
if (spec->end_of_month && p == spec->day) { v = c->value;
t = *tm; e = c->range_end;
t.tm_mon++;
t.tm_mday = 1 - c->value;
if (mktime_or_timegm(&t, spec->utc) == (time_t) -1 || if (spec->end_of_month && p == spec->day) {
t.tm_mon != tm->tm_mon) v = find_end_of_month(tm, spec->utc, v);
v = -1; e = find_end_of_month(tm, spec->utc, e);
else
v = t.tm_mday; if (e > 0)
} else SWAP_TWO(v, e);
v = c->value; }
if (v >= *val) { if (v >= *val) {
@ -1053,9 +1069,9 @@ static int find_matching_component(const CalendarSpec *spec, const CalendarCompo
} else if (c->repeat > 0) { } else if (c->repeat > 0) {
int k; int k;
k = v + c->repeat * ((*val - v + c->repeat -1) / c->repeat); k = v + c->repeat * ((*val - v + c->repeat - 1) / c->repeat);
if (!d_set || k < d) { if ((!d_set || k < d) && (e < 0 || k <= e)) {
d = k; d = k;
d_set = true; d_set = true;
} }

View file

@ -29,6 +29,7 @@
typedef struct CalendarComponent { typedef struct CalendarComponent {
int value; int value;
int range_end;
int repeat; int repeat;
struct CalendarComponent *next; struct CalendarComponent *next;

View file

@ -149,8 +149,8 @@ int main(int argc, char* argv[]) {
test_one("*-*-7 0:0:0", "*-*-07 00:00:00"); test_one("*-*-7 0:0:0", "*-*-07 00:00:00");
test_one("10-15", "*-10-15 00:00:00"); test_one("10-15", "*-10-15 00:00:00");
test_one("monday *-12-* 17:00", "Mon *-12-* 17:00:00"); test_one("monday *-12-* 17:00", "Mon *-12-* 17:00:00");
test_one("Mon,Fri *-*-3,1,2 *:30:45", "Mon,Fri *-*-01..03 *:30:45"); test_one("Mon,Fri *-*-3,1,2 *:30:45", "Mon,Fri *-*-01,02,03 *:30:45");
test_one("12,14,13,12:20,10,30", "*-*-* 12..14:10,20,30:00"); test_one("12,14,13,12:20,10,30", "*-*-* 12,13,14:10,20,30:00");
test_one("mon,fri *-1/2-1,3 *:30:45", "Mon,Fri *-01/2-01,03 *:30:45"); test_one("mon,fri *-1/2-1,3 *:30:45", "Mon,Fri *-01/2-01,03 *:30:45");
test_one("03-05 08:05:40", "*-03-05 08:05:40"); test_one("03-05 08:05:40", "*-03-05 08:05:40");
test_one("08:05:40", "*-*-* 08:05:40"); test_one("08:05:40", "*-*-* 08:05:40");
@ -172,13 +172,12 @@ int main(int argc, char* argv[]) {
test_one("2015-10-25 01:00:00 uTc", "2015-10-25 01:00:00 UTC"); test_one("2015-10-25 01:00:00 uTc", "2015-10-25 01:00:00 UTC");
test_one("2016-03-27 03:17:00.4200005", "2016-03-27 03:17:00.420001"); test_one("2016-03-27 03:17:00.4200005", "2016-03-27 03:17:00.420001");
test_one("2016-03-27 03:17:00/0.42", "2016-03-27 03:17:00/0.420000"); test_one("2016-03-27 03:17:00/0.42", "2016-03-27 03:17:00/0.420000");
test_one("2016-03-27 03:17:00/0.42", "2016-03-27 03:17:00/0.420000");
test_one("9..11,13:00,30", "*-*-* 09..11,13:00,30:00"); test_one("9..11,13:00,30", "*-*-* 09..11,13:00,30:00");
test_one("1..3-1..3 1..3:1..3", "*-01..03-01..03 01..03:01..03:00"); test_one("1..3-1..3 1..3:1..3", "*-01..03-01..03 01..03:01..03:00");
test_one("00:00:1.125..2.125", "*-*-* 00:00:01.125000,02.125000"); 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..03"); test_one("00:00:1.0..3.8", "*-*-* 00:00:01..03");
test_one("00:00:01..03", "*-*-* 00:00:01..03"); test_one("00:00:01..03", "*-*-* 00:00:01..03");
test_one("00:00:01/2,02..03", "*-*-* 00:00:01/2,02,03"); test_one("00:00:01/2,02..03", "*-*-* 00:00:01/2,02..03");
test_one("*-*~1 Utc", "*-*~01 00:00:00 UTC"); test_one("*-*~1 Utc", "*-*~01 00:00:00 UTC");
test_one("*-*~05,3 ", "*-*~03,05 00:00:00"); test_one("*-*~05,3 ", "*-*~03,05 00:00:00");
test_one("*-*~* 00:00:00", "*-*-* 00:00:00"); test_one("*-*~* 00:00:00", "*-*-* 00:00:00");
@ -189,6 +188,10 @@ int main(int argc, char* argv[]) {
test_one("*:*", "*-*-* *:*:00"); test_one("*:*", "*-*-* *:*:00");
test_one("12:*", "*-*-* 12:*:00"); test_one("12:*", "*-*-* 12:*:00");
test_one("*:30", "*-*-* *:30:00"); test_one("*:30", "*-*-* *:30:00");
test_one("93..00-*-*", "1993..2000-*-* 00:00:00");
test_one("00..07-*-*", "2000..2007-*-* 00:00:00");
test_one("*:20..39/5", "*-*-* *:20..35/5:00");
test_one("00:00:20..40/1", "*-*-* 00:00:20..40");
test_next("2016-03-27 03:17:00", "", 12345, 1459048620000000); test_next("2016-03-27 03:17:00", "", 12345, 1459048620000000);
test_next("2016-03-27 03:17:00", "CET", 12345, 1459041420000000); test_next("2016-03-27 03:17:00", "CET", 12345, 1459041420000000);
@ -207,6 +210,9 @@ int main(int argc, char* argv[]) {
test_next("2016-02~01 UTC", "", 12345, 1456704000000000); test_next("2016-02~01 UTC", "", 12345, 1456704000000000);
test_next("Mon 2017-05~01..07 UTC", "", 12345, 1496016000000000); test_next("Mon 2017-05~01..07 UTC", "", 12345, 1496016000000000);
test_next("Mon 2017-05~07/1 UTC", "", 12345, 1496016000000000); test_next("Mon 2017-05~07/1 UTC", "", 12345, 1496016000000000);
test_next("2017-08-06 9,11,13,15,17:00 UTC", "", 1502029800000000, 1502031600000000);
test_next("2017-08-06 9..17/2:00 UTC", "", 1502029800000000, 1502031600000000);
test_next("2016-12-* 3..21/6:00 UTC", "", 1482613200000001, 1482634800000000);
assert_se(calendar_spec_from_string("test", &c) < 0); assert_se(calendar_spec_from_string("test", &c) < 0);
assert_se(calendar_spec_from_string(" utc", &c) < 0); assert_se(calendar_spec_from_string(" utc", &c) < 0);
@ -225,6 +231,13 @@ int main(int argc, char* argv[]) {
assert_se(calendar_spec_from_string("-00:+00/-5", &c) < 0); assert_se(calendar_spec_from_string("-00:+00/-5", &c) < 0);
assert_se(calendar_spec_from_string("00:+00/-5", &c) < 0); assert_se(calendar_spec_from_string("00:+00/-5", &c) < 0);
assert_se(calendar_spec_from_string("2016- 11- 24 12: 30: 00", &c) < 0); assert_se(calendar_spec_from_string("2016- 11- 24 12: 30: 00", &c) < 0);
assert_se(calendar_spec_from_string("*~29", &c) < 0);
assert_se(calendar_spec_from_string("*~16..31", &c) < 0);
assert_se(calendar_spec_from_string("12..1/2-*", &c) < 0);
assert_se(calendar_spec_from_string("*:05..05", &c) < 0);
assert_se(calendar_spec_from_string("*:05..10/6", &c) < 0);
assert_se(calendar_spec_from_string("20/4:00", &c) < 0);
assert_se(calendar_spec_from_string("00:00/60", &c) < 0);
test_timestamp(); test_timestamp();
test_hourly_bug_4031(); test_hourly_bug_4031();