Live video on the AT Protocol
1package aqtime
2
3import (
4 "fmt"
5 "regexp"
6 "strings"
7 "time"
8)
9
10// RE matches the canonical internal format: 2006-01-02T15:04:05.000Z
11// It also accepts the file-safe variant with dashes/dots swapped, for backward compat.
12var RE *regexp.Regexp
13var Pattern string = `(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d)(?:[:-])(\d\d)(?:[:-])(\d\d)(?:[.-])(\d\d\d)Z`
14
15func init() {
16 RE = regexp.MustCompile(fmt.Sprintf(`^%s$`, Pattern))
17}
18
19var fstr = "2006-01-02T15:04:05.000Z"
20
21type AQTime string
22
23// return a consistently formatted timestamp
24func FromMillis(ms int64) AQTime {
25 return AQTime(time.UnixMilli(ms).UTC().Format(fstr))
26}
27
28// return a consistently formatted timestamp
29func FromSec(sec int64) AQTime {
30 return AQTime(time.Unix(sec, 0).UTC().Format(fstr))
31}
32
33func FromString(str string) (AQTime, error) {
34 // Reject -00:00 (valid RFC 3339 but disallowed by ATProto)
35 if strings.HasSuffix(str, "-00:00") {
36 return "", fmt.Errorf("bad time format, -00:00 timezone offset is not allowed, got=%s", str)
37 }
38
39 t, err := time.Parse(time.RFC3339Nano, str)
40 if err != nil {
41 // Fall back to file-safe variant (e.g. 2024-09-13T18-10-17-090Z)
42 if bits := RE.FindStringSubmatch(str); bits != nil {
43 if bits[2] < "01" || bits[2] > "12" || bits[3] < "01" || bits[3] > "31" ||
44 bits[4] > "23" || bits[5] > "59" || bits[6] > "60" {
45 return "", fmt.Errorf("bad time format, invalid date/time values in %s", str)
46 }
47 return AQTime(str), nil
48 }
49 return "", fmt.Errorf("bad time format: %w", err)
50 }
51
52 // Reject if UTC normalization results in a negative year
53 utc := t.UTC()
54 if utc.Year() < 0 {
55 return "", fmt.Errorf("bad time format, datetime normalizes to negative year: %s", str)
56 }
57
58 // Normalize to canonical UTC millisecond format
59 return AQTime(utc.Format(fstr)), nil
60}
61
62func FromTime(t time.Time) AQTime {
63 return AQTime(t.UTC().Format(fstr))
64}
65
66// year, month, day, hour, min, sec, millisecond
67func (aqt AQTime) Parts() (string, string, string, string, string, string, string) {
68 bits := RE.FindStringSubmatch(aqt.String())
69 return bits[1], bits[2], bits[3], bits[4], bits[5], bits[6], bits[7]
70}
71
72func (aqt AQTime) String() string {
73 return string(aqt)
74}
75
76// version of AQTime suitable for saving as a file (esp on windows)
77func (aqt AQTime) FileSafeString() string {
78 str := string(aqt)
79 str = strings.ReplaceAll(str, ":", "-")
80 str = strings.ReplaceAll(str, ".", "-")
81 return str
82}
83
84func (aqt AQTime) Time() time.Time {
85 t, err := time.Parse(fstr, aqt.String())
86 if err != nil {
87 panic(err)
88 }
89 return t
90}