Files
doormile_backend/utils/epoch.go

104 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"fmt"
"time"
)
// Timestamps on the customer surface.
//
// The requirement is epoch milliseconds, UTC, integer — a real instant, not a
// wall clock. That is not what this database hands back. The DSN sets
// TimeZone=Asia/Kolkata and the timestamp columns hold IST wall-clock digits
// (see DBNow), so a stored value read into a time.Time carries the right
// digits with the wrong (or no) zone. Calling UnixMilli on it directly is off
// by 5h30m — the same class of defect as sending a naive local string with a Z
// on it, which has already produced "yesterday's work shown as today" on the
// miler app. So every timestamp leaving /customer/* goes through here.
//
// Reinterpreting the wall clock in IST is correct for both shapes this
// database produces: a value tagged UTC that actually holds IST digits, and a
// value correctly tagged +05:30. Both name the same instant afterwards.
// istLocation is Asia/Kolkata, with an exact fixed-offset fallback for
// containers shipped without tzdata. IST observes no DST, so +05:30 is not an
// approximation.
var istLocation = func() *time.Location {
if loc, err := time.LoadLocation("Asia/Kolkata"); err == nil {
return loc
}
return time.FixedZone("IST", 5*3600+30*60)
}()
// IST reinterprets a database timestamp's wall clock as Indian Standard Time,
// yielding the instant it actually names.
func IST(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), istLocation)
}
// EpochMillis converts a database timestamp to UTC epoch milliseconds.
func EpochMillis(t time.Time) int64 {
return IST(t).UnixMilli()
}
// EpochMillisPtr is EpochMillis over a nullable column. A nil timestamp stays
// null in JSON rather than becoming the epoch, which the client would render
// as 1 Jan 1970.
func EpochMillisPtr(t *time.Time) *int64 {
if t == nil || t.IsZero() {
return nil
}
ms := EpochMillis(*t)
return &ms
}
// ISTNow is the current moment in IST, for formatting and for comparing
// against a value already put through IST().
func ISTNow() time.Time {
return time.Now().In(istLocation)
}
// ISTLocation exposes the zone for callers building their own times.
func ISTLocation() *time.Location { return istLocation }
// FormatISTDate renders a date the way the customer app shows it: "Thu, 12 Sep".
// Display strings are formatted server-side, in IST, so the client never has to
// know the operating timezone.
func FormatISTDate(t time.Time) string {
return IST(t).Format("Mon, 2 Jan")
}
// FormatISTDay renders a date relative to today where that reads better —
// "Today", "Tomorrow", otherwise "Mon, 8 Sep". Used for pickup slot days.
func FormatISTDay(t time.Time) string {
d := IST(t)
today := ISTNow()
y1, m1, d1 := d.Date()
y2, m2, d2 := today.Date()
switch {
case y1 == y2 && m1 == m2 && d1 == d2:
return "Today"
case y1 == y2 && m1 == m2 && d1 == d2+1:
return "Tomorrow"
default:
tomorrow := today.AddDate(0, 0, 1)
y3, m3, d3 := tomorrow.Date()
if y1 == y3 && m1 == m3 && d1 == d3 {
return "Tomorrow"
}
return d.Format("Mon, 2 Jan")
}
}
// FormatISTWindow renders a pickup window as the design writes it:
// "2:00 – 4:00 PM" — en dash, spaced, and the meridiem stated once when both
// ends share it.
func FormatISTWindow(from, to time.Time) string {
f, t := IST(from), IST(to)
fMer, tMer := f.Format("PM"), t.Format("PM")
if fMer == tMer {
return fmt.Sprintf("%s – %s", f.Format("3:04"), t.Format("3:04 PM"))
}
return fmt.Sprintf("%s – %s", f.Format("3:04 PM"), t.Format("3:04 PM"))
}