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) }