Compare commits
No commits in common. "main" and "v1.2.3" have entirely different histories.
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,5 +1 @@
|
||||
.gemini/
|
||||
.ai/
|
||||
.geminiignore
|
||||
.gemini
|
||||
/CODE-FULL.md
|
||||
go.sum
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,27 +1,11 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [v1.2.9] - 2026-05-09
|
||||
### Changed
|
||||
- **移除第三方依赖**: 移除了对 `jsontag` 模块的依赖,统一使用标准库及自有基础设施对齐,增强了模块的独立性与长期稳定性。
|
||||
|
||||
## [v1.2.8] - 2026-05-05
|
||||
### Fixed
|
||||
- **JSON 解码器深度增强**: 修复了 `cast.UnmarshalJSON` 在反序列化到 `interface{}` 类型字段时会跳过对象和数组的重大缺陷。
|
||||
- **高精度整数解析**: 优化了 JSON 解码过程中的数字处理逻辑。当目标为 `interface{}` 时,优先将整数解析为 `int64` 而非 `float64`,彻底解决了纳秒级时间戳(Nanosecond Timestamp)和 64 位长 ID 的精度丢失问题。
|
||||
- **接口兼容性**: 完善了对 `interface{}` 类型的自动类型识别,支持正确解析为 `map[string]any` 和 `[]any`。
|
||||
|
||||
## [v1.2.6] - 2026-05-04
|
||||
### Fixed
|
||||
- **Map 深度合并修复**: 修复了在 `Convert` 或 `ToMap` 过程中,如果目标 Map 已存在该 Key,其原有结构体/Map 值会被直接覆盖而非深度合并的问题。通过引入 `dst.MapIndex` 预读取与临时寻址变量,现已完美支持 Map 下非指针结构体的局部字段覆盖。
|
||||
|
||||
### Added
|
||||
- **深度合并测试集**: 新增 `merge_test.go` 与 `complex_conversion_test.go`,覆盖了 Struct-to-Map, Map-to-Map, Slice-to-Map 等多种深度嵌套合并场景,确保配置覆盖逻辑的健壮性。
|
||||
|
||||
## [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` 时区。
|
||||
|
||||
21
README.md
21
README.md
@ -7,9 +7,9 @@
|
||||
|
||||
`@go/cast` 是一个为“极致敏捷”设计的 Go 基础工具库。其核心目标是**彻底消除摩擦**:
|
||||
|
||||
* **万能零摩擦入口**:`To[T]、Convert` 作为核心 API,永不返回 `error`。在失败或非法转换时静默返回类型零值。
|
||||
* **万能零摩擦入口**:`To[T]` 作为核心 API,永不返回 `error`。在失败或非法转换时静默返回类型零值。
|
||||
* **语义化 As 包装**:提供 `As` 函数用于将传统“值+错误”双返回结果一键转化为单值,消除外部库带来的摩擦。
|
||||
* **智能自动穿透**:`To[T]、Convert` 自动识别 JSON 文本、复杂容器映射、指针穿透等场景。
|
||||
* **智能自动穿透**:`To[T]` 自动识别 JSON 文本、复杂容器映射、指针穿透等场景。
|
||||
* **极致性能 JSON**:内置高性能 `fastjson` 引擎,支持 Struct 元数据缓存、`time.Time` 原生处理、零分配 Key 匹配及批量转义优化。
|
||||
|
||||
## 📦 安装
|
||||
@ -77,27 +77,16 @@ list, _ := cast.ToSlice[int]([]string{"1", "2", "3"})
|
||||
* `Ptr[T any](T) *T` —— 快速取指针。
|
||||
* `ArrayToBoolMap[T comparable]([]T) map[T]bool` —— 快速构建索引 Map。
|
||||
|
||||
6. **基础转换与时间 (直接调用,极致性能)**
|
||||
6. **基础转换 (直接调用,极致性能)**
|
||||
* `Int`, `Int64`, `Uint`, `Uint64`, `Float`, `Float64`, `String`, `Bool`, `Duration`
|
||||
* `RealValue(reflect.Value) reflect.Value` —— 递归获取指针或接口下的真实值。
|
||||
* `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)。
|
||||
* `DescribeDuration(time.Duration) string` —— 将时长转化为自然语言描述(如 1h 5m)。
|
||||
|
||||
7. **字符串与参数处理**
|
||||
* `Split(s, sep string) []string` —— 增强型分割,自动 Trim 并过滤空字符串。
|
||||
* `SplitArgs(string) []string` —— 命令行参数分割,支持引号与转义。
|
||||
* `JoinArgs([]string, sep string) string` —— 命令行参数合并,自动处理引号与转义。
|
||||
* `UniqueAppend(to []string, from ...any) []string` —— 去重追加。
|
||||
* `GetLowerName(string) string` | `GetUpperName(string) string` —— 首字母大小写转换工具。
|
||||
* `FixUpperCase([]byte, []string)` —— 针对 JSON Key 的特殊大小写修复工具。
|
||||
|
||||
8. **时区支持**
|
||||
7. **时区支持**
|
||||
* `DefaultTimeZone` —— 全局默认时区上下文。
|
||||
* `SetDefaultTimeZone(*time.Location)` —— 修改全局默认时区(影响所有 Convert 与 ToTime 操作)。
|
||||
* `DefaultTimeZone.Now()` —— 获取时区上下文下的当前时间。
|
||||
* `NewTimeZone(*time.Location)` —— 创建一个时区上下文,支持 Parse、Format、Add、Now 等操作。
|
||||
* `TimeZone.Now()` —— 获取时区上下文下的当前时间。
|
||||
|
||||
## 🧪 验证状态
|
||||
测试全部通过,性能达标。
|
||||
|
||||
9
TEST.md
9
TEST.md
@ -14,7 +14,6 @@
|
||||
- **零分配摩擦匹配**: `normalizeEqual` 算法实现 0 内存分配的归一化 Key 匹配,支持 UTF-8。
|
||||
- **智能 Slice 扩容**: 尊重预设 Capacity,减少反序列化时的内存重分配。
|
||||
- **FastDecoder**: 实现单路径流式解析,支持嵌套指针、Slice 和 Map 的智能初始化。
|
||||
- **深度合并支持**: 支持在 `Convert` 过程中对 Map 和 Struct 进行深度递归合并。即使目标 Map 存储的是非指针结构体,也能通过反射寻址实现局部字段覆盖,而不丢失未提及的默认值。
|
||||
- **指针与接口**: `RealValue` 处理多级指针与接口解包。
|
||||
- **实用工具**: `UniqueAppend` ($O(n)$ 去重),`If` (泛型三元),`SplitArgs` (支持引用格式)。
|
||||
|
||||
@ -24,11 +23,9 @@
|
||||
- `ToJSON (SimpleStruct)`: **~297 ns/op** (96 B/op) - 相比 v1.2.0 提升约 50%
|
||||
- `ToJSON (Map - No Sort)`: **~649 ns/op** (152 B/op) - 移除排序后性能大幅提升
|
||||
- `ToJSON (String Escaping)`: **~172 ns/op** (64 B/op) - 批量转义优化成果
|
||||
- `ToJSON (Time Support)`: **~522 ns/op** (232 B/op) - 原生 time.Time 处理
|
||||
- `ParseTime (RFC3339)`: **~143 ns/op** (72 B/op) - 极致优化解析
|
||||
- `ParseTime (Numeric String)`: **~188 ns/op** (0 B/op) - 紧凑格式零分配
|
||||
- `ParseTime (Native Number)`: **~7 ns/op** (0 B/op) - 原生数字时间戳 0 分配 & 极致性能
|
||||
- `FormatTime`: **~628 ns/op** (120 B/op) - 使用 Replacer 性能倍增
|
||||
- `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 匹配
|
||||
- `ToMap`: ~816 ns/op (含 Struct 拍平与类型转换)
|
||||
- `ToSlice`: ~1819 ns/op
|
||||
|
||||
@ -66,8 +66,8 @@ func BenchmarkString(b *testing.B) {
|
||||
|
||||
func BenchmarkJSON(b *testing.B) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
u := User{ID: 1, Name: "Benchmark User"}
|
||||
for i := 0; i < b.N; i++ {
|
||||
@ -140,8 +140,8 @@ func BenchmarkString_FastPath(b *testing.B) {
|
||||
// 4. 测试 ToJSON 性能 (对比自定义逻辑与标准库)
|
||||
func BenchmarkToJSON_SimpleStruct(b *testing.B) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
u := User{ID: 1, Name: "Benchmark User"}
|
||||
b.ResetTimer()
|
||||
|
||||
62
cast.go
62
cast.go
@ -342,30 +342,13 @@ 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)
|
||||
|
||||
// 1. 创建一个【可寻址】的临时变量
|
||||
newVal := reflect.New(vt).Elem()
|
||||
|
||||
// 2. 尝试从目标 Map 中获取已经存在的老值
|
||||
existingVal := dst.MapIndex(newKey)
|
||||
if existingVal.IsValid() {
|
||||
// 如果老值存在,把它拷贝到我们刚才创建的临时变量里作“底本”
|
||||
// 这样就能保留原本没被覆盖的字段
|
||||
newVal.Set(existingVal)
|
||||
} else {
|
||||
// 如果老值不存在,就用零值兜底
|
||||
newVal.Set(reflect.Zero(vt))
|
||||
}
|
||||
|
||||
// 3. 对这个可寻址的临时变量进行深度合并
|
||||
newVal.Set(reflect.Zero(vt))
|
||||
performRecursiveTo(iter.Value(), newVal)
|
||||
|
||||
// 4. 将合并后的全新值,重新塞回目标 Map
|
||||
dst.SetMapIndex(newKey, newVal)
|
||||
}
|
||||
}
|
||||
@ -374,7 +357,7 @@ 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)
|
||||
@ -387,22 +370,8 @@ func recursiveStructToMap(src, dst reflect.Value) {
|
||||
}
|
||||
newKey.Set(reflect.Zero(kt))
|
||||
performRecursiveTo(reflect.ValueOf(GetLowerName(field.Name)), newKey)
|
||||
|
||||
// 1. 创建一个【可寻址】的临时变量
|
||||
newVal := reflect.New(vt).Elem()
|
||||
|
||||
// 2. 尝试获取老值
|
||||
existingVal := dst.MapIndex(newKey)
|
||||
if existingVal.IsValid() {
|
||||
newVal.Set(existingVal)
|
||||
} else {
|
||||
newVal.Set(reflect.Zero(vt))
|
||||
}
|
||||
|
||||
// 3. 合并
|
||||
newVal.Set(reflect.Zero(vt))
|
||||
performRecursiveTo(src.Field(i), newVal)
|
||||
|
||||
// 4. 塞回
|
||||
dst.SetMapIndex(newKey, newVal)
|
||||
}
|
||||
}
|
||||
@ -411,18 +380,11 @@ 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 := reflect.New(vt).Elem()
|
||||
existingVal := dst.MapIndex(newKey)
|
||||
if existingVal.IsValid() {
|
||||
newVal.Set(existingVal)
|
||||
} else {
|
||||
newVal.Set(reflect.Zero(vt))
|
||||
}
|
||||
|
||||
newVal.Set(reflect.Zero(vt))
|
||||
if i+1 < src.Len() {
|
||||
performRecursiveTo(src.Index(i+1), newVal)
|
||||
}
|
||||
@ -527,12 +489,6 @@ func isComplexValue(v any) bool {
|
||||
}
|
||||
|
||||
func parseInt(s string) int64 {
|
||||
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
||||
i, err := strconv.ParseInt(s, 0, 64)
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
i, err := strconv.ParseInt(s, 10, 64)
|
||||
if err == nil {
|
||||
return i
|
||||
@ -544,12 +500,6 @@ func parseInt(s string) int64 {
|
||||
}
|
||||
|
||||
func parseUint(s string) uint64 {
|
||||
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
||||
i, err := strconv.ParseUint(s, 0, 64)
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
i, err := strconv.ParseUint(s, 10, 64)
|
||||
if err == nil {
|
||||
return i
|
||||
|
||||
@ -55,7 +55,7 @@ func TestJSONToStruct(t *testing.T) {
|
||||
func TestSpecialJSON(t *testing.T) {
|
||||
// 关键测试:特殊 HTML 字符序列化不应被转义
|
||||
type Content struct {
|
||||
Text string
|
||||
Text string `json:"text"`
|
||||
}
|
||||
c := Content{Text: "<a> & <b>"}
|
||||
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
package cast_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"apigo.cc/go/cast"
|
||||
)
|
||||
|
||||
type SubConfig struct {
|
||||
Level int
|
||||
Tag string
|
||||
}
|
||||
|
||||
type MainConfig struct {
|
||||
Name string
|
||||
Sub SubConfig
|
||||
Items []string
|
||||
Options map[string]int
|
||||
}
|
||||
|
||||
func TestDeepMergeComplex(t *testing.T) {
|
||||
dst := MainConfig{
|
||||
Name: "Base",
|
||||
Sub: SubConfig{
|
||||
Level: 1,
|
||||
Tag: "original",
|
||||
},
|
||||
Items: []string{"a", "b"},
|
||||
Options: map[string]int{
|
||||
"debug": 1,
|
||||
"trace": 0,
|
||||
},
|
||||
}
|
||||
|
||||
src := map[string]any{
|
||||
"Sub": map[string]any{
|
||||
"Level": 2,
|
||||
},
|
||||
"Options": map[string]any{
|
||||
"trace": 1,
|
||||
"new": 100,
|
||||
},
|
||||
}
|
||||
|
||||
cast.Convert(&dst, src)
|
||||
|
||||
if dst.Name != "Base" {
|
||||
t.Errorf("Expected Name Base, got %s", dst.Name)
|
||||
}
|
||||
if dst.Sub.Level != 2 {
|
||||
t.Errorf("Expected Sub.Level 2, got %d", dst.Sub.Level)
|
||||
}
|
||||
if dst.Sub.Tag != "original" {
|
||||
t.Errorf("Expected Sub.Tag original, got %s", dst.Sub.Tag)
|
||||
}
|
||||
if len(dst.Items) != 2 {
|
||||
t.Errorf("Expected Items length 2, got %d", len(dst.Items))
|
||||
}
|
||||
if dst.Options["debug"] != 1 {
|
||||
t.Errorf("Expected Options.debug 1, got %d", dst.Options["debug"])
|
||||
}
|
||||
if dst.Options["trace"] != 1 {
|
||||
t.Errorf("Expected Options.trace 1, got %d", dst.Options["trace"])
|
||||
}
|
||||
if dst.Options["new"] != 100 {
|
||||
t.Errorf("Expected Options.new 100, got %d", dst.Options["new"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapToMapMergeComplex(t *testing.T) {
|
||||
dst := map[string]MainConfig{
|
||||
"c1": {
|
||||
Name: "Config1",
|
||||
Sub: SubConfig{Level: 10},
|
||||
},
|
||||
}
|
||||
|
||||
src := map[string]any{
|
||||
"c1": map[string]any{
|
||||
"Sub": map[string]any{
|
||||
"Tag": "updated",
|
||||
},
|
||||
},
|
||||
"c2": map[string]any{
|
||||
"Name": "Config2",
|
||||
},
|
||||
}
|
||||
|
||||
cast.Convert(&dst, src)
|
||||
|
||||
if dst["c1"].Name != "Config1" {
|
||||
t.Errorf("Expected c1.Name Config1, got %s", dst["c1"].Name)
|
||||
}
|
||||
if dst["c1"].Sub.Level != 10 {
|
||||
t.Errorf("Expected c1.Sub.Level 10, got %d", dst["c1"].Sub.Level)
|
||||
}
|
||||
if dst["c1"].Sub.Tag != "updated" {
|
||||
t.Errorf("Expected c1.Sub.Tag updated, got %s", dst["c1"].Sub.Tag)
|
||||
}
|
||||
if dst["c2"].Name != "Config2" {
|
||||
t.Errorf("Expected c2.Name Config2, got %s", dst["c2"].Name)
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,7 @@ func TestToSlice(t *testing.T) {
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
Port int `json:"port"`
|
||||
}
|
||||
data := `{"port": 8080}`
|
||||
|
||||
|
||||
@ -103,24 +103,8 @@ func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) err
|
||||
|
||||
switch char {
|
||||
case '{':
|
||||
if reflectValue.Kind() == reflect.Interface {
|
||||
m := make(map[string]any)
|
||||
if err := d.decodeObject(reflect.ValueOf(&m).Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
reflectValue.Set(reflect.ValueOf(m))
|
||||
return nil
|
||||
}
|
||||
return d.decodeObject(reflectValue)
|
||||
case '[':
|
||||
if reflectValue.Kind() == reflect.Interface {
|
||||
var s []any
|
||||
if err := d.decodeArray(reflect.ValueOf(&s).Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
reflectValue.Set(reflect.ValueOf(s))
|
||||
return nil
|
||||
}
|
||||
return d.decodeArray(reflectValue)
|
||||
case '"':
|
||||
str, err := d.parseString()
|
||||
@ -139,8 +123,6 @@ func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) err
|
||||
reflectValue.SetFloat(Float64(str))
|
||||
case reflect.Bool:
|
||||
reflectValue.SetBool(Bool(str))
|
||||
case reflect.Interface:
|
||||
reflectValue.Set(reflect.ValueOf(str))
|
||||
default:
|
||||
// 尝试将字符串解析为具体对象(比如内部又是 JSON)
|
||||
if strings.HasPrefix(str, "{") || strings.HasPrefix(str, "[") {
|
||||
@ -166,20 +148,7 @@ func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) err
|
||||
case reflect.String:
|
||||
reflectValue.SetString(literal)
|
||||
case reflect.Interface:
|
||||
// 优先作为数字处理以保留精度 (int64)
|
||||
if literal == "true" {
|
||||
reflectValue.Set(reflect.ValueOf(true))
|
||||
} else if literal == "false" {
|
||||
reflectValue.Set(reflect.ValueOf(false))
|
||||
} else if literal == "null" {
|
||||
reflectValue.Set(reflect.Zero(reflectValue.Type()))
|
||||
} else if strings.Contains(literal, ".") || strings.Contains(literal, "e") || strings.Contains(literal, "E") {
|
||||
reflectValue.Set(reflect.ValueOf(Float64(literal)))
|
||||
} else if i, err := strconv.ParseInt(literal, 10, 64); err == nil {
|
||||
reflectValue.Set(reflect.ValueOf(i))
|
||||
} else {
|
||||
reflectValue.Set(reflect.ValueOf(literal))
|
||||
}
|
||||
reflectValue.Set(reflect.ValueOf(literal))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -445,10 +414,7 @@ func getDecoderFieldMap(reflectType reflect.Type) *decoderStructDescriptor {
|
||||
|
||||
// 1. Tag
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
if tag != "" {
|
||||
if tag != "" && tag != "-" {
|
||||
parts := strings.Split(tag, ",")
|
||||
tagName := parts[0]
|
||||
for _, part := range parts {
|
||||
|
||||
@ -217,12 +217,9 @@ func getEncoderStructDescriptor(reflectType reflect.Type) *encoderStructDescript
|
||||
}
|
||||
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
fieldDesc.keepKey = strings.Contains(string(field.Tag), "keepKey")
|
||||
|
||||
if tag != "" {
|
||||
if tag != "" && tag != "-" {
|
||||
parts := strings.Split(tag, ",")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "format=") {
|
||||
|
||||
119
merge_test.go
119
merge_test.go
@ -1,119 +0,0 @@
|
||||
package cast_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"apigo.cc/go/cast"
|
||||
)
|
||||
|
||||
type DBConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func TestMapStructMerge(t *testing.T) {
|
||||
// Initial configuration with default values
|
||||
dst := map[string]DBConfig{
|
||||
"mysql": {Host: "localhost", Port: 3306},
|
||||
}
|
||||
|
||||
// New data (e.g., from environment variables) that only overrides Host
|
||||
src := map[string]any{
|
||||
"mysql": map[string]any{
|
||||
"Host": "127.0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
// Perform conversion/merge
|
||||
cast.Convert(&dst, src)
|
||||
|
||||
// Verify results
|
||||
mysql, ok := dst["mysql"]
|
||||
if !ok {
|
||||
t.Fatal("mysql config not found")
|
||||
}
|
||||
|
||||
if mysql.Host != "127.0.0.1" {
|
||||
t.Errorf("Expected Host 127.0.0.1, got %s", mysql.Host)
|
||||
}
|
||||
|
||||
if mysql.Port != 3306 {
|
||||
t.Errorf("Expected Port 3306, got %d", mysql.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapStructPointerMerge(t *testing.T) {
|
||||
// Initial configuration with default values (using pointers)
|
||||
dst := map[string]*DBConfig{
|
||||
"mysql": {Host: "localhost", Port: 3306},
|
||||
}
|
||||
|
||||
// New data that only overrides Host
|
||||
src := map[string]any{
|
||||
"mysql": map[string]any{
|
||||
"Host": "127.0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
// Perform conversion/merge
|
||||
cast.Convert(&dst, src)
|
||||
|
||||
// Verify results
|
||||
mysql, ok := dst["mysql"]
|
||||
if !ok {
|
||||
t.Fatal("mysql config not found")
|
||||
}
|
||||
|
||||
if mysql.Host != "127.0.0.1" {
|
||||
t.Errorf("Expected Host 127.0.0.1, got %s", mysql.Host)
|
||||
}
|
||||
|
||||
if mysql.Port != 3306 {
|
||||
t.Errorf("Expected Port 3306, got %d", mysql.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceToMapMerge(t *testing.T) {
|
||||
dst := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
}
|
||||
src := []any{"b", 20, "c", 30}
|
||||
|
||||
cast.Convert(&dst, src)
|
||||
|
||||
if dst["a"] != 1 {
|
||||
t.Errorf("Expected a=1, got %d", dst["a"])
|
||||
}
|
||||
if dst["b"] != 20 {
|
||||
t.Errorf("Expected b=20, got %d", dst["b"])
|
||||
}
|
||||
if dst["c"] != 30 {
|
||||
t.Errorf("Expected c=30, got %d", dst["c"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructToMapMerge(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
dst := map[string]any{
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
}
|
||||
// Note: Struct fields are always "present", so Port:0 will overwrite port:3306
|
||||
src := Config{Host: "127.0.0.1", Port: 8080}
|
||||
|
||||
cast.Convert(&dst, src)
|
||||
|
||||
if dst["host"] != "127.0.0.1" {
|
||||
t.Errorf("Expected host 127.0.0.1, got %v", dst["host"])
|
||||
}
|
||||
if cast.Int(dst["port"]) != 8080 {
|
||||
t.Errorf("Expected port 8080, got %v", dst["port"])
|
||||
}
|
||||
if dst["user"] != "root" {
|
||||
t.Errorf("Expected user root, got %v", dst["user"])
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ package cast_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
)
|
||||
|
||||
@ -29,7 +28,7 @@ func TestTo(t *testing.T) {
|
||||
|
||||
// JSON Auto conversion (Struct to String)
|
||||
type User struct {
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
}
|
||||
u := User{Name: "Alice"}
|
||||
js := cast.To[string](u)
|
||||
|
||||
86
time.go
86
time.go
@ -31,17 +31,6 @@ func SetDefaultTimeZone(loc *time.Location) {
|
||||
}
|
||||
}
|
||||
|
||||
// 在包级别预先初始化 Replacer,0 额外分配
|
||||
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 的静默风格。
|
||||
@ -54,25 +43,6 @@ func (tz *TimeZone) ParseTime(v any) time.Time {
|
||||
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{}
|
||||
@ -81,8 +51,9 @@ func (tz *TimeZone) ParseTime(v any) time.Time {
|
||||
var tm time.Time
|
||||
var err error
|
||||
|
||||
// 3. 处理纯数字字符串 (紧凑格式或时间戳)
|
||||
if num, err := strconv.ParseInt(str, 10, 64); err == nil && num > 0 {
|
||||
// 1. 处理纯数字 (时间戳或紧凑格式)
|
||||
num := Int64(v)
|
||||
if num > 0 {
|
||||
// 紧凑格式解析
|
||||
switch len(str) {
|
||||
case 14: // 20060102150405
|
||||
@ -256,8 +227,23 @@ func (tz *TimeZone) FormatTime(layout string, v any) string {
|
||||
if tm.IsZero() {
|
||||
return ""
|
||||
}
|
||||
// 一次性流式替换,性能直接翻倍
|
||||
return tm.Format(timeFormatReplacer.Replace(layout))
|
||||
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 格式化时间。
|
||||
@ -355,37 +341,5 @@ func AddTime(expr string, v any) time.Time {
|
||||
// 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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user