mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Merge 84998e9a77 into 88a149c085
This commit is contained in:
commit
71b074e25c
23 changed files with 285 additions and 154 deletions
|
|
@ -1,31 +1,63 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"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"
|
// Date wraps a time.Time with a format of "YYYY-MM-DD"
|
||||||
type Date struct {
|
type Date struct {
|
||||||
time.Time
|
time.Time
|
||||||
|
Precision DatePrecision
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateFormat = "2006-01-02"
|
var dateFormatPrecision = []string{
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01",
|
||||||
|
"2006",
|
||||||
|
}
|
||||||
|
|
||||||
func (d Date) String() string {
|
func (d Date) String() string {
|
||||||
return d.Format(dateFormat)
|
return d.Format(dateFormatPrecision[d.Precision])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Date) After(o Date) bool {
|
func (d Date) After(o Date) bool {
|
||||||
return d.Time.After(o.Time)
|
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) {
|
func ParseDate(s string) (Date, error) {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
// default parse to day precision
|
||||||
ret, err := utils.ParseDateStringAsTime(s)
|
ret, err := utils.ParseDateStringAsTime(s)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return Date{}, err
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
pkg/models/date_test.go
Normal file
50
pkg/models/date_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ const (
|
||||||
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 74
|
var appSchemaVersion uint = 75
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"gopkg.in/guregu/null.v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sqliteDateLayout = "2006-01-02"
|
const sqliteDateLayout = "2006-01-02"
|
||||||
|
|
@ -54,12 +55,12 @@ func (d NullDate) Value() (driver.Value, error) {
|
||||||
return d.Date.Format(sqliteDateLayout), nil
|
return d.Date.Format(sqliteDateLayout), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *NullDate) DatePtr() *models.Date {
|
func (d *NullDate) DatePtr(precision null.Int) *models.Date {
|
||||||
if d == nil || !d.Valid {
|
if d == nil || !d.Valid {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.Date{Time: d.Date}
|
return &models.Date{Time: d.Date, Precision: models.DatePrecision(precision.Int64)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NullDateFromDatePtr(d *models.Date) NullDate {
|
func NullDateFromDatePtr(d *models.Date) NullDate {
|
||||||
|
|
@ -68,3 +69,11 @@ func NullDateFromDatePtr(d *models.Date) NullDate {
|
||||||
}
|
}
|
||||||
return NullDate{Date: d.Time, Valid: true}
|
return NullDate{Date: d.Time, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func datePrecisionFromDatePtr(d *models.Date) null.Int {
|
||||||
|
if d == nil {
|
||||||
|
// default to day precision
|
||||||
|
return null.Int{}
|
||||||
|
}
|
||||||
|
return null.IntFrom(int64(d.Precision))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ type galleryRow struct {
|
||||||
Title zero.String `db:"title"`
|
Title zero.String `db:"title"`
|
||||||
Code zero.String `db:"code"`
|
Code zero.String `db:"code"`
|
||||||
Date NullDate `db:"date"`
|
Date NullDate `db:"date"`
|
||||||
|
DatePrecision null.Int `db:"date_precision"`
|
||||||
Details zero.String `db:"details"`
|
Details zero.String `db:"details"`
|
||||||
Photographer zero.String `db:"photographer"`
|
Photographer zero.String `db:"photographer"`
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
|
|
@ -50,6 +51,7 @@ func (r *galleryRow) fromGallery(o models.Gallery) {
|
||||||
r.Title = zero.StringFrom(o.Title)
|
r.Title = zero.StringFrom(o.Title)
|
||||||
r.Code = zero.StringFrom(o.Code)
|
r.Code = zero.StringFrom(o.Code)
|
||||||
r.Date = NullDateFromDatePtr(o.Date)
|
r.Date = NullDateFromDatePtr(o.Date)
|
||||||
|
r.DatePrecision = datePrecisionFromDatePtr(o.Date)
|
||||||
r.Details = zero.StringFrom(o.Details)
|
r.Details = zero.StringFrom(o.Details)
|
||||||
r.Photographer = zero.StringFrom(o.Photographer)
|
r.Photographer = zero.StringFrom(o.Photographer)
|
||||||
r.Rating = intFromPtr(o.Rating)
|
r.Rating = intFromPtr(o.Rating)
|
||||||
|
|
@ -74,7 +76,7 @@ func (r *galleryQueryRow) resolve() *models.Gallery {
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Title: r.Title.String,
|
Title: r.Title.String,
|
||||||
Code: r.Code.String,
|
Code: r.Code.String,
|
||||||
Date: r.Date.DatePtr(),
|
Date: r.Date.DatePtr(r.DatePrecision),
|
||||||
Details: r.Details.String,
|
Details: r.Details.String,
|
||||||
Photographer: r.Photographer.String,
|
Photographer: r.Photographer.String,
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
|
|
@ -102,7 +104,7 @@ type galleryRowRecord struct {
|
||||||
func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
|
func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
|
||||||
r.setNullString("title", o.Title)
|
r.setNullString("title", o.Title)
|
||||||
r.setNullString("code", o.Code)
|
r.setNullString("code", o.Code)
|
||||||
r.setNullDate("date", o.Date)
|
r.setNullDate("date", "date_precision", o.Date)
|
||||||
r.setNullString("details", o.Details)
|
r.setNullString("details", o.Details)
|
||||||
r.setNullString("photographer", o.Photographer)
|
r.setNullString("photographer", o.Photographer)
|
||||||
r.setNullInt("rating", o.Rating)
|
r.setNullInt("rating", o.Rating)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ type groupRow struct {
|
||||||
Aliases zero.String `db:"aliases"`
|
Aliases zero.String `db:"aliases"`
|
||||||
Duration null.Int `db:"duration"`
|
Duration null.Int `db:"duration"`
|
||||||
Date NullDate `db:"date"`
|
Date NullDate `db:"date"`
|
||||||
|
DatePrecision null.Int `db:"date_precision"`
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
StudioID null.Int `db:"studio_id,omitempty"`
|
StudioID null.Int `db:"studio_id,omitempty"`
|
||||||
|
|
@ -56,6 +57,7 @@ func (r *groupRow) fromGroup(o models.Group) {
|
||||||
r.Aliases = zero.StringFrom(o.Aliases)
|
r.Aliases = zero.StringFrom(o.Aliases)
|
||||||
r.Duration = intFromPtr(o.Duration)
|
r.Duration = intFromPtr(o.Duration)
|
||||||
r.Date = NullDateFromDatePtr(o.Date)
|
r.Date = NullDateFromDatePtr(o.Date)
|
||||||
|
r.DatePrecision = datePrecisionFromDatePtr(o.Date)
|
||||||
r.Rating = intFromPtr(o.Rating)
|
r.Rating = intFromPtr(o.Rating)
|
||||||
r.StudioID = intFromPtr(o.StudioID)
|
r.StudioID = intFromPtr(o.StudioID)
|
||||||
r.Director = zero.StringFrom(o.Director)
|
r.Director = zero.StringFrom(o.Director)
|
||||||
|
|
@ -70,7 +72,7 @@ func (r *groupRow) resolve() *models.Group {
|
||||||
Name: r.Name.String,
|
Name: r.Name.String,
|
||||||
Aliases: r.Aliases.String,
|
Aliases: r.Aliases.String,
|
||||||
Duration: nullIntPtr(r.Duration),
|
Duration: nullIntPtr(r.Duration),
|
||||||
Date: r.Date.DatePtr(),
|
Date: r.Date.DatePtr(r.DatePrecision),
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
StudioID: nullIntPtr(r.StudioID),
|
StudioID: nullIntPtr(r.StudioID),
|
||||||
Director: r.Director.String,
|
Director: r.Director.String,
|
||||||
|
|
@ -90,7 +92,7 @@ func (r *groupRowRecord) fromPartial(o models.GroupPartial) {
|
||||||
r.setNullString("name", o.Name)
|
r.setNullString("name", o.Name)
|
||||||
r.setNullString("aliases", o.Aliases)
|
r.setNullString("aliases", o.Aliases)
|
||||||
r.setNullInt("duration", o.Duration)
|
r.setNullInt("duration", o.Duration)
|
||||||
r.setNullDate("date", o.Date)
|
r.setNullDate("date", "date_precision", o.Date)
|
||||||
r.setNullInt("rating", o.Rating)
|
r.setNullInt("rating", o.Rating)
|
||||||
r.setNullInt("studio_id", o.StudioID)
|
r.setNullInt("studio_id", o.StudioID)
|
||||||
r.setNullString("director", o.Director)
|
r.setNullString("director", o.Director)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ type imageRow struct {
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Date NullDate `db:"date"`
|
Date NullDate `db:"date"`
|
||||||
|
DatePrecision null.Int `db:"date_precision"`
|
||||||
Details zero.String `db:"details"`
|
Details zero.String `db:"details"`
|
||||||
Photographer zero.String `db:"photographer"`
|
Photographer zero.String `db:"photographer"`
|
||||||
Organized bool `db:"organized"`
|
Organized bool `db:"organized"`
|
||||||
|
|
@ -51,6 +52,7 @@ func (r *imageRow) fromImage(i models.Image) {
|
||||||
r.Code = zero.StringFrom(i.Code)
|
r.Code = zero.StringFrom(i.Code)
|
||||||
r.Rating = intFromPtr(i.Rating)
|
r.Rating = intFromPtr(i.Rating)
|
||||||
r.Date = NullDateFromDatePtr(i.Date)
|
r.Date = NullDateFromDatePtr(i.Date)
|
||||||
|
r.DatePrecision = datePrecisionFromDatePtr(i.Date)
|
||||||
r.Details = zero.StringFrom(i.Details)
|
r.Details = zero.StringFrom(i.Details)
|
||||||
r.Photographer = zero.StringFrom(i.Photographer)
|
r.Photographer = zero.StringFrom(i.Photographer)
|
||||||
r.Organized = i.Organized
|
r.Organized = i.Organized
|
||||||
|
|
@ -74,7 +76,7 @@ func (r *imageQueryRow) resolve() *models.Image {
|
||||||
Title: r.Title.String,
|
Title: r.Title.String,
|
||||||
Code: r.Code.String,
|
Code: r.Code.String,
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
Date: r.Date.DatePtr(),
|
Date: r.Date.DatePtr(r.DatePrecision),
|
||||||
Details: r.Details.String,
|
Details: r.Details.String,
|
||||||
Photographer: r.Photographer.String,
|
Photographer: r.Photographer.String,
|
||||||
Organized: r.Organized,
|
Organized: r.Organized,
|
||||||
|
|
@ -103,7 +105,7 @@ func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
|
||||||
r.setNullString("title", i.Title)
|
r.setNullString("title", i.Title)
|
||||||
r.setNullString("code", i.Code)
|
r.setNullString("code", i.Code)
|
||||||
r.setNullInt("rating", i.Rating)
|
r.setNullInt("rating", i.Rating)
|
||||||
r.setNullDate("date", i.Date)
|
r.setNullDate("date", "date_precision", i.Date)
|
||||||
r.setNullString("details", i.Details)
|
r.setNullString("details", i.Details)
|
||||||
r.setNullString("photographer", i.Photographer)
|
r.setNullString("photographer", i.Photographer)
|
||||||
r.setBool("organized", i.Organized)
|
r.setBool("organized", i.Organized)
|
||||||
|
|
|
||||||
13
pkg/sqlite/migrations/75_date_precision.up.sql
Normal file
13
pkg/sqlite/migrations/75_date_precision.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
ALTER TABLE "scenes" ADD COLUMN "date_precision" TINYINT;
|
||||||
|
ALTER TABLE "images" ADD COLUMN "date_precision" TINYINT;
|
||||||
|
ALTER TABLE "galleries" ADD COLUMN "date_precision" TINYINT;
|
||||||
|
ALTER TABLE "groups" ADD COLUMN "date_precision" TINYINT;
|
||||||
|
ALTER TABLE "performers" ADD COLUMN "birthdate_precision" TINYINT;
|
||||||
|
ALTER TABLE "performers" ADD COLUMN "death_date_precision" TINYINT;
|
||||||
|
|
||||||
|
UPDATE "scenes" SET "date_precision" = 0 WHERE "date" IS NOT NULL;
|
||||||
|
UPDATE "images" SET "date_precision" = 0 WHERE "date" IS NOT NULL;
|
||||||
|
UPDATE "galleries" SET "date_precision" = 0 WHERE "date" IS NOT NULL;
|
||||||
|
UPDATE "groups" SET "date_precision" = 0 WHERE "date" IS NOT NULL;
|
||||||
|
UPDATE "performers" SET "birthdate_precision" = 0 WHERE "birthdate" IS NOT NULL;
|
||||||
|
UPDATE "performers" SET "death_date_precision" = 0 WHERE "death_date" IS NOT NULL;
|
||||||
|
|
@ -35,6 +35,7 @@ type performerRow struct {
|
||||||
Disambigation zero.String `db:"disambiguation"`
|
Disambigation zero.String `db:"disambiguation"`
|
||||||
Gender zero.String `db:"gender"`
|
Gender zero.String `db:"gender"`
|
||||||
Birthdate NullDate `db:"birthdate"`
|
Birthdate NullDate `db:"birthdate"`
|
||||||
|
BirthdatePrecision null.Int `db:"birthdate_precision"`
|
||||||
Ethnicity zero.String `db:"ethnicity"`
|
Ethnicity zero.String `db:"ethnicity"`
|
||||||
Country zero.String `db:"country"`
|
Country zero.String `db:"country"`
|
||||||
EyeColor zero.String `db:"eye_color"`
|
EyeColor zero.String `db:"eye_color"`
|
||||||
|
|
@ -53,6 +54,7 @@ type performerRow struct {
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Details zero.String `db:"details"`
|
Details zero.String `db:"details"`
|
||||||
DeathDate NullDate `db:"death_date"`
|
DeathDate NullDate `db:"death_date"`
|
||||||
|
DeathDatePrecision null.Int `db:"death_date_precision"`
|
||||||
HairColor zero.String `db:"hair_color"`
|
HairColor zero.String `db:"hair_color"`
|
||||||
Weight null.Int `db:"weight"`
|
Weight null.Int `db:"weight"`
|
||||||
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
||||||
|
|
@ -69,6 +71,7 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||||
r.Gender = zero.StringFrom(o.Gender.String())
|
r.Gender = zero.StringFrom(o.Gender.String())
|
||||||
}
|
}
|
||||||
r.Birthdate = NullDateFromDatePtr(o.Birthdate)
|
r.Birthdate = NullDateFromDatePtr(o.Birthdate)
|
||||||
|
r.BirthdatePrecision = datePrecisionFromDatePtr(o.Birthdate)
|
||||||
r.Ethnicity = zero.StringFrom(o.Ethnicity)
|
r.Ethnicity = zero.StringFrom(o.Ethnicity)
|
||||||
r.Country = zero.StringFrom(o.Country)
|
r.Country = zero.StringFrom(o.Country)
|
||||||
r.EyeColor = zero.StringFrom(o.EyeColor)
|
r.EyeColor = zero.StringFrom(o.EyeColor)
|
||||||
|
|
@ -88,6 +91,7 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||||
r.Rating = intFromPtr(o.Rating)
|
r.Rating = intFromPtr(o.Rating)
|
||||||
r.Details = zero.StringFrom(o.Details)
|
r.Details = zero.StringFrom(o.Details)
|
||||||
r.DeathDate = NullDateFromDatePtr(o.DeathDate)
|
r.DeathDate = NullDateFromDatePtr(o.DeathDate)
|
||||||
|
r.DeathDatePrecision = datePrecisionFromDatePtr(o.DeathDate)
|
||||||
r.HairColor = zero.StringFrom(o.HairColor)
|
r.HairColor = zero.StringFrom(o.HairColor)
|
||||||
r.Weight = intFromPtr(o.Weight)
|
r.Weight = intFromPtr(o.Weight)
|
||||||
r.IgnoreAutoTag = o.IgnoreAutoTag
|
r.IgnoreAutoTag = o.IgnoreAutoTag
|
||||||
|
|
@ -98,7 +102,7 @@ func (r *performerRow) resolve() *models.Performer {
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Name: r.Name.String,
|
Name: r.Name.String,
|
||||||
Disambiguation: r.Disambigation.String,
|
Disambiguation: r.Disambigation.String,
|
||||||
Birthdate: r.Birthdate.DatePtr(),
|
Birthdate: r.Birthdate.DatePtr(r.BirthdatePrecision),
|
||||||
Ethnicity: r.Ethnicity.String,
|
Ethnicity: r.Ethnicity.String,
|
||||||
Country: r.Country.String,
|
Country: r.Country.String,
|
||||||
EyeColor: r.EyeColor.String,
|
EyeColor: r.EyeColor.String,
|
||||||
|
|
@ -115,7 +119,7 @@ func (r *performerRow) resolve() *models.Performer {
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
Details: r.Details.String,
|
Details: r.Details.String,
|
||||||
DeathDate: r.DeathDate.DatePtr(),
|
DeathDate: r.DeathDate.DatePtr(r.DeathDatePrecision),
|
||||||
HairColor: r.HairColor.String,
|
HairColor: r.HairColor.String,
|
||||||
Weight: nullIntPtr(r.Weight),
|
Weight: nullIntPtr(r.Weight),
|
||||||
IgnoreAutoTag: r.IgnoreAutoTag,
|
IgnoreAutoTag: r.IgnoreAutoTag,
|
||||||
|
|
@ -142,7 +146,7 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
||||||
r.setString("name", o.Name)
|
r.setString("name", o.Name)
|
||||||
r.setNullString("disambiguation", o.Disambiguation)
|
r.setNullString("disambiguation", o.Disambiguation)
|
||||||
r.setNullString("gender", o.Gender)
|
r.setNullString("gender", o.Gender)
|
||||||
r.setNullDate("birthdate", o.Birthdate)
|
r.setNullDate("birthdate", "birthdate_precision", o.Birthdate)
|
||||||
r.setNullString("ethnicity", o.Ethnicity)
|
r.setNullString("ethnicity", o.Ethnicity)
|
||||||
r.setNullString("country", o.Country)
|
r.setNullString("country", o.Country)
|
||||||
r.setNullString("eye_color", o.EyeColor)
|
r.setNullString("eye_color", o.EyeColor)
|
||||||
|
|
@ -159,7 +163,7 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
||||||
r.setTimestamp("updated_at", o.UpdatedAt)
|
r.setTimestamp("updated_at", o.UpdatedAt)
|
||||||
r.setNullInt("rating", o.Rating)
|
r.setNullInt("rating", o.Rating)
|
||||||
r.setNullString("details", o.Details)
|
r.setNullString("details", o.Details)
|
||||||
r.setNullDate("death_date", o.DeathDate)
|
r.setNullDate("death_date", "death_date_precision", o.DeathDate)
|
||||||
r.setNullString("hair_color", o.HairColor)
|
r.setNullString("hair_color", o.HairColor)
|
||||||
r.setNullInt("weight", o.Weight)
|
r.setNullInt("weight", o.Weight)
|
||||||
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,9 @@ func (r *updateRecord) setNullTimestamp(destField string, v models.OptionalTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *updateRecord) setNullDate(destField string, v models.OptionalDate) {
|
func (r *updateRecord) setNullDate(destField string, precisionField string, v models.OptionalDate) {
|
||||||
if v.Set {
|
if v.Set {
|
||||||
r.set(destField, NullDateFromDatePtr(v.Ptr()))
|
r.set(destField, NullDateFromDatePtr(v.Ptr()))
|
||||||
|
r.set(precisionField, datePrecisionFromDatePtr(v.Ptr()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ type sceneRow struct {
|
||||||
Details zero.String `db:"details"`
|
Details zero.String `db:"details"`
|
||||||
Director zero.String `db:"director"`
|
Director zero.String `db:"director"`
|
||||||
Date NullDate `db:"date"`
|
Date NullDate `db:"date"`
|
||||||
|
DatePrecision null.Int `db:"date_precision"`
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Organized bool `db:"organized"`
|
Organized bool `db:"organized"`
|
||||||
|
|
@ -102,6 +103,7 @@ func (r *sceneRow) fromScene(o models.Scene) {
|
||||||
r.Details = zero.StringFrom(o.Details)
|
r.Details = zero.StringFrom(o.Details)
|
||||||
r.Director = zero.StringFrom(o.Director)
|
r.Director = zero.StringFrom(o.Director)
|
||||||
r.Date = NullDateFromDatePtr(o.Date)
|
r.Date = NullDateFromDatePtr(o.Date)
|
||||||
|
r.DatePrecision = datePrecisionFromDatePtr(o.Date)
|
||||||
r.Rating = intFromPtr(o.Rating)
|
r.Rating = intFromPtr(o.Rating)
|
||||||
r.Organized = o.Organized
|
r.Organized = o.Organized
|
||||||
r.StudioID = intFromPtr(o.StudioID)
|
r.StudioID = intFromPtr(o.StudioID)
|
||||||
|
|
@ -127,7 +129,7 @@ func (r *sceneQueryRow) resolve() *models.Scene {
|
||||||
Code: r.Code.String,
|
Code: r.Code.String,
|
||||||
Details: r.Details.String,
|
Details: r.Details.String,
|
||||||
Director: r.Director.String,
|
Director: r.Director.String,
|
||||||
Date: r.Date.DatePtr(),
|
Date: r.Date.DatePtr(r.DatePrecision),
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
Organized: r.Organized,
|
Organized: r.Organized,
|
||||||
StudioID: nullIntPtr(r.StudioID),
|
StudioID: nullIntPtr(r.StudioID),
|
||||||
|
|
@ -159,7 +161,7 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) {
|
||||||
r.setNullString("code", o.Code)
|
r.setNullString("code", o.Code)
|
||||||
r.setNullString("details", o.Details)
|
r.setNullString("details", o.Details)
|
||||||
r.setNullString("director", o.Director)
|
r.setNullString("director", o.Director)
|
||||||
r.setNullDate("date", o.Date)
|
r.setNullDate("date", "date_precision", o.Date)
|
||||||
r.setNullInt("rating", o.Rating)
|
r.setNullInt("rating", o.Rating)
|
||||||
r.setBool("organized", o.Organized)
|
r.setBool("organized", o.Organized)
|
||||||
r.setNullInt("studio_id", o.StudioID)
|
r.setNullInt("studio_id", o.StudioID)
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,5 @@ func ParseDateStringAsTime(dateString string) (time.Time, error) {
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support partial dates: year-month format
|
|
||||||
t, e = time.Parse("2006-01", dateString)
|
|
||||||
if e == nil {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support partial dates: year only format
|
|
||||||
t, e = time.Parse("2006", dateString)
|
|
||||||
if e == nil {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Time{}, fmt.Errorf("ParseDateStringAsTime failed: dateString <%s>", dateString)
|
return time.Time{}, fmt.Errorf("ParseDateStringAsTime failed: dateString <%s>", dateString)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseDateStringAsTime(t *testing.T) {
|
func TestParseDateStringAsTime(t *testing.T) {
|
||||||
|
|
@ -16,13 +15,11 @@ func TestParseDateStringAsTime(t *testing.T) {
|
||||||
{"Date only", "2014-01-02", false},
|
{"Date only", "2014-01-02", false},
|
||||||
{"Date with time", "2014-01-02 15:04:05", false},
|
{"Date with time", "2014-01-02 15:04:05", false},
|
||||||
|
|
||||||
// Partial date formats (new support)
|
|
||||||
{"Year-Month", "2006-08", false},
|
|
||||||
{"Year only", "2014", false},
|
|
||||||
|
|
||||||
// Invalid formats
|
// Invalid formats
|
||||||
{"Invalid format", "not-a-date", true},
|
{"Invalid format", "not-a-date", true},
|
||||||
{"Empty string", "", true},
|
{"Empty string", "", true},
|
||||||
|
{"Year-Month", "2006-08", true},
|
||||||
|
{"Year only", "2014", true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
@ -44,37 +41,3 @@ func TestParseDateStringAsTime(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseDateStringAsTime_YearOnly(t *testing.T) {
|
|
||||||
result, err := ParseDateStringAsTime("2014")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse year-only date: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Year() != 2014 {
|
|
||||||
t.Errorf("Expected year 2014, got %d", result.Year())
|
|
||||||
}
|
|
||||||
if result.Month() != time.January {
|
|
||||||
t.Errorf("Expected month January, got %s", result.Month())
|
|
||||||
}
|
|
||||||
if result.Day() != 1 {
|
|
||||||
t.Errorf("Expected day 1, got %d", result.Day())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDateStringAsTime_YearMonth(t *testing.T) {
|
|
||||||
result, err := ParseDateStringAsTime("2006-08")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse year-month date: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Year() != 2006 {
|
|
||||||
t.Errorf("Expected year 2006, got %d", result.Year())
|
|
||||||
}
|
|
||||||
if result.Month() != time.August {
|
|
||||||
t.Errorf("Expected month August, got %s", result.Month())
|
|
||||||
}
|
|
||||||
if result.Day() != 1 {
|
|
||||||
t.Errorf("Expected day 1, got %d", result.Day())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
RouteComponentProps,
|
RouteComponentProps,
|
||||||
Redirect,
|
Redirect,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
|
|
@ -44,6 +44,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { useConfigurationContext } from "src/hooks/Config";
|
import { useConfigurationContext } from "src/hooks/Config";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { goBackOrReplace } from "src/utils/history";
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
import { FormattedDate } from "src/components/Shared/Date";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: GQL.GalleryDataFragment;
|
gallery: GQL.GalleryDataFragment;
|
||||||
|
|
@ -410,11 +411,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
|
||||||
<div className="gallery-subheader">
|
<div className="gallery-subheader">
|
||||||
{!!gallery.date && (
|
{!!gallery.date && (
|
||||||
<span className="date" data-value={gallery.date}>
|
<span className="date" data-value={gallery.date}>
|
||||||
<FormattedDate
|
<FormattedDate value={gallery.date} />
|
||||||
value={gallery.date}
|
|
||||||
format="long"
|
|
||||||
timeZone="utc"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||||
)}
|
)}
|
||||||
<TruncatedText text={performers.join(", ")} />
|
<TruncatedText text={performers.join(", ")} />
|
||||||
<div>
|
<div>
|
||||||
{gallery.date && TextUtils.formatDate(intl, gallery.date)}
|
{gallery.date && TextUtils.formatFuzzyDate(intl, gallery.date)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export const GroupDetailsPanel: React.FC<IGroupDetailsPanel> = ({
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
id="date"
|
id="date"
|
||||||
value={group.date ? TextUtils.formatDate(intl, group.date) : ""}
|
value={group.date ? TextUtils.formatFuzzyDate(intl, group.date) : ""}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useHistory, Link, RouteComponentProps } from "react-router-dom";
|
import { useHistory, Link, RouteComponentProps } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import {
|
import {
|
||||||
|
|
@ -35,6 +35,7 @@ import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { goBackOrReplace } from "src/utils/history";
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
import { FormattedDate } from "src/components/Shared/Date";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
|
|
@ -319,13 +320,7 @@ const ImagePage: React.FC<IProps> = ({ image }) => {
|
||||||
|
|
||||||
<div className="image-subheader">
|
<div className="image-subheader">
|
||||||
<span className="date" data-value={image.date}>
|
<span className="date" data-value={image.date}>
|
||||||
{!!image.date && (
|
{!!image.date && <FormattedDate value={image.date} />}
|
||||||
<FormattedDate
|
|
||||||
value={image.date}
|
|
||||||
format="long"
|
|
||||||
timeZone="utc"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
{resolution ? (
|
{resolution ? (
|
||||||
<span className="resolution" data-value={resolution}>
|
<span className="resolution" data-value={resolution}>
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> =
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
!fullWidth
|
!fullWidth
|
||||||
? TextUtils.formatDate(intl, performer.birthdate ?? undefined)
|
? TextUtils.formatFuzzyDate(
|
||||||
|
intl,
|
||||||
|
performer.birthdate ?? undefined
|
||||||
|
)
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
|
|
@ -218,7 +221,7 @@ export const CompressedPerformerDetailsPanel: React.FC<IPerformerDetails> =
|
||||||
<span className="detail-divider">/</span>
|
<span className="detail-divider">/</span>
|
||||||
<span
|
<span
|
||||||
className="performer-age"
|
className="performer-age"
|
||||||
title={TextUtils.formatDate(
|
title={TextUtils.formatFuzzyDate(
|
||||||
intl,
|
intl,
|
||||||
performer.birthdate ?? undefined
|
performer.birthdate ?? undefined
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
||||||
<span
|
<span
|
||||||
title={
|
title={
|
||||||
performer.birthdate
|
performer.birthdate
|
||||||
? TextUtils.formatDate(intl, performer.birthdate ?? undefined)
|
? TextUtils.formatFuzzyDate(intl, performer.birthdate ?? undefined)
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Link, RouteComponentProps } from "react-router-dom";
|
import { Link, RouteComponentProps } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
@ -51,6 +51,7 @@ import cx from "classnames";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { PatchComponent, PatchContainerComponent } from "src/patch";
|
import { PatchComponent, PatchContainerComponent } from "src/patch";
|
||||||
import { goBackOrReplace } from "src/utils/history";
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
import { FormattedDate } from "src/components/Shared/Date";
|
||||||
|
|
||||||
const SubmitStashBoxDraft = lazyComponent(
|
const SubmitStashBoxDraft = lazyComponent(
|
||||||
() => import("src/components/Dialogs/SubmitDraft")
|
() => import("src/components/Dialogs/SubmitDraft")
|
||||||
|
|
@ -613,13 +614,7 @@ const ScenePage: React.FC<IProps> = PatchComponent("ScenePage", (props) => {
|
||||||
|
|
||||||
<div className="scene-subheader">
|
<div className="scene-subheader">
|
||||||
<span className="date" data-value={scene.date}>
|
<span className="date" data-value={scene.date}>
|
||||||
{!!scene.date && (
|
{!!scene.date && <FormattedDate value={scene.date} />}
|
||||||
<FormattedDate
|
|
||||||
value={scene.date}
|
|
||||||
format="long"
|
|
||||||
timeZone="utc"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<VideoFrameRateResolution
|
<VideoFrameRateResolution
|
||||||
width={file?.width}
|
width={file?.width}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,9 @@ export const SceneWallItem: React.FC<
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TruncatedText text={performers.join(", ")} />
|
<TruncatedText text={performers.join(", ")} />
|
||||||
<div>{scene.date && TextUtils.formatDate(intl, scene.date)}</div>
|
<div>
|
||||||
|
{scene.date && TextUtils.formatFuzzyDate(intl, scene.date)}
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
36
ui/v2.5/src/components/Shared/Date.tsx
Normal file
36
ui/v2.5/src/components/Shared/Date.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
import { FormattedDate as IntlDate } from "react-intl";
|
||||||
|
import { PatchComponent } from "src/patch";
|
||||||
|
|
||||||
|
// wraps FormattedDate to handle year or year/month dates
|
||||||
|
export const FormattedDate: React.FC<{
|
||||||
|
value: string | number | Date | undefined;
|
||||||
|
}> = PatchComponent("Date", ({ value }) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
// try parsing as year or year/month
|
||||||
|
const yearMatch = value.match(/^(\d{4})$/);
|
||||||
|
if (yearMatch) {
|
||||||
|
const year = parseInt(yearMatch[1], 10);
|
||||||
|
return (
|
||||||
|
<IntlDate value={Date.UTC(year, 0)} year="numeric" timeZone="utc" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearMonthMatch = value.match(/^(\d{4})-(\d{2})$/);
|
||||||
|
if (yearMonthMatch) {
|
||||||
|
const year = parseInt(yearMonthMatch[1], 10);
|
||||||
|
const month = parseInt(yearMonthMatch[2], 10) - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlDate
|
||||||
|
value={Date.UTC(year, month)}
|
||||||
|
year="numeric"
|
||||||
|
month="long"
|
||||||
|
timeZone="utc"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <IntlDate value={value} format="long" timeZone="utc" />;
|
||||||
|
});
|
||||||
|
|
@ -336,8 +336,10 @@ function dateTimeToString(date: Date) {
|
||||||
const getAge = (dateString?: string | null, fromDateString?: string | null) => {
|
const getAge = (dateString?: string | null, fromDateString?: string | null) => {
|
||||||
if (!dateString) return 0;
|
if (!dateString) return 0;
|
||||||
|
|
||||||
const birthdate = stringToDate(dateString);
|
const birthdate = stringToFuzzyDate(dateString);
|
||||||
const fromDate = fromDateString ? stringToDate(fromDateString) : new Date();
|
const fromDate = fromDateString
|
||||||
|
? stringToFuzzyDate(fromDateString)
|
||||||
|
: new Date();
|
||||||
|
|
||||||
if (!birthdate || !fromDate) return 0;
|
if (!birthdate || !fromDate) return 0;
|
||||||
|
|
||||||
|
|
@ -459,6 +461,38 @@ const formatDate = (intl: IntlShape, date?: string, utc = true) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatFuzzyDate = (intl: IntlShape, date?: string, utc = true) => {
|
||||||
|
if (!date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle year or year/month dates
|
||||||
|
const yearMatch = date.match(/^(\d{4})$/);
|
||||||
|
if (yearMatch) {
|
||||||
|
const year = parseInt(yearMatch[1], 10);
|
||||||
|
return intl.formatDate(Date.UTC(year, 0), {
|
||||||
|
year: "numeric",
|
||||||
|
timeZone: utc ? "utc" : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearMonthMatch = date.match(/^(\d{4})-(\d{2})$/);
|
||||||
|
if (yearMonthMatch) {
|
||||||
|
const year = parseInt(yearMonthMatch[1], 10);
|
||||||
|
const month = parseInt(yearMonthMatch[2], 10) - 1;
|
||||||
|
return intl.formatDate(Date.UTC(year, month), {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
timeZone: utc ? "utc" : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return intl.formatDate(date, {
|
||||||
|
format: "long",
|
||||||
|
timeZone: utc ? "utc" : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formatDateTime = (intl: IntlShape, dateTime?: string, utc = false) =>
|
const formatDateTime = (intl: IntlShape, dateTime?: string, utc = false) =>
|
||||||
`${formatDate(intl, dateTime, utc)} ${intl.formatTime(dateTime, {
|
`${formatDate(intl, dateTime, utc)} ${intl.formatTime(dateTime, {
|
||||||
timeZone: utc ? "utc" : undefined,
|
timeZone: utc ? "utc" : undefined,
|
||||||
|
|
@ -519,6 +553,7 @@ const TextUtils = {
|
||||||
sanitiseURL,
|
sanitiseURL,
|
||||||
domainFromURL,
|
domainFromURL,
|
||||||
formatDate,
|
formatDate,
|
||||||
|
formatFuzzyDate,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
secondsAsTimeString,
|
secondsAsTimeString,
|
||||||
abbreviateCounter,
|
abbreviateCounter,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue