cast/time.go

392 lines
9.4 KiB
Go
Raw Normal View History

package cast
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
// TimeZone 定义了特定时区上下文下的时间操作
type TimeZone struct {
loc *time.Location
}
// NewTimeZone 创建一个时区上下文
func NewTimeZone(loc *time.Location) *TimeZone {
if loc == nil {
loc = time.Local
}
return &TimeZone{loc: loc}
}
// DefaultTimeZone 全局默认时区,默认为本地时区
var DefaultTimeZone = NewTimeZone(time.Local)
// SetDefaultTimeZone 修改全局默认时区
func SetDefaultTimeZone(loc *time.Location) {
if loc != nil {
DefaultTimeZone = NewTimeZone(loc)
}
}
// 在包级别预先初始化 Replacer0 额外分配
var timeFormatReplacer = strings.NewReplacer(
"YYYY", "2006", "YY", "06",
"MM", "01", "M", "1",
"DD", "02", "D", "2",
"HH", "15", "hh", "03", "h", "3",
"mm", "04", "ss", "05",
"a", "pm", "A", "PM",
"ZZ", "-0700", "Z", "-07:00",
)
// ParseTime 将任意类型转换为 time.Time。
// 支持time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
// 转换失败返回零值 time.Time{} 以保持 cast 的静默风格。
func (tz *TimeZone) ParseTime(v any) time.Time {
if v == nil {
return time.Time{}
}
if tm, ok := v.(time.Time); ok {
return tm.In(tz.loc)
}
// 1. 优先探测是否为原生数字类型 (时间戳),彻底 0 分配
switch val := v.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
num := Int64(val)
if num > 0 {
switch {
case num < 1e10: // 秒
return time.Unix(num, 0).In(tz.loc)
case num < 1e13: // 毫秒
return time.UnixMilli(num).In(tz.loc)
case num < 1e16: // 微秒
return time.UnixMicro(num).In(tz.loc)
default: // 纳秒
return time.Unix(0, num).In(tz.loc)
}
}
}
// 2. 数字没命中,再老老实实当字符串处理
str := strings.TrimSpace(String(v))
if str == "" {
return time.Time{}
}
var tm time.Time
var err error
// 3. 处理纯数字字符串 (紧凑格式或时间戳)
if num, err := strconv.ParseInt(str, 10, 64); err == nil && num > 0 {
// 紧凑格式解析
switch len(str) {
case 14: // 20060102150405
if tm, err = time.ParseInLocation("20060102150405", str, tz.loc); err == nil {
return tm
}
case 8: // 20060102
if tm, err = time.ParseInLocation("20060102", str, tz.loc); err == nil {
return tm
}
case 6: // 150405 或 060102
if tm, err = time.ParseInLocation("150405", str, tz.loc); err == nil {
return tm
}
if tm, err = time.ParseInLocation("060102", str, tz.loc); err == nil {
return tm
}
}
// 时间戳处理
switch {
case num < 1e10: // 秒
return time.Unix(num, 0).In(tz.loc)
case num < 1e13: // 毫秒
return time.UnixMilli(num).In(tz.loc)
case num < 1e16: // 微秒
return time.UnixMicro(num).In(tz.loc)
default: // 纳秒
return time.Unix(0, num).In(tz.loc)
}
}
// 2. 标准格式与常见变体
// RFC3339 (带 T 和 Z 或偏移量)
if strings.Contains(str, "T") {
if tm, err = time.Parse(time.RFC3339Nano, str); err == nil {
return tm.In(tz.loc)
}
if tm, err = time.Parse(time.RFC3339, str); err == nil {
return tm.In(tz.loc)
}
}
// 处理分隔符:-, /, .
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, tz.loc); err == nil {
return tm
}
} else {
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", tsep), str[:19], tz.loc); err == nil {
return tm
}
}
}
// 纯日期
if tm, err = time.ParseInLocation(layout, str[:10], tz.loc); 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], tz.loc); err == nil {
return tm
}
}
if tm, err = time.ParseInLocation(layout, str[:8], tz.loc); err == nil {
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 {
return tm.In(tz.loc)
}
}
}
// 中文格式解析
if strings.ContainsAny(str, "年月日时点分秒") {
return tz.parseCN(str)
}
// 纯时间格式
if strings.Contains(str, ":") {
if tm, err = time.ParseInLocation("15:04:05.999999", str, tz.loc); err == nil {
return tm
}
if tm, err = time.ParseInLocation("15:04:05", str, tz.loc); err == nil {
return tm
}
if tm, err = time.ParseInLocation("15:04", str, tz.loc); err == nil {
return tm
}
}
return time.Time{}
}
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 = Int(matches[1])
m = Int(matches[2])
d = Int(matches[3])
if y > 0 && y < 100 {
y += 2000
}
}
if matches := cnTimeRegex.FindStringSubmatch(str); len(matches) == 5 {
h = Int(matches[2])
mm = Int(matches[3])
s = Int(matches[4])
if matches[1] == "下午" && h < 12 {
h += 12
}
if matches[1] == "上午" && h >= 12 {
h -= 12
}
}
now := time.Now().In(tz.loc)
if y == 0 {
y = now.Year()
}
if m == 0 {
m = int(now.Month())
}
if d == 0 {
d = now.Day()
}
return time.Date(y, time.Month(m), d, h, mm, s, 0, tz.loc)
}
// ParseTime 将任意类型转换为 time.Time。
func ParseTime(v any) time.Time {
return DefaultTimeZone.ParseTime(v)
}
// FormatTime 格式化时间。
// layout 支持: YYYY-MM-DD HH:mm:ss, YYYY/MM/DD, HH:mm 等直观格式。
func (tz *TimeZone) FormatTime(layout string, v any) string {
tm := tz.ParseTime(v)
if tm.IsZero() {
return ""
}
// 一次性流式替换,性能直接翻倍
return tm.Format(timeFormatReplacer.Replace(layout))
}
// FormatTime 格式化时间。
func FormatTime(layout string, v any) string {
return DefaultTimeZone.FormatTime(layout, v)
}
// AddTime 时间加减 DSL。
// 格式如: "+1Y-2M+3D", "+1h30m", "-1s"。
// 单位支持: Y (年), M (月), D (天), h, m, s, ms, us, ns。
func (tz *TimeZone) AddTime(expr string, v any) time.Time {
tm := tz.ParseTime(v)
if tm.IsZero() {
return tm
}
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)
}
// AddTime 时间加减 DSL。
func AddTime(expr string, v any) time.Time {
return DefaultTimeZone.AddTime(expr, v)
}
// Now 获取当前时间
func (tz *TimeZone) Now() time.Time { return time.Now().In(tz.loc) }
// Location 获取当前时区
func (tz *TimeZone) Location() *time.Location { return tz.loc }
// Now 获取当前时间
func Now() time.Time { return DefaultTimeZone.Now() }
// 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, " ")
}
// DescribeDuration 将时长转化为自然语言描述
func DescribeDuration(d time.Duration) string {
return DefaultTimeZone.DescribeDuration(d)
}