From b4b1f4d7a1e13845976d7ca4e001dd9c07fc3fd3 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:02:06 +1100 Subject: [PATCH] Add precision field to Date and handle parsing year/month-only dates --- pkg/models/date.go | 44 +++++++++++++++++++++++++++++++----- pkg/models/date_test.go | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 pkg/models/date_test.go diff --git a/pkg/models/date.go b/pkg/models/date.go index 151e32c1d..0d5d69f17 100644 --- a/pkg/models/date.go +++ b/pkg/models/date.go @@ -1,31 +1,63 @@ package models import ( + "fmt" "time" "github.com/stashapp/stash/pkg/utils" ) +type DatePrecision int + +const ( + // default precision is day + DatePrecisionDay DatePrecision = iota + DatePrecisionMonth + DatePrecisionYear +) + // Date wraps a time.Time with a format of "YYYY-MM-DD" type Date struct { time.Time + Precision DatePrecision } -const dateFormat = "2006-01-02" +var dateFormatPrecision []string = []string{ + "2006-01-02", + "2006-01", + "2006", +} func (d Date) String() string { - return d.Format(dateFormat) + return d.Format(dateFormatPrecision[d.Precision]) } func (d Date) After(o Date) bool { return d.Time.After(o.Time) } -// ParseDate uses utils.ParseDateStringAsTime to parse a string into a date. +// ParseDate tries to parse the input string into a date using utils.ParseDateStringAsTime. +// If that fails, it attempts to parse the string with decreasing precision (month, then year). +// It returns a Date struct with the appropriate precision set, or an error if all parsing attempts fail. func ParseDate(s string) (Date, error) { + var errs []error + + // default parse to day precision ret, err := utils.ParseDateStringAsTime(s) - if err != nil { - return Date{}, err + if err == nil { + return Date{Time: ret, Precision: DatePrecisionDay}, nil } - return Date{Time: ret}, nil + + errs = append(errs, err) + + // try month and year precision + for i, format := range dateFormatPrecision[1:] { + ret, err := time.Parse(format, s) + if err == nil { + return Date{Time: ret, Precision: DatePrecision(i + 1)}, nil + } + errs = append(errs, err) + } + + return Date{}, fmt.Errorf("failed to parse date %q: %v", s, errs) } diff --git a/pkg/models/date_test.go b/pkg/models/date_test.go new file mode 100644 index 000000000..b6cca9ee1 --- /dev/null +++ b/pkg/models/date_test.go @@ -0,0 +1,50 @@ +package models + +import ( + "testing" + "time" +) + +func TestParseDateStringAsTime(t *testing.T) { + tests := []struct { + name string + input string + output Date + expectError bool + }{ + // Full date formats (existing support) + {"RFC3339", "2014-01-02T15:04:05Z", Date{Time: time.Date(2014, 1, 2, 15, 4, 5, 0, time.UTC), Precision: DatePrecisionDay}, false}, + {"Date only", "2014-01-02", Date{Time: time.Date(2014, 1, 2, 0, 0, 0, 0, time.UTC), Precision: DatePrecisionDay}, false}, + {"Date with time", "2014-01-02 15:04:05", Date{Time: time.Date(2014, 1, 2, 15, 4, 5, 0, time.UTC), Precision: DatePrecisionDay}, false}, + + // Partial date formats (new support) + {"Year-Month", "2006-08", Date{Time: time.Date(2006, 8, 1, 0, 0, 0, 0, time.UTC), Precision: DatePrecisionMonth}, false}, + {"Year only", "2014", Date{Time: time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC), Precision: DatePrecisionYear}, false}, + + // Invalid formats + {"Invalid format", "not-a-date", Date{}, true}, + {"Empty string", "", Date{}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseDate(tt.input) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + return + } + + if err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + return + } + + if !result.Time.Equal(tt.output.Time) || result.Precision != tt.output.Precision { + t.Errorf("For input %q, expected output %+v, got %+v", tt.input, tt.output, result) + } + }) + } +}