Neale Pickett
·
2021-10-14
award.go
1// Package award defines a MOTH award, and provides tools to use them.
2package award
3
4import (
5 "bytes"
6 "encoding/json"
7 "fmt"
8 "net/url"
9 "reflect"
10 "strconv"
11 "strings"
12)
13
14// T represents a single award event.
15type T struct {
16 // Unix epoch time of this event
17 When int64
18 TeamID string
19 Category string
20 Points int
21}
22
23// List is a collection of award events.
24type List []T
25
26// Len returns the length of the awards list.
27func (awards List) Len() int {
28 return len(awards)
29}
30
31// Less returns true if i was awarded before j.
32func (awards List) Less(i, j int) bool {
33 return awards[i].When < awards[j].When
34}
35
36// Swap exchanges the awards in positions i and j.
37func (awards List) Swap(i, j int) {
38 tmp := awards[i]
39 awards[i] = awards[j]
40 awards[j] = tmp
41}
42
43// Parse parses a string log entry into an award.T.
44func Parse(s string) (T, error) {
45 ret := T{}
46
47 s = strings.TrimSpace(s)
48
49 n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamID, &ret.Category, &ret.Points)
50 if err != nil {
51 return ret, err
52 } else if n != 4 {
53 return ret, fmt.Errorf("malformed award string: only parsed %d fields", n)
54 }
55
56 return ret, nil
57}
58
59// String returns a log entry string for an award.T.
60func (a T) String() string {
61 return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points)
62}
63
64// Filename returns a string version of an award suitable for a filesystem
65func (a T) Filename() string {
66 return fmt.Sprintf(
67 "%d-%s-%s-%d.award",
68 a.When,
69 url.PathEscape(a.TeamID),
70 url.PathEscape(a.Category),
71 a.Points,
72 )
73}
74
75// MarshalJSON returns the award event, encoded as a list.
76func (a T) MarshalJSON() ([]byte, error) {
77 ao := []interface{}{
78 a.When,
79 a.TeamID,
80 a.Category,
81 a.Points,
82 }
83
84 return json.Marshal(ao)
85}
86
87// UnmarshalJSON decodes the JSON string b.
88func (a T) UnmarshalJSON(b []byte) error {
89 r := bytes.NewReader(b)
90 dec := json.NewDecoder(r)
91 dec.UseNumber() // Don't use floats
92
93 // All this to make sure we get `[`
94 t, err := dec.Token()
95 if err != nil {
96 return err
97 }
98 switch token := t.(type) {
99 case json.Delim:
100 if token.String() != "[" {
101 return &json.UnmarshalTypeError{
102 Value: token.String(),
103 Type: reflect.TypeOf(a),
104 Offset: 0,
105 }
106 }
107 default:
108 return &json.UnmarshalTypeError{
109 Value: fmt.Sprintf("%v", t),
110 Type: reflect.TypeOf(a),
111 Offset: 0,
112 }
113 }
114
115 var num json.Number
116 if err := dec.Decode(&num); err != nil {
117 return err
118 }
119 if a.When, err = strconv.ParseInt(string(num), 10, 64); err != nil {
120 return err
121 }
122 if err := dec.Decode(&a.Category); err != nil {
123 return err
124 }
125 if err := dec.Decode(&a.TeamID); err != nil {
126 return err
127 }
128 if err := dec.Decode(&num); err != nil {
129 return err
130 }
131 if a.Points, err = strconv.Atoi(string(num)); err != nil {
132 return err
133 }
134
135 // All this to make sure we get `]`
136 t, err = dec.Token()
137 if err != nil {
138 return err
139 }
140 switch token := t.(type) {
141 case json.Delim:
142 if token.String() != "]" {
143 return &json.UnmarshalTypeError{
144 Value: token.String(),
145 Type: reflect.TypeOf(a),
146 Offset: 0,
147 }
148 }
149 default:
150 return &json.UnmarshalTypeError{
151 Value: fmt.Sprintf("%v", t),
152 Type: reflect.TypeOf(a),
153 Offset: 0,
154 }
155 }
156
157 return nil
158}
159
160// Equal returns true if two award events represent the same award.
161// Timestamps are ignored in this comparison!
162func (a T) Equal(o T) bool {
163 switch {
164 case a.TeamID != o.TeamID:
165 return false
166 case a.Category != o.Category:
167 return false
168 case a.Points != o.Points:
169 return false
170 }
171 return true
172}