Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions pkg/models/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -1835,16 +1835,17 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
}

newTask.Reminders = oldTask.Reminders
// When repeating from the current date, all reminders should keep their difference to each other.
// To make this easier, we sort them first because we can then rely on the fact the first is the smallest
// The earliest reminder moves to now + interval, all others keep their distance to it.
if len(oldTask.Reminders) > 0 {
sort.Slice(oldTask.Reminders, func(i, j int) bool {
return oldTask.Reminders[i].Reminder.Unix() < oldTask.Reminders[j].Reminder.Unix()
})
first := oldTask.Reminders[0].Reminder
for _, r := range oldTask.Reminders[1:] {
if r.Reminder.Before(first) {
first = r.Reminder
}
}
newFirst := now.Add(repeatDuration)
for in, r := range oldTask.Reminders {
diff := r.Reminder.Sub(first)
newTask.Reminders[in].Reminder = now.Add(repeatDuration + diff)
newTask.Reminders[in].Reminder = shiftTime(r.Reminder, first, newFirst)
}
}

Expand All @@ -1856,9 +1857,8 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
// end date should keep the difference to the start date when setting
// them as new
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
diff := oldTask.EndDate.Sub(oldTask.StartDate)
newTask.StartDate = now.Add(repeatDuration)
newTask.EndDate = now.Add(repeatDuration + diff)
newTask.EndDate = shiftTime(oldTask.EndDate, oldTask.StartDate, newTask.StartDate)
} else {
if !oldTask.StartDate.IsZero() {
newTask.StartDate = now.Add(repeatDuration)
Expand All @@ -1872,21 +1872,32 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
// If the old task has a start and due date, we set the new start date
// to preserve the interval between them.
if !oldTask.StartDate.IsZero() {
diff := oldTask.DueDate.Sub(oldTask.StartDate)
newTask.StartDate = newTask.DueDate.Add(-diff)
newTask.StartDate = shiftTime(oldTask.StartDate, oldTask.DueDate, newTask.DueDate)
}

// If the old task has an end and due date, we set the new end date
// to preserve the interval between them.
if !oldTask.EndDate.IsZero() {
diff := oldTask.DueDate.Sub(oldTask.EndDate)
newTask.EndDate = newTask.DueDate.Add(-diff)
newTask.EndDate = shiftTime(oldTask.EndDate, oldTask.DueDate, newTask.DueDate)
}
}

newTask.Done = false
}

// shiftTime moves t by the same offset that takes from to to.
// time.Time.Sub saturates at ~292 years, so for larger spans the offset is
// applied as whole years plus a small remainder instead of one Duration.
func shiftTime(t, from, to time.Time) time.Time {
diff := to.Sub(from)
if diff != math.MaxInt64 && diff != math.MinInt64 {
return t.Add(diff)
}
years := to.Year() - from.Year()
rest := to.Sub(from.AddDate(years, 0, 0))
return t.AddDate(years, 0, 0).Add(rest)
}

var (
checklistTiptapCheckedRegex = regexp.MustCompile(`(data-checked=")true(")`)
checklistInputCheckedRegex = regexp.MustCompile(`(<input[^>]*type=["']checkbox["'][^>]*?)\s+checked(?:=["'][^"']*["'])?`)
Expand Down
23 changes: 23 additions & 0 deletions pkg/models/tasks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,29 @@ func TestUpdateDone(t *testing.T) {
assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[1].Reminder.Unix())
assert.False(t, newTask.Done)
})
t.Run("reminders spanning more than 292 years", func(t *testing.T) {
// time.Duration saturates at ~292 years; the offset between reminders
// must not be computed as a single Duration.
oldTask := &Task{
Done: false,
RepeatAfter: 315360000,
RepeatMode: TaskRepeatModeFromCurrentDate,
Reminders: []*TaskReminder{
{Reminder: time.Date(1734, 1, 1, 0, 0, 0, 0, time.UTC)},
{Reminder: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)},
}}
newTask := &Task{
Done: true,
}
updateDone(oldTask, newTask)

assert.Len(t, newTask.Reminders, 2)
expectedFirst := time.Now().Add(time.Duration(oldTask.RepeatAfter) * time.Second)
assert.Equal(t, expectedFirst.Unix(), newTask.Reminders[0].Reminder.Unix())
assert.Equal(t, expectedFirst.Year()+292, newTask.Reminders[1].Reminder.Year())
assert.True(t, newTask.Reminders[1].Reminder.After(newTask.Reminders[0].Reminder))
assert.False(t, newTask.Done)
})
t.Run("start date", func(t *testing.T) {
oldTask := &Task{
Done: false,
Expand Down
Loading