2026-04-22 09:56:32 +08:00
|
|
|
|
package time
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"strconv"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/cast"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-01 11:50:05 +08:00
|
|
|
|
// TimeZone 定义了特定时区上下文下的时间操作
|
|
|
|
|
|
type TimeZone struct {
|
|
|
|
|
|
loc *time.Location
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func New(loc *time.Location) *TimeZone {
|
|
|
|
|
|
if loc == nil {
|
|
|
|
|
|
loc = time.Local
|
|
|
|
|
|
}
|
|
|
|
|
|
return &TimeZone{loc: loc}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var defaultTZ = New(time.Local)
|
|
|
|
|
|
|
2026-04-22 09:56:32 +08:00
|
|
|
|
// Parse 将任意类型转换为 time.Time。
|
|
|
|
|
|
// 支持:time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
|
|
|
|
|
|
// 转换失败返回当前时间 time.Now()。
|
2026-05-01 11:50:05 +08:00
|
|
|
|
func (tz *TimeZone) Parse(v any) time.Time {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
if v == nil {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return time.Now().In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if tm, ok := v.(time.Time); ok {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return tm.In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
str := strings.TrimSpace(cast.String(v))
|
|
|
|
|
|
if str == "" {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return time.Now().In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var tm time.Time
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 处理纯数字 (时间戳或紧凑格式)
|
|
|
|
|
|
if num := cast.Int64(v); num > 0 {
|
|
|
|
|
|
// 紧凑格式解析
|
|
|
|
|
|
switch len(str) {
|
|
|
|
|
|
case 14: // 20060102150405
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation("20060102150405", str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
case 8: // 20060102
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation("20060102", str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
case 6: // 150405 或 060102
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation("150405", str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation("060102", str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 时间戳处理
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case num < 1e10: // 秒
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return time.Unix(num, 0).In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
case num < 1e13: // 毫秒
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return time.UnixMilli(num).In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
case num < 1e16: // 微秒
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return time.UnixMicro(num).In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
default: // 纳秒
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return time.Unix(0, num).In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 标准格式与常见变体
|
|
|
|
|
|
// RFC3339 (带 T 和 Z 或偏移量)
|
|
|
|
|
|
if strings.Contains(str, "T") {
|
|
|
|
|
|
if tm, err = time.Parse(time.RFC3339Nano, str); err == nil {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return tm.In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
if tm, err = time.Parse(time.RFC3339, str); err == nil {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return tm.In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理分隔符:-, /, .
|
|
|
|
|
|
if len(str) >= 10 && (str[4] == '-' || str[4] == '/' || str[4] == '.') {
|
|
|
|
|
|
sep := str[4]
|
|
|
|
|
|
layout := fmt.Sprintf("2006%c01%c02", sep, sep)
|
|
|
|
|
|
|
|
|
|
|
|
// 完整日期时间
|
|
|
|
|
|
if len(str) >= 19 {
|
|
|
|
|
|
tsep := str[10] // 通常是 ' ' 或 'T'
|
|
|
|
|
|
if len(str) >= 20 && str[19] == '.' {
|
|
|
|
|
|
// 带纳秒
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05.999999999", tsep), str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", tsep), str[:19], tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 纯日期
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation(layout, str[:10], tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 06-01-02 这种短年份格式
|
|
|
|
|
|
if len(str) >= 8 && (str[2] == '-' || str[2] == '/' || str[2] == '.') {
|
|
|
|
|
|
sep := str[2]
|
|
|
|
|
|
layout := fmt.Sprintf("06%c01%c02", sep, sep)
|
|
|
|
|
|
if len(str) >= 17 && (str[8] == ' ' || str[8] == 'T') {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", str[8]), str[:17], tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation(layout, str[:8], tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 特殊格式:JS, HTTP, 中文
|
|
|
|
|
|
if strings.Contains(str, "GMT") || strings.Contains(str, "CST") {
|
|
|
|
|
|
cleanStr := strings.SplitN(str, " (", 2)[0]
|
|
|
|
|
|
cleanStr = strings.Replace(cleanStr, "CST", "+0800", 1)
|
|
|
|
|
|
|
|
|
|
|
|
formats := []string{
|
|
|
|
|
|
"Mon Jan 02 2006 15:04:05 GMT-0700",
|
|
|
|
|
|
"Mon, 02 Jan 2006 15:04:05 GMT",
|
|
|
|
|
|
time.RFC1123Z,
|
|
|
|
|
|
time.RFC1123,
|
|
|
|
|
|
time.UnixDate,
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, f := range formats {
|
|
|
|
|
|
if tm, err = time.Parse(f, cleanStr); err == nil {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return tm.In(tz.loc)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 中文格式解析
|
|
|
|
|
|
if strings.ContainsAny(str, "年月日时点分秒") {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return tz.parseCN(str)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 纯时间格式
|
|
|
|
|
|
if strings.Contains(str, ":") {
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation("15:04:05.999999", str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation("15:04:05", str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
2026-05-01 11:50:05 +08:00
|
|
|
|
if tm, err = time.ParseInLocation("15:04", str, tz.loc); err == nil {
|
2026-04-22 09:56:32 +08:00
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 11:50:05 +08:00
|
|
|
|
return time.Now().In(tz.loc)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var cnDateRegex = regexp.MustCompile(`(\d{2,4})?年?(\d{1,2})月(\d{1,2})日`)
|
|
|
|
|
|
var cnTimeRegex = regexp.MustCompile(`(上午|下午)?(\d{1,2})(?:时|点|:)(\d{1,2})(?:分|:)(\d{1,2})?秒?`)
|
|
|
|
|
|
|
|
|
|
|
|
func (tz *TimeZone) parseCN(str string) time.Time {
|
|
|
|
|
|
str = strings.ReplaceAll(str, ":", ":")
|
|
|
|
|
|
str = strings.ReplaceAll(str, " ", "")
|
|
|
|
|
|
var y, m, d, h, mm, s int
|
|
|
|
|
|
if matches := cnDateRegex.FindStringSubmatch(str); len(matches) == 4 {
|
|
|
|
|
|
y = cast.Int(matches[1])
|
|
|
|
|
|
m = cast.Int(matches[2])
|
|
|
|
|
|
d = cast.Int(matches[3])
|
|
|
|
|
|
if y > 0 && y < 100 { y += 2000 }
|
|
|
|
|
|
}
|
|
|
|
|
|
if matches := cnTimeRegex.FindStringSubmatch(str); len(matches) == 5 {
|
|
|
|
|
|
h = cast.Int(matches[2])
|
|
|
|
|
|
mm = cast.Int(matches[3])
|
|
|
|
|
|
s = cast.Int(matches[4])
|
|
|
|
|
|
if matches[1] == "下午" && h < 12 { h += 12 }
|
|
|
|
|
|
if matches[1] == "上午" && h >= 12 { h -= 12 }
|
|
|
|
|
|
}
|
|
|
|
|
|
if y == 0 { y = time.Now().Year() }
|
|
|
|
|
|
if m == 0 { m = int(time.Now().Month()) }
|
|
|
|
|
|
if d == 0 { d = time.Now().Day() }
|
|
|
|
|
|
return time.Date(y, time.Month(m), d, h, mm, s, 0, tz.loc)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse 将任意类型转换为 time.Time。
|
|
|
|
|
|
// 支持:time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
|
|
|
|
|
|
// 转换失败返回当前时间 time.Now()。
|
|
|
|
|
|
func Parse(v any) time.Time {
|
|
|
|
|
|
return defaultTZ.Parse(v)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Format 格式化时间。
|
|
|
|
|
|
// layout 支持: YYYY-MM-DD HH:mm:ss, YYYY/MM/DD, HH:mm 等直观格式。
|
2026-05-01 11:50:05 +08:00
|
|
|
|
func (tz *TimeZone) Format(layout string, v any) string {
|
|
|
|
|
|
tm := tz.Parse(v)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
l := layout
|
|
|
|
|
|
l = strings.ReplaceAll(l, "YYYY", "2006")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "YY", "06")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "MM", "01")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "M", "1")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "DD", "02")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "D", "2")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "HH", "15")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "hh", "03")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "h", "3")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "mm", "04")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "ss", "05")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "a", "pm")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "A", "PM")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "ZZ", "-0700")
|
|
|
|
|
|
l = strings.ReplaceAll(l, "Z", "-07:00")
|
|
|
|
|
|
return tm.Format(l)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 11:50:05 +08:00
|
|
|
|
func Format(layout string, v any) string {
|
|
|
|
|
|
return defaultTZ.Format(layout, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 09:56:32 +08:00
|
|
|
|
// Add 时间加减 DSL。
|
|
|
|
|
|
// 格式如: "+1Y-2M+3D", "+1h30m", "-1s"。
|
|
|
|
|
|
// 单位支持: Y (年), M (月), D (天), h, m, s, ms, us, ns。
|
2026-05-01 11:50:05 +08:00
|
|
|
|
func (tz *TimeZone) Add(expr string, v any) time.Time {
|
|
|
|
|
|
tm := tz.Parse(v)
|
2026-04-22 09:56:32 +08:00
|
|
|
|
if expr == "" {
|
|
|
|
|
|
return tm
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
i := 0
|
|
|
|
|
|
years, months, days := 0, 0, 0
|
|
|
|
|
|
var duration time.Duration
|
|
|
|
|
|
|
|
|
|
|
|
for i < len(expr) {
|
|
|
|
|
|
sign := 1
|
|
|
|
|
|
if expr[i] == '+' {
|
|
|
|
|
|
i++
|
|
|
|
|
|
} else if expr[i] == '-' {
|
|
|
|
|
|
sign = -1
|
|
|
|
|
|
i++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
j := i
|
|
|
|
|
|
for j < len(expr) && expr[j] >= '0' && expr[j] <= '9' {
|
|
|
|
|
|
j++
|
|
|
|
|
|
}
|
|
|
|
|
|
num := 1
|
|
|
|
|
|
if j > i {
|
|
|
|
|
|
num, _ = strconv.Atoi(expr[i:j])
|
|
|
|
|
|
}
|
|
|
|
|
|
val := num * sign
|
|
|
|
|
|
|
|
|
|
|
|
i = j
|
|
|
|
|
|
unit := ""
|
|
|
|
|
|
// 匹配双字符单位 (ms, us, ns)
|
|
|
|
|
|
if i+2 <= len(expr) {
|
|
|
|
|
|
u2 := expr[i : i+2]
|
|
|
|
|
|
if u2 == "ms" || u2 == "us" || u2 == "ns" {
|
|
|
|
|
|
unit = u2
|
|
|
|
|
|
i += 2
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 匹配单字符单位
|
|
|
|
|
|
if unit == "" && i < len(expr) {
|
|
|
|
|
|
unit = expr[i : i+1]
|
|
|
|
|
|
i++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch unit {
|
|
|
|
|
|
case "Y": years += val
|
|
|
|
|
|
case "M": months += val
|
|
|
|
|
|
case "D": days += val
|
|
|
|
|
|
case "h": duration += time.Duration(val) * time.Hour
|
|
|
|
|
|
case "m": duration += time.Duration(val) * time.Minute
|
|
|
|
|
|
case "s": duration += time.Duration(val) * time.Second
|
|
|
|
|
|
case "ms": duration += time.Duration(val) * time.Millisecond
|
|
|
|
|
|
case "us": duration += time.Duration(val) * time.Microsecond
|
|
|
|
|
|
case "ns": duration += time.Duration(val) * time.Nanosecond
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 默认秒
|
|
|
|
|
|
duration += time.Duration(val) * time.Second
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if years != 0 || months != 0 || days != 0 {
|
|
|
|
|
|
tm = tm.AddDate(years, months, days)
|
|
|
|
|
|
}
|
|
|
|
|
|
return tm.Add(duration)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 11:50:05 +08:00
|
|
|
|
func Add(expr string, v any) time.Time {
|
|
|
|
|
|
return defaultTZ.Add(expr, v)
|
|
|
|
|
|
}
|
2026-04-22 09:56:32 +08:00
|
|
|
|
|
2026-05-01 11:50:05 +08:00
|
|
|
|
// Timer 计时器
|
|
|
|
|
|
type Timer struct {
|
|
|
|
|
|
start time.Time
|
|
|
|
|
|
last time.Time
|
|
|
|
|
|
laps []Lap
|
|
|
|
|
|
pause time.Duration
|
|
|
|
|
|
}
|
2026-04-22 09:56:32 +08:00
|
|
|
|
|
2026-05-01 11:50:05 +08:00
|
|
|
|
type Lap struct {
|
|
|
|
|
|
Label string
|
|
|
|
|
|
Duration time.Duration
|
|
|
|
|
|
At time.Time
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start 开始计时
|
|
|
|
|
|
func Start() *Timer {
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
return &Timer{start: now, last: now}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Record 记录一段耗时 (Lap),并返回段耗时
|
|
|
|
|
|
func (t *Timer) Record(label string) time.Duration {
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
d := now.Sub(t.last)
|
|
|
|
|
|
t.last = now
|
|
|
|
|
|
t.laps = append(t.laps, Lap{Label: label, Duration: d, At: now})
|
|
|
|
|
|
return d
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Pause 暂停记录
|
|
|
|
|
|
func (t *Timer) Pause(label string) {
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
d := now.Sub(t.last)
|
|
|
|
|
|
t.laps = append(t.laps, Lap{Label: label, Duration: d, At: now})
|
|
|
|
|
|
t.last = now // 暂时标记下一次开始点
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Resume 从暂停中恢复
|
|
|
|
|
|
func (t *Timer) Resume() {
|
|
|
|
|
|
t.last = time.Now()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stop 结束计时,返回总持续时间
|
|
|
|
|
|
func (t *Timer) Stop() time.Duration {
|
|
|
|
|
|
return time.Since(t.start)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Summarize 返回所有记录段
|
|
|
|
|
|
func (t *Timer) Summarize() []Lap {
|
|
|
|
|
|
return t.laps
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Describe 返回格式化后的统计字符串
|
|
|
|
|
|
func (t *Timer) Describe() string {
|
|
|
|
|
|
var sb strings.Builder
|
|
|
|
|
|
total := t.Stop()
|
|
|
|
|
|
for _, lap := range t.laps {
|
|
|
|
|
|
sb.WriteString(fmt.Sprintf("[%s] %v; ", lap.Label, lap.Duration))
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
2026-05-01 11:50:05 +08:00
|
|
|
|
sb.WriteString(fmt.Sprintf("Total: %v", total))
|
|
|
|
|
|
return sb.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DescribeDuration 将时长转化为自然语言描述,例如 "1h 1m 1s"
|
|
|
|
|
|
func (tz *TimeZone) DescribeDuration(d time.Duration) string {
|
|
|
|
|
|
h := d / time.Hour
|
|
|
|
|
|
d -= h * time.Hour
|
|
|
|
|
|
m := d / time.Minute
|
|
|
|
|
|
d -= m * time.Minute
|
|
|
|
|
|
s := d / time.Second
|
|
|
|
|
|
|
|
|
|
|
|
var parts []string
|
|
|
|
|
|
if h > 0 { parts = append(parts, fmt.Sprintf("%dh", h)) }
|
|
|
|
|
|
if m > 0 { parts = append(parts, fmt.Sprintf("%dm", m)) }
|
|
|
|
|
|
if s > 0 { parts = append(parts, fmt.Sprintf("%ds", s)) }
|
|
|
|
|
|
if len(parts) == 0 { return "0s" }
|
|
|
|
|
|
return strings.Join(parts, " ")
|
2026-04-22 09:56:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 12:21:23 +08:00
|
|
|
|
// Now 获取当前时间
|
2026-05-01 11:50:05 +08:00
|
|
|
|
func (tz *TimeZone) Now() time.Time { return time.Now().In(tz.loc) }
|
|
|
|
|
|
|
|
|
|
|
|
func Now() time.Time { return defaultTZ.Now() }
|