You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ntool/ntime/format.go

241 lines
5.9 KiB
Go

package ntime
import (
"errors"
"fmt"
"git.noahlan.cn/noahlan/ntool/ngo"
"regexp"
"strconv"
"strings"
"time"
)
var (
// ErrDateLayout error
ErrDateLayout = errors.New("invalid date layout string")
// ErrInvalidParam error
ErrInvalidParam = errors.New("invalid input for parse time")
)
// Format convert time to string use default layout
func Format(t time.Time) string {
return t.Format(DefaultLayout)
}
// FormatBy convert time to string use given layout or template(java),
func FormatBy(t time.Time, layout ...string) string {
return t.Format(ToLayout(ngo.FirstOr(layout, DefaultLayout)))
}
// FormatUnix time seconds use default layout
func FormatUnix(sec int64, layout ...string) string {
return time.Unix(sec, 0).Format(ToLayout(ngo.FirstOr(layout, DefaultLayout)))
}
var timeFormats = [][]int{
{0},
{1},
{2, 1},
{60},
{120, 60},
{3600},
{7200, 3600},
{86400},
{172800, 86400},
}
var timeMessages = []string{
"< 1 sec", "1 sec", "secs", "1 min", "mins", "1 hr", "hrs", "1 day", "days",
}
// HowLongAgo format given timestamp to string.
func HowLongAgo(sec int64) string {
intVal := int(sec)
length := len(timeFormats)
for i, item := range timeFormats {
if intVal >= item[0] {
ni := i + 1
match := false
if ni < length { // next exists
next := timeFormats[ni]
if intVal < next[0] { // current <= intVal < next
match = true
}
} else if ni == length { // current is last
match = true
}
if match { // match success
if len(item) == 1 {
return timeMessages[i]
}
// len is 2
return fmt.Sprintf("%d %s", intVal/item[1], timeMessages[i])
}
}
}
return "unknown" // He should never happen
}
// auto match use some commonly layouts.
// key is layout length.
var layoutMap = map[int][]string{
6: {"200601", "060102", time.Kitchen},
8: {"20060102"},
10: {time.DateOnly},
13: {"2006-01-02 15"},
15: {time.Stamp},
16: {"2006-01-02 15:04"},
19: {time.DateTime, time.RFC822, time.StampMilli},
20: {"2006-01-02 15:04:05Z"},
21: {time.RFC822Z},
22: {time.StampMicro},
23: {"2006-01-02 15:04:05.000", "2006-01-02 15:04:05.999"},
24: {time.ANSIC},
25: {time.RFC3339, time.StampNano},
// time.Layout}, // must go >= 1.19
26: {"2006-01-02 15:04:05.000000"},
28: {time.UnixDate},
29: {time.RFC1123, "2006-01-02 15:04:05.000000000"},
30: {time.RFC850},
31: {time.RFC1123Z},
35: {time.RFC3339Nano},
}
// MustParseTime must convert date time string to time.Time
// it will return ZeroTime when parsing error occurred. see MustParseTimeD
func MustParseTime(s string, layouts ...string) time.Time {
return MustParseTimeD(s, ZeroTime, layouts...)
}
// MustParseTimeD must convert date time string to time.Time
// it will return defaultTime when parsing error occurred. see ParseTime
func MustParseTimeD(s string, defaultTime time.Time, layouts ...string) time.Time {
t, err := ParseTime(s, layouts...)
if err != nil {
return defaultTime
}
return t
}
// ParseTime convert date time string to time.Time
// it will use some commonly layouts when layouts is empty or nil
func ParseTime(s string, layouts ...string) (t time.Time, err error) {
// custom layout
if len(layouts) > 0 {
if len(layouts[0]) > 0 {
return time.Parse(ToLayout(layouts[0]), s)
}
err = ErrDateLayout
return
}
// auto match use some commonly layouts.
strLn := len(s)
maybeLayouts, ok := layoutMap[strLn]
if !ok {
err = ErrInvalidParam
return
}
var hasAlphaT bool
if pos := strings.IndexByte(s, 'T'); pos > 0 && pos < 12 {
hasAlphaT = true
}
hasSlashR := strings.IndexByte(s, '/') > 0
for _, layout := range maybeLayouts {
// date string has "T". eg: "2006-01-02T15:04:05"
if hasAlphaT {
layout = strings.Replace(layout, " ", "T", 1)
}
// date string has "/". eg: "2006/01/02 15:04:05"
if hasSlashR {
layout = strings.Replace(layout, "-", "/", -1)
}
t, err = time.Parse(layout, s)
if err == nil {
return
}
}
// t, err = time.ParseInLocation(layout, s, time.Local)
return
}
var (
// TIP: extend unit d,w
// time.ParseDuration() is not supported. eg: "1d", "2w"
durStrReg = regexp.MustCompile(`^(-?\d+)(ns|us|µs|ms|s|m|h|d|w)$`)
// match long duration string, such as "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks"
// time.ParseDuration() is not supported.
durStrRegL = regexp.MustCompile(`^(-?\d+)([a-zA-Z]{3,})$`)
)
// IsDuration check the string is a duration string.
func IsDuration(s string) bool {
if s == "0" || durStrReg.MatchString(s) {
return true
}
return durStrRegL.MatchString(s)
}
// ToDuration parses a duration string. such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
//
// Diff of time.ParseDuration:
// - support extend unit d, w at the end of string. such as "1d", "2w".
// - support long string unit at end. such as "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks".
//
// If the string is not a valid duration string, it will return an error.
func ToDuration(s string) (time.Duration, error) {
ln := len(s)
if ln == 0 {
return 0, fmt.Errorf("empty duration string")
}
s = strings.ToLower(s)
if s == "0" {
return 0, nil
}
// extend unit d,w, time.ParseDuration() is not supported. eg: "1d", "2w"
if lastUnit := s[ln-1]; lastUnit == 'd' {
s = s + "ay"
} else if lastUnit == 'w' {
s = s + "eek"
}
// long unit, time.ParseDuration() is not supported. eg: "-3sec" => [3sec -3 sec]
ss := durStrRegL.FindStringSubmatch(s)
if len(ss) == 3 {
num, unit := ss[1], ss[2]
// convert to short unit
switch unit {
case "week", "weeks":
// max unit is hour, so need convert by 24 * 7 * n
n, _ := strconv.Atoi(num)
s = strconv.Itoa(n*24*7) + "h"
case "day", "days":
// max unit is hour, so need convert by 24 * n
n, _ := strconv.Atoi(num)
s = strconv.Itoa(n*24) + "h"
case "hour", "hours":
s = num + "h"
case "min", "mins", "minute", "minutes":
s = num + "m"
case "sec", "secs", "second", "seconds":
s = num + "s"
}
}
return time.ParseDuration(s)
}