time/time.go

287 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package time
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"apigo.cc/go/cast"
)
// Parse 将任意类型转换为 time.Time。
// 支持time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
// 转换失败返回当前时间 time.Now()。
func Parse(v any) time.Time {
if v == nil {
return time.Now()
}
if tm, ok := v.(time.Time); ok {
return tm
}
str := strings.TrimSpace(cast.String(v))
if str == "" {
return time.Now()
}
var tm time.Time
var err error
// 1. 处理纯数字 (时间戳或紧凑格式)
if num := cast.Int64(v); num > 0 {
// 紧凑格式解析
switch len(str) {
case 14: // 20060102150405
if tm, err = time.ParseInLocation("20060102150405", str, time.Local); err == nil {
return tm
}
case 8: // 20060102
if tm, err = time.ParseInLocation("20060102", str, time.Local); err == nil {
return tm
}
case 6: // 150405 或 060102
if tm, err = time.ParseInLocation("150405", str, time.Local); err == nil {
return tm
}
if tm, err = time.ParseInLocation("060102", str, time.Local); err == nil {
return tm
}
}
// 时间戳处理
switch {
case num < 1e10: // 秒
return time.Unix(num, 0)
case num < 1e13: // 毫秒
return time.UnixMilli(num)
case num < 1e16: // 微秒
return time.UnixMicro(num)
default: // 纳秒
return time.Unix(0, num)
}
}
// 2. 标准格式与常见变体
// RFC3339 (带 T 和 Z 或偏移量)
if strings.Contains(str, "T") {
if tm, err = time.Parse(time.RFC3339Nano, str); err == nil {
return tm.In(time.Local)
}
if tm, err = time.Parse(time.RFC3339, str); err == nil {
return tm.In(time.Local)
}
// 兼容带 T 但没 Z 的情况,尝试按普通日期时间解析
}
// 处理分隔符:-, /, .
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] == '.' {
// 带纳秒
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05.999999999", tsep), str, time.Local); err == nil {
return tm
}
} else {
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", tsep), str[:19], time.Local); err == nil {
return tm
}
}
}
// 纯日期
if tm, err = time.ParseInLocation(layout, str[:10], time.Local); err == nil {
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') {
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", str[8]), str[:17], time.Local); err == nil {
return tm
}
}
if tm, err = time.ParseInLocation(layout, str[:8], time.Local); err == nil {
return tm
}
}
// 3. 特殊格式JS, HTTP, 中文
if strings.Contains(str, "GMT") || strings.Contains(str, "CST") {
// 简单清理 JS 冗余部分
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 {
return tm.In(time.Local)
}
}
}
// 中文格式解析
if strings.ContainsAny(str, "年月日时点分秒") {
return parseCN(str)
}
// 纯时间格式
if strings.Contains(str, ":") {
if tm, err = time.ParseInLocation("15:04:05.999999", str, time.Local); err == nil {
return tm
}
if tm, err = time.ParseInLocation("15:04:05", str, time.Local); err == nil {
return tm
}
if tm, err = time.ParseInLocation("15:04", str, time.Local); err == nil {
return tm
}
}
return time.Now()
}
// Format 格式化时间。
// layout 支持: YYYY-MM-DD HH:mm:ss, YYYY/MM/DD, HH:mm 等直观格式。
func Format(layout string, v any) string {
tm := Parse(v)
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)
}
// Add 时间加减 DSL。
// 格式如: "+1Y-2M+3D", "+1h30m", "-1s"。
// 单位支持: Y (年), M (月), D (天), h, m, s, ms, us, ns。
func Add(expr string, v any) time.Time {
tm := Parse(v)
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)
}
// 辅助函数
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 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, time.Local)
}
// Helpers
func Now() time.Time { return time.Now() }
func Today() time.Time { return time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local) }
func Yesterday() time.Time { return Today().AddDate(0, 0, -1) }
func Tomorrow() time.Time { return Today().AddDate(0, 0, 1) }