2020-08-17 11:04:08 -06:00
|
|
|
// Package award defines a MOTH award, and provides tools to use them.
|
|
|
|
package award
|
|
|
|
|
|
|
|
import (
|
2020-08-21 16:56:52 -06:00
|
|
|
"bytes"
|
2020-08-17 11:04:08 -06:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2021-10-14 19:01:12 -06:00
|
|
|
"net/url"
|
2020-08-21 16:56:52 -06:00
|
|
|
"reflect"
|
|
|
|
"strconv"
|
2020-08-17 11:04:08 -06:00
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// T represents a single award event.
|
|
|
|
type T struct {
|
|
|
|
// Unix epoch time of this event
|
|
|
|
When int64
|
|
|
|
TeamID string
|
|
|
|
Category string
|
|
|
|
Points int
|
|
|
|
}
|
|
|
|
|
|
|
|
// List is a collection of award events.
|
2020-08-17 17:43:57 -06:00
|
|
|
type List []T
|
2020-08-17 11:04:08 -06:00
|
|
|
|
2020-08-19 15:38:13 -06:00
|
|
|
// Len returns the length of the awards list.
|
2020-08-17 11:04:08 -06:00
|
|
|
func (awards List) Len() int {
|
|
|
|
return len(awards)
|
|
|
|
}
|
|
|
|
|
2020-08-19 15:38:13 -06:00
|
|
|
// Less returns true if i was awarded before j.
|
2020-08-17 11:04:08 -06:00
|
|
|
func (awards List) Less(i, j int) bool {
|
|
|
|
return awards[i].When < awards[j].When
|
|
|
|
}
|
|
|
|
|
2020-08-19 15:38:13 -06:00
|
|
|
// Swap exchanges the awards in positions i and j.
|
2020-08-17 11:04:08 -06:00
|
|
|
func (awards List) Swap(i, j int) {
|
|
|
|
tmp := awards[i]
|
|
|
|
awards[i] = awards[j]
|
|
|
|
awards[j] = tmp
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse parses a string log entry into an award.T.
|
2020-08-17 17:43:57 -06:00
|
|
|
func Parse(s string) (T, error) {
|
2020-08-17 11:04:08 -06:00
|
|
|
ret := T{}
|
|
|
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
|
|
|
n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamID, &ret.Category, &ret.Points)
|
|
|
|
if err != nil {
|
2020-08-17 17:43:57 -06:00
|
|
|
return ret, err
|
2020-08-17 11:04:08 -06:00
|
|
|
} else if n != 4 {
|
2021-10-14 19:01:12 -06:00
|
|
|
return ret, fmt.Errorf("malformed award string: only parsed %d fields", n)
|
2020-08-17 11:04:08 -06:00
|
|
|
}
|
|
|
|
|
2020-08-17 17:43:57 -06:00
|
|
|
return ret, nil
|
2020-08-17 11:04:08 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a log entry string for an award.T.
|
2020-08-17 17:43:57 -06:00
|
|
|
func (a T) String() string {
|
2020-08-17 11:04:08 -06:00
|
|
|
return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points)
|
|
|
|
}
|
|
|
|
|
2021-10-14 19:01:12 -06:00
|
|
|
// Filename returns a string version of an award suitable for a filesystem
|
|
|
|
func (a T) Filename() string {
|
|
|
|
return fmt.Sprintf(
|
|
|
|
"%d-%s-%s-%d.award",
|
|
|
|
a.When,
|
|
|
|
url.PathEscape(a.TeamID),
|
|
|
|
url.PathEscape(a.Category),
|
|
|
|
a.Points,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-08-17 11:04:08 -06:00
|
|
|
// MarshalJSON returns the award event, encoded as a list.
|
2020-08-17 17:43:57 -06:00
|
|
|
func (a T) MarshalJSON() ([]byte, error) {
|
2020-08-17 11:04:08 -06:00
|
|
|
ao := []interface{}{
|
|
|
|
a.When,
|
|
|
|
a.TeamID,
|
|
|
|
a.Category,
|
|
|
|
a.Points,
|
|
|
|
}
|
|
|
|
|
|
|
|
return json.Marshal(ao)
|
|
|
|
}
|
|
|
|
|
2020-08-21 16:56:52 -06:00
|
|
|
// UnmarshalJSON decodes the JSON string b.
|
|
|
|
func (a T) UnmarshalJSON(b []byte) error {
|
|
|
|
r := bytes.NewReader(b)
|
|
|
|
dec := json.NewDecoder(r)
|
|
|
|
dec.UseNumber() // Don't use floats
|
|
|
|
|
|
|
|
// All this to make sure we get `[`
|
2020-09-17 19:48:17 -06:00
|
|
|
t, err := dec.Token()
|
|
|
|
if err != nil {
|
2020-08-21 16:56:52 -06:00
|
|
|
return err
|
2020-09-17 19:48:17 -06:00
|
|
|
}
|
|
|
|
switch token := t.(type) {
|
|
|
|
case json.Delim:
|
|
|
|
if token.String() != "[" {
|
2020-08-21 16:56:52 -06:00
|
|
|
return &json.UnmarshalTypeError{
|
2020-09-17 19:48:17 -06:00
|
|
|
Value: token.String(),
|
2020-08-21 16:56:52 -06:00
|
|
|
Type: reflect.TypeOf(a),
|
|
|
|
Offset: 0,
|
|
|
|
}
|
|
|
|
}
|
2020-09-17 19:48:17 -06:00
|
|
|
default:
|
|
|
|
return &json.UnmarshalTypeError{
|
|
|
|
Value: fmt.Sprintf("%v", t),
|
|
|
|
Type: reflect.TypeOf(a),
|
|
|
|
Offset: 0,
|
|
|
|
}
|
2020-08-21 16:56:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
var num json.Number
|
|
|
|
if err := dec.Decode(&num); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if a.When, err = strconv.ParseInt(string(num), 10, 64); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := dec.Decode(&a.Category); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := dec.Decode(&a.TeamID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := dec.Decode(&num); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if a.Points, err = strconv.Atoi(string(num)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// All this to make sure we get `]`
|
2020-09-17 19:48:17 -06:00
|
|
|
t, err = dec.Token()
|
|
|
|
if err != nil {
|
2020-08-21 16:56:52 -06:00
|
|
|
return err
|
2020-09-17 19:48:17 -06:00
|
|
|
}
|
|
|
|
switch token := t.(type) {
|
|
|
|
case json.Delim:
|
|
|
|
if token.String() != "]" {
|
2020-08-21 16:56:52 -06:00
|
|
|
return &json.UnmarshalTypeError{
|
2020-09-17 19:48:17 -06:00
|
|
|
Value: token.String(),
|
2020-08-21 16:56:52 -06:00
|
|
|
Type: reflect.TypeOf(a),
|
|
|
|
Offset: 0,
|
|
|
|
}
|
|
|
|
}
|
2020-09-17 19:48:17 -06:00
|
|
|
default:
|
|
|
|
return &json.UnmarshalTypeError{
|
|
|
|
Value: fmt.Sprintf("%v", t),
|
|
|
|
Type: reflect.TypeOf(a),
|
|
|
|
Offset: 0,
|
|
|
|
}
|
2020-08-21 16:56:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-17 11:04:08 -06:00
|
|
|
// Equal returns true if two award events represent the same award.
|
|
|
|
// Timestamps are ignored in this comparison!
|
2020-08-17 17:43:57 -06:00
|
|
|
func (a T) Equal(o T) bool {
|
2020-08-17 11:04:08 -06:00
|
|
|
switch {
|
|
|
|
case a.TeamID != o.TeamID:
|
|
|
|
return false
|
|
|
|
case a.Category != o.Category:
|
|
|
|
return false
|
|
|
|
case a.Points != o.Points:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|