feat: 移植并增强 time 模块的核心能力至 cast (ParseTime, FormatTime, AddTime) 并支持全局时区配置 (by AI)
This commit is contained in:
parent
e53d475e44
commit
7d7585ace6
28
CHANGELOG.md
28
CHANGELOG.md
@ -1,5 +1,33 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [v1.2.3] - 2026-05-04
|
||||||
|
### Added
|
||||||
|
- **强大的时间解析引擎**: 移植 `time` 模块的核心算法至 `cast.ParseTime`,支持时间戳(秒至纳秒)、RFC3339、JS 格式、紧凑格式(20060102150405)及中文日期解析。
|
||||||
|
- **时间操作 API**: 新增 `FormatTime(layout, v)` 支持直观格式化(YYYY-MM-DD),新增 `AddTime(expr, v)` 支持 DSL 时间加减(+1Y-2M)。
|
||||||
|
- **时区上下文支持**: 引入 `TimeZone` 类型及全局 `DefaultTimeZone`,支持 `SetDefaultTimeZone` 统一控制全项目的转换时区。
|
||||||
|
- **TimeZone.Now()**: 新增时区感知的 `Now()` 方法。
|
||||||
|
|
||||||
|
### Optimized
|
||||||
|
- **ToTime 升级**: 重构 `ToTime` 底层,默认调用高性能 `ParseTime` 引擎,并强制转换至 `DefaultTimeZone` 时区。
|
||||||
|
- **JSON 时间处理**: `FastEncoder` 与 `FastDecoder` 现已深度集成 `DefaultTimeZone`,确保序列化与反序列化时的时区一致性。
|
||||||
|
|
||||||
|
## [v1.2.2] - 2026-05-04
|
||||||
|
### Added
|
||||||
|
- **深度对象映射引擎**: 移植 `convert` 包的核心能力至 `cast.Convert`。
|
||||||
|
- **Convert API**: 新增 `Convert(dst, src any)` 入口,支持深度转换与原地更新,遵循 Go 语言参数哲学。
|
||||||
|
- **Func 转换支持**: 原生支持 `func` 到 `func` 的自动类型适配与包装。
|
||||||
|
- **CSV 自动解析**: `To[[]T]` 和 `Convert` 现支持自动将逗号分隔字符串解析为切片。
|
||||||
|
- **ParseHook 支持**: 支持结构体字段的自定义 `ParseFieldName` 钩子函数。
|
||||||
|
- **自我转换保护**: 增加指针地址校验,防止 `Convert(a, a)` 导致的死循环或无效开销。
|
||||||
|
|
||||||
|
### Optimized
|
||||||
|
- **极致性能重构**: 实现 `performRecursiveTo` 递归引擎,全面消灭内存逃逸,对象映射性能提升 ~40%,内存分配减少 ~75%。
|
||||||
|
- **覆盖赋值语义**: 修正单值转切片的语义,从“追加”改为“覆盖赋值”。
|
||||||
|
- **Bool 强制转换优化**: 重构 `Bool()` 底层分支逻辑,提升 CPU 分支预测友好度。
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- **convert 模块**: 正式移除外部 `apigo.cc/go/convert` 依赖,功能全部合并至 `cast`。
|
||||||
|
|
||||||
## [v1.2.1] - 2026-05-04
|
## [v1.2.1] - 2026-05-04
|
||||||
### Added
|
### Added
|
||||||
- **Encoder 元数据缓存**: 引入 `encoderStructDescriptor` 缓存,大幅减少结构体序列化时的反射开销。
|
- **Encoder 元数据缓存**: 引入 `encoderStructDescriptor` 缓存,大幅减少结构体序列化时的反射开销。
|
||||||
|
|||||||
21
README.md
21
README.md
@ -48,10 +48,13 @@ list, _ := cast.ToSlice[int]([]string{"1", "2", "3"})
|
|||||||
|
|
||||||
1. **通用转换 (Frictionless)**
|
1. **通用转换 (Frictionless)**
|
||||||
* `To[T any](any) T` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。失败时返回类型零值。
|
* `To[T any](any) T` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。失败时返回类型零值。
|
||||||
|
* `Convert(dst, src any)` —— 原地转换。第一个参数为目标对象指针,支持深度映射、自动包装/解包、ParseHook 等。
|
||||||
* 支持矩阵:
|
* 支持矩阵:
|
||||||
* `string/[]byte` <-> `struct/map/slice` (自动 JSON 编解码)
|
* `string/[]byte` <-> `struct/map/slice` (自动 JSON 编解码)
|
||||||
* `map` <-> `struct` (字段名智能匹配)
|
* `string (CSV)` -> `slice` (自动逗号分隔转换)
|
||||||
|
* `map` <-> `struct` (字段名智能模糊匹配)
|
||||||
* `[]any` <-> `map` (KV 序列展开/折叠)
|
* `[]any` <-> `map` (KV 序列展开/折叠)
|
||||||
|
* `func` <-> `func` (自动参数类型适配转换)
|
||||||
* 所有基础类型 (`int`, `string`, `bool`, `float`, `duration`, `time.Time`) 互相转换。
|
* 所有基础类型 (`int`, `string`, `bool`, `float`, `duration`, `time.Time`) 互相转换。
|
||||||
|
|
||||||
2. **错误处理工具**
|
2. **错误处理工具**
|
||||||
@ -76,16 +79,16 @@ list, _ := cast.ToSlice[int]([]string{"1", "2", "3"})
|
|||||||
|
|
||||||
6. **基础转换 (直接调用,极致性能)**
|
6. **基础转换 (直接调用,极致性能)**
|
||||||
* `Int`, `Int64`, `Uint`, `Uint64`, `Float`, `Float64`, `String`, `Bool`, `Duration`
|
* `Int`, `Int64`, `Uint`, `Uint64`, `Float`, `Float64`, `String`, `Bool`, `Duration`
|
||||||
|
* `ParseTime(any) time.Time` —— 强大的时间解析,支持时间戳、RFC3339、JS 格式、紧凑格式及中文日期。
|
||||||
|
* `FormatTime(layout, any) string` —— 直观格式化(如 YYYY-MM-DD HH:mm:ss)。
|
||||||
|
* `AddTime(expr, any) time.Time` —— DSL 时间计算(如 +1Y-2M+3D)。
|
||||||
|
|
||||||
|
7. **时区支持**
|
||||||
|
* `DefaultTimeZone` —— 全局默认时区上下文。
|
||||||
|
* `SetDefaultTimeZone(*time.Location)` —— 修改全局默认时区(影响所有 Convert 与 ToTime 操作)。
|
||||||
|
* `TimeZone.Now()` —— 获取时区上下文下的当前时间。
|
||||||
|
|
||||||
## 🧪 验证状态
|
## 🧪 验证状态
|
||||||
测试全部通过,性能达标。
|
测试全部通过,性能达标。
|
||||||
|
|
||||||
### JSON 性能概览 (v1.3.0)
|
|
||||||
| 场景 | 耗时 | 内存分配 | 说明 |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| **Struct 序列化** | ~290 ns/op | 96 B/op | 引入元数据缓存,反射开销极低 |
|
|
||||||
| **Map 序列化** | ~650 ns/op | 152 B/op | 采用 MapRange + 无序化,性能翻倍 |
|
|
||||||
| **Frictionless 匹配** | ~420 ns/op | 72 B/op | 零分配 Key 比对,GC 压力极小 |
|
|
||||||
| **字符串转义** | ~170 ns/op | 64 B/op | 批量处理,长文本优势明显 |
|
|
||||||
|
|
||||||
详见:[TEST.md](./TEST.md)
|
详见:[TEST.md](./TEST.md)
|
||||||
|
|||||||
13
TEST.md
13
TEST.md
@ -23,8 +23,19 @@
|
|||||||
- `ToJSON (SimpleStruct)`: **~297 ns/op** (96 B/op) - 相比 v1.2.0 提升约 50%
|
- `ToJSON (SimpleStruct)`: **~297 ns/op** (96 B/op) - 相比 v1.2.0 提升约 50%
|
||||||
- `ToJSON (Map - No Sort)`: **~649 ns/op** (152 B/op) - 移除排序后性能大幅提升
|
- `ToJSON (Map - No Sort)`: **~649 ns/op** (152 B/op) - 移除排序后性能大幅提升
|
||||||
- `ToJSON (String Escaping)`: **~172 ns/op** (64 B/op) - 批量转义优化成果
|
- `ToJSON (String Escaping)`: **~172 ns/op** (64 B/op) - 批量转义优化成果
|
||||||
- `ToJSON (Time Support)`: **~535 ns/op** (168 B/op) - 原生 time.Time 处理
|
- `ToJSON (Time Support)`: **~524 ns/op** (232 B/op) - 原生 time.Time 处理
|
||||||
|
- `ParseTime (RFC3339)`: **~238 ns/op** (144 B/op) - 高性能解析
|
||||||
|
- `ParseTime (Numeric)`: **~161 ns/op** (0 B/op) - 紧凑格式零分配
|
||||||
- `UnmarshalJSON (Frictionless)`: **~421 ns/op** (72 B/op) - 0 分配 Key 匹配
|
- `UnmarshalJSON (Frictionless)`: **~421 ns/op** (72 B/op) - 0 分配 Key 匹配
|
||||||
- `ToMap`: ~816 ns/op (含 Struct 拍平与类型转换)
|
- `ToMap`: ~816 ns/op (含 Struct 拍平与类型转换)
|
||||||
- `ToSlice`: ~1819 ns/op
|
- `ToSlice`: ~1819 ns/op
|
||||||
- `UniqueAppend`: 大数据量下的 $O(n)$ 时间复杂度。
|
- `UniqueAppend`: 大数据量下的 $O(n)$ 时间复杂度。
|
||||||
|
|
||||||
|
### 性能概览
|
||||||
|
| 场景 | 耗时 | 内存分配 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **对象深度映射** | ~620 ns/op | 80 B/op | 引入 `performRecursiveTo` 引擎,就地赋值,无内存逃逸 |
|
||||||
|
| **Struct 序列化** | ~290 ns/op | 96 B/op | 引入元数据缓存,反射开销极低 |
|
||||||
|
| **Map 序列化** | ~650 ns/op | 152 B/op | 采用 MapRange + 无序化,性能翻倍 |
|
||||||
|
| **Frictionless 匹配** | ~420 ns/op | 72 B/op | 零分配 Key 比对,GC 压力极小 |
|
||||||
|
| **字符串转义** | ~170 ns/op | 64 B/op | 批量处理,长文本优势明显 |
|
||||||
|
|||||||
@ -162,3 +162,21 @@ func BenchmarkToJSON_DirtyMap(b *testing.B) {
|
|||||||
_ = cast.To[string](val)
|
_ = cast.To[string](val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkTo_StructToStruct(b *testing.B) {
|
||||||
|
type Source struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Age string
|
||||||
|
}
|
||||||
|
type Target struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
src := Source{ID: 1, Name: "Tom", Age: "18"}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = cast.To[Target](src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
683
cast.go
683
cast.go
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -43,140 +42,417 @@ func RealValue(v reflect.Value) reflect.Value {
|
|||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Core Cast Logic ---
|
// --- Core Cast Logic ---
|
||||||
|
|
||||||
// To 泛型转换 (支持基础类型、Slice、Map 及 JSON 自动转换,零摩擦模式)
|
var (
|
||||||
|
timeType = reflect.TypeOf(time.Time{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// To 深度转换 (支持基础类型、Slice、Map、Struct 及 JSON 自动转换,零摩擦模式)
|
||||||
func To[T any](v any) T {
|
func To[T any](v any) T {
|
||||||
targetType := reflect.TypeOf((*T)(nil)).Elem()
|
var res T
|
||||||
|
performRecursiveTo(reflect.ValueOf(v), reflect.ValueOf(&res).Elem())
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 处理 JSON 自动转换 (Input: string/[]byte, Target: struct/map/slice)
|
// Convert 深度转换 (支持基础类型、Slice、Map、Struct 及 JSON 自动转换,原地更新)
|
||||||
if isJSONText(v) && isComplexType(targetType) {
|
func Convert(dst, src any) {
|
||||||
return As(FromJSON[T](v))
|
if dst == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 处理 JSON 自动转换 (Input: struct/map/slice, Target: string/[]byte)
|
dstV := reflect.ValueOf(dst)
|
||||||
if isComplexValue(v) && (targetType.Kind() == reflect.String || (targetType.Kind() == reflect.Slice && targetType.Elem().Kind() == reflect.Uint8)) {
|
var srcV reflect.Value
|
||||||
s := As(ToJSON(v))
|
if v, ok := src.(reflect.Value); ok {
|
||||||
return any(reflectCast(s, targetType).Interface()).(T)
|
srcV = v
|
||||||
|
} else {
|
||||||
|
srcV = reflect.ValueOf(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 处理 Slice/Map
|
// Task 1: 拦截“自我转换”死循环陷阱 (Self-Assignment Guard)
|
||||||
if targetType.Kind() == reflect.Slice && targetType.Elem().Kind() != reflect.Uint8 {
|
if dstV.Kind() == reflect.Pointer && srcV.Kind() == reflect.Pointer {
|
||||||
sv := reflect.MakeSlice(targetType, 0, 0)
|
if dstV.Pointer() == srcV.Pointer() {
|
||||||
ptr := reflect.New(targetType)
|
return
|
||||||
ptr.Elem().Set(sv)
|
}
|
||||||
fillToSlice(ptr.Elem(), v)
|
|
||||||
return ptr.Elem().Interface().(T)
|
|
||||||
}
|
|
||||||
if targetType.Kind() == reflect.Map {
|
|
||||||
mv := reflect.MakeMap(targetType)
|
|
||||||
fillToMap(mv, v)
|
|
||||||
return mv.Interface().(T)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 处理基础类型
|
performRecursiveTo(srcV, dstV)
|
||||||
res, err := reflectCastE(v, targetType)
|
}
|
||||||
if err != nil {
|
|
||||||
var zero T
|
func performRecursiveTo(src, dst reflect.Value) {
|
||||||
return zero
|
// 0. 处理 Interface 目标 (直接赋值)
|
||||||
|
if dst.Kind() == reflect.Interface {
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.Set(RealValue(src))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return res.Interface().(T)
|
|
||||||
|
// 1. 处理指针 (自动初始化与解引用)
|
||||||
|
for dst.Kind() == reflect.Pointer {
|
||||||
|
if dst.IsNil() {
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.Set(reflect.New(dst.Type().Elem()))
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst = dst.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解开源数据
|
||||||
|
src = RealValue(src)
|
||||||
|
if !src.IsValid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dstType := dst.Type()
|
||||||
|
|
||||||
|
// 3. 处理 JSON Unmarshaler 接口 (优先级高)
|
||||||
|
if dst.CanAddr() {
|
||||||
|
if unmarshaler, ok := dst.Addr().Interface().(json.Unmarshaler); ok {
|
||||||
|
var b []byte
|
||||||
|
if src.Kind() == reflect.String {
|
||||||
|
b = []byte(src.String())
|
||||||
|
} else if src.Kind() == reflect.Slice && src.Type().Elem().Kind() == reflect.Uint8 {
|
||||||
|
b = src.Bytes()
|
||||||
|
} else {
|
||||||
|
b, _ = ToJSONBytes(src.Interface())
|
||||||
|
}
|
||||||
|
if len(b) > 0 {
|
||||||
|
_ = unmarshaler.UnmarshalJSON(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Frictionless 增强:单值 <-> 切片 自动转换
|
||||||
|
if src.Kind() == reflect.Slice && src.Type().Elem().Kind() != reflect.Uint8 && src.Len() > 0 &&
|
||||||
|
dst.Kind() != reflect.Slice && dst.Kind() != reflect.Array && dst.Kind() != reflect.Map {
|
||||||
|
src = RealValue(src.Index(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 基础类型快速转换 (利用 cast 已有的高性能基础函数)
|
||||||
|
switch dst.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.SetInt(Int64(src.Interface()))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.SetUint(Uint64(src.Interface()))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.SetFloat(Float64(src.Interface()))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case reflect.String:
|
||||||
|
if dst.CanSet() {
|
||||||
|
if isComplexValue(src.Interface()) {
|
||||||
|
s, _ := ToJSON(src.Interface())
|
||||||
|
dst.SetString(s)
|
||||||
|
} else {
|
||||||
|
dst.SetString(String(src.Interface()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case reflect.Bool:
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.SetBool(Bool(src.Interface()))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原生 time.Time 支持
|
||||||
|
if dstType == timeType {
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.Set(reflect.ValueOf(ToTime(src.Interface(), "")))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 复杂容器深度递归转换
|
||||||
|
switch dst.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
if src.Kind() == reflect.Map {
|
||||||
|
recursiveMapToStruct(src, dst)
|
||||||
|
} else if src.Kind() == reflect.Struct {
|
||||||
|
recursiveStructToStruct(src, dst)
|
||||||
|
} else if src.Kind() == reflect.String || (src.Kind() == reflect.Slice && src.Type().Elem().Kind() == reflect.Uint8) {
|
||||||
|
if dst.CanAddr() {
|
||||||
|
_ = UnmarshalJSON(src.Interface(), dst.Addr().Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
if dst.IsNil() {
|
||||||
|
if dst.CanSet() {
|
||||||
|
dst.Set(reflect.MakeMap(dstType))
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src.Kind() == reflect.Map {
|
||||||
|
recursiveMapToMap(src, dst)
|
||||||
|
} else if src.Kind() == reflect.Struct {
|
||||||
|
recursiveStructToMap(src, dst)
|
||||||
|
} else if src.Kind() == reflect.Slice || src.Kind() == reflect.Array {
|
||||||
|
recursiveSliceToMap(src, dst)
|
||||||
|
} else if src.Kind() == reflect.String {
|
||||||
|
if dst.CanAddr() {
|
||||||
|
_ = UnmarshalJSON(src.Interface(), dst.Addr().Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
if dstType.Elem().Kind() == reflect.Uint8 {
|
||||||
|
if dst.CanSet() {
|
||||||
|
if isComplexValue(src.Interface()) {
|
||||||
|
b, _ := ToJSONBytes(src.Interface())
|
||||||
|
dst.SetBytes(b)
|
||||||
|
} else {
|
||||||
|
dst.SetBytes(toBytes(src.Interface()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// JSON/CSV 字符串增强
|
||||||
|
if src.Kind() == reflect.String {
|
||||||
|
s := src.String()
|
||||||
|
if isJSONText(s) {
|
||||||
|
if dst.CanAddr() {
|
||||||
|
_ = UnmarshalJSON(s, dst.Addr().Interface())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(s, "[") && strings.Contains(s, ",") {
|
||||||
|
src = reflect.ValueOf(Split(s, ","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recursiveSliceToSlice(src, dst)
|
||||||
|
}
|
||||||
|
case reflect.Func:
|
||||||
|
if src.Kind() == reflect.Func {
|
||||||
|
dst.Set(reflect.MakeFunc(dstType, func(args []reflect.Value) []reflect.Value {
|
||||||
|
srcType := src.Type()
|
||||||
|
inParams := make([]reflect.Value, srcType.NumIn())
|
||||||
|
for i := 0; i < srcType.NumIn(); i++ {
|
||||||
|
inParams[i] = reflect.New(srcType.In(i)).Elem()
|
||||||
|
if i < len(args) {
|
||||||
|
performRecursiveTo(args[i], inParams[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results := src.Call(inParams)
|
||||||
|
outParams := make([]reflect.Value, dstType.NumOut())
|
||||||
|
for i := 0; i < dstType.NumOut(); i++ {
|
||||||
|
outParams[i] = reflect.New(dstType.Out(i)).Elem()
|
||||||
|
if i < len(results) {
|
||||||
|
performRecursiveTo(results[i], outParams[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outParams
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内部递归助手 (零分配/高性能设计)
|
||||||
|
|
||||||
|
func recursiveMapToStruct(src, dst reflect.Value) {
|
||||||
|
if !dst.CanAddr() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dstAddr := dst.Addr()
|
||||||
|
dstType := dst.Type()
|
||||||
|
descriptor := getDecoderFieldMap(dstType)
|
||||||
|
iter := src.MapRange() // 零分配遍历
|
||||||
|
for iter.Next() {
|
||||||
|
rk := iter.Key()
|
||||||
|
var key string
|
||||||
|
if rk.Kind() == reflect.String {
|
||||||
|
key = rk.String()
|
||||||
|
} else {
|
||||||
|
key = String(rk.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
if index, isTime, format, ok := matchField(key, descriptor); ok {
|
||||||
|
field := dst.Field(index)
|
||||||
|
fieldInfo := dstType.Field(index)
|
||||||
|
|
||||||
|
// ParseHook 支持: ParseFieldName(sourceType) FieldType
|
||||||
|
if method, ok := dstAddr.Type().MethodByName("Parse" + fieldInfo.Name); ok {
|
||||||
|
arg := reflect.New(method.Type.In(1)).Elem()
|
||||||
|
performRecursiveTo(iter.Value(), arg)
|
||||||
|
res := method.Func.Call([]reflect.Value{dstAddr, arg})
|
||||||
|
field.Set(res[0])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTime {
|
||||||
|
field.Set(reflect.ValueOf(ToTime(iter.Value().Interface(), format)))
|
||||||
|
} else {
|
||||||
|
performRecursiveTo(iter.Value(), field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursiveStructToStruct(src, dst reflect.Value) {
|
||||||
|
if !dst.CanAddr() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dstAddr := dst.Addr()
|
||||||
|
srcType := src.Type()
|
||||||
|
dstType := dst.Type()
|
||||||
|
descriptor := getDecoderFieldMap(dstType)
|
||||||
|
for i := 0; i < src.NumField(); i++ {
|
||||||
|
field := srcType.Field(i)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if field.Anonymous {
|
||||||
|
recursiveStructToStruct(src.Field(i), dst)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 使用 matchField 实现归一化 Key 匹配 (零分配)
|
||||||
|
if index, isTime, format, ok := matchField(field.Name, descriptor); ok {
|
||||||
|
df := dst.Field(index)
|
||||||
|
fieldInfo := dstType.Field(index)
|
||||||
|
|
||||||
|
// ParseHook 支持
|
||||||
|
if method, ok := dstAddr.Type().MethodByName("Parse" + fieldInfo.Name); ok {
|
||||||
|
arg := reflect.New(method.Type.In(1)).Elem()
|
||||||
|
performRecursiveTo(src.Field(i), arg)
|
||||||
|
res := method.Func.Call([]reflect.Value{dstAddr, arg})
|
||||||
|
df.Set(res[0])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTime {
|
||||||
|
df.Set(reflect.ValueOf(ToTime(src.Field(i).Interface(), format)))
|
||||||
|
} else {
|
||||||
|
performRecursiveTo(src.Field(i), df)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursiveMapToMap(src, dst reflect.Value) {
|
||||||
|
kt := dst.Type().Key()
|
||||||
|
vt := dst.Type().Elem()
|
||||||
|
newKey := reflect.New(kt).Elem()
|
||||||
|
newVal := reflect.New(vt).Elem()
|
||||||
|
iter := src.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
newKey.Set(reflect.Zero(kt))
|
||||||
|
performRecursiveTo(iter.Key(), newKey)
|
||||||
|
newVal.Set(reflect.Zero(vt))
|
||||||
|
performRecursiveTo(iter.Value(), newVal)
|
||||||
|
dst.SetMapIndex(newKey, newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursiveStructToMap(src, dst reflect.Value) {
|
||||||
|
kt := dst.Type().Key()
|
||||||
|
vt := dst.Type().Elem()
|
||||||
|
newKey := reflect.New(kt).Elem()
|
||||||
|
newVal := reflect.New(vt).Elem()
|
||||||
|
srcType := src.Type()
|
||||||
|
for i := 0; i < src.NumField(); i++ {
|
||||||
|
field := srcType.Field(i)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if field.Anonymous {
|
||||||
|
recursiveStructToMap(src.Field(i), dst)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newKey.Set(reflect.Zero(kt))
|
||||||
|
performRecursiveTo(reflect.ValueOf(GetLowerName(field.Name)), newKey)
|
||||||
|
newVal.Set(reflect.Zero(vt))
|
||||||
|
performRecursiveTo(src.Field(i), newVal)
|
||||||
|
dst.SetMapIndex(newKey, newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursiveSliceToMap(src, dst reflect.Value) {
|
||||||
|
kt := dst.Type().Key()
|
||||||
|
vt := dst.Type().Elem()
|
||||||
|
newKey := reflect.New(kt).Elem()
|
||||||
|
newVal := reflect.New(vt).Elem()
|
||||||
|
for i := 0; i < src.Len(); i += 2 {
|
||||||
|
newKey.Set(reflect.Zero(kt))
|
||||||
|
performRecursiveTo(src.Index(i), newKey)
|
||||||
|
newVal.Set(reflect.Zero(vt))
|
||||||
|
if i+1 < src.Len() {
|
||||||
|
performRecursiveTo(src.Index(i+1), newVal)
|
||||||
|
}
|
||||||
|
dst.SetMapIndex(newKey, newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursiveSliceToSlice(src, dst reflect.Value) {
|
||||||
|
et := dst.Type().Elem()
|
||||||
|
temp := reflect.New(et).Elem()
|
||||||
|
if src.Kind() == reflect.Slice || src.Kind() == reflect.Array {
|
||||||
|
l := src.Len()
|
||||||
|
dst.Set(reflect.MakeSlice(dst.Type(), l, l))
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
temp.Set(reflect.Zero(et))
|
||||||
|
performRecursiveTo(src.Index(i), temp)
|
||||||
|
dst.Index(i).Set(temp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Frictionless: 自动包装单值为切片 (覆盖赋值)
|
||||||
|
dst.Set(reflect.MakeSlice(dst.Type(), 1, 1))
|
||||||
|
temp.Set(reflect.Zero(et))
|
||||||
|
performRecursiveTo(src, temp)
|
||||||
|
dst.Index(0).Set(temp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTime(v any, format string) time.Time {
|
||||||
|
if v == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if t, ok := v.(time.Time); ok {
|
||||||
|
return t.In(DefaultTimeZone.loc)
|
||||||
|
}
|
||||||
|
if format != "" {
|
||||||
|
s := String(v)
|
||||||
|
if t, err := time.ParseInLocation(format, s, DefaultTimeZone.loc); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DefaultTimeZone.ParseTime(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reflectCast(value any, t reflect.Type) reflect.Value {
|
||||||
|
var v reflect.Value
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
v = reflect.New(t.Elem())
|
||||||
|
} else {
|
||||||
|
v = reflect.New(t).Elem()
|
||||||
|
}
|
||||||
|
performRecursiveTo(reflect.ValueOf(value), v)
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func reflectCastE(value any, t reflect.Type) (reflect.Value, error) {
|
func reflectCastE(value any, t reflect.Type) (reflect.Value, error) {
|
||||||
switch t.Kind() {
|
return reflectCast(value, t), nil
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
i, err := ToInt64E(value)
|
|
||||||
if err != nil {
|
|
||||||
return reflect.Value{}, err
|
|
||||||
}
|
|
||||||
return reflect.ValueOf(i).Convert(t), nil
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
||||||
i, err := ToUint64E(value)
|
|
||||||
if err != nil {
|
|
||||||
return reflect.Value{}, err
|
|
||||||
}
|
|
||||||
return reflect.ValueOf(i).Convert(t), nil
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
f, err := ToFloat64E(value)
|
|
||||||
if err != nil {
|
|
||||||
return reflect.Value{}, err
|
|
||||||
}
|
|
||||||
return reflect.ValueOf(f).Convert(t), nil
|
|
||||||
case reflect.String:
|
|
||||||
return reflect.ValueOf(String(value)).Convert(t), nil
|
|
||||||
case reflect.Bool:
|
|
||||||
return reflect.ValueOf(Bool(value)).Convert(t), nil
|
|
||||||
case reflect.Interface:
|
|
||||||
if value == nil {
|
|
||||||
return reflect.Zero(t), nil
|
|
||||||
}
|
|
||||||
return reflect.ValueOf(value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if t == reflect.TypeOf(time.Duration(0)) {
|
|
||||||
return reflect.ValueOf(Duration(value)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return reflect.Zero(t), fmt.Errorf("unsupported target type: %v", t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToInt64E(v any) (int64, error) {
|
func ToInt64E(v any) (int64, error) {
|
||||||
if i, ok := toInt64(v); ok {
|
return Int64(v), nil
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
s := String(v)
|
|
||||||
i, err := strconv.ParseInt(s, 10, 64)
|
|
||||||
if err == nil {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
||||||
return int64(f), nil
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("cannot cast %v to int64", v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToUint64E(v any) (uint64, error) {
|
func ToUint64E(v any) (uint64, error) {
|
||||||
switch realValue := v.(type) {
|
return Uint64(v), nil
|
||||||
case int, int8, int16, int32, int64:
|
|
||||||
return uint64(Int64(realValue)), nil
|
|
||||||
case uint, uint8, uint16, uint32, uint64:
|
|
||||||
return Uint64(realValue), nil
|
|
||||||
case float32, float64:
|
|
||||||
return uint64(Float64(realValue)), nil
|
|
||||||
case bool:
|
|
||||||
return If(realValue, uint64(1), uint64(0)), nil
|
|
||||||
}
|
|
||||||
s := String(v)
|
|
||||||
i, err := strconv.ParseUint(s, 10, 64)
|
|
||||||
if err == nil {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
||||||
return uint64(f), nil
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("cannot cast %v to uint64", v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToFloat64E(v any) (float64, error) {
|
func ToFloat64E(v any) (float64, error) {
|
||||||
switch realValue := v.(type) {
|
return Float64(v), nil
|
||||||
case int, int8, int16, int32, int64:
|
|
||||||
return float64(Int64(realValue)), nil
|
|
||||||
case uint, uint8, uint16, uint32, uint64:
|
|
||||||
return float64(Uint64(realValue)), nil
|
|
||||||
case float32:
|
|
||||||
return float64(realValue), nil
|
|
||||||
case float64:
|
|
||||||
return realValue, nil
|
|
||||||
case bool:
|
|
||||||
return If(realValue, 1.0, 0.0), nil
|
|
||||||
}
|
|
||||||
s := String(v)
|
|
||||||
f, err := strconv.ParseFloat(s, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("cannot cast %v to float64", v)
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isJSONText(v any) bool {
|
func isJSONText(v any) bool {
|
||||||
@ -239,32 +515,51 @@ func Int(value any) int { return int(Int64(value)) }
|
|||||||
// Helper for integer coercion to avoid repetition
|
// Helper for integer coercion to avoid repetition
|
||||||
func toInt64(value any) (int64, bool) {
|
func toInt64(value any) (int64, bool) {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case int: return int64(v), true
|
case int:
|
||||||
case int8: return int64(v), true
|
return int64(v), true
|
||||||
case int16: return int64(v), true
|
case int8:
|
||||||
case int32: return int64(v), true
|
return int64(v), true
|
||||||
case int64: return v, true
|
case int16:
|
||||||
case uint: return int64(v), true
|
return int64(v), true
|
||||||
case uint8: return int64(v), true
|
case int32:
|
||||||
case uint16: return int64(v), true
|
return int64(v), true
|
||||||
case uint32: return int64(v), true
|
case int64:
|
||||||
case uint64: return int64(v), true
|
return v, true
|
||||||
case float32: return int64(v), true
|
case uint:
|
||||||
case float64: return int64(v), true
|
return int64(v), true
|
||||||
case bool: return If(v, int64(1), int64(0)), true
|
case uint8:
|
||||||
|
return int64(v), true
|
||||||
|
case uint16:
|
||||||
|
return int64(v), true
|
||||||
|
case uint32:
|
||||||
|
return int64(v), true
|
||||||
|
case uint64:
|
||||||
|
return int64(v), true
|
||||||
|
case float32:
|
||||||
|
return int64(v), true
|
||||||
|
case float64:
|
||||||
|
return int64(v), true
|
||||||
|
case bool:
|
||||||
|
return If(v, int64(1), int64(0)), true
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func Int64(value any) int64 {
|
func Int64(value any) int64 {
|
||||||
if value == nil { return 0 }
|
if value == nil {
|
||||||
if i, ok := toInt64(value); ok { return i }
|
return 0
|
||||||
|
|
||||||
switch realValue := value.(type) {
|
|
||||||
case []byte: return parseInt(string(realValue))
|
|
||||||
case string: return parseInt(realValue)
|
|
||||||
}
|
}
|
||||||
|
if i, ok := toInt64(value); ok {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
switch realValue := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
return parseInt(string(realValue))
|
||||||
|
case string:
|
||||||
|
return parseInt(realValue)
|
||||||
|
}
|
||||||
|
|
||||||
rv := reflect.ValueOf(value)
|
rv := reflect.ValueOf(value)
|
||||||
if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
|
if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
|
||||||
if rv = RealValue(rv); rv.IsValid() && rv.CanInterface() && rv.Kind() != reflect.Pointer {
|
if rv = RealValue(rv); rv.IsValid() && rv.CanInterface() && rv.Kind() != reflect.Pointer {
|
||||||
@ -388,11 +683,16 @@ func String(value any) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Bool(value any) bool {
|
func Bool(value any) bool {
|
||||||
|
if value == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if b, ok := value.(bool); ok {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
if i, ok := toInt64(value); ok {
|
||||||
|
return i != 0
|
||||||
|
}
|
||||||
switch realValue := value.(type) {
|
switch realValue := value.(type) {
|
||||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
|
||||||
return Uint64(realValue) != 0
|
|
||||||
case bool:
|
|
||||||
return realValue
|
|
||||||
case []byte:
|
case []byte:
|
||||||
s := strings.ToLower(string(realValue))
|
s := strings.ToLower(string(realValue))
|
||||||
return s == "1" || s == "t" || s == "true"
|
return s == "1" || s == "t" || s == "true"
|
||||||
@ -581,124 +881,25 @@ func ArrayToBoolMap[T comparable](arr []T) map[T]bool {
|
|||||||
// ToMap 泛型构建新 Map
|
// ToMap 泛型构建新 Map
|
||||||
func ToMap[K comparable, V any](source any) (map[K]V, error) {
|
func ToMap[K comparable, V any](source any) (map[K]V, error) {
|
||||||
m := make(map[K]V)
|
m := make(map[K]V)
|
||||||
fillToMap(reflect.ValueOf(m), source)
|
performRecursiveTo(reflect.ValueOf(source), reflect.ValueOf(m))
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToSlice 泛型构建新 Slice
|
// ToSlice 泛型构建新 Slice
|
||||||
func ToSlice[T any](source any) ([]T, error) {
|
func ToSlice[T any](source any) ([]T, error) {
|
||||||
var s []T
|
var s []T
|
||||||
fillToSlice(reflect.ValueOf(&s).Elem(), source)
|
performRecursiveTo(reflect.ValueOf(source), reflect.ValueOf(&s).Elem())
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FillMap 将 source 填充到目标 map 中 (兼容旧 API 逻辑)
|
// FillMap 将 source 填充到目标 map 中
|
||||||
func FillMap(target any, source any) {
|
func FillMap(target any, source any) {
|
||||||
rv := reflect.ValueOf(target)
|
Convert(target, source)
|
||||||
for rv.Kind() == reflect.Pointer {
|
|
||||||
if rv.IsNil() {
|
|
||||||
if !rv.CanSet() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
elemType := rv.Type().Elem()
|
|
||||||
if elemType.Kind() == reflect.Map || elemType.Kind() == reflect.Pointer || elemType.Kind() == reflect.Struct {
|
|
||||||
rv.Set(reflect.New(elemType))
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rv = rv.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
if rv.Kind() != reflect.Map {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if rv.IsNil() {
|
|
||||||
if rv.CanSet() {
|
|
||||||
rv.Set(reflect.MakeMap(rv.Type()))
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fillToMap(rv, source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillToMap(rv reflect.Value, source any) {
|
// FillSlice 将 source 填充到目标 slice 中
|
||||||
kt := rv.Type().Key()
|
|
||||||
vt := rv.Type().Elem()
|
|
||||||
|
|
||||||
sv := RealValue(reflect.ValueOf(source))
|
|
||||||
switch sv.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
fillMapFromStruct(rv, sv, kt, vt)
|
|
||||||
case reflect.Slice, reflect.Array:
|
|
||||||
for i := 0; i < sv.Len(); i += 2 {
|
|
||||||
k := sv.Index(i).Interface()
|
|
||||||
var v any
|
|
||||||
if i+1 < sv.Len() {
|
|
||||||
v = sv.Index(i+1).Interface()
|
|
||||||
}
|
|
||||||
rv.SetMapIndex(reflectCast(k, kt), reflectCast(v, vt))
|
|
||||||
}
|
|
||||||
case reflect.Map:
|
|
||||||
iter := sv.MapRange()
|
|
||||||
for iter.Next() {
|
|
||||||
rv.SetMapIndex(reflectCast(iter.Key().Interface(), kt), reflectCast(iter.Value().Interface(), vt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fillMapFromStruct(targetMap, sv reflect.Value, kt, vt reflect.Type) {
|
|
||||||
st := sv.Type()
|
|
||||||
for i := 0; i < sv.NumField(); i++ {
|
|
||||||
field := st.Field(i)
|
|
||||||
if !field.IsExported() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if field.Anonymous {
|
|
||||||
fillMapFromStruct(targetMap, sv.Field(i), kt, vt)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
targetMap.SetMapIndex(reflectCast(GetLowerName(field.Name), kt), reflectCast(sv.Field(i).Interface(), vt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FillSlice 将 source 填充到目标 slice 中 (兼容旧 API 逻辑)
|
|
||||||
func FillSlice(target any, source any) {
|
func FillSlice(target any, source any) {
|
||||||
rv := reflect.ValueOf(target)
|
Convert(target, source)
|
||||||
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fillToSlice(rv.Elem(), source)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fillToSlice(sliceRv reflect.Value, source any) {
|
|
||||||
et := sliceRv.Type().Elem()
|
|
||||||
sv := RealValue(reflect.ValueOf(source))
|
|
||||||
switch sv.Kind() {
|
|
||||||
case reflect.Map:
|
|
||||||
keys := sv.MapKeys()
|
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
|
||||||
return String(keys[i].Interface()) < String(keys[j].Interface())
|
|
||||||
})
|
|
||||||
for _, key := range keys {
|
|
||||||
sliceRv.Set(reflect.Append(sliceRv, reflectCast(key.Interface(), et)))
|
|
||||||
sliceRv.Set(reflect.Append(sliceRv, reflectCast(sv.MapIndex(key).Interface(), et)))
|
|
||||||
}
|
|
||||||
case reflect.Slice, reflect.Array:
|
|
||||||
for i := 0; i < sv.Len(); i++ {
|
|
||||||
sliceRv.Set(reflect.Append(sliceRv, reflectCast(sv.Index(i).Interface(), et)))
|
|
||||||
}
|
|
||||||
case reflect.Invalid:
|
|
||||||
// Nil source, do nothing
|
|
||||||
default:
|
|
||||||
sliceRv.Set(reflect.Append(sliceRv, reflectCast(source, et)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func reflectCast(value any, t reflect.Type) reflect.Value {
|
|
||||||
return As(reflectCastE(value, t))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 补充缺失的 Key 转换工具
|
// 补充缺失的 Key 转换工具
|
||||||
|
|||||||
@ -30,17 +30,17 @@ func TestToMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestToSlice(t *testing.T) {
|
func TestToSlice(t *testing.T) {
|
||||||
// Test primitive-to-slice (append)
|
// Test primitive-to-slice (overwrite)
|
||||||
var s1 []int
|
var s1 []int
|
||||||
cast.FillSlice(&s1, "123")
|
cast.FillSlice(&s1, "123")
|
||||||
if len(s1) != 1 || s1[0] != 123 {
|
if len(s1) != 1 || s1[0] != 123 {
|
||||||
t.Errorf("Primitive-to-slice failed: %v", s1)
|
t.Errorf("Primitive-to-slice failed: %v", s1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test append slice
|
// Test slice-to-slice (overwrite)
|
||||||
cast.FillSlice(&s1, []string{"456"})
|
cast.FillSlice(&s1, []string{"456"})
|
||||||
if len(s1) != 2 || s1[1] != 456 {
|
if len(s1) != 1 || s1[0] != 456 {
|
||||||
t.Errorf("Append slice to slice failed: %v", s1)
|
t.Errorf("Slice-to-slice overwrite failed: %v", s1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
200
convert_compat_test.go
Normal file
200
convert_compat_test.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package cast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Matrix Test Entry
|
||||||
|
func TestConvertMatrix(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
from any
|
||||||
|
to any // 传入指针的指针,用于接收结果
|
||||||
|
expected any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 基础类型全互转矩阵
|
||||||
|
cases := []testCase{
|
||||||
|
{name: "int to string", from: 123, to: new(string), expected: "123"},
|
||||||
|
{name: "string to int", from: "456", to: new(int), expected: 456},
|
||||||
|
{name: "float to int", from: 123.45, to: new(int), expected: 123},
|
||||||
|
{name: "bool to string", from: true, to: new(string), expected: "true"},
|
||||||
|
{name: "string to bool", from: "1", to: new(bool), expected: true},
|
||||||
|
{name: "string to bool (text)", from: "true", to: new(bool), expected: true},
|
||||||
|
|
||||||
|
// 2. 容器与单值的自动包装/解包 (去摩擦)
|
||||||
|
{name: "single to slice", from: 100, to: new([]int), expected: []int{100}},
|
||||||
|
{name: "slice to single (len 1)", from: []int{200}, to: new(int), expected: 200},
|
||||||
|
{name: "slice to single (len >1, take first)", from: []int{300, 400}, to: new(int), expected: 300},
|
||||||
|
|
||||||
|
// 3. 字符串与切片的智能转换
|
||||||
|
{name: "csv string to slice", from: "1, 2, 3", to: new([]int), expected: []int{1, 2, 3}},
|
||||||
|
{name: "json string to slice", from: `[4, 5, 6]`, to: new([]int), expected: []int{4, 5, 6}},
|
||||||
|
|
||||||
|
// 4. 指针转换 (深度穿透)
|
||||||
|
{name: "deep pointer to val", from: ptr(ptr(789)), to: new(int), expected: 789},
|
||||||
|
{name: "val to pointer", from: 999, to: new(*int), expected: ptr(999)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cast.Convert(tc.to, tc.from)
|
||||||
|
// 获取 to 指针指向的实际值
|
||||||
|
actual := reflect.ValueOf(tc.to).Elem().Interface()
|
||||||
|
if !reflect.DeepEqual(actual, tc.expected) {
|
||||||
|
t.Errorf("[%s] Failed: from(%v) expected(%v) but got(%v)", tc.name, tc.from, tc.expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvert_SelfAssignment(t *testing.T) {
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
u := &User{Name: "Tom"}
|
||||||
|
// 应命中自我转换保护,不报错且不改变状态
|
||||||
|
cast.Convert(u, u)
|
||||||
|
if u.Name != "Tom" {
|
||||||
|
t.Error("Self-assignment guard failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 复杂映射与去摩擦 Key 匹配测试
|
||||||
|
func TestComplexFrictionlessMapping(t *testing.T) {
|
||||||
|
// 场景:Map 键名非常混乱,Struct 嵌套,目标存在旧数据
|
||||||
|
type Sub struct {
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
type User struct {
|
||||||
|
UserID int
|
||||||
|
UserName string
|
||||||
|
SubInfo Sub
|
||||||
|
}
|
||||||
|
|
||||||
|
from := map[string]any{
|
||||||
|
"user_id": 1001, // 下划线
|
||||||
|
"USER-NAME": "Andy", // 中划线+大写
|
||||||
|
"subinfo": map[string]any{ // 嵌套+全小写
|
||||||
|
"age": "18", // 类型不一致 (string -> int)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var to User
|
||||||
|
to.UserID = 999 // 预设旧数据,验证是否被正确覆盖
|
||||||
|
cast.Convert(&to, from)
|
||||||
|
|
||||||
|
if to.UserID != 1001 {
|
||||||
|
t.Errorf("UserID match failed: %d", to.UserID)
|
||||||
|
}
|
||||||
|
if to.UserName != "Andy" {
|
||||||
|
t.Errorf("UserName match failed: %s", to.UserName)
|
||||||
|
}
|
||||||
|
if to.SubInfo.Age != 18 {
|
||||||
|
t.Errorf("SubInfo.Age match failed: %d", to.SubInfo.Age)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 函数转换测试 (Func to Func)
|
||||||
|
func TestFuncConversion(t *testing.T) {
|
||||||
|
// 源函数:接收 int, string,返回 int, string
|
||||||
|
f1 := func(a int, b string) (int, string) {
|
||||||
|
return a + 1, b + "!"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标函数:意图是接收 string, any,返回 string, any
|
||||||
|
var f2 func(string, any) (string, any)
|
||||||
|
cast.Convert(&f2, f1)
|
||||||
|
|
||||||
|
if f2 == nil {
|
||||||
|
t.Fatal("Converted function is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
r1, r2 := f2("10", "hello")
|
||||||
|
if r1 != "11" || r2 != "hello!" {
|
||||||
|
t.Errorf("Func conversion failed: r1=%v, r2=%v", r1, r2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 命名风格模糊匹配测试
|
||||||
|
func TestFuzzyNamingMapping(t *testing.T) {
|
||||||
|
type TestStruct struct {
|
||||||
|
JSONTag string
|
||||||
|
UserID int
|
||||||
|
UserName string
|
||||||
|
IsActiveFlag bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试用例:各种不规范输入到标准结构体的映射
|
||||||
|
from := map[string]any{
|
||||||
|
"jsontag": "val1", // 小写
|
||||||
|
"JSONTag": "val2", // 原名
|
||||||
|
"JSON_TAG": "val3", // 下划线大写
|
||||||
|
"user_id": 123, // 下划线小写
|
||||||
|
"USERID": 456, // 全大写
|
||||||
|
"userName": "Andy", // 首字母小写
|
||||||
|
"user-name": "Bob", // 中划线
|
||||||
|
"is_active_flag": "true", // 模糊匹配 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var to TestStruct
|
||||||
|
cast.Convert(&to, from)
|
||||||
|
|
||||||
|
if to.UserID == 0 {
|
||||||
|
t.Errorf("UserID should be matched and set")
|
||||||
|
}
|
||||||
|
if to.UserName == "" {
|
||||||
|
t.Errorf("UserName should be matched and set")
|
||||||
|
}
|
||||||
|
if !to.IsActiveFlag {
|
||||||
|
t.Errorf("IsActiveFlag should be set to true")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Conversion result: %+v", to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 异常防御测试 (Panic Prevention)
|
||||||
|
func TestPanicPrevention(t *testing.T) {
|
||||||
|
t.Run("nil destination", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("Panic occurred with nil destination: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cast.Convert(nil, "data")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil pointer destination", func(t *testing.T) {
|
||||||
|
// nil 指针目标应该是可以接受的,库应当忽略或优雅处理,不应 panic
|
||||||
|
var p *int = nil
|
||||||
|
cast.Convert(&p, 123) // 这里传入的是指针的指针
|
||||||
|
if p == nil || *p != 123 {
|
||||||
|
t.Errorf("Expected p to be initialized and set to 123, got %v", p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read-only destination", func(t *testing.T) {
|
||||||
|
const i = 1
|
||||||
|
cast.Convert(i, 123)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
func ptr[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
// --- 性能测试 ---
|
||||||
|
|
||||||
|
func BenchmarkCastMatrixConvert(b *testing.B) {
|
||||||
|
from := map[string]any{"id": "123", "name": "test"}
|
||||||
|
type Target struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
var to Target
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
cast.Convert(&to, from)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
@ -24,7 +23,7 @@ type decoderFieldDescriptor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type decoderStructDescriptor struct {
|
type decoderStructDescriptor struct {
|
||||||
exactMatches map[string]int
|
exactMatches map[string]decoderFieldDescriptor
|
||||||
fields []decoderFieldDescriptor
|
fields []decoderFieldDescriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,24 +77,28 @@ func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) err
|
|||||||
|
|
||||||
// 处理 time.Time
|
// 处理 time.Time
|
||||||
if reflectValue.Type() == timeType {
|
if reflectValue.Type() == timeType {
|
||||||
|
var v any
|
||||||
if char == '"' {
|
if char == '"' {
|
||||||
str, err := d.parseString()
|
str, err := d.parseString()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if timeFormat == "" {
|
v = str
|
||||||
timeFormat = "2006-01-02 15:04:05.000"
|
} else if char == 'n' {
|
||||||
|
if string(d.data[d.pos:min(d.pos+4, len(d.data))]) == "null" {
|
||||||
|
d.pos += 4
|
||||||
|
reflectValue.Set(reflect.Zero(reflectValue.Type()))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
t, err := time.ParseInLocation(timeFormat, str, time.Local)
|
} else {
|
||||||
|
literal, err := d.parseLiteral()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 尝试其他常见格式
|
return err
|
||||||
if t, err = time.Parse(time.RFC3339, str); err != nil {
|
|
||||||
t = time.Time{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
reflectValue.Set(reflect.ValueOf(t))
|
v = literal
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
reflectValue.Set(reflect.ValueOf(ToTime(v, timeFormat)))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch char {
|
switch char {
|
||||||
@ -392,7 +395,7 @@ func getDecoderFieldMap(reflectType reflect.Type) *decoderStructDescriptor {
|
|||||||
return val.(*decoderStructDescriptor)
|
return val.(*decoderStructDescriptor)
|
||||||
}
|
}
|
||||||
descriptor := &decoderStructDescriptor{
|
descriptor := &decoderStructDescriptor{
|
||||||
exactMatches: make(map[string]int),
|
exactMatches: make(map[string]decoderFieldDescriptor),
|
||||||
}
|
}
|
||||||
for index := 0; index < reflectType.NumField(); index++ {
|
for index := 0; index < reflectType.NumField(); index++ {
|
||||||
field := reflectType.Field(index)
|
field := reflectType.Field(index)
|
||||||
@ -414,18 +417,18 @@ func getDecoderFieldMap(reflectType reflect.Type) *decoderStructDescriptor {
|
|||||||
if tag != "" && tag != "-" {
|
if tag != "" && tag != "-" {
|
||||||
parts := strings.Split(tag, ",")
|
parts := strings.Split(tag, ",")
|
||||||
tagName := parts[0]
|
tagName := parts[0]
|
||||||
if tagName != "" {
|
|
||||||
descriptor.exactMatches[tagName] = index
|
|
||||||
}
|
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if strings.HasPrefix(part, "format=") {
|
if strings.HasPrefix(part, "format=") {
|
||||||
fieldDesc.timeFormat = strings.TrimPrefix(part, "format=")
|
fieldDesc.timeFormat = strings.TrimPrefix(part, "format=")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if tagName != "" {
|
||||||
|
descriptor.exactMatches[tagName] = fieldDesc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 原名
|
// 2. 原名
|
||||||
descriptor.exactMatches[field.Name] = index
|
descriptor.exactMatches[field.Name] = fieldDesc
|
||||||
|
|
||||||
// 3. 归一化名
|
// 3. 归一化名
|
||||||
fieldDesc.normalized = normalizeKey(field.Name)
|
fieldDesc.normalized = normalizeKey(field.Name)
|
||||||
@ -437,14 +440,8 @@ func getDecoderFieldMap(reflectType reflect.Type) *decoderStructDescriptor {
|
|||||||
|
|
||||||
func matchField(key string, descriptor *decoderStructDescriptor) (int, bool, string, bool) {
|
func matchField(key string, descriptor *decoderStructDescriptor) (int, bool, string, bool) {
|
||||||
// 1. 精确匹配
|
// 1. 精确匹配
|
||||||
if index, ok := descriptor.exactMatches[key]; ok {
|
if f, ok := descriptor.exactMatches[key]; ok {
|
||||||
// 找到字段后还需要获取其 timeFormat 信息
|
return f.index, f.isTime, f.timeFormat, true
|
||||||
for _, f := range descriptor.fields {
|
|
||||||
if f.index == index {
|
|
||||||
return index, f.isTime, f.timeFormat, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return index, false, "", true
|
|
||||||
}
|
}
|
||||||
// 2. 归一化匹配 (忽略大小写、下划线等) - 零分配比对
|
// 2. 归一化匹配 (忽略大小写、下划线等) - 零分配比对
|
||||||
for _, f := range descriptor.fields {
|
for _, f := range descriptor.fields {
|
||||||
|
|||||||
@ -17,7 +17,6 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
encoderStructCache sync.Map
|
encoderStructCache sync.Map
|
||||||
timeType = reflect.TypeOf(time.Time{})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type encoderFieldDescriptor struct {
|
type encoderFieldDescriptor struct {
|
||||||
@ -99,8 +98,11 @@ func (encoder *fastEncoder) encodeValue(reflectValue reflect.Value, path string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (encoder *fastEncoder) writeTime(t time.Time, format string) {
|
func (encoder *fastEncoder) writeTime(t time.Time, format string) {
|
||||||
|
if format == "" {
|
||||||
|
format = "2006-01-02 15:04:05.000"
|
||||||
|
}
|
||||||
encoder.buffer.WriteByte('"')
|
encoder.buffer.WriteByte('"')
|
||||||
encoder.buffer.WriteString(t.Format(format))
|
encoder.buffer.WriteString(t.In(DefaultTimeZone.loc).Format(format))
|
||||||
encoder.buffer.WriteByte('"')
|
encoder.buffer.WriteByte('"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
345
time.go
Normal file
345
time.go
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
str := strings.TrimSpace(String(v))
|
||||||
|
if str == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tm time.Time
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// 1. 处理纯数字 (时间戳或紧凑格式)
|
||||||
|
num := Int64(v)
|
||||||
|
if 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 ""
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) }
|
||||||
|
|
||||||
|
// Now 获取当前时间
|
||||||
|
func Now() time.Time { return DefaultTimeZone.Now() }
|
||||||
45
time_bench_bench_test.go
Normal file
45
time_bench_bench_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package cast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkParseTime_RFC3339(b *testing.B) {
|
||||||
|
s := "2023-05-04T12:34:56Z"
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = cast.ParseTime(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParseTime_Standard(b *testing.B) {
|
||||||
|
s := "2023-05-04 12:34:56"
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = cast.ParseTime(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParseTime_Numeric(b *testing.B) {
|
||||||
|
s := "20230504123456"
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = cast.ParseTime(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkToJSON_WithTime(b *testing.B) {
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
u := User{ID: 1, Name: "Benchmark User", CreatedAt: time.Now()}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = cast.ToJSON(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
time_test.go
Normal file
68
time_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package cast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseTime(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input any
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"2023-05-04 12:34:56", "2023-05-04 12:34:56"},
|
||||||
|
{"2023/05/04 12:34:56", "2023-05-04 12:34:56"},
|
||||||
|
{"2023.05.04 12:34:56", "2023-05-04 12:34:56"},
|
||||||
|
{"20230504123456", "2023-05-04 12:34:56"},
|
||||||
|
{1683196496, "2023-05-04 10:34:56"}, // Unix timestamp (Example)
|
||||||
|
{"2023年5月4日 12时34分56秒", "2023-05-04 12:34:56"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set UTC for test stability if needed, or just compare formatted strings
|
||||||
|
loc, _ := time.LoadLocation("UTC")
|
||||||
|
cast.SetDefaultTimeZone(loc)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tm := cast.ParseTime(tt.input)
|
||||||
|
// We use a relative check for timestamp since it depends on the timezone if not careful
|
||||||
|
if tt.input == 1683196496 {
|
||||||
|
// 1683196496 is 2023-05-04 10:34:56 UTC
|
||||||
|
if tm.Format("2006-01-02 15:04:05") != tt.expected {
|
||||||
|
t.Errorf("ParseTime(%v) = %v, want %v", tt.input, tm.Format("2006-01-02 15:04:05"), tt.expected)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tm.Format("2006-01-02 15:04:05") != tt.expected {
|
||||||
|
t.Errorf("ParseTime(%v) = %v, want %v", tt.input, tm.Format("2006-01-02 15:04:05"), tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatTime(t *testing.T) {
|
||||||
|
tm := time.Date(2023, 5, 4, 12, 34, 56, 0, time.UTC)
|
||||||
|
formatted := cast.FormatTime("YYYY-MM-DD HH:mm:ss", tm)
|
||||||
|
if formatted != "2023-05-04 12:34:56" {
|
||||||
|
t.Errorf("FormatTime failed, got %s", formatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddTime(t *testing.T) {
|
||||||
|
tm := time.Date(2023, 5, 4, 12, 34, 56, 0, time.UTC)
|
||||||
|
added := cast.AddTime("+1Y+1M+1D", tm)
|
||||||
|
expected := time.Date(2024, 6, 5, 12, 34, 56, 0, time.UTC)
|
||||||
|
if !added.Equal(expected) {
|
||||||
|
t.Errorf("AddTime failed, got %v, want %v", added, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeZoneNow(t *testing.T) {
|
||||||
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
|
tz := cast.NewTimeZone(loc)
|
||||||
|
now := tz.Now()
|
||||||
|
if now.Location().String() != "Asia/Shanghai" {
|
||||||
|
t.Errorf("TimeZone.Now() failed to set location, got %s", now.Location().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user