Refactor scheduleAdaptiveNextDueDate function for improved readability and fix bug, Add New Tests for it
This commit is contained in:
parent
042cd3b473
commit
4f22460313
3 changed files with 251 additions and 12 deletions
|
@ -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 {
|
||||
|
|
229
internal/chore/scheduler_test.go
Normal file
229
internal/chore/scheduler_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -158,3 +158,7 @@ func (r *UserRepository) GetAllUserTokens(c context.Context, userID int) ([]*uMo
|
|||
func (r *UserRepository) DeleteAPIToken(c context.Context, userID int, tokenID string) error {
|
||||
return r.db.WithContext(c).Where("id = ? AND user_id = ?", tokenID, userID).Delete(&uModel.APIToken{}).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateNotificationTarget(c context.Context, userID int, targetID string, targetType uModel.UserNotificationType) error {
|
||||
return r.db.WithContext(c).Model(&uModel.UserNotificationTarget{}).Where("user_id = ? AND type = ?", userID, targetType).Update("target_id", targetID).Error
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue