Plan stuff. Log stuff.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

278 lines
6.1 KiB

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:"-"`
Goals []Goal `json:"goals"`
Logs []Log `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 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.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.Goals = make([]Goal, len(period.Goals))
copy(periodCopy.Goals, period.Goals)
periodCopy.Logs = make([]Log, 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.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
}
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
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.ReplaceLog != nil {
log := *update.ReplaceLog
found := false
for i, log2 := range period.Logs {
if log.ID == log2.ID {
found = true
goal := period.Goal(log.GoalID)
if goal == nil {
err = slerrors.NotFound("Goal")
return
}
period.Logs[i] = log
break
}
}
if !found {
err = slerrors.NotFound("Log")
return
}
}
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([]Log, 0, len(period.Logs))
for _, log := range period.Logs {
goal := period.Goal(log.GoalID)
if goal == nil {
continue
}
goodLogs = append(goodLogs, log)
}
period.Logs = goodLogs
}
func (period *Period) Goal(id string) *Goal {
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
}
// 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 *Log `json:"addLog"`
ReplaceLog *Log `json:"replaceGoal"`
RemoveLog *string `json:"removeLog"`
AddGoal *Goal `json:"addGoal"`
ReplaceGoal *Goal `json:"replaceGoal"`
RemoveGoal *string `json:"removeGoal"`
}
type Score struct {
ActivityScore float64 `json:"activityScore"`
Amount int `json:"amount"`
ItemMultiplier *float64 `json:"subGoalMultiplier"`
DailyBonus int `json:"dailyBonus"`
Total int64 `json:"total"`
}
func (s *Score) Calc() {
subGoalMultiplier := 1.0
if s.ItemMultiplier != nil {
subGoalMultiplier = *s.ItemMultiplier
}
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]
}