// Package normalize converts timestamps between the formats the tilstream
// feed writers need. Every writer is happy with RFC3339, but RSS 2.0
// readers still expect RFC1123Z, and JSON Feed readers tolerate either.
package normalize
import (
"fmt"
"strings"
"time"
)
// ParseFlexible attempts to parse a timestamp in one of the usual formats.
// It returns the first successful interpretation.
func ParseFlexible(s string) (time.Time, error) {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}, fmt.Errorf("normalize: empty date string")
}
formats := []string{
time.RFC3339Nano,
time.RFC3339,
time.RFC1123Z,
time.RFC1123,
"2006-01-02T15:04:05",
"2006-01-02 15:04:05",
"2006-01-02",
"2006/01/02",
}
var firstErr error
for _, f := range formats {
t, err := time.Parse(f, s)
if err == nil {
return t, nil
}
if firstErr == nil {
firstErr = err
}
}
return time.Time{}, fmt.Errorf("normalize: could not parse %q: %w", s, firstErr)
}
// FormatRFC1123Z returns the RSS 2.0 date string, or an empty string for
// the zero time.
func FormatRFC1123Z(t time.Time) string {
if t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC1123Z)
}
// FormatRFC3339 returns the Atom/JSON Feed date string.
func FormatRFC3339(t time.Time) string {
if t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}
// FormatHumanDate formats t as "Jan 2, 2006" which I use in templates.
func FormatHumanDate(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("Jan 2, 2006")
}
// MustParse is a test helper that panics on parse failure.
func MustParse(s string) time.Time {
t, err := ParseFlexible(s)
if err != nil {
panic(err)
}
return t
}
// TruncateToDay returns t with its time-of-day stripped, in its location.
func TruncateToDay(t time.Time) time.Time {
y, m, d := t.Date()
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
}
// ISOWeek returns a pair (year, week) for t, following ISO 8601.
func ISOWeek(t time.Time) (int, int) {
return t.ISOWeek()
}
// SortDescending sorts a slice of times in place, newest first.
func SortDescending(ts []time.Time) {
// Simple insertion sort; len is expected to be small (<= 2 per item).
for i := 1; i < len(ts); i++ {
j := i
for j > 0 && ts[j].After(ts[j-1]) {
ts[j], ts[j-1] = ts[j-1], ts[j]
j--
}
}
}
// CoerceZone interprets naive times in the given location. If t already
// has a non-UTC location it is returned unchanged.
func CoerceZone(t time.Time, loc *time.Location) time.Time {
if loc == nil || t.IsZero() || t.Location() != time.UTC {
return t
}
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc)
}