package fa import ( "fmt" "strings" "time" ) // faDateLayouts lists every layout FA has been observed emitting in // title= attributes of date elements. Tried in order. var faDateLayouts = []string{ "January 2, 2006 03:04:05 PM", // "March 23, 2026 09:01:08 AM" current beta popup_date title "January 2, 2006 3:04:05 PM", "Jan 2, 2006 03:04:05 PM", // 3-letter month variant "Jan 2, 2006 3:04:05 PM", "Jan 2, 2006 03:04 PM", // legacy beta layout (no seconds) "Jan 2, 2006 3:04 PM", "2006-01-02T15:04:05Z07:00", time.RFC3339, } // ParseFADate parses a FurAffinity-formatted date string. FA renders dates // either as a "popup" with the full timestamp in a title attribute, or as a // relative phrase ("5 hours ago") in the visible text. Callers should pass // the title attribute when available. // // FA does not include timezone information in its displayed format; the site // uses server-local time historically labelled as UTC-7. We treat parsed // values as UTC because that is what the SDK consistently exposes callers // who need a wall-clock display should convert. func ParseFADate(s string) (time.Time, error) { s = strings.TrimSpace(s) if s == "" { return time.Time{}, fmt.Errorf("parse fa date: empty string") } cleaned := stripOrdinals(s) for _, layout := range faDateLayouts { if t, err := time.ParseInLocation(layout, cleaned, time.UTC); err == nil { return t, nil } } return time.Time{}, fmt.Errorf("parse fa date %q: no matching layout", s) } // stripOrdinals removes English ordinal suffixes (st, nd, rd, th) from a date // string so it can be parsed by Go's reference layout. "Mar 17th, 2026" → // "Mar 17, 2026". func stripOrdinals(s string) string { var b strings.Builder b.Grow(len(s)) for i := 0; i < len(s); i++ { c := s[i] if (c >= '0' && c <= '9') && i+2 < len(s) { next2 := strings.ToLower(s[i+1 : i+3]) if next2 == "st" || next2 == "nd" || next2 == "rd" || next2 == "th" { b.WriteByte(c) i += 2 continue } } b.WriteByte(c) } return b.String() }