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

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
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. Goals []Goal `json:"goals"`
  16. Logs []Log `json:"logs"`
  17. }
  18. // GenerateIDs generates IDs. It should only be used initially.
  19. func (period *Period) GenerateIDs() {
  20. period.ID = generate.ID("P", 12)
  21. for i := range period.Goals {
  22. period.Goals[i].ID = generate.ID("PG", 16)
  23. }
  24. for i := range period.Logs {
  25. period.Logs[i].ID = generate.ID("PL", 16)
  26. }
  27. }
  28. // Clear clears the period, but doesn't re-alloc the slices if they're set.
  29. func (period *Period) Clear() {
  30. period.ID = ""
  31. period.UserID = ""
  32. period.From = time.Time{}
  33. period.To = time.Time{}
  34. period.Name = ""
  35. period.ShouldReScore = false
  36. period.Goals = period.Goals[:0]
  37. period.Logs = period.Logs[:0]
  38. }
  39. // Copy makes a deep copy of the period.
  40. func (period *Period) Copy() *Period {
  41. periodCopy := *period
  42. periodCopy.Goals = make([]Goal, len(period.Goals))
  43. copy(periodCopy.Goals, period.Goals)
  44. periodCopy.Logs = make([]Log, len(period.Logs))
  45. copy(periodCopy.Logs, period.Logs)
  46. return &periodCopy
  47. }
  48. // IncludesDate returns true if the date is within the interval of SetFrom..SetTo.
  49. func (period *Period) IncludesDate(date time.Time) bool {
  50. return (date == period.From || date == period.To) || (date.After(period.From) && date.Before(period.To))
  51. }
  52. // ApplyUpdate applies an update. It does not calculate/validate the scores or remote IDs.
  53. //
  54. // It may still have been changed even if it errors.
  55. func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error) {
  56. if update.SetFrom != nil {
  57. changed = true
  58. period.From = *update.SetFrom
  59. }
  60. if update.SetTo != nil {
  61. changed = true
  62. period.To = *update.SetTo
  63. }
  64. if update.SetName != nil {
  65. changed = true
  66. period.Name = *update.SetName
  67. }
  68. if update.AddGoal != nil {
  69. goal := *update.AddGoal
  70. goal.ID = generate.ID("PG", 16)
  71. for _, existingGoal := range period.Goals {
  72. if existingGoal.ActivityID == goal.ActivityID {
  73. return false, slerrors.PreconditionFailed("Activity already exists in another goal.")
  74. }
  75. }
  76. period.Goals = append(period.Goals, goal)
  77. if update.AddLog != nil && update.AddLog.GoalID == "NEW" {
  78. update.AddLog.GoalID = goal.ID
  79. }
  80. changed = true
  81. }
  82. if update.ReplaceGoal != nil {
  83. goal := period.Goal(update.ReplaceGoal.ID)
  84. if goal == nil {
  85. err = slerrors.NotFound("Goal")
  86. return
  87. }
  88. goal.PointCount = update.ReplaceGoal.PointCount
  89. changed = true
  90. period.ShouldReScore = true
  91. }
  92. if update.RemoveGoal != nil {
  93. found := false
  94. for i, goal := range period.Goals {
  95. if goal.ID == *update.RemoveGoal {
  96. period.Goals = append(period.Goals[:i], period.Goals[i+1:]...)
  97. found = true
  98. break
  99. }
  100. }
  101. if !found {
  102. err = slerrors.NotFound("Goal you're trying to remove")
  103. return
  104. }
  105. changed = true
  106. period.ShouldReScore = true
  107. }
  108. if update.AddLog != nil {
  109. log := *update.AddLog
  110. log.ID = generate.ID("L", 16)
  111. log.SubmitTime = time.Now()
  112. goal := period.Goal(log.GoalID)
  113. if goal == nil {
  114. err = slerrors.NotFound("Goal")
  115. return
  116. }
  117. period.Logs = append(period.Logs, log)
  118. changed = true
  119. }
  120. if update.ReplaceLog != nil {
  121. log := *update.ReplaceLog
  122. found := false
  123. for i, log2 := range period.Logs {
  124. if log.ID == log2.ID {
  125. found = true
  126. goal := period.Goal(log.GoalID)
  127. if goal == nil {
  128. err = slerrors.NotFound("Goal")
  129. return
  130. }
  131. period.Logs[i] = log
  132. break
  133. }
  134. }
  135. if !found {
  136. err = slerrors.NotFound("Log")
  137. return
  138. }
  139. }
  140. if update.RemoveLog != nil {
  141. found := false
  142. for i, log := range period.Logs {
  143. if log.ID == *update.RemoveLog {
  144. found = true
  145. changed = true
  146. period.Logs = append(period.Logs[:i], period.Logs[i+1:]...)
  147. break
  148. }
  149. }
  150. if !found {
  151. err = slerrors.NotFound("Log you're trying to remove")
  152. return
  153. }
  154. }
  155. return
  156. }
  157. func (period *Period) RemoveBrokenLogs() {
  158. goodLogs := make([]Log, 0, len(period.Logs))
  159. for _, log := range period.Logs {
  160. goal := period.Goal(log.GoalID)
  161. if goal == nil {
  162. continue
  163. }
  164. goodLogs = append(goodLogs, log)
  165. }
  166. period.Logs = goodLogs
  167. }
  168. func (period *Period) Goal(id string) *Goal {
  169. for i, goal := range period.Goals {
  170. if goal.ID == id {
  171. return &period.Goals[i]
  172. }
  173. }
  174. return nil
  175. }
  176. func (period *Period) DayHadActivity(date time.Time, activityID string) bool {
  177. date = date.UTC().Truncate(time.Hour * 24)
  178. if !period.IncludesDate(date) {
  179. return false
  180. }
  181. for _, log := range period.Logs {
  182. goal := period.Goal(log.GoalID)
  183. if goal == nil || goal.ActivityID != activityID {
  184. continue
  185. }
  186. if date.Equal(log.Date.UTC().Truncate(time.Hour * 24)) {
  187. return true
  188. }
  189. }
  190. return false
  191. }
  192. // PeriodUpdate describes a change to a period
  193. type PeriodUpdate struct {
  194. SetFrom *time.Time `json:"setFrom"`
  195. SetTo *time.Time `json:"setTo"`
  196. SetName *string `json:"setName"`
  197. AddLog *Log `json:"addLog"`
  198. ReplaceLog *Log `json:"replaceGoal"`
  199. RemoveLog *string `json:"removeLog"`
  200. AddGoal *Goal `json:"addGoal"`
  201. ReplaceGoal *Goal `json:"replaceGoal"`
  202. RemoveGoal *string `json:"removeGoal"`
  203. }
  204. type Score struct {
  205. ActivityScore float64 `json:"activityScore"`
  206. Amount int `json:"amount"`
  207. ItemMultiplier *float64 `json:"subGoalMultiplier"`
  208. DailyBonus int `json:"dailyBonus"`
  209. Total int64 `json:"total"`
  210. }
  211. func (s *Score) Calc() {
  212. subGoalMultiplier := 1.0
  213. if s.ItemMultiplier != nil {
  214. subGoalMultiplier = *s.ItemMultiplier
  215. }
  216. s.Total = int64(s.DailyBonus) + int64(s.ActivityScore*float64(s.Amount)*subGoalMultiplier)
  217. }
  218. type PeriodsByFrom []*Period
  219. func (periods PeriodsByFrom) Len() int {
  220. return len(periods)
  221. }
  222. func (periods PeriodsByFrom) Less(i, j int) bool {
  223. return periods[i].From.Before(periods[j].From)
  224. }
  225. func (periods PeriodsByFrom) Swap(i, j int) {
  226. periods[i], periods[j] = periods[j], periods[i]
  227. }