ntfy/util/time.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

139 lines
3.8 KiB
Go
Raw Normal View History

2021-12-11 03:59:51 +03:00
package util
import (
"errors"
"github.com/olebedev/when"
"regexp"
"strconv"
"strings"
"time"
)
var (
2024-03-07 20:22:35 +03:00
errInvalidDuration = errors.New("unable to parse duration")
durationStrRegex = regexp.MustCompile(`(?i)^(\d+)\s*(d|days?|h|hours?|m|mins?|minutes?|s|secs?|seconds?)$`)
2021-12-11 03:59:51 +03:00
)
2023-03-03 21:56:48 +03:00
const (
timestampFormat = "2006-01-02T15:04:05.999Z07:00" // Like RFC3339, but with milliseconds
)
// FormatTime formats a time.Time in a RFC339-like format that includes milliseconds
func FormatTime(t time.Time) string {
return t.Format(timestampFormat)
}
2023-01-11 06:51:51 +03:00
// NextOccurrenceUTC takes a time of day (e.g. 9:00am), and returns the next occurrence
// of that time from the current time (in UTC).
func NextOccurrenceUTC(timeOfDay, base time.Time) time.Time {
2023-01-27 06:57:18 +03:00
hour, minute, seconds := timeOfDay.UTC().Clock()
2023-01-11 06:51:51 +03:00
now := base.UTC()
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, seconds, 0, time.UTC)
if next.Before(now) {
next = next.AddDate(0, 0, 1)
}
return next
}
2021-12-11 08:06:25 +03:00
// ParseFutureTime parses a date/time string to a time.Time. It supports unix timestamps, durations
// and natural language dates
2021-12-11 03:59:51 +03:00
func ParseFutureTime(s string, now time.Time) (time.Time, error) {
s = strings.TrimSpace(s)
t, err := parseUnixTime(s, now)
if err == nil {
return t, nil
}
t, err = parseFromDuration(s, now)
if err == nil {
return t, nil
}
t, err = parseNaturalTime(s, now)
if err == nil {
return t, nil
}
2024-03-07 20:22:35 +03:00
return time.Time{}, errInvalidDuration
2021-12-11 03:59:51 +03:00
}
2023-02-27 22:34:05 +03:00
// ParseDuration is like time.ParseDuration, except that it also understands days (d), which
// translates to 24 hours, e.g. "2d" or "20h".
func ParseDuration(s string) (time.Duration, error) {
2021-12-11 03:59:51 +03:00
d, err := time.ParseDuration(s)
if err == nil {
return d, nil
}
matches := durationStrRegex.FindStringSubmatch(s)
if matches != nil {
number, err := strconv.Atoi(matches[1])
if err != nil {
2024-03-07 20:22:35 +03:00
return 0, errInvalidDuration
2021-12-11 03:59:51 +03:00
}
switch unit := matches[2][0:1]; unit {
case "d":
return time.Duration(number) * 24 * time.Hour, nil
case "h":
return time.Duration(number) * time.Hour, nil
case "m":
return time.Duration(number) * time.Minute, nil
case "s":
return time.Duration(number) * time.Second, nil
default:
2024-03-07 20:22:35 +03:00
return 0, errInvalidDuration
2021-12-11 03:59:51 +03:00
}
}
2024-03-07 20:22:35 +03:00
return 0, errInvalidDuration
2021-12-11 03:59:51 +03:00
}
2024-03-07 20:53:39 +03:00
// FormatDuration formats a time.Duration into a human-readable string, e.g. "2d", "20h", "30m", "40s".
// It rounds to the largest unit that is not zero, thereby effectively rounding down.
2024-03-07 19:53:12 +03:00
func FormatDuration(d time.Duration) string {
if d >= 24*time.Hour {
return strconv.Itoa(int(d/(24*time.Hour))) + "d"
}
if d >= time.Hour {
return strconv.Itoa(int(d/time.Hour)) + "h"
}
if d >= time.Minute {
return strconv.Itoa(int(d/time.Minute)) + "m"
}
if d >= time.Second {
return strconv.Itoa(int(d/time.Second)) + "s"
}
return "0s"
}
2023-02-27 22:34:05 +03:00
func parseFromDuration(s string, now time.Time) (time.Time, error) {
d, err := ParseDuration(s)
if err == nil {
return now.Add(d), nil
}
2024-03-07 20:22:35 +03:00
return time.Time{}, errInvalidDuration
2023-02-27 22:34:05 +03:00
}
2021-12-11 03:59:51 +03:00
func parseUnixTime(s string, now time.Time) (time.Time, error) {
t, err := strconv.Atoi(s)
if err != nil {
return time.Time{}, err
} else if int64(t) < now.Unix() {
2024-03-07 20:22:35 +03:00
return time.Time{}, errInvalidDuration
2021-12-11 03:59:51 +03:00
}
2021-12-11 06:57:01 +03:00
return time.Unix(int64(t), 0).UTC(), nil
2021-12-11 03:59:51 +03:00
}
func parseNaturalTime(s string, now time.Time) (time.Time, error) {
r, err := when.EN.Parse(s, now) // returns "nil, nil" if no matches!
if err != nil || r == nil {
2024-03-07 20:22:35 +03:00
return time.Time{}, errInvalidDuration
2021-12-11 03:59:51 +03:00
} else if r.Time.After(now) {
return r.Time, nil
}
// Hack: If the time is parsable, but not in the future,
// simply append "tomorrow, " to it.
r, err = when.EN.Parse("tomorrow, "+s, now) // returns "nil, nil" if no matches!
if err != nil || r == nil {
2024-03-07 20:22:35 +03:00
return time.Time{}, errInvalidDuration
2021-12-11 03:59:51 +03:00
} else if r.Time.After(now) {
return r.Time, nil
}
2024-03-07 20:22:35 +03:00
return time.Time{}, errInvalidDuration
2021-12-11 03:59:51 +03:00
}