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.

356 lines
8.5 KiB

4 years ago
  1. package models
  2. import (
  3. "github.com/gisle/stufflog/internal/generate"
  4. "github.com/gisle/stufflog/slerrors"
  5. "time"
  6. )
  7. // A Period is a chunk of time in which activities should be performed. They should always be created manually.
  8. type Period struct {
  9. ID string `json:"id"`
  10. UserID string `json:"userId"`
  11. From time.Time `json:"from"`
  12. To time.Time `json:"to"`
  13. Name string `json:"name"`
  14. ShouldReScore bool `json:"-" msgpack:"-"`
  15. Tags []string `json:"tags"`
  16. Goals []PeriodGoal `json:"goals"`
  17. Logs []PeriodLog `json:"logs"`
  18. }
  19. // GenerateIDs generates IDs. It should only be used initially.
  20. func (period *Period) GenerateIDs() {
  21. period.ID = generate.ID("P", 12)
  22. for i := range period.Goals {
  23. period.Goals[i].ID = generate.ID("PG", 16)
  24. for j := range period.Goals[i].SubGoals {
  25. period.Goals[i].SubGoals[j].ID = generate.ID("PGS", 20)
  26. }
  27. }
  28. for i := range period.Logs {
  29. period.Logs[i].ID = generate.ID("PL", 16)
  30. }
  31. }
  32. // Clear clears the period, but doesn't re-alloc the slices if they're set.
  33. func (period *Period) Clear() {
  34. period.ID = ""
  35. period.UserID = ""
  36. period.From = time.Time{}
  37. period.To = time.Time{}
  38. period.Name = ""
  39. period.ShouldReScore = false
  40. period.Tags = period.Tags[:0]
  41. period.Goals = period.Goals[:0]
  42. period.Logs = period.Logs[:0]
  43. }
  44. // Copy makes a deep copy of the period.
  45. func (period *Period) Copy() *Period {
  46. periodCopy := *period
  47. periodCopy.Tags = make([]string, len(period.Tags))
  48. copy(periodCopy.Tags, period.Tags)
  49. periodCopy.Goals = make([]PeriodGoal, len(period.Goals))
  50. copy(periodCopy.Goals, period.Goals)
  51. periodCopy.Logs = make([]PeriodLog, len(period.Logs))
  52. copy(periodCopy.Logs, period.Logs)
  53. return &periodCopy
  54. }
  55. // IncludesDate returns true if the date is within the interval of SetFrom..SetTo.
  56. func (period *Period) IncludesDate(date time.Time) bool {
  57. return (date == period.From || date == period.To) || (date.After(period.From) && date.Before(period.To))
  58. }
  59. // ApplyUpdate applies an update. It does not calculate/validate the scores or remote IDs.
  60. //
  61. // It may still have been changed even if it errors.
  62. func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error) {
  63. if update.SetFrom != nil {
  64. changed = true
  65. period.From = *update.SetFrom
  66. }
  67. if update.SetTo != nil {
  68. changed = true
  69. period.To = *update.SetTo
  70. }
  71. if update.SetName != nil {
  72. changed = true
  73. period.Name = *update.SetName
  74. }
  75. if update.RemoveTag != nil {
  76. for i, existingTag := range period.Tags {
  77. if *update.AddTag == existingTag {
  78. period.Tags = append(period.Tags[:i], period.Tags[i+1:]...)
  79. changed = true
  80. break
  81. }
  82. }
  83. }
  84. if update.AddTag != nil {
  85. found := false
  86. for _, existingTag := range period.Tags {
  87. if *update.AddTag == existingTag {
  88. found = true
  89. break
  90. }
  91. }
  92. if !found {
  93. changed = true
  94. period.Tags = append(period.Tags, *update.AddTag)
  95. }
  96. }
  97. if update.AddGoal != nil {
  98. goal := *update.AddGoal
  99. goal.ID = generate.ID("PG", 16)
  100. for _, existingGoal := range period.Goals {
  101. if existingGoal.ActivityID == goal.ActivityID {
  102. return false, slerrors.PreconditionFailed("Activity already exists in another goal.")
  103. }
  104. }
  105. period.Goals = append(period.Goals, goal)
  106. if update.AddLog != nil && update.AddLog.GoalID == "NEW" {
  107. update.AddLog.GoalID = goal.ID
  108. }
  109. for i := range goal.SubGoals {
  110. goal.SubGoals[i].ID = generate.ID("PGS", 20)
  111. }
  112. changed = true
  113. }
  114. if update.ReplaceGoal != nil {
  115. goal := period.Goal(update.ReplaceGoal.ID)
  116. if goal == nil {
  117. err = slerrors.NotFound("Goal")
  118. return
  119. }
  120. goal.PointCount = update.ReplaceGoal.PointCount
  121. goal.SubGoals = update.ReplaceGoal.SubGoals
  122. changed = true
  123. period.ShouldReScore = true
  124. }
  125. if update.RemoveGoal != nil {
  126. found := false
  127. for i, goal := range period.Goals {
  128. if goal.ID == *update.RemoveGoal {
  129. period.Goals = append(period.Goals[:i], period.Goals[i+1:]...)
  130. found = true
  131. break
  132. }
  133. }
  134. if !found {
  135. err = slerrors.NotFound("Goal you're trying to remove")
  136. return
  137. }
  138. changed = true
  139. period.ShouldReScore = true
  140. }
  141. if update.AddLog != nil {
  142. log := *update.AddLog
  143. log.ID = generate.ID("L", 16)
  144. log.SubmitTime = time.Now()
  145. goal := period.Goal(log.GoalID)
  146. if goal == nil {
  147. err = slerrors.NotFound("Goal")
  148. return
  149. }
  150. period.Logs = append(period.Logs, log)
  151. changed = true
  152. }
  153. if update.RemoveLog != nil {
  154. found := false
  155. for i, log := range period.Logs {
  156. if log.ID == *update.RemoveLog {
  157. found = true
  158. changed = true
  159. period.Logs = append(period.Logs[:i], period.Logs[i+1:]...)
  160. break
  161. }
  162. }
  163. if !found {
  164. err = slerrors.NotFound("Log you're trying to remove")
  165. return
  166. }
  167. }
  168. return
  169. }
  170. func (period *Period) RemoveBrokenLogs() {
  171. goodLogs := make([]PeriodLog, 0, len(period.Logs))
  172. for _, log := range period.Logs {
  173. goal := period.Goal(log.GoalID)
  174. if goal == nil {
  175. continue
  176. }
  177. if log.SubGoalID != "" {
  178. if goal.SubGoal(log.SubGoalID) == nil {
  179. continue
  180. }
  181. }
  182. goodLogs = append(goodLogs, log)
  183. }
  184. period.Logs = goodLogs
  185. }
  186. func (period *Period) RemoveDuplicateTags() {
  187. deleteList := make([]int, 0, 2)
  188. found := make(map[string]bool)
  189. for i, tag := range period.Tags {
  190. if found[tag] {
  191. deleteList = append(deleteList, i-len(deleteList))
  192. }
  193. found[tag] = true
  194. }
  195. for _, index := range deleteList {
  196. period.Tags = append(period.Tags[:index], period.Tags[index+1:]...)
  197. }
  198. }
  199. func (period *Period) Goal(id string) *PeriodGoal {
  200. for i, goal := range period.Goals {
  201. if goal.ID == id {
  202. return &period.Goals[i]
  203. }
  204. }
  205. return nil
  206. }
  207. func (period *Period) DayHadActivity(date time.Time, activityID string) bool {
  208. date = date.UTC().Truncate(time.Hour * 24)
  209. if !period.IncludesDate(date) {
  210. return false
  211. }
  212. for _, log := range period.Logs {
  213. goal := period.Goal(log.GoalID)
  214. if goal == nil || goal.ActivityID != activityID {
  215. continue
  216. }
  217. if date.Equal(log.Date.UTC().Truncate(time.Hour * 24)) {
  218. return true
  219. }
  220. }
  221. return false
  222. }
  223. // A PeriodGoal is a declared investment into the goal.
  224. type PeriodGoal struct {
  225. ID string `json:"id"`
  226. ActivityID string `json:"activityId"`
  227. PointCount int `json:"pointCount"`
  228. SubGoals []PeriodSubGoal `json:"subGoals"`
  229. }
  230. func (goal *PeriodGoal) SubGoal(id string) *PeriodSubGoal {
  231. if goal == nil {
  232. return nil
  233. }
  234. for i, subGoal := range goal.SubGoals {
  235. if subGoal.ID == id {
  236. return &goal.SubGoals[i]
  237. }
  238. }
  239. return nil
  240. }
  241. // A PeriodSubGoal is a specific sub-goal that should either punish or boost performance. For example, it could represent
  242. // a project or technique that should take priority, or a guilty pleasure that should definitely not be.
  243. type PeriodSubGoal struct {
  244. ID string `json:"id"`
  245. Name string `json:"name"`
  246. Multiplier float64 `json:"multiplier"`
  247. }
  248. // PeriodLog is a logged performance of an activity during the period. SubGoalID is optional, but GoalID is not. The
  249. // points is the points calculated on the time of submission and does not need to reflect the latest.
  250. type PeriodLog struct {
  251. Date time.Time `json:"date"`
  252. ID string `json:"id"`
  253. SubActivityID string `json:"subActivityId"`
  254. GoalID string `json:"goalId"`
  255. SubGoalID string `json:"subGoalId"`
  256. Description string `json:"description"`
  257. SubmitTime time.Time `json:"submitTime"`
  258. Amount int `json:"amount"`
  259. Score *Score `json:"score"`
  260. }
  261. // PeriodUpdate describes a change to a period
  262. type PeriodUpdate struct {
  263. SetFrom *time.Time `json:"setFrom"`
  264. SetTo *time.Time `json:"setTo"`
  265. SetName *string `json:"setName"`
  266. AddLog *PeriodLog `json:"addLog"`
  267. RemoveLog *string `json:"removeLog"`
  268. AddGoal *PeriodGoal `json:"addGoal"`
  269. ReplaceGoal *PeriodGoal `json:"replaceGoal"`
  270. RemoveGoal *string `json:"removeGoal"`
  271. AddTag *string `json:"addTag"`
  272. RemoveTag *string `json:"removeTag"`
  273. }
  274. type Score struct {
  275. ActivityScore float64 `json:"activityScore"`
  276. Amount int `json:"amount"`
  277. SubGoalMultiplier *float64 `json:"subGoalMultiplier"`
  278. DailyBonus int `json:"dailyBonus"`
  279. Total int64 `json:"total"`
  280. }
  281. func (s *Score) Calc() {
  282. subGoalMultiplier := 1.0
  283. if s.SubGoalMultiplier != nil {
  284. subGoalMultiplier = *s.SubGoalMultiplier
  285. }
  286. s.Total = int64(s.DailyBonus) + int64(s.ActivityScore*float64(s.Amount)*subGoalMultiplier)
  287. }
  288. type PeriodsByFrom []*Period
  289. func (periods PeriodsByFrom) Len() int {
  290. return len(periods)
  291. }
  292. func (periods PeriodsByFrom) Less(i, j int) bool {
  293. return periods[i].From.Before(periods[j].From)
  294. }
  295. func (periods PeriodsByFrom) Swap(i, j int) {
  296. periods[i], periods[j] = periods[j], periods[i]
  297. }