Fix timezone handling in completeChore and correct frequency type constants
Add Tests for scheduler Build on PR and run tests
This commit is contained in:
parent
3e0b68bbff
commit
2145299638
5 changed files with 385 additions and 361 deletions
|
@ -12,129 +12,123 @@ import (
|
|||
)
|
||||
|
||||
func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.Time, error) {
|
||||
// if Chore is rolling then the next due date calculated from the completed date, otherwise it's calculated from the due date
|
||||
var nextDueDate time.Time
|
||||
var baseDate time.Time
|
||||
var frequencyMetadata chModel.FrequencyMetadata
|
||||
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling frequency metadata")
|
||||
}
|
||||
if chore.FrequencyType == "once" {
|
||||
if chore.FrequencyType == "once" || chore.FrequencyType == "no_repeat" || chore.FrequencyType == "trigger" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var baseDate time.Time
|
||||
if chore.NextDueDate != nil {
|
||||
// no due date set, use the current date
|
||||
|
||||
baseDate = chore.NextDueDate.UTC()
|
||||
} else {
|
||||
baseDate = completedDate.UTC()
|
||||
}
|
||||
if chore.FrequencyType == "day_of_the_month" || chore.FrequencyType == "days_of_the_week" || chore.FrequencyType == "interval" {
|
||||
// time in frequency metadata stored as RFC3339 format like `2024-07-07T13:27:00-04:00`
|
||||
// parse it to time.Time:
|
||||
t, err := time.Parse(time.RFC3339, frequencyMetadata.Time)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing time in frequency metadata")
|
||||
}
|
||||
// set the time to the time in the frequency metadata:
|
||||
baseDate = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
|
||||
|
||||
}
|
||||
if chore.IsRolling {
|
||||
baseDate = completedDate.UTC()
|
||||
}
|
||||
if chore.FrequencyType == "daily" {
|
||||
nextDueDate = baseDate.AddDate(0, 0, 1)
|
||||
} else if chore.FrequencyType == "weekly" {
|
||||
nextDueDate = baseDate.AddDate(0, 0, 7)
|
||||
} else if chore.FrequencyType == "monthly" {
|
||||
nextDueDate = baseDate.AddDate(0, 1, 0)
|
||||
} else if chore.FrequencyType == "yearly" {
|
||||
nextDueDate = baseDate.AddDate(1, 0, 0)
|
||||
} else if chore.FrequencyType == "adaptive" {
|
||||
|
||||
// TODO: calculate next due date based on the history of the chore
|
||||
// calculate the difference between the due date and now in days:
|
||||
diff := completedDate.UTC().Sub(chore.NextDueDate.UTC())
|
||||
nextDueDate = completedDate.UTC().Add(diff)
|
||||
} else if chore.FrequencyType == "once" {
|
||||
// if the chore is a one-time chore, then the next due date is nil
|
||||
} else if chore.FrequencyType == "interval" {
|
||||
// calculate the difference between the due date and now in days:
|
||||
if *frequencyMetadata.Unit == "hours" {
|
||||
nextDueDate = baseDate.UTC().Add(time.Hour * time.Duration(chore.Frequency))
|
||||
} else if *frequencyMetadata.Unit == "days" {
|
||||
nextDueDate = baseDate.UTC().AddDate(0, 0, chore.Frequency)
|
||||
} else if *frequencyMetadata.Unit == "weeks" {
|
||||
nextDueDate = baseDate.UTC().AddDate(0, 0, chore.Frequency*7)
|
||||
} else if *frequencyMetadata.Unit == "months" {
|
||||
nextDueDate = baseDate.UTC().AddDate(0, chore.Frequency, 0)
|
||||
} else if *frequencyMetadata.Unit == "years" {
|
||||
nextDueDate = baseDate.UTC().AddDate(chore.Frequency, 0, 0)
|
||||
} else {
|
||||
frequencyMetadata := chModel.FrequencyMetadata{}
|
||||
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling frequency metadata: %w", err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid frequency unit, cannot calculate next due date")
|
||||
}
|
||||
} else if chore.FrequencyType == "days_of_the_week" {
|
||||
// TODO : this logic is bad, need to be refactored and be better.
|
||||
// coding at night is almost always bad idea.
|
||||
// calculate the difference between the due date and now in days:
|
||||
var frequencyMetadata chModel.FrequencyMetadata
|
||||
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
||||
// Handle time-based frequencies, ensure time is in the future
|
||||
if chore.FrequencyType == "day_of_the_month" || chore.FrequencyType == "days_of_the_week" || chore.FrequencyType == "interval" {
|
||||
t, err := time.Parse(time.RFC3339, frequencyMetadata.Time)
|
||||
if err != nil {
|
||||
|
||||
return nil, fmt.Errorf("error unmarshalling frequency metadata")
|
||||
return nil, fmt.Errorf("error parsing time in frequency metadata: %w", err)
|
||||
}
|
||||
//we can only assign to days of the week that part of the frequency metadata.days
|
||||
//it's array of days of the week, for example ["monday", "tuesday", "wednesday"]
|
||||
|
||||
// we need to find the next day of the week in the frequency metadata.days that we can schedule
|
||||
// if this the last or there is only one. will use same otherwise find the next one:
|
||||
baseDate = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), t.Hour(), t.Minute(), t.Second(), 0, time.UTC)
|
||||
|
||||
// find the index of the chore day in the frequency metadata.days
|
||||
// loop for next 7 days from the base, if the day in the frequency metadata.days then we can schedule it:
|
||||
// If the time is in the past today, move it to tomorrow
|
||||
if baseDate.Before(completedDate) {
|
||||
baseDate = baseDate.AddDate(0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
switch chore.FrequencyType {
|
||||
case "daily":
|
||||
baseDate = baseDate.AddDate(0, 0, 1)
|
||||
case "weekly":
|
||||
baseDate = baseDate.AddDate(0, 0, 7)
|
||||
case "monthly":
|
||||
baseDate = baseDate.AddDate(0, 1, 0)
|
||||
case "yearly":
|
||||
baseDate = baseDate.AddDate(1, 0, 0)
|
||||
case "adaptive":
|
||||
// TODO: Implement a more sophisticated adaptive logic
|
||||
diff := completedDate.UTC().Sub(chore.NextDueDate.UTC())
|
||||
baseDate = completedDate.UTC().Add(diff)
|
||||
case "interval":
|
||||
switch *frequencyMetadata.Unit {
|
||||
case "hours":
|
||||
baseDate = baseDate.Add(time.Duration(chore.Frequency) * time.Hour)
|
||||
case "days":
|
||||
baseDate = baseDate.AddDate(0, 0, chore.Frequency)
|
||||
case "weeks":
|
||||
baseDate = baseDate.AddDate(0, 0, chore.Frequency*7)
|
||||
case "months":
|
||||
baseDate = baseDate.AddDate(0, chore.Frequency, 0)
|
||||
case "years":
|
||||
baseDate = baseDate.AddDate(chore.Frequency, 0, 0)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid frequency unit: %s", *frequencyMetadata.Unit)
|
||||
}
|
||||
case "days_of_the_week":
|
||||
if len(frequencyMetadata.Days) == 0 {
|
||||
return nil, fmt.Errorf("days_of_the_week requires at least one day")
|
||||
}
|
||||
// Find the next valid day of the week
|
||||
for i := 1; i <= 7; i++ {
|
||||
nextDueDate = baseDate.AddDate(0, 0, i)
|
||||
nextDueDate := baseDate.AddDate(0, 0, i)
|
||||
nextDay := strings.ToLower(nextDueDate.Weekday().String())
|
||||
for _, day := range frequencyMetadata.Days {
|
||||
if strings.ToLower(*day) == nextDay {
|
||||
nextDate := nextDueDate.UTC()
|
||||
return &nextDate, nil
|
||||
return &nextDueDate, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if chore.FrequencyType == "day_of_the_month" {
|
||||
var frequencyMetadata chModel.FrequencyMetadata
|
||||
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
||||
if err != nil {
|
||||
|
||||
return nil, fmt.Errorf("error unmarshalling frequency metadata")
|
||||
return nil, fmt.Errorf("no matching day of the week found")
|
||||
case "day_of_the_month":
|
||||
if len(frequencyMetadata.Months) == 0 {
|
||||
return nil, fmt.Errorf("day_of_the_month requires at least one month")
|
||||
}
|
||||
// Ensure the day of the month is valid
|
||||
if chore.Frequency <= 0 || chore.Frequency > 31 {
|
||||
return nil, fmt.Errorf("invalid day of the month: %d", chore.Frequency)
|
||||
}
|
||||
|
||||
for i := 1; i <= 12; i++ {
|
||||
nextDueDate = baseDate.AddDate(0, i, 0)
|
||||
// set the date to the first day of the month:
|
||||
nextDueDate = time.Date(nextDueDate.Year(), nextDueDate.Month(), chore.Frequency, nextDueDate.Hour(), nextDueDate.Minute(), 0, 0, nextDueDate.Location())
|
||||
nextMonth := strings.ToLower(nextDueDate.Month().String())
|
||||
// Find the next valid day of the month, considering the year
|
||||
currentMonth := int(baseDate.Month())
|
||||
for i := 0; i < 12; i++ { // Start from 0 to check the current month first
|
||||
nextDueDate := baseDate.AddDate(0, i, 0)
|
||||
nextMonth := (currentMonth + i) % 12 // Use modulo to cycle through months
|
||||
if nextMonth == 0 {
|
||||
nextMonth = 12 // Adjust for December
|
||||
}
|
||||
|
||||
// Ensure the target day exists in the month (e.g., Feb 30th is invalid)
|
||||
lastDayOfMonth := time.Date(nextDueDate.Year(), time.Month(nextMonth+1), 0, 0, 0, 0, 0, time.UTC).Day()
|
||||
targetDay := chore.Frequency
|
||||
if targetDay > lastDayOfMonth {
|
||||
targetDay = lastDayOfMonth
|
||||
}
|
||||
|
||||
nextDueDate = time.Date(nextDueDate.Year(), time.Month(nextMonth), targetDay, nextDueDate.Hour(), nextDueDate.Minute(), 0, 0, time.UTC)
|
||||
|
||||
for _, month := range frequencyMetadata.Months {
|
||||
if *month == nextMonth {
|
||||
nextDate := nextDueDate.UTC()
|
||||
return &nextDate, nil
|
||||
if strings.ToLower(*month) == strings.ToLower(time.Month(nextMonth).String()) {
|
||||
return &nextDueDate, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if chore.FrequencyType == "no_repeat" {
|
||||
return nil, nil
|
||||
} else if chore.FrequencyType == "trigger" {
|
||||
// if the chore is a trigger chore, then the next due date is nil
|
||||
return nil, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid frequency type, cannot calculate next due date")
|
||||
return nil, fmt.Errorf("no matching month found")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid frequency type: %s", chore.FrequencyType)
|
||||
}
|
||||
return &nextDueDate, nil
|
||||
|
||||
return &baseDate, nil
|
||||
}
|
||||
func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time, history []*chModel.ChoreHistory) (*time.Time, error) {
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue