Skip to content

Commit 69dddb7

Browse files
committed
Replace Days with a freeform string describing when a task is due
Examples: 1 day before start 4.5 hours after start 4 days before end etc.
1 parent b66e0ed commit 69dddb7

File tree

5 files changed

+209
-43
lines changed

5 files changed

+209
-43
lines changed

main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,18 @@ func createProject(uc *userConfig, trip tripit.Trip, cl []tasks.ChecklistItem, t
191191
return
192192
}
193193

194+
end, err := trip.End()
195+
if err != nil {
196+
log.Printf("Unable to get end date from trip: %v\n", err)
197+
return
198+
}
199+
194200
name := fmt.Sprintf("Trip: %s", trip.DisplayName)
195201
log.Printf("Processing %s", name)
196202

197203
p := tasks.Project{
198204
Name: name,
199-
Tasks: tasks.Expand(cl, start, taskCutoff)}
205+
Tasks: tasks.Expand(cl, start, end, taskCutoff)}
200206

201207
if p.Empty() {
202208
log.Println("No tasks within cutoff window, skipping.")

tasks/checklist.go

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,14 @@ import (
77
"os"
88
"strconv"
99
"strings"
10-
"time"
1110
)
1211

1312
type ChecklistItem struct {
1413
Template string
1514

1615
Indent int
1716

18-
// This may not be expressive enough. E.g; some tasks might want to be quantised to units
19-
// of weekends rather than days. I suspect anything sub-day is not useful.
20-
Days int
21-
}
22-
23-
func (t ChecklistItem) Due(due time.Time) time.Time {
24-
// Add (probably subtract) the relevant t.Days from the given due time, then add back
25-
// 20 hours to actually complete the task. This works best with tasks due at the *start*
26-
// of some day, e.g; at 00:00.
27-
return due.Add(time.Duration(t.Days) * 24 * time.Hour).Add(20 * time.Hour)
17+
Due string
2818
}
2919

3020
func Load(templateFilename string) ([]ChecklistItem, error) {
@@ -66,16 +56,10 @@ func load(ior io.Reader) ([]ChecklistItem, error) {
6656
continue
6757
}
6858

69-
d, err := strconv.Atoi(rec[2])
70-
if err != nil {
71-
errors = append(errors, fmt.Sprintf("line %d: %v", l, err))
72-
continue
73-
}
74-
7559
ret = append(ret, ChecklistItem{
7660
Template: rec[0],
7761
Indent: i,
78-
Days: d,
62+
Due: rec[2],
7963
})
8064
}
8165

tasks/checklist_test.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,24 @@ func TestLoad(t *testing.T) {
1616
want: []ChecklistItem{},
1717
err: false,
1818
}, {
19-
csv: "foo,1,1",
20-
want: []ChecklistItem{{"foo", 1, 1}},
19+
csv: "foo,1,1 day before start",
20+
want: []ChecklistItem{{"foo", 1, "1 day before start"}},
2121
err: false,
2222
}, {
23-
csv: "foo,1,1\nbar,1,2",
24-
want: []ChecklistItem{{"foo", 1, 1}, {"bar", 1, 2}},
23+
csv: "foo,1,bizzle\nbar,1,wizzle",
24+
want: []ChecklistItem{{"foo", 1, "bizzle"}, {"bar", 1, "wizzle"}},
2525
err: false,
2626
}, {
27-
csv: "foo,1,not-a-num",
27+
csv: "foo\nbar,1,error",
2828
want: []ChecklistItem{},
2929
err: true,
3030
}, {
31-
csv: "foo\nbar,1,1",
32-
want: []ChecklistItem{},
33-
err: true,
34-
}, {
35-
csv: "foo,1,1\nnot-enough-fields\nbar,3,2",
36-
want: []ChecklistItem{{"foo", 1, 1}, {"bar", 3, 2}},
31+
csv: "foo,1,e\nnot-enough-fields\nbar,3,f",
32+
want: []ChecklistItem{{"foo", 1, "e"}, {"bar", 3, "f"}},
3733
err: true,
3834
}, {
39-
csv: "foo,0,1\nbar,5,1\nbaz,1,1", // Indent out of range.
40-
want: []ChecklistItem{{"baz", 1, 1}},
35+
csv: "foo,0,oof\nbar,5,rab\nbaz,1,zab", // Indent out of range.
36+
want: []ChecklistItem{{"baz", 1, "zab"}},
4137
err: true,
4238
}}
4339

tasks/expand.go

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,132 @@
11
package tasks
22

33
import (
4+
"fmt"
5+
"log"
6+
"strconv"
7+
"strings"
48
"time"
59
)
610

11+
type due struct {
12+
duration time.Duration
13+
14+
// end is true if the duration should be counted from the end of
15+
// the trip.
16+
end bool
17+
}
18+
19+
func abs(t time.Duration) time.Duration {
20+
if t < 0 {
21+
return -t
22+
}
23+
return t
24+
}
25+
726
// Expand expands a travel checklist into a list of Tasks. If a task has a due date
827
// after cutoff, it is ignored.
9-
func Expand(cl []ChecklistItem, start, cutoff time.Time) []Task {
28+
func Expand(cl []ChecklistItem, start, end, cutoff time.Time) []Task {
1029
ret := []Task{}
1130

1231
for pos, i := range cl {
13-
due := i.Due(start)
14-
if due.Before(cutoff) {
32+
var dd time.Time
33+
d, err := parseDue(i.Due)
34+
if err != nil {
35+
log.Printf("Could not process due for task %q: %v (ignored)", i.Template, err)
36+
continue
37+
}
38+
if d.end {
39+
dd = end.Add(d.duration)
40+
} else {
41+
dd = start.Add(d.duration)
42+
}
43+
if abs(d.duration) >= 24*time.Hour {
44+
dd = time.Date(dd.Year(), dd.Month(), dd.Day(), 20, 00, 00, 00, time.UTC)
45+
}
46+
47+
if dd.Before(cutoff) {
1548
ret = append(ret, Task{
1649
Content: i.Template,
1750
Indent: i.Indent,
18-
DueDate: due,
51+
DueDate: dd,
1952
Position: pos,
2053
})
2154
}
2255
}
2356

2457
return ret
2558
}
59+
60+
// parseDue expands a humanized due string into a due structure. A due
61+
// string looks like: "16 hours before start" or "1 day after end"
62+
func parseDue(s string) (due, error) {
63+
var ret due
64+
parts := strings.SplitN(strings.ToLower(s), " ", 4)
65+
if len(parts) < 4 {
66+
return ret, fmt.Errorf("due date not fully specified %q", s)
67+
}
68+
69+
t, err := strconv.Atoi(parts[0])
70+
if err != nil {
71+
return ret, err
72+
}
73+
74+
switch d := parts[1]; d {
75+
case "minute":
76+
fallthrough
77+
case "minutes":
78+
ret.duration = time.Duration(t) * time.Minute
79+
80+
case "hour":
81+
fallthrough
82+
case "hours":
83+
ret.duration = time.Duration(t) * time.Hour
84+
85+
case "day":
86+
fallthrough
87+
case "days":
88+
ret.duration = time.Duration(t*24) * time.Hour
89+
90+
case "week":
91+
fallthrough
92+
case "weeks":
93+
ret.duration = time.Duration(t*24*7) * time.Hour
94+
95+
default:
96+
// Just try it.
97+
ret.duration, err = time.ParseDuration(d)
98+
if err != nil {
99+
return ret, fmt.Errorf("unknown unit %q", d)
100+
}
101+
}
102+
103+
switch parts[2] {
104+
case "after":
105+
// Nothing.
106+
107+
case "from":
108+
fallthrough
109+
case "before":
110+
ret.duration = -ret.duration
111+
112+
default:
113+
return ret, fmt.Errorf("unknown relation %q", parts[2])
114+
}
115+
116+
switch parts[3] {
117+
case "departure":
118+
fallthrough
119+
case "start":
120+
// Nothing.
121+
122+
case "return":
123+
fallthrough
124+
case "end":
125+
ret.end = true
126+
127+
default:
128+
return ret, fmt.Errorf("unknown reference %q", parts[3])
129+
}
130+
131+
return ret, nil
132+
}

tasks/expand_test.go

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,50 @@ import (
88

99
func TestExpand(t *testing.T) {
1010
tripStart := time.Date(2016, 07, 15, 00, 00, 00, 00, time.UTC)
11-
cutoff := time.Date(2016, 07, 10, 00, 00, 00, 00, time.UTC)
11+
tripEnd := time.Date(2016, 07, 20, 12, 30, 00, 00, time.UTC)
12+
stdCutoff := time.Date(2016, 07, 10, 00, 00, 00, 00, time.UTC)
1213

1314
cases := []struct {
14-
in []ChecklistItem
15-
want []Task
15+
in []ChecklistItem
16+
cutoff time.Time
17+
want []Task
1618
}{{
1719
in: []ChecklistItem{},
1820
want: []Task{},
1921
}, {
20-
in: []ChecklistItem{{Template: "foo", Indent: 1, Days: -14}},
22+
in: []ChecklistItem{{Template: "foo", Indent: 1, Due: "14 days before start"}},
23+
cutoff: stdCutoff,
2124
want: []Task{{
2225
Content: "foo",
2326
Indent: 1,
2427
DueDate: time.Date(2016, 07, 01, 20, 00, 00, 00, time.UTC),
2528
}},
2629
}, {
2730
in: []ChecklistItem{
28-
{Template: "before cutoff", Indent: 1, Days: -8},
29-
{Template: "after cutoff", Indent: 1, Days: -4},
31+
{Template: "no day adjustment", Indent: 1, Due: "1 hour before start"},
32+
{Template: "end", Indent: 1, Due: "2 days before end"},
33+
{Template: "after", Indent: 1, Due: "4 hours after start"},
3034
},
35+
cutoff: tripEnd,
36+
want: []Task{
37+
{Content: "no day adjustment",
38+
Indent: 1,
39+
DueDate: time.Date(2016, 07, 14, 23, 00, 00, 00, time.UTC)},
40+
{Content: "end",
41+
Indent: 1,
42+
Position: 1,
43+
DueDate: time.Date(2016, 07, 18, 20, 00, 00, 00, time.UTC)},
44+
{Content: "after",
45+
Indent: 1,
46+
Position: 2,
47+
DueDate: time.Date(2016, 07, 15, 04, 00, 00, 00, time.UTC)},
48+
},
49+
}, {
50+
in: []ChecklistItem{
51+
{Template: "before cutoff", Indent: 1, Due: "8 days before start"},
52+
{Template: "after cutoff", Indent: 1, Due: "4 days before start"},
53+
},
54+
cutoff: stdCutoff,
3155
want: []Task{{
3256
Content: "before cutoff",
3357
Indent: 1,
@@ -36,9 +60,58 @@ func TestExpand(t *testing.T) {
3660
}}
3761

3862
for _, c := range cases {
39-
got := Expand(c.in, tripStart, cutoff)
63+
got := Expand(c.in, tripStart, tripEnd, c.cutoff)
4064
if !reflect.DeepEqual(got, c.want) {
4165
t.Errorf("Expand(%v) == %v, want %v", c.in, got, c.want)
4266
}
4367
}
4468
}
69+
70+
func TestParseDue(t *testing.T) {
71+
cases := []struct {
72+
in string
73+
want due
74+
wantError bool
75+
}{{
76+
in: "1 hour from start",
77+
want: due{duration: -time.Hour},
78+
}, {
79+
in: "3 hours after end",
80+
want: due{duration: 3 * time.Hour, end: true},
81+
}, {
82+
in: "1 day before end",
83+
want: due{duration: -24 * time.Hour, end: true},
84+
}, {
85+
in: "2 weeks before start",
86+
want: due{duration: -2 * 7 * 24 * time.Hour},
87+
}, {
88+
in: "",
89+
wantError: true,
90+
}, {
91+
in: "A days before start",
92+
wantError: true,
93+
}, {
94+
in: "1 month before start", // month not supported.
95+
wantError: true,
96+
}, {
97+
in: "1 day prior to start", // prior to not supported.
98+
wantError: true,
99+
}, {
100+
in: "1 day before commencement",
101+
wantError: true,
102+
}}
103+
104+
for _, c := range cases {
105+
got, err := parseDue(c.in)
106+
if err != nil {
107+
if !c.wantError {
108+
t.Errorf("parseDue(%q) error %v want no error", c.in, err)
109+
}
110+
continue
111+
}
112+
if !reflect.DeepEqual(got, c.want) {
113+
t.Errorf("parseDue(%q) == %v want %v", c.in, got, c.want)
114+
}
115+
}
116+
117+
}

0 commit comments

Comments
 (0)