-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtask.go
301 lines (278 loc) · 8.79 KB
/
task.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
package main
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/inconshreveable/log15"
)
type Task struct {
// Friendly name for the task.
Name string
// either a specific date with correct format or range of weekdays for repeating events
// ex 15:00 01/02/2006 EST
// ex 15:00 first Monday
// ex 15:00 both Tue, Thur
Deadline string
// The amount of time you estimate that this task will take to complete.
// This can be changed as progress is made in a task or at any other time your estimate changes
// Setting this to zero signals the task is complete
EstimatedHours int
Urgency float32
RemainingHours int
BusyHours int
Raw string
}
func (t *Task) AddRaw(line string) {
if t == nil {
return
}
if genTextMatcher.MatchString(line) {
return
}
t.Raw += "\n" + line
}
func (t *Task) PrintRaw() string {
if t == nil {
return ""
}
out := t.Raw
if out == "" {
out += fmt.Sprintf("Name; %s ", t.Name)
out += fmt.Sprintf("Deadline; %s ", t.Deadline)
out += fmt.Sprintf("Estimated hours; %d ", t.EstimatedHours)
}
// add in generated text: urgency, hours remaining, hours blocked
out += fmt.Sprintf(genTextFmt, fmt.Sprintf("Urgency; %.2f%%", t.Urgency*100))
out += fmt.Sprintf(genTextFmt, fmt.Sprintf("Free Time Left; %d", t.RemainingHours))
out += fmt.Sprintf(genTextFmt, fmt.Sprintf("Blocked Hours; %d", t.BusyHours))
return out
}
func (t *Task) String() string {
return t.PrintRaw()
}
func (t *Task) getHoursLeft(now time.Time, blockedHours []int, logger log15.Logger) (int, error) {
deadlineHourBlock := 0
interveningFortnites := 0
nowHourBlock := nextHourBlock(now, logger)
logger = logger.New("NOW", nowHourBlock)
logger.Debug("Getting hours left for task")
valErr := t.validate()
if valErr != nil {
logger.Error("Task did not pass validation", "err", valErr.Error())
return 0, valErr
}
// try to parse Deadline into time
deadline, err := time.Parse(taskDateFmt, t.Deadline)
if err == nil { // this is a single dated task
logger = logger.New("task mode", "single")
logger.Debug("This is a single dated task", "deadline", deadline)
for {
earlyDeadline := deadline.Add(-1 * time.Hour * fullTwoWeeks)
if getZeroSunday(now).After(earlyDeadline) { // We should be comparing deadline to the beginning of the two week rotation, not the day itself!
logger.Debug("We subtracted enough fortnights", "subtracted", interveningFortnites, "newDeadline", deadline)
break
}
interveningFortnites++
deadline = earlyDeadline
}
if interveningFortnites == 0 {
for {
// there is a chance that the deadline is actually really far back in the past so we need to do the opposite operation to pin it down
laterDeadline := deadline.Add(time.Hour * fullTwoWeeks)
if getZeroSunday(now).Before(laterDeadline) {
logger.Debug("We added enough fortnites", "added", interveningFortnites, "newDeadline", deadline)
break
}
interveningFortnites--
deadline = laterDeadline
}
}
deadlineHourBlock = nextHourBlock(deadline, logger)
} else { // this is a repeating task, (or actual error) we need to find the next instance of this deadline
logger = logger.New("task mode", "repeating")
logger.Debug("This is a repeating task", "repeatingDays", t.Deadline)
deadlineHour, weekRotation, days, repeatingDaysErr := t.parseRepeatingDays(logger)
if repeatingDaysErr != nil {
return 0, repeatingDaysErr
}
hours := generateBlockedHours(days, weekRotation, deadlineHour, 1)
wrap := 0
for index := 0; ; index++ {
if index == len(hours) {
index = 0
wrap = 1
logger.Debug("wrapping")
}
hour := hours[index] + (wrap * fullTwoWeeks)
logger.Debug(fmt.Sprintf("we're comparing hour %d to nowHourBlock %d\n", hour, nowHourBlock))
if hour > nowHourBlock {
deadlineHourBlock = hour
break
}
}
}
// now we have a deadline hour block but it's normalized to this rotation; let's un-normalize it
logger.Debug("Deadline hour calculated", "normalizedDeadline", deadlineHourBlock)
deadlineHourBlock += interveningFortnites * fullTwoWeeks
remainingFreeHours := deadlineHourBlock - nowHourBlock
wraps := 0
logger.Debug("Ticking off remaining free hours", "freeHours", remainingFreeHours)
for index := 0; ; index++ {
// loopLogger := logger.New("freeHours", remainingFreeHours)
if index == len(blockedHours) {
if index == 0 {
break
}
index = 0
wraps += 1
}
eventHourBlock := blockedHours[index] + (wraps * fullTwoWeeks)
if eventHourBlock < nowHourBlock {
continue
}
if eventHourBlock >= deadlineHourBlock {
break
}
remainingFreeHours += -1
t.BusyHours += 1
// loopLogger.Debug(fmt.Sprintf("Hour %d (%d) is blocked, remainingFreeHours is %d \n", blockedHours[index], eventHourBlock, remainingFreeHours))
}
t.RemainingHours = remainingFreeHours
return remainingFreeHours, nil
}
func (t *Task) calculateUrgency(now time.Time, genEvents []*GeneralEvent, logger log15.Logger) error {
blockedHours, err := getNextBlockedHours(now, genEvents, logger)
if err != nil {
return err
}
hoursLeft, err := t.getHoursLeft(now, blockedHours, logger)
if err != nil {
return err
}
t.Urgency = float32(t.EstimatedHours) / float32(hoursLeft)
logger.Debug("Calculated urgency", "urgency", t.Urgency)
return nil
}
func (t *Task) parseRepeatingDays(logger log15.Logger) (hour int, weekRotation rotation, days []time.Weekday, err error) {
logger.Debug("Parsing deadline for repeating task", "deadline", t.Deadline)
tokens := strings.Split(strings.ToLower(t.Deadline), " ")
militaryTime := tokens[0]
hour, err = strconv.Atoi(strings.Split(militaryTime, ":")[0])
if err != nil {
logger.Error("Problem converting hour portion of repeating deadline", "err", err.Error())
return
}
weekRotation = rotation(tokens[1])
if weekRotation != firstWeek && weekRotation != secondWeek && weekRotation != bothWeeks {
err = fmt.Errorf("Malformed Deadline! '%s' is not a repeating date.", t.Deadline)
logger.Error("Malformed deadline. This is not a repeating deadline. Perhaps missing time zone or rotation")
return
}
//everything else is days
daysStr := strings.Join(tokens[2:], " ")
days, err = parseDayStrings(daysStr)
if err != nil {
logger.Error("Problem converting days portion of repeating deadline", "err", err.Error())
}
return
}
func (t *Task) validate() error {
if t.Name == "" {
return fmt.Errorf("Task is missing a name")
}
if t.Deadline == "" {
return fmt.Errorf("Task '%s' has no deadline", t.Name)
}
return nil
}
func sortTasks(tasks []*Task, now time.Time, genEvents []*GeneralEvent, topLogger log15.Logger) error {
topLogger.Debug("Sorting Tasks")
for _, task := range tasks {
logger := topLogger.New("task", task)
err := task.calculateUrgency(now, genEvents, logger)
if err != nil {
return err
}
}
sort.Sort(byUrgency(tasks))
return nil
}
type byUrgency []*Task
func (t byUrgency) Len() int {
return len(t)
}
func (t byUrgency) Swap(i, j int) {
t[i], t[j] = t[j], t[i]
}
func (t byUrgency) Less(i, j int) bool {
return t[i].Urgency > t[j].Urgency
}
func outputTasks(taskList []*Task) string {
outStr := ""
upcoming := []*Task{}
finished := []*Task{}
deadlinePassed := []*Task{}
for _, task := range taskList {
if task.Urgency > 0 {
upcoming = append(upcoming, task)
}
if task.Urgency == 0 {
finished = append(finished, task)
}
if task.Urgency < 0 {
deadlinePassed = append(deadlinePassed, task)
}
}
if len(deadlinePassed) != 0 {
outStr += fmt.Sprintf(headerLineFmt, overdueTasks)
for _, task := range deadlinePassed {
outStr += task.PrintRaw()
}
}
if len(upcoming) != 0 {
outStr += fmt.Sprintf(headerLineFmt, upcomingTasks)
for _, task := range upcoming {
outStr += task.PrintRaw()
}
}
if len(finished) != 0 {
outStr += fmt.Sprintf(headerLineFmt, completedTasks)
for _, task := range finished {
outStr += task.PrintRaw()
}
}
outStr = strings.Replace(outStr, "\n\n", "\n", -1)
return outStr
}
func compareLists(a, b []*Task, logger log15.Logger) bool {
if len(a) != len(b) {
return true
}
for ind, aTask := range a {
bTask := b[ind]
logger.Debug("Comparing tasks", "taskA", aTask.Name, "taskB", bTask.Name)
if aTask.Name != bTask.Name {
return true
}
}
return false
}
/*
config file will have file names for the following:
a list of events corresponding to a normal calendar week,
a list of special events with specific dates as needed, and
a list of tasks
program will run with a flag to config file and flag to output file
if no output, program will just print to console
program takes in task list, calculates urgency and orders them in output
program also prints warnings for expired events or tasks but does not remove them
maybe add a flag that will automatically remove those, but default is false.
commands:
run --config --output
update --config [editor]
regular
special
tasks <<these will just open the files in an editor
*/