aboutsummaryrefslogtreecommitdiffstats
path: root/internal/chore
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/chore/handler.go2
-rw-r--r--internal/chore/model/model.go18
-rw-r--r--internal/chore/scheduler.go30
-rw-r--r--internal/chore/scheduler_test.go229
4 files changed, 265 insertions, 14 deletions
diff --git a/internal/chore/handler.go b/internal/chore/handler.go
index 16c80ef..01490cc 100644
--- a/internal/chore/handler.go
+++ b/internal/chore/handler.go
@@ -33,7 +33,7 @@ type ThingTrigger struct {
type ChoreReq struct {
Name string `json:"name" binding:"required"`
- FrequencyType string `json:"frequencyType"`
+ FrequencyType chModel.FrequencyType `json:"frequencyType"`
ID int `json:"id"`
DueDate string `json:"dueDate"`
Assignees []chModel.ChoreAssignees `json:"assignees"`
diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go
index 38d8354..b2633b3 100644
--- a/internal/chore/model/model.go
+++ b/internal/chore/model/model.go
@@ -6,10 +6,26 @@ import (
tModel "donetick.com/core/internal/thing/model"
)
+type FrequencyType string
+
+const (
+ FrequancyTypeOnce FrequencyType = "once"
+ FrequancyTypeDaily FrequencyType = "daily"
+ FrequancyTypeWeekly FrequencyType = "weekly"
+ FrequancyTypeMonthly FrequencyType = "monthly"
+ FrequancyTypeYearly FrequencyType = "yearly"
+ FrequancyTypeAdaptive FrequencyType = "adaptive"
+ FrequancyTypeIntervel FrequencyType = "interval"
+ FrequancyTypeDayOfTheWeek FrequencyType = "days_of_the_week"
+ FrequancyTypeDayOfTheMonth FrequencyType = "day_of_the_month"
+ FrequancyTypeTrigger FrequencyType = "trigger"
+ FrequancyTypeNoRepeat FrequencyType = "no_repeat"
+)
+
type Chore struct {
ID int `json:"id" gorm:"primary_key"`
Name string `json:"name" gorm:"column:name"` // Chore description
- FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"` // "daily", "weekly", "monthly", "yearly", "adaptive",or "custom"
+ FrequencyType FrequencyType `json:"frequencyType" gorm:"column:frequency_type"` // "daily", "weekly", "monthly", "yearly", "adaptive",or "custom"
Frequency int `json:"frequency" gorm:"column:frequency"` // Number of days, weeks, months, or years between chores
FrequencyMetadata *string `json:"frequencyMetadata" gorm:"column:frequency_meta"` // Additional frequency information
NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date;index"` // When the chore is due
diff --git a/internal/chore/scheduler.go b/internal/chore/scheduler.go
index 5413447..cec120d 100644
--- a/internal/chore/scheduler.go
+++ b/internal/chore/scheduler.go
@@ -3,6 +3,7 @@ package chore
import (
"encoding/json"
"fmt"
+ "math"
"math/rand"
"strings"
"time"
@@ -141,11 +142,15 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T
return &nextDueDate, nil
}
-
func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time, history []*chModel.ChoreHistory) (*time.Time, error) {
- // will generate due date base on history and the different between the completed date and the due date
- // the more recent the higher weight
- if len(history) <= 1 {
+
+ history = append([]*chModel.ChoreHistory{
+ {
+ CompletedAt: &completedDate,
+ },
+ }, history...)
+
+ if len(history) < 2 {
if chore.NextDueDate != nil {
diff := completedDate.UTC().Sub(chore.NextDueDate.UTC())
nextDueDate := completedDate.UTC().Add(diff)
@@ -153,22 +158,23 @@ func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time,
}
return nil, nil
}
- var weight float64
+
+ var totalDelay float64
var totalWeight float64
- var nextDueDate time.Time
+ decayFactor := 0.5 // Adjust this value to control the decay rate
+
for i := 0; i < len(history)-1; i++ {
delay := history[i].CompletedAt.UTC().Sub(history[i+1].CompletedAt.UTC()).Seconds()
- weight = delay * float64(len(history)-i)
+ weight := math.Pow(decayFactor, float64(i))
+ totalDelay += delay * weight
totalWeight += weight
}
- // calculate the average delay
- averageDelay := totalWeight / float64(len(history)-1)
- // calculate the difference between the completed date and the due date
- nextDueDate = completedDate.UTC().Add(time.Duration(averageDelay) * time.Second)
+
+ averageDelay := totalDelay / totalWeight
+ nextDueDate := completedDate.UTC().Add(time.Duration(averageDelay) * time.Second)
return &nextDueDate, nil
}
-
func RemoveAssigneeAndReassign(chore *chModel.Chore, userID int) {
for i, assignee := range chore.Assignees {
if assignee.UserID == userID {
diff --git a/internal/chore/scheduler_test.go b/internal/chore/scheduler_test.go
new file mode 100644
index 0000000..21d064a
--- /dev/null
+++ b/internal/chore/scheduler_test.go
@@ -0,0 +1,229 @@
+package chore
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ chModel "donetick.com/core/internal/chore/model"
+)
+
+func TestScheduleNextDueDateBasic(t *testing.T) {
+ choreTime := time.Now()
+ freqencyMetadataBytes := `{"time":"2024-07-07T14:30:00-04:00"}`
+
+ testTable := []struct {
+ chore *chModel.Chore
+ expected time.Time
+ }{
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeDaily,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(0, 0, 1),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeWeekly,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(0, 0, 7),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeMonthly,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(0, 1, 0),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeYearly,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(1, 0, 0),
+ },
+
+ //
+ }
+ for _, tt := range testTable {
+ t.Run(string(tt.chore.FrequencyType), func(t *testing.T) {
+
+ actual, err := scheduleNextDueDate(tt.chore, choreTime)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ if actual != nil && actual.UTC().Format(time.RFC3339) != tt.expected.UTC().Format(time.RFC3339) {
+ t.Errorf("Expected: %v, Actual: %v", tt.expected, actual)
+ }
+ })
+ }
+}
+
+func TestScheduleNextDueDateDayOfTheWeek(t *testing.T) {
+ choreTime := time.Now()
+
+ Monday := "monday"
+ Wednesday := "wednesday"
+
+ timeOfChore := "2024-07-07T16:30:00-04:00"
+ getExpectedTime := func(choreTime time.Time, timeOfChore string) time.Time {
+ t, err := time.Parse(time.RFC3339, timeOfChore)
+ if err != nil {
+ return time.Time{}
+ }
+ return time.Date(choreTime.Year(), choreTime.Month(), choreTime.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
+ }
+ nextSaturday := choreTime.AddDate(0, 0, 1)
+ for nextSaturday.Weekday() != time.Saturday {
+ nextSaturday = nextSaturday.AddDate(0, 0, 1)
+ }
+
+ nextMonday := choreTime.AddDate(0, 0, 1)
+ for nextMonday.Weekday() != time.Monday {
+ nextMonday = nextMonday.AddDate(0, 0, 1)
+ }
+
+ nextTuesday := choreTime.AddDate(0, 0, 1)
+ for nextTuesday.Weekday() != time.Tuesday {
+ nextTuesday = nextTuesday.AddDate(0, 0, 1)
+ }
+
+ nextWednesday := choreTime.AddDate(0, 0, 1)
+ for nextWednesday.Weekday() != time.Wednesday {
+ nextWednesday = nextWednesday.AddDate(0, 0, 1)
+ }
+
+ nextThursday := choreTime.AddDate(0, 0, 1)
+ for nextThursday.Weekday() != time.Thursday {
+ nextThursday = nextThursday.AddDate(0, 0, 1)
+ }
+
+ testTable := []struct {
+ chore *chModel.Chore
+ frequencyMetadata *chModel.FrequencyMetadata
+ expected time.Time
+ }{
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeDayOfTheWeek,
+ NextDueDate: &nextSaturday,
+ },
+ frequencyMetadata: &chModel.FrequencyMetadata{
+ Time: timeOfChore,
+ Days: []*string{&Monday, &Wednesday},
+ },
+
+ expected: getExpectedTime(nextMonday, timeOfChore),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeDayOfTheWeek,
+ NextDueDate: &nextMonday,
+ },
+ frequencyMetadata: &chModel.FrequencyMetadata{
+ Time: timeOfChore,
+ Days: []*string{&Monday, &Wednesday},
+ },
+ expected: getExpectedTime(nextWednesday, timeOfChore),
+ },
+ }
+ for _, tt := range testTable {
+ t.Run(string(tt.chore.FrequencyType), func(t *testing.T) {
+ bytesFrequencyMetadata, err := json.Marshal(tt.frequencyMetadata)
+ stringFrequencyMetadata := string(bytesFrequencyMetadata)
+
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ tt.chore.FrequencyMetadata = &stringFrequencyMetadata
+ actual, err := scheduleNextDueDate(tt.chore, choreTime)
+
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ if actual != nil && actual.UTC().Format(time.RFC3339) != tt.expected.UTC().Format(time.RFC3339) {
+ t.Errorf("Expected: %v, Actual: %v", tt.expected, actual)
+ }
+ })
+ }
+}
+func TestScheduleAdaptiveNextDueDate(t *testing.T) {
+ getTimeFromDate := func(timeOfChore string) *time.Time {
+ t, err := time.Parse(time.RFC3339, timeOfChore)
+ if err != nil {
+ return nil
+ }
+ return &t
+ }
+ testTable := []struct {
+ description string
+ history []*chModel.ChoreHistory
+ chore *chModel.Chore
+ expected *time.Time
+ completeDate *time.Time
+ }{
+ {
+ description: "Every Two days",
+ chore: &chModel.Chore{
+ NextDueDate: getTimeFromDate("2024-07-13T01:30:00-00:00"),
+ },
+ history: []*chModel.ChoreHistory{
+ {
+ CompletedAt: getTimeFromDate("2024-07-11T01:30:00-00:00"),
+ },
+ // {
+ // CompletedAt: getTimeFromDate("2024-07-09T01:30:00-00:00"),
+ // },
+ // {
+ // CompletedAt: getTimeFromDate("2024-07-07T01:30:00-00:00"),
+ // },
+ },
+ expected: getTimeFromDate("2024-07-15T01:30:00-00:00"),
+ },
+ {
+ description: "Every 8 days",
+ chore: &chModel.Chore{
+ NextDueDate: getTimeFromDate("2024-07-13T01:30:00-00:00"),
+ },
+ history: []*chModel.ChoreHistory{
+ {
+ CompletedAt: getTimeFromDate("2024-07-05T01:30:00-00:00"),
+ },
+ {
+ CompletedAt: getTimeFromDate("2024-06-27T01:30:00-00:00"),
+ },
+ },
+ expected: getTimeFromDate("2024-07-21T01:30:00-00:00"),
+ },
+ {
+ description: "40 days with limit Data",
+ chore: &chModel.Chore{
+ NextDueDate: getTimeFromDate("2024-07-13T01:30:00-00:00"),
+ },
+ history: []*chModel.ChoreHistory{
+ {CompletedAt: getTimeFromDate("2024-06-03T01:30:00-00:00")},
+ },
+ expected: getTimeFromDate("2024-08-22T01:30:00-00:00"),
+ },
+ }
+ for _, tt := range testTable {
+ t.Run(tt.description, func(t *testing.T) {
+ expectedNextDueDate := tt.expected
+
+ actualNextDueDate, err := scheduleAdaptiveNextDueDate(tt.chore, *tt.chore.NextDueDate, tt.history)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+
+ if actualNextDueDate == nil || !actualNextDueDate.Equal(*expectedNextDueDate) {
+ t.Errorf("Expected: %v, Actual: %v", expectedNextDueDate, actualNextDueDate)
+ }
+ })
+ }
+}