package chrome
import (
"fmt"
"html/template"
"time"
)
// Funcs returns the template helpers every service was carrying its own copy
// of. Merge into a service's FuncMap before its own helpers, so a service can
// still shadow a name deliberately.
func Funcs() template.FuncMap {
return template.FuncMap{
"dict": Dict,
"shortsha": ShortSHA,
"reltime": RelTime,
"abstime": AbsTime,
}
}
// Dict builds a map from alternating key/value arguments, so a partial that
// needs several fields can be invoked with an inline context:
// {{template "x" (dict "A" .A "B" .B)}}. An odd argument count or a non-string
// key is a template authoring error and surfaces as a render error.
func Dict(kv ...any) (map[string]any, error) {
if len(kv)%2 != 0 {
return nil, fmt.Errorf("dict: expected an even number of arguments, got %d", len(kv))
}
m := make(map[string]any, len(kv)/2)
for i := 0; i < len(kv); i += 2 {
k, ok := kv[i].(string)
if !ok {
return nil, fmt.Errorf("dict: key %d is not a string", i)
}
m[k] = kv[i+1]
}
return m, nil
}
// ShortSHA abbreviates an object id to its first 8 characters (or returns it
// unchanged if shorter), the convention used everywhere commits are listed.
func ShortSHA(s string) string {
if len(s) > 8 {
return s[:8]
}
return s
}
// RelTime is the coarse "3 hours ago" a listing wants, and AbsTime the exact
// UTC stamp an investigation wants. Both exist, and both are shared, because
// every service on the instance shows the same two columns and had grown its
// own spelling of them: the copies disagreed about the future, printing "in 3
// hours" on one service and "just now" on the next for the same instant.
//
// A future instant gets the same arithmetic as a past one. "in 3 weeks"
// answers "do I have to deal with this today" without the reader working it
// out from a calendar stamp.
func RelTime(t time.Time) string {
d := time.Since(t)
switch {
case d < -time.Minute:
return "in " + coarse(-d)
case d < time.Minute:
// Covers both an instant that has just passed and one about to, which
// is also what two machines with unsynchronised clocks produce for the
// same instant.
return "just now"
default:
return coarse(d) + " ago"
}
}
// AbsTime is the unambiguous stamp, one hover away from a RelTime.
func AbsTime(t time.Time) string {
return t.UTC().Format("2006-01-02 15:04:05 UTC")
}
// coarse names a positive duration in its largest whole unit. The direction is
// the caller's to add, so that "in 3 weeks" and "3 weeks ago" cannot end up
// counting in different units.
func coarse(d time.Duration) string {
switch {
case d < time.Hour:
return plural(int(d/time.Minute), "minute")
case d < 24*time.Hour:
return plural(int(d/time.Hour), "hour")
case d < 30*24*time.Hour:
return plural(int(d/(24*time.Hour)), "day")
case d < 365*24*time.Hour:
return plural(int(d/(30*24*time.Hour)), "month")
default:
return plural(int(d/(365*24*time.Hour)), "year")
}
}
func plural(n int, unit string) string {
if n == 1 {
return "1 " + unit
}
return fmt.Sprintf("%d %ss", n, unit)
}