|
|
package models
import ( "github.com/gisle/stufflog/internal/generate" "github.com/gisle/stufflog/slerrors" "time" )
// A Period is a chunk of time in which activities should be performed. They should always be created manually.
type Period struct { ID string `json:"id"` UserID string `json:"userId"` From time.Time `json:"from"` To time.Time `json:"to"` Name string `json:"name"`
ShouldReScore bool `json:"-" msgpack:"-"`
Tags []string `json:"tags"` Goals []PeriodGoal `json:"goals"` Logs []PeriodLog `json:"logs"` }
// GenerateIDs generates IDs. It should only be used initially.
func (period *Period) GenerateIDs() { period.ID = generate.ID("P", 12) for i := range period.Goals { period.Goals[i].ID = generate.ID("PG", 16)
for j := range period.Goals[i].SubGoals { period.Goals[i].SubGoals[j].ID = generate.ID("PGS", 20) } } for i := range period.Logs { period.Logs[i].ID = generate.ID("PL", 16) } }
// Clear clears the period, but doesn't re-alloc the slices if they're set.
func (period *Period) Clear() { period.ID = "" period.UserID = "" period.From = time.Time{} period.To = time.Time{} period.Name = ""
period.ShouldReScore = false
period.Tags = period.Tags[:0] period.Goals = period.Goals[:0] period.Logs = period.Logs[:0] }
// Copy makes a deep copy of the period.
func (period *Period) Copy() *Period { periodCopy := *period
periodCopy.Tags = make([]string, len(period.Tags)) copy(periodCopy.Tags, period.Tags) periodCopy.Goals = make([]PeriodGoal, len(period.Goals)) copy(periodCopy.Goals, period.Goals) periodCopy.Logs = make([]PeriodLog, len(period.Logs)) copy(periodCopy.Logs, period.Logs)
return &periodCopy }
// IncludesDate returns true if the date is within the interval of SetFrom..SetTo.
func (period *Period) IncludesDate(date time.Time) bool { return (date == period.From || date == period.To) || (date.After(period.From) && date.Before(period.To)) }
// ApplyUpdate applies an update. It does not calculate/validate the scores or remote IDs.
//
// It may still have been changed even if it errors.
func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error) { if update.SetFrom != nil { changed = true period.From = *update.SetFrom } if update.SetTo != nil { changed = true period.To = *update.SetTo } if update.SetName != nil { changed = true period.Name = *update.SetName }
if update.RemoveTag != nil { for i, existingTag := range period.Tags { if *update.AddTag == existingTag { period.Tags = append(period.Tags[:i], period.Tags[i+1:]...) changed = true break } } } if update.AddTag != nil { found := false for _, existingTag := range period.Tags { if *update.AddTag == existingTag { found = true break } } if !found { changed = true period.Tags = append(period.Tags, *update.AddTag) } }
if update.AddGoal != nil { goal := *update.AddGoal goal.ID = generate.ID("PG", 16)
for _, existingGoal := range period.Goals { if existingGoal.ActivityID == goal.ActivityID { return false, slerrors.PreconditionFailed("Activity already exists in another goal.") } }
period.Goals = append(period.Goals, goal)
if update.AddLog != nil && update.AddLog.GoalID == "NEW" { update.AddLog.GoalID = goal.ID } for i := range goal.SubGoals { goal.SubGoals[i].ID = generate.ID("PGS", 20) }
changed = true } if update.ReplaceGoal != nil { goal := period.Goal(update.ReplaceGoal.ID) if goal == nil { err = slerrors.NotFound("Goal") return }
goal.PointCount = update.ReplaceGoal.PointCount goal.SubGoals = update.ReplaceGoal.SubGoals
changed = true period.ShouldReScore = true } if update.RemoveGoal != nil { found := false for i, goal := range period.Goals { if goal.ID == *update.RemoveGoal { period.Goals = append(period.Goals[:i], period.Goals[i+1:]...) found = true break } } if !found { err = slerrors.NotFound("Goal you're trying to remove") return }
changed = true period.ShouldReScore = true }
if update.AddLog != nil { log := *update.AddLog log.ID = generate.ID("L", 16) log.SubmitTime = time.Now()
goal := period.Goal(log.GoalID) if goal == nil { err = slerrors.NotFound("Goal") return }
period.Logs = append(period.Logs, log) changed = true } if update.RemoveLog != nil { found := false for i, log := range period.Logs { if log.ID == *update.RemoveLog { found = true changed = true period.Logs = append(period.Logs[:i], period.Logs[i+1:]...)
break } } if !found { err = slerrors.NotFound("Log you're trying to remove") return } }
return }
func (period *Period) RemoveBrokenLogs() { goodLogs := make([]PeriodLog, 0, len(period.Logs))
for _, log := range period.Logs { goal := period.Goal(log.GoalID) if goal == nil { continue }
if log.SubGoalID != "" { if goal.SubGoal(log.SubGoalID) == nil { continue } }
goodLogs = append(goodLogs, log) }
period.Logs = goodLogs }
func (period *Period) RemoveDuplicateTags() { deleteList := make([]int, 0, 2)
found := make(map[string]bool) for i, tag := range period.Tags { if found[tag] { deleteList = append(deleteList, i-len(deleteList)) } found[tag] = true } for _, index := range deleteList { period.Tags = append(period.Tags[:index], period.Tags[index+1:]...) } }
func (period *Period) Goal(id string) *PeriodGoal { for i, goal := range period.Goals { if goal.ID == id { return &period.Goals[i] } }
return nil }
func (period *Period) DayHadActivity(date time.Time, activityID string) bool { date = date.UTC().Truncate(time.Hour * 24)
if !period.IncludesDate(date) { return false }
for _, log := range period.Logs { goal := period.Goal(log.GoalID) if goal == nil || goal.ActivityID != activityID { continue }
if date.Equal(log.Date.UTC().Truncate(time.Hour * 24)) { return true } }
return false }
// A PeriodGoal is a declared investment into the goal.
type PeriodGoal struct { ID string `json:"id"` ActivityID string `json:"activityId"` PointCount int `json:"pointCount"`
SubGoals []PeriodSubGoal `json:"subGoals"` }
func (goal *PeriodGoal) SubGoal(id string) *PeriodSubGoal { if goal == nil { return nil }
for i, subGoal := range goal.SubGoals { if subGoal.ID == id { return &goal.SubGoals[i] } }
return nil }
// A PeriodSubGoal is a specific sub-goal that should either punish or boost performance. For example, it could represent
// a project or technique that should take priority, or a guilty pleasure that should definitely not be.
type PeriodSubGoal struct { ID string `json:"id"` Name string `json:"name"` Multiplier float64 `json:"multiplier"` }
// PeriodLog is a logged performance of an activity during the period. SubGoalID is optional, but GoalID is not. The
// points is the points calculated on the time of submission and does not need to reflect the latest.
type PeriodLog struct { Date time.Time `json:"date"` ID string `json:"id"` SubActivityID string `json:"subActivityId"` GoalID string `json:"goalId"` SubGoalID string `json:"subGoalId"` Description string `json:"description"` SubmitTime time.Time `json:"submitTime"` Amount int `json:"amount"` Score *Score `json:"score"` }
// PeriodUpdate describes a change to a period
type PeriodUpdate struct { SetFrom *time.Time `json:"setFrom"` SetTo *time.Time `json:"setTo"` SetName *string `json:"setName"`
AddLog *PeriodLog `json:"addLog"` RemoveLog *string `json:"removeLog"` AddGoal *PeriodGoal `json:"addGoal"` ReplaceGoal *PeriodGoal `json:"replaceGoal"` RemoveGoal *string `json:"removeGoal"` AddTag *string `json:"addTag"` RemoveTag *string `json:"removeTag"` }
type Score struct { ActivityScore float64 `json:"activityScore"` Amount int `json:"amount"` SubGoalMultiplier *float64 `json:"subGoalMultiplier"` DailyBonus int `json:"dailyBonus"`
Total int64 `json:"total"` }
func (s *Score) Calc() { subGoalMultiplier := 1.0 if s.SubGoalMultiplier != nil { subGoalMultiplier = *s.SubGoalMultiplier }
s.Total = int64(s.DailyBonus) + int64(s.ActivityScore*float64(s.Amount)*subGoalMultiplier) }
type PeriodsByFrom []*Period
func (periods PeriodsByFrom) Len() int { return len(periods) }
func (periods PeriodsByFrom) Less(i, j int) bool { return periods[i].From.Before(periods[j].From) }
func (periods PeriodsByFrom) Swap(i, j int) { periods[i], periods[j] = periods[j], periods[i] }
|