init push
This commit is contained in:
82
internal/shared/pkg/nextdue/parser.go
Normal file
82
internal/shared/pkg/nextdue/parser.go
Normal file
@@ -0,0 +1,82 @@
|
||||
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)
|
||||
}
|
||||
77
internal/shared/pkg/nextdue/parser_test.go
Normal file
77
internal/shared/pkg/nextdue/parser_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package nextdue
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseErrorError(t *testing.T) {
|
||||
var nilErr *ParseError
|
||||
if nilErr.Error() != "" {
|
||||
t.Fatal("nil ParseError should produce empty string")
|
||||
}
|
||||
if (&ParseError{Msg: "boom"}).Error() != "boom" {
|
||||
t.Fatal("fieldless error should be just the message")
|
||||
}
|
||||
if (&ParseError{Field: "due", Msg: "boom"}).Error() != "due: boom" {
|
||||
t.Fatal("field error should be 'field: msg'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDefaultDateLayout(t *testing.T) {
|
||||
p := Parser{} // empty DateLayout -> falls back to default
|
||||
v, err := p.Parse("due", "2026-06-17")
|
||||
if err != nil || v.Kind != ValueKindDate {
|
||||
t.Fatalf("default layout should parse date, got kind=%q err=%v", v.Kind, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
p := NewParser()
|
||||
cases := []struct {
|
||||
raw string
|
||||
kind ValueKind
|
||||
ok bool
|
||||
}{
|
||||
{"", ValueKindUnknown, true},
|
||||
{"1200", ValueKindHours, true},
|
||||
{"1200.5", ValueKindHours, true},
|
||||
{"2026-06-17", ValueKindDate, true},
|
||||
{"1200 FH", ValueKindUnknown, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.raw, func(t *testing.T) {
|
||||
v, err := p.Parse("due", tc.raw)
|
||||
if tc.ok && err != nil {
|
||||
t.Fatalf("Parse(%q) err = %v", tc.raw, err)
|
||||
}
|
||||
if !tc.ok && err == nil {
|
||||
t.Fatalf("Parse(%q) expected error", tc.raw)
|
||||
}
|
||||
if tc.ok && v.Kind != tc.kind {
|
||||
t.Fatalf("Parse(%q) kind = %q, want %q", tc.raw, v.Kind, tc.kind)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUnset(t *testing.T) {
|
||||
p := NewParser()
|
||||
cases := []struct {
|
||||
raw string
|
||||
unset bool
|
||||
}{
|
||||
{"", true},
|
||||
{"0", true},
|
||||
{"100", false},
|
||||
{"2026-06-17", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.raw, func(t *testing.T) {
|
||||
v, err := p.Parse("due", tc.raw)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse(%q) err = %v", tc.raw, err)
|
||||
}
|
||||
if IsUnset(v) != tc.unset {
|
||||
t.Fatalf("IsUnset(%q) = %v, want %v", tc.raw, IsUnset(v), tc.unset)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user