83 lines
1.6 KiB
Go
83 lines
1.6 KiB
Go
package nextdue
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ValueKind string
|
|
|
|
const (
|
|
ValueKindUnknown ValueKind = ""
|
|
ValueKindHours ValueKind = "hours"
|
|
ValueKindDate ValueKind = "date"
|
|
)
|
|
|
|
type Value struct {
|
|
Kind ValueKind
|
|
Raw string
|
|
Hours *float64
|
|
Date *time.Time
|
|
}
|
|
|
|
type ParseError struct {
|
|
Field string
|
|
Raw string
|
|
Msg string
|
|
}
|
|
|
|
func (e *ParseError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
if strings.TrimSpace(e.Field) == "" {
|
|
return e.Msg
|
|
}
|
|
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
|
|
}
|
|
|
|
type Parser struct {
|
|
DateLayout string
|
|
}
|
|
|
|
func NewParser() Parser { return Parser{DateLayout: "2006-01-02"} }
|
|
|
|
func (p Parser) Parse(field, raw string) (Value, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return Value{Kind: ValueKindUnknown, Raw: ""}, nil
|
|
}
|
|
if d, err := time.Parse(p.dateLayout(), raw); err == nil {
|
|
dd := d.UTC()
|
|
return Value{Kind: ValueKindDate, Raw: raw, Date: &dd}, nil
|
|
}
|
|
if h, err := strconv.ParseFloat(raw, 64); err == nil {
|
|
return Value{Kind: ValueKindHours, Raw: raw, Hours: &h}, nil
|
|
}
|
|
return Value{}, &ParseError{Field: field, Raw: raw, Msg: "must be either an ISO date (YYYY-MM-DD) or a numeric hour value"}
|
|
}
|
|
|
|
func IsUnset(v Value) bool {
|
|
switch v.Kind {
|
|
case ValueKindHours:
|
|
return v.Hours == nil || *v.Hours <= 0
|
|
case ValueKindDate:
|
|
return v.Date == nil || v.Date.IsZero()
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (p Parser) dateLayout() string {
|
|
if strings.TrimSpace(p.DateLayout) == "" {
|
|
return "2006-01-02"
|
|
}
|
|
return p.DateLayout
|
|
}
|
|
|
|
func NormalizeDate(t time.Time) time.Time {
|
|
return time.Date(t.UTC().Year(), t.UTC().Month(), t.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
|
}
|