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) }