chore: 升级 time 至 v1.0.4,引入 TimeZone 上下文及计时工具 (AI 维护)
This commit is contained in:
parent
f389dd5260
commit
9e40558718
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
go.sum
|
||||||
35
AI.md
35
AI.md
@ -1,35 +0,0 @@
|
|||||||
# AI Coding Context: @go/time
|
|
||||||
|
|
||||||
本索引供 AI 模型理解 `@go/time` 的逻辑与调用原则,以生成符合本项目“极简、无摩擦”哲学的代码。
|
|
||||||
|
|
||||||
## 🤖 AI 行为准则
|
|
||||||
|
|
||||||
1. **优先使用包级函数**:直接使用 `time.Parse(v)`,自动归一化处理。
|
|
||||||
2. **默认本地时间**:所有解析结果统一归一化为 `time.Local`。
|
|
||||||
3. **使用 DSL 替代手动计算**:时间偏移(如“30天后”)强制使用 `time.Add("+30D", v)`。
|
|
||||||
4. **容错处理**:解析失败返回 `time.Now()`,无 Panic 风险。
|
|
||||||
|
|
||||||
## 🛠 API Reference
|
|
||||||
|
|
||||||
### 核心解析与格式化
|
|
||||||
- `func Parse(v any) time.Time`:全能解析,支持时间戳、ISO、RFC3339、中文习惯等。
|
|
||||||
- `func Format(layout string, v any) string`:人性化格式化。支持 `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`, `ms`, `ZZ` 等直观占位符。
|
|
||||||
- `func Add(expr string, v any) time.Time`:时间计算 DSL。支持 `Y, M, D, h, m, s, ms, us, ns` 组合。
|
|
||||||
|
|
||||||
### 快捷助手
|
|
||||||
- `func Now() time.Time`:获取当前时间。
|
|
||||||
- `func Today() time.Time`:获取今日 00:00:00。
|
|
||||||
- `func Yesterday() time.Time`:获取昨日 00:00:00。
|
|
||||||
- `func Tomorrow() time.Time`:获取明日 00:00:00。
|
|
||||||
|
|
||||||
## 🧩 典型模式 (Best Practices)
|
|
||||||
|
|
||||||
* **✅ 推荐**:
|
|
||||||
```go
|
|
||||||
// 解析与偏移
|
|
||||||
tm := time.Parse("2025-06-23")
|
|
||||||
nextMonth := time.Add("+1M", tm)
|
|
||||||
|
|
||||||
// 格式化输出
|
|
||||||
str := time.Format("YYYY-MM-DD HH:mm:ss", time.Now())
|
|
||||||
```
|
|
||||||
7
CHANGELOG.md
Normal file
7
CHANGELOG.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [v1.0.4] - 2026-05-01
|
||||||
|
- **新增**: 引入 `TimeZone` 上下文,支持 explicit 时区控制,兼容原有包级 API。
|
||||||
|
- **新增**: 引入 `Timer` 计时工具,支持 `Start`, `Record`, `Pause`, `Resume` 及统计描述。
|
||||||
|
- **优化**: 扩展 `DescribeDuration` 方法,支持直观的时长自然语言格式化。
|
||||||
|
- **重构**: 内部逻辑重构至 `TimeZone` 结构体,提升架构扩展性。
|
||||||
24
TEST.md
24
TEST.md
@ -1,26 +1,28 @@
|
|||||||
# Test Report: @go/time
|
# Test Report: @go/time
|
||||||
|
|
||||||
## 📋 测试概览
|
## 📋 测试概览
|
||||||
- **测试时间**: 2026-04-22
|
- **测试时间**: 2026-05-01
|
||||||
- **测试环境**: darwin/amd64 (Intel i9-9980HK)
|
- **测试环境**: darwin/amd64 (Intel i9-9980HK)
|
||||||
- **Go 版本**: 1.25.0
|
- **Go 版本**: 1.25.0
|
||||||
|
|
||||||
## ✅ 功能测试 (Functional Tests)
|
## ✅ 功能测试 (Functional Tests)
|
||||||
| 场景 | 状态 | 描述 |
|
| 场景 | 状态 | 描述 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| `TestParse` | PASS | 覆盖时间戳、紧凑格式、RFC3339、标准/斜杠/点日期、短年份、中文格式、纯时间等。 |
|
| `TestParse` | PASS | 覆盖时间戳、紧凑格式、RFC3339、中文格式等。 |
|
||||||
| `TestFormat` | PASS | 验证直观布局 (YYYY-MM-DD HH:mm:ss) 的正确转换与格式化。 |
|
| `TestFormat` | PASS | 验证直观布局转换。 |
|
||||||
| `TestAdd` | PASS | 验证 DSL 时间加减 (+1Y-2M+3D, +1h30m 等) 的逻辑。 |
|
| `TestAdd` | PASS | 验证 DSL 时间加减逻辑。 |
|
||||||
| `TestHelpers` | PASS | 验证 `Now()`, `Today()`, `Yesterday()`, `Tomorrow()` 辅助函数。 |
|
| `TestHelpers` | PASS | 验证辅助函数逻辑。 |
|
||||||
|
| `TestTimeZoneRobustness` | PASS | 验证跨时区解析、转换与异常回退逻辑。 |
|
||||||
|
| `Timer` | PASS | 验证计时器的启动、分段记录、暂停/恢复及统计输出。 |
|
||||||
|
| `DescribeDuration` | PASS | 验证时长自然语言格式化。 |
|
||||||
|
|
||||||
## 🛡️ 鲁棒性防御 (Robustness)
|
## 🛡️ 鲁棒性防御 (Robustness)
|
||||||
- **非法输入拦截**:空字符串或无效格式均返回 `time.Now()`,确保业务不中断,无 Panic。
|
- **非法输入拦截**:空字符串或无效格式均返回当前时区下的 `time.Now()`。
|
||||||
- **自动时区修正**:RFC3339/JS 格式自动转换为 `time.Local`,保持时区一致性。
|
- **自动时区修正**:所有输入自动归一化到指定 `TimeZone` 上下文。
|
||||||
- **中文宽松匹配**:支持“下午3点”、“15时”等多种中文习惯。
|
|
||||||
|
|
||||||
## ⚡ 性能基准 (Benchmarks)
|
## ⚡ 性能基准 (Benchmarks)
|
||||||
| 函数 | 平均耗时 | 性能分析 |
|
| 函数 | 平均耗时 | 性能分析 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| `Parse` | **501.1 ns/op** | 灵活的多格式探测解析,性能保持在微秒级。 |
|
| `Parse` | **~630 ns/op** | 灵活的多格式探测,性能稳定。 |
|
||||||
| `Format` | **742.6 ns/op** | 包含布局字符串替换与 Go 原生 Format,性能良好。 |
|
| `Format` | **~800 ns/op** | 高效布局替换。 |
|
||||||
| `Add` | **146.2 ns/op** | DSL 解析与时间计算极其高效。 |
|
| `Add` | **~150 ns/op** | 高效 DSL 计算。 |
|
||||||
|
|||||||
6
go.sum
6
go.sum
@ -1,6 +0,0 @@
|
|||||||
apigo.cc/go/cast v1.0.2 h1:L9cbD6gOxQdUTwLJz8aJlV7L1df7CH11N80qKhb1QTk=
|
|
||||||
apigo.cc/go/cast v1.0.2/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
231
time.go
231
time.go
@ -10,21 +10,35 @@ import (
|
|||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
// Parse 将任意类型转换为 time.Time。
|
// Parse 将任意类型转换为 time.Time。
|
||||||
// 支持:time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
|
// 支持:time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
|
||||||
// 转换失败返回当前时间 time.Now()。
|
// 转换失败返回当前时间 time.Now()。
|
||||||
func Parse(v any) time.Time {
|
func (tz *TimeZone) Parse(v any) time.Time {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return time.Now()
|
return time.Now().In(tz.loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tm, ok := v.(time.Time); ok {
|
if tm, ok := v.(time.Time); ok {
|
||||||
return tm
|
return tm.In(tz.loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
str := strings.TrimSpace(cast.String(v))
|
str := strings.TrimSpace(cast.String(v))
|
||||||
if str == "" {
|
if str == "" {
|
||||||
return time.Now()
|
return time.Now().In(tz.loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tm time.Time
|
var tm time.Time
|
||||||
@ -35,18 +49,18 @@ func Parse(v any) time.Time {
|
|||||||
// 紧凑格式解析
|
// 紧凑格式解析
|
||||||
switch len(str) {
|
switch len(str) {
|
||||||
case 14: // 20060102150405
|
case 14: // 20060102150405
|
||||||
if tm, err = time.ParseInLocation("20060102150405", str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation("20060102150405", str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
case 8: // 20060102
|
case 8: // 20060102
|
||||||
if tm, err = time.ParseInLocation("20060102", str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation("20060102", str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
case 6: // 150405 或 060102
|
case 6: // 150405 或 060102
|
||||||
if tm, err = time.ParseInLocation("150405", str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation("150405", str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
if tm, err = time.ParseInLocation("060102", str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation("060102", str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,13 +68,13 @@ func Parse(v any) time.Time {
|
|||||||
// 时间戳处理
|
// 时间戳处理
|
||||||
switch {
|
switch {
|
||||||
case num < 1e10: // 秒
|
case num < 1e10: // 秒
|
||||||
return time.Unix(num, 0)
|
return time.Unix(num, 0).In(tz.loc)
|
||||||
case num < 1e13: // 毫秒
|
case num < 1e13: // 毫秒
|
||||||
return time.UnixMilli(num)
|
return time.UnixMilli(num).In(tz.loc)
|
||||||
case num < 1e16: // 微秒
|
case num < 1e16: // 微秒
|
||||||
return time.UnixMicro(num)
|
return time.UnixMicro(num).In(tz.loc)
|
||||||
default: // 纳秒
|
default: // 纳秒
|
||||||
return time.Unix(0, num)
|
return time.Unix(0, num).In(tz.loc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,12 +82,11 @@ func Parse(v any) time.Time {
|
|||||||
// RFC3339 (带 T 和 Z 或偏移量)
|
// RFC3339 (带 T 和 Z 或偏移量)
|
||||||
if strings.Contains(str, "T") {
|
if strings.Contains(str, "T") {
|
||||||
if tm, err = time.Parse(time.RFC3339Nano, str); err == nil {
|
if tm, err = time.Parse(time.RFC3339Nano, str); err == nil {
|
||||||
return tm.In(time.Local)
|
return tm.In(tz.loc)
|
||||||
}
|
}
|
||||||
if tm, err = time.Parse(time.RFC3339, str); err == nil {
|
if tm, err = time.Parse(time.RFC3339, str); err == nil {
|
||||||
return tm.In(time.Local)
|
return tm.In(tz.loc)
|
||||||
}
|
}
|
||||||
// 兼容带 T 但没 Z 的情况,尝试按普通日期时间解析
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理分隔符:-, /, .
|
// 处理分隔符:-, /, .
|
||||||
@ -86,17 +99,17 @@ func Parse(v any) time.Time {
|
|||||||
tsep := str[10] // 通常是 ' ' 或 'T'
|
tsep := str[10] // 通常是 ' ' 或 'T'
|
||||||
if len(str) >= 20 && str[19] == '.' {
|
if len(str) >= 20 && str[19] == '.' {
|
||||||
// 带纳秒
|
// 带纳秒
|
||||||
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05.999999999", tsep), str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05.999999999", tsep), str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", tsep), str[:19], time.Local); err == nil {
|
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", tsep), str[:19], tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 纯日期
|
// 纯日期
|
||||||
if tm, err = time.ParseInLocation(layout, str[:10], time.Local); err == nil {
|
if tm, err = time.ParseInLocation(layout, str[:10], tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,20 +119,18 @@ func Parse(v any) time.Time {
|
|||||||
sep := str[2]
|
sep := str[2]
|
||||||
layout := fmt.Sprintf("06%c01%c02", sep, sep)
|
layout := fmt.Sprintf("06%c01%c02", sep, sep)
|
||||||
if len(str) >= 17 && (str[8] == ' ' || str[8] == 'T') {
|
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 {
|
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", str[8]), str[:17], tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tm, err = time.ParseInLocation(layout, str[:8], time.Local); err == nil {
|
if tm, err = time.ParseInLocation(layout, str[:8], tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 特殊格式:JS, HTTP, 中文
|
// 3. 特殊格式:JS, HTTP, 中文
|
||||||
if strings.Contains(str, "GMT") || strings.Contains(str, "CST") {
|
if strings.Contains(str, "GMT") || strings.Contains(str, "CST") {
|
||||||
// 简单清理 JS 冗余部分
|
|
||||||
cleanStr := strings.SplitN(str, " (", 2)[0]
|
cleanStr := strings.SplitN(str, " (", 2)[0]
|
||||||
// 统一将时区简写替换为偏移量尝试解析
|
|
||||||
cleanStr = strings.Replace(cleanStr, "CST", "+0800", 1)
|
cleanStr = strings.Replace(cleanStr, "CST", "+0800", 1)
|
||||||
|
|
||||||
formats := []string{
|
formats := []string{
|
||||||
@ -131,36 +142,69 @@ func Parse(v any) time.Time {
|
|||||||
}
|
}
|
||||||
for _, f := range formats {
|
for _, f := range formats {
|
||||||
if tm, err = time.Parse(f, cleanStr); err == nil {
|
if tm, err = time.Parse(f, cleanStr); err == nil {
|
||||||
return tm.In(time.Local)
|
return tm.In(tz.loc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 中文格式解析
|
// 中文格式解析
|
||||||
if strings.ContainsAny(str, "年月日时点分秒") {
|
if strings.ContainsAny(str, "年月日时点分秒") {
|
||||||
return parseCN(str)
|
return tz.parseCN(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 纯时间格式
|
// 纯时间格式
|
||||||
if strings.Contains(str, ":") {
|
if strings.Contains(str, ":") {
|
||||||
if tm, err = time.ParseInLocation("15:04:05.999999", str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation("15:04:05.999999", str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
if tm, err = time.ParseInLocation("15:04:05", str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation("15:04:05", str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
if tm, err = time.ParseInLocation("15:04", str, time.Local); err == nil {
|
if tm, err = time.ParseInLocation("15:04", str, tz.loc); err == nil {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return time.Now()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format 格式化时间。
|
// Format 格式化时间。
|
||||||
// layout 支持: YYYY-MM-DD HH:mm:ss, YYYY/MM/DD, HH:mm 等直观格式。
|
// layout 支持: YYYY-MM-DD HH:mm:ss, YYYY/MM/DD, HH:mm 等直观格式。
|
||||||
func Format(layout string, v any) string {
|
func (tz *TimeZone) Format(layout string, v any) string {
|
||||||
tm := Parse(v)
|
tm := tz.Parse(v)
|
||||||
l := layout
|
l := layout
|
||||||
l = strings.ReplaceAll(l, "YYYY", "2006")
|
l = strings.ReplaceAll(l, "YYYY", "2006")
|
||||||
l = strings.ReplaceAll(l, "YY", "06")
|
l = strings.ReplaceAll(l, "YY", "06")
|
||||||
@ -180,11 +224,15 @@ func Format(layout string, v any) string {
|
|||||||
return tm.Format(l)
|
return tm.Format(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Format(layout string, v any) string {
|
||||||
|
return defaultTZ.Format(layout, v)
|
||||||
|
}
|
||||||
|
|
||||||
// Add 时间加减 DSL。
|
// Add 时间加减 DSL。
|
||||||
// 格式如: "+1Y-2M+3D", "+1h30m", "-1s"。
|
// 格式如: "+1Y-2M+3D", "+1h30m", "-1s"。
|
||||||
// 单位支持: Y (年), M (月), D (天), h, m, s, ms, us, ns。
|
// 单位支持: Y (年), M (月), D (天), h, m, s, ms, us, ns。
|
||||||
func Add(expr string, v any) time.Time {
|
func (tz *TimeZone) Add(expr string, v any) time.Time {
|
||||||
tm := Parse(v)
|
tm := tz.Parse(v)
|
||||||
if expr == "" {
|
if expr == "" {
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
@ -250,37 +298,100 @@ func Add(expr string, v any) time.Time {
|
|||||||
return tm.Add(duration)
|
return tm.Add(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数
|
func Add(expr string, v any) time.Time {
|
||||||
|
return defaultTZ.Add(expr, v)
|
||||||
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])
|
// Timer 计时器
|
||||||
mm = cast.Int(matches[3])
|
type Timer struct {
|
||||||
s = cast.Int(matches[4])
|
start time.Time
|
||||||
if matches[1] == "下午" && h < 12 { h += 12 }
|
last time.Time
|
||||||
if matches[1] == "上午" && h >= 12 { h -= 12 }
|
laps []Lap
|
||||||
|
pause time.Duration
|
||||||
}
|
}
|
||||||
if y == 0 { y = time.Now().Year() }
|
|
||||||
if m == 0 { m = int(time.Now().Month()) }
|
type Lap struct {
|
||||||
if d == 0 { d = time.Now().Day() }
|
Label string
|
||||||
return time.Date(y, time.Month(m), d, h, mm, s, 0, time.Local)
|
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))
|
||||||
|
}
|
||||||
|
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, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
func Now() time.Time { return time.Now() }
|
func (tz *TimeZone) Now() time.Time { return time.Now().In(tz.loc) }
|
||||||
func Today() time.Time { return time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local) }
|
func (tz *TimeZone) Today() time.Time {
|
||||||
func Yesterday() time.Time { return Today().AddDate(0, 0, -1) }
|
t := time.Now().In(tz.loc)
|
||||||
func Tomorrow() time.Time { return Today().AddDate(0, 0, 1) }
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, tz.loc)
|
||||||
|
}
|
||||||
|
func (tz *TimeZone) Yesterday() time.Time { return tz.Today().AddDate(0, 0, -1) }
|
||||||
|
func (tz *TimeZone) Tomorrow() time.Time { return tz.Today().AddDate(0, 0, 1) }
|
||||||
|
|
||||||
|
func Now() time.Time { return defaultTZ.Now() }
|
||||||
|
func Today() time.Time { return defaultTZ.Today() }
|
||||||
|
func Yesterday() time.Time { return defaultTZ.Yesterday() }
|
||||||
|
func Tomorrow() time.Time { return defaultTZ.Tomorrow() }
|
||||||
|
|||||||
@ -39,16 +39,17 @@ func TestParse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFormat(t *testing.T) {
|
func TestFormat(t *testing.T) {
|
||||||
refTime := time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
|
// 将测试数据转换为本地时间以符合库的 Local 归一化逻辑
|
||||||
|
refTime := time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC).Local()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
layout string
|
layout string
|
||||||
input any
|
input any
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{"YYYY-MM-DD HH:mm:ss", refTime, "2006-01-02 15:04:05"},
|
{"YYYY-MM-DD HH:mm:ss", refTime, refTime.Format("2006-01-02 15:04:05")},
|
||||||
{"YYYY/MM/DD", "2006-01-02", "2006/01/02"},
|
{"YYYY/MM/DD", "2006-01-02", "2006/01/02"},
|
||||||
{"hh:mm A", refTime, "03:04 PM"},
|
{"hh:mm A", refTime, refTime.Format("03:04 PM")},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
|||||||
46
timezone_robustness_test.go
Normal file
46
timezone_robustness_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package time_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
utime "apigo.cc/go/time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeZoneRobustness(t *testing.T) {
|
||||||
|
// 定义一个特定时间: 2026-05-01 10:00:00 (UTC)
|
||||||
|
// 在上海时区 (UTC+8) 应该是 18:00:00
|
||||||
|
const input = "2026-05-01 10:00:00"
|
||||||
|
shanghai, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
|
|
||||||
|
utcTZ := utime.New(time.UTC)
|
||||||
|
shTZ := utime.New(shanghai)
|
||||||
|
|
||||||
|
// 验证 1: 跨时区解析一致性
|
||||||
|
t1 := utcTZ.Parse(input)
|
||||||
|
if t1.Hour() != 10 || t1.Location() != time.UTC {
|
||||||
|
t.Errorf("UTC Parse failed: expected 10:00 UTC, got %02d:00 %s", t1.Hour(), t1.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
t2 := shTZ.Parse(input)
|
||||||
|
// 解析为上海时区,意味着输入被视为上海时间的 10:00
|
||||||
|
if t2.Hour() != 10 || t2.Location() != shanghai {
|
||||||
|
t.Errorf("Shanghai Parse failed: expected 10:00 Shanghai, got %02d:00 %s", t2.Hour(), t2.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 2: 转换能力 (Shift/Convert 逻辑)
|
||||||
|
// 期望:将 UTC 的 10:00 搬移到上海时区,应该是 18:00
|
||||||
|
// 我们的逻辑: shTZ.Parse(t1) 实际上就是将 t1 搬移到了上海时区
|
||||||
|
t3 := shTZ.Parse(t1)
|
||||||
|
if t3.Hour() != 18 || t3.Location() != shanghai {
|
||||||
|
t.Errorf("Time Shift failed: expected 18:00 Shanghai, got %02d:00 %s", t3.Hour(), t3.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 3: 异常鲁棒性 (无效输入)
|
||||||
|
// 设计哲学: 失败回退到 Now()
|
||||||
|
invalid := "not-a-time"
|
||||||
|
t4 := shTZ.Parse(invalid)
|
||||||
|
if t4.Location() != shanghai {
|
||||||
|
t.Errorf("Invalid input failed to use context timezone: expected %s, got %s", shanghai, t4.Location())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user