From 0a44f5375982e12954660d55382ce6a5ab197297 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 4 Aug 2025 13:41:28 +0200 Subject: [PATCH] feat(caldav): return proper caldav intervals instead of FREQ=SECONDLY (#1230) --- pkg/caldav/caldav.go | 25 ++++++++++++++++++++++- pkg/caldav/parsing_test.go | 2 +- pkg/caldav/repeat_test.go | 42 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 pkg/caldav/repeat_test.go diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index fd32393fe..fd6f98f64 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -104,6 +104,28 @@ func formatDuration(duration time.Duration) string { strconv.FormatFloat(seconds, 'f', 0, 64) + `S` } +func getRruleFromInterval(interval int64) (freq string, newInterval int64) { + const ( + minute = 60 + hour = minute * 60 + day = hour * 24 + week = day * 7 + ) + + switch { + case interval%week == 0: + return "WEEKLY", interval / week + case interval%day == 0: + return "DAILY", interval / day + case interval%hour == 0: + return "HOURLY", interval / hour + case interval%minute == 0: + return "MINUTELY", interval / minute + default: + return "SECONDLY", interval + } +} + // ParseTodos returns a caldav vcalendar string with todos func ParseTodos(config *Config, todos []*Todo) (caldavtodos string) { caldavtodos = `BEGIN:VCALENDAR @@ -172,8 +194,9 @@ PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority)) caldavtodos += ` RRULE:FREQ=MONTHLY;BYMONTHDAY=` + t.DueDate.Format("02") // Day of the month } else { + freq, interval := getRruleFromInterval(t.RepeatAfter) caldavtodos += ` -RRULE:FREQ=SECONDLY;INTERVAL=` + strconv.FormatInt(t.RepeatAfter, 10) +RRULE:FREQ=` + freq + `;INTERVAL=` + strconv.FormatInt(interval, 10) } } diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index f852f5cab..1f3516ab8 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -578,7 +578,7 @@ STATUS:COMPLETED DUE:20181201T011202Z CREATED:20181201T011201Z PRIORITY:3 -RRULE:FREQ=SECONDLY;INTERVAL=86400 +RRULE:FREQ=DAILY;INTERVAL=1 CATEGORIES:label1,label2 LAST-MODIFIED:20181201T011205Z BEGIN:VALARM diff --git a/pkg/caldav/repeat_test.go b/pkg/caldav/repeat_test.go new file mode 100644 index 000000000..6f146575e --- /dev/null +++ b/pkg/caldav/repeat_test.go @@ -0,0 +1,42 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package caldav + +import "testing" + +func Test_getRruleFromInterval(t *testing.T) { + tests := []struct { + name string + interval int64 + wantFreq string + wantInterval int64 + }{ + {"seconds", 435, "SECONDLY", 435}, + {"minutes", 120, "MINUTELY", 2}, + {"hours", 7200, "HOURLY", 2}, + {"daily", 86400, "DAILY", 1}, + {"weekly", 1209600, "WEEKLY", 2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFreq, gotInterval := getRruleFromInterval(tt.interval) + if gotFreq != tt.wantFreq || gotInterval != tt.wantInterval { + t.Errorf("getRruleFromInterval() = %s,%d; want %s,%d", gotFreq, gotInterval, tt.wantFreq, tt.wantInterval) + } + }) + } +}