diff --git a/man/systemd.time.xml b/man/systemd.time.xml
index 820e5e499ec..6dbf5db7341 100644
--- a/man/systemd.time.xml
+++ b/man/systemd.time.xml
@@ -255,6 +255,13 @@ tomorrow Pacific/Auckland → Thu 2012-11-23 19:00:00
the local timezone, similar to the supported syntax of timestamps (see above), or the timezone
in the IANA timezone database format (also see above).
+ There is a special format for periodic events in the form
+ «timestamp» and every «timespan» to describe general
+ periodic intervals. For example timespan 2w, 3w, 2d…
+ The reference point for such an interval must be a single point in time
+ (timestamp). This syntax can't be combined with previously described wild-card
+ formats. The minimum value for time period is 60s.
+
The following special expressions may be used as shorthands for longer normalized forms:
minutely → *-*-* *:*:00
@@ -302,7 +309,8 @@ Wed..Sat,Tue 12-10-15 1:2:3 → Tue..Sat 2012-10-15 01:02:03
weekly Pacific/Auckland → Mon *-*-* 00:00:00 Pacific/Auckland
yearly → *-01-01 00:00:00
annually → *-01-01 00:00:00
- *:2/3 → *-*-* *:02/3:00
+ *:2/3 → *-*-* *:02/3:00
+ 25-1-1 10 CET and every 2w → 2025-01-01 09:00:00 UTC and every 2w
Calendar events are used by timer units, see
systemd.timer5
diff --git a/src/shared/calendarspec.c b/src/shared/calendarspec.c
index 5fdae235366..0a51b887d38 100644
--- a/src/shared/calendarspec.c
+++ b/src/shared/calendarspec.c
@@ -26,6 +26,7 @@
#define BITS_WEEKDAYS 127
#define MIN_YEAR 1970
#define MAX_YEAR 2199
+#define MIN_PERIOD (60 * USEC_PER_SEC)
/* An arbitrary limit on the length of the chains of components. We don't want to
* build a very long linked list, which would be slow to iterate over and might cause
@@ -382,6 +383,13 @@ int calendar_spec_to_string(const CalendarSpec *c, char **ret) {
}
}
+ if (c->period) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ fputs(" and every ", f);
+ (void) format_timespan(buf, sizeof(buf), c->period, 0);
+ fputs(buf, f);
+ }
+
return memstream_finalize(&m, ret, NULL);
}
@@ -620,6 +628,39 @@ static int calendarspec_from_time_t(CalendarSpec *c, time_t time) {
return 0;
}
+static bool is_single_value_component(CalendarComponent *cc) {
+ assert(cc);
+ return cc->stop == -1 && cc->repeat == 0 && cc->next == NULL;
+}
+
+static int calendarspec_to_usec_t(const CalendarSpec *c, usec_t *t) {
+ struct tm tm;
+ int r;
+
+ assert(c);
+ assert(is_single_value_component(c->year));
+ assert(is_single_value_component(c->month));
+ assert(is_single_value_component(c->day));
+ assert(is_single_value_component(c->hour));
+ assert(is_single_value_component(c->minute));
+ assert(is_single_value_component(c->microsecond));
+
+ tm.tm_year = c->year->start - 1900;
+ tm.tm_mon = c->month->start - 1;
+ tm.tm_mday = c->day->start;
+ tm.tm_hour = c->hour->start;
+ tm.tm_min = c->minute->start;
+ tm.tm_sec = c->microsecond->start / USEC_PER_SEC;
+ tm.tm_isdst = -1;
+
+ r = mktime_or_timegm_usec(&tm, c->utc, t);
+ if (r < 0)
+ return r;
+ if (t)
+ *t += c->microsecond->start % USEC_PER_SEC;
+ return 0;
+}
+
static int prepend_component(const char **p, bool usec, unsigned nesting, CalendarComponent **c) {
int r, start, stop = -1, repeat = 0;
CalendarComponent *cc;
@@ -872,6 +913,38 @@ finish:
return 0;
}
+static int parse_and_every(const char *s, usec_t *ts, usec_t *rep) {
+ _cleanup_free_ char *p_ts = NULL;
+ int i, r;
+
+ char const *p = strcasestr(s, " and ");
+ if ( !p )
+ return -EINVAL;
+
+ p_ts = strndup(s, p - s);
+ i = strlen(p_ts);
+ while ( i > 0 && isspace(p_ts[--i]) )
+ p_ts[i] = (char)0;
+
+ r = parse_timestamp(p_ts, ts);
+ if (r < 0)
+ return r;
+
+ p += strspn(p, " ");
+ if ( !(p = startswith_no_case(p, "and ")) )
+ return -EINVAL;
+ p += strspn(p, " ");
+ if ( !(p = startswith_no_case(p, "every ")) )
+ return -EINVAL;
+ p += strspn(p, " ");
+
+ r = parse_time(p, rep, USEC_PER_SEC);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
int calendar_spec_from_string(const char *p, CalendarSpec **ret) {
const char *utc;
_cleanup_(calendar_spec_freep) CalendarSpec *c = NULL;
@@ -889,6 +962,21 @@ int calendar_spec_from_string(const char *p, CalendarSpec **ret) {
.timezone = NULL,
};
+ if (strcasestr(p, " and ")) {
+ usec_t ts;
+ r = parse_and_every(p, &ts, &(c->period));
+ if (r < 0)
+ return r;
+ if (c->period < MIN_PERIOD)
+ return -ERANGE;
+ r = calendarspec_from_time_t(c, (time_t)(ts / USEC_PER_SEC));
+ if (r < 0)
+ return r;
+ c->microsecond->start += ts % USEC_PER_SEC;
+ c->weekdays_bits = -1;
+ goto finish;
+ }
+
utc = endswith_no_case(p, " UTC");
if (utc) {
c->utc = true;
@@ -1089,6 +1177,7 @@ int calendar_spec_from_string(const char *p, CalendarSpec **ret) {
calendar_spec_normalize(c);
+finish:
if (!calendar_spec_valid(c))
return -EINVAL;
@@ -1359,6 +1448,67 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
"Infinite loop in calendar calculation: %s", strna(s));
}
+static int usec_dstoffset_sec(usec_t usec, time_t *ret_dstoffset) {
+ struct tm tm;
+ time_t t = usec / USEC_PER_SEC;
+ if ( !localtime_r(&t, &tm) )
+ return -EINVAL;
+ if ( tm.tm_isdst) {
+ tm.tm_isdst = 0;
+ time_t tmp = mktime(&tm);
+ if ( tmp == -1 )
+ return -EINVAL;
+ *ret_dstoffset = tmp - t;
+ } else {
+ *ret_dstoffset = 0;
+ }
+ return 0;
+}
+
+static int calendar_spec_next_usec_period(const CalendarSpec *spec, usec_t usec, usec_t *ret_next) {
+ usec_t ts;
+ int r = calendarspec_to_usec_t(spec, &ts);
+ if (r < 0)
+ return r;
+ if (usec < ts) {
+ /* The timestamp is in the future. The first event of the period. */
+ *ret_next = ts;
+ return 0;
+ }
+
+ if ( spec->period % (24 * 3600 * USEC_PER_SEC) ) {
+ /* The period is not multiple of the whole day.
+ * The next event is on the multiple of the period duration. */
+ *ret_next = ts + ( (usec - ts) / spec->period +1 ) * spec->period;
+ return 0;
+ }
+
+ /* The period is multiple of the whole day. We need to compensate daylight
+ * saving. The repeating events should be at the same time of day according to
+ * daylight saving. We need to test up to three points around and detect
+ * localtime discontinuity on change of DST/Non DST localtime mode. */
+ time_t dstoffset;
+ if ( (r = usec_dstoffset_sec(ts, &dstoffset)) )
+ return r;
+ const size_t NRT = 3;
+ size_t i;
+ usec_t u = ts + ( (usec - ts) / spec->period ) * spec->period;
+ for(i = 0; i < NRT; i++) {
+ time_t dstoffset2;
+ r = usec_dstoffset_sec(u, &dstoffset2);
+ if (r < 0)
+ return r;
+ u += ( dstoffset - dstoffset2 ) * USEC_PER_SEC;
+ if ( usec < u ) {
+ *ret_next = u;
+ return 0;
+ }
+ dstoffset = dstoffset2;
+ u += spec->period;
+ }
+ return -EINVAL;
+}
+
static int calendar_spec_next_usec_impl(const CalendarSpec *spec, usec_t usec, usec_t *ret_next) {
usec_t tm_usec;
struct tm tm;
@@ -1369,6 +1519,9 @@ static int calendar_spec_next_usec_impl(const CalendarSpec *spec, usec_t usec, u
if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX)
return -EINVAL;
+ if ( spec->period )
+ return calendar_spec_next_usec_period(spec, usec, ret_next);
+
usec++;
r = localtime_or_gmtime_usec(usec, spec->utc, &tm);
if (r < 0)
diff --git a/src/shared/calendarspec.h b/src/shared/calendarspec.h
index 60c1c792673..a5d1d910155 100644
--- a/src/shared/calendarspec.h
+++ b/src/shared/calendarspec.h
@@ -22,6 +22,7 @@ typedef struct CalendarSpec {
bool utc:1;
signed int dst:2;
char *timezone;
+ usec_t period;
CalendarComponent *year;
CalendarComponent *month;
diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c
index 0fc3a43a23b..9e27956d40d 100644
--- a/src/test/test-calendarspec.c
+++ b/src/test/test-calendarspec.c
@@ -181,6 +181,7 @@ TEST(calendar_spec_one) {
test_one("@0 UTC", "1970-01-01 00:00:00 UTC");
test_one("*:05..05", "*-*-* *:05:00");
test_one("*:05..10/6", "*-*-* *:05:00");
+ test_one("2025-3-1 10:0 UTC and every 14d", "2025-03-01 10:00:00 UTC and every 2w");
}
TEST(calendar_spec_next) {