diff --git a/tag.bat b/tag.bat index e61705a..fe32d0b 100644 --- a/tag.bat +++ b/tag.bat @@ -1,4 +1,4 @@ -git tag v1.1.23 +git tag v1.1.24 git tag bank_card/v1.1.5 git tag geoip/v1.1.5 diff --git a/timeutil/consts.go b/timeutil/consts.go index 19012cd..6598f6b 100644 --- a/timeutil/consts.go +++ b/timeutil/consts.go @@ -10,4 +10,52 @@ const ( DefaultTimeLocationName = "Asia/Shanghai" ) +// More predefined layouts for use in Time.Format and time.Parse. +const ( + DT14 = "20060102150405" + DT8 = "20060102" + DT8MDY = "01022006" + DT6 = "200601" + MonthDay = "1/2" + DIN5008FullDate = "02.01.2006" // German DIN 5008 standard + DIN5008Date = "02.01.06" + RFC3339FullDate = time.DateOnly + RFC3339Milli = "2006-01-02T15:04:05.999Z07:00" + RFC3339Dash = "2006-01-02T15-04-05Z07-00" + ISO8601 = "2006-01-02T15:04:05Z0700" + ISO8601TZHour = "2006-01-02T15:04:05Z07" + ISO8601NoTZ = "2006-01-02T15:04:05" + ISO8601MilliNoTZ = "2006-01-02T15:04:05.999" + ISO8601Milli = "2006-01-02T15:04:05.999Z0700" + ISO8601CompactZ = "20060102T150405Z0700" + ISO8601CompactNoTZ = "20060102T150405" + ISO8601YM = "2006-01" + ISO9075 = time.DateTime // ISO/IEC 9075 used by MySQL, BigQuery, etc. + ISO9075MicroTZ = "2006-01-02 15:04:05.999999-07" // ISO/IEC 9075 used by PostgreSQL + RFC5322 = "Mon, 2 Jan 2006 15:04:05 -0700" // RFC5322 = "Mon Jan 02 15:04:05 -0700 2006" + SQLTimestamp = ISO9075 + SQLTimestampMinutes = "2006-01-02 15:04" + Ruby = "2006-01-02 15:04:05 -0700" // Ruby Time.now.to_s + InsightlyAPIQuery = "_1/_2/2006 _3:04:05 PM" + DateMDY = "1/2/2006" // an underscore results in a space. + DateMDYSlash = "01/02/2006" + DateDMYDash = "_2-01-2006" // Jira XML Date format + DateDMYHM2 = "02:01:06 15:04" // GMT time in format dd:mm:yy hh:mm + DateYMD = RFC3339FullDate + DateTextUS = "January 2, 2006" + DateTextUSAbbr3 = "Jan 2, 2006" + DateTextEU = "2 January 2006" + DateTextEUAbbr3 = "2 Jan 2006" + MonthAbbrYear = "Jan 2006" + MonthYear = "January 2006" +) + +const ( + RFC3339Min = "0000-01-01T00:00:00Z" + RFC3339Max = "9999-12-31T23:59:59Z" + RFC3339Zero = "0001-01-01T00:00:00Z" // Golang zero value + RFC3339ZeroUnix = "1970-01-01T00:00:00Z" + RFC3339YMDZeroUnix = int64(-62135596800) +) + var ReferenceTimeValue time.Time = time.Date(2006, 1, 2, 15, 4, 5, 999999999, time.FixedZone("MST", -7*60*60)) diff --git a/timeutil/format.go b/timeutil/format.go index 60797f6..6ac458c 100644 --- a/timeutil/format.go +++ b/timeutil/format.go @@ -1,7 +1,9 @@ package timeutil import ( + "errors" "fmt" + "strconv" "strings" "time" ) @@ -43,3 +45,291 @@ func DurationHMS(d time.Duration) (int, int, int) { return int(h), int(m), int(s) } + +// Reformat a time string from one format to another +// Deprecated... +func FromTo(value, fromLayout, toLayout string) (string, error) { + t, err := time.Parse(fromLayout, strings.TrimSpace(value)) + if err != nil { + return "", err + } + return t.Format(toLayout), nil +} + +// Reformat a time string from one format to another +func FromTo2(fromLayout, toLayout, value string) (string, error) { + t, err := time.Parse(fromLayout, strings.TrimSpace(value)) + if err != nil { + return "", err + } + return t.Format(toLayout), nil +} + +func FromToFirstValueOrEmpty(fromLayout, toLayout string, values []string) string { + dtString, err := FromToFirstValue(fromLayout, toLayout, values) + if err != nil { + return "" + } + return dtString +} + +func FromToFirstValue(fromLayout, toLayout string, values []string) (string, error) { + for _, val := range values { + dt, err := time.Parse(fromLayout, val) + if err == nil { + return dt.Format(toLayout), nil + } + } + return "", errors.New("no match") +} + +func ParseFirstValueOrZero(layout string, values []string) time.Time { + dt, err := ParseFirstValue(layout, values) + if err != nil { + return TimeZeroRFC3339() + } + return dt +} + +func ParseFirstValue(layout string, values []string) (time.Time, error) { + for _, val := range values { + try, err := time.Parse(layout, val) + if err == nil { + return try, nil + } + } + numVals := len(values) + if numVals == 0 { + return time.Now(), errors.New("no time values supplied") + } + return time.Now(), fmt.Errorf("no valid string of [%v] supplied values", strconv.Itoa(numVals)) +} + +// ParseOrZero returns a parsed time.Time or the RFC-3339 zero time. +func ParseOrZero(layout, value string) time.Time { + t, err := time.Parse(layout, value) + if err != nil { + return TimeZeroRFC3339() + } + return t +} + +// ParseFirst attempts to parse a string with a set of layouts. +func ParseFirst(layouts []string, value string) (time.Time, error) { + value = strings.TrimSpace(value) + if len(value) == 0 || len(layouts) == 0 { + return time.Now(), fmt.Errorf( + "requires value [%v] and at least one layout [%v]", value, strings.Join(layouts, ",")) + } + for _, layout := range layouts { + layout = strings.TrimSpace(layout) + if len(layout) == 0 { + continue + } + if dt, err := time.Parse(layout, value); err == nil { + return dt, nil + } + } + return time.Now(), fmt.Errorf("cannot parse time [%v] with layouts [%v]", + value, strings.Join(layouts, ",")) +} + +var FormatMap = map[string]string{ + "RFC3339": time.RFC3339, + "RFC3339YMD": RFC3339FullDate, + "ISO8601YM": ISO8601YM, +} + +func GetFormat(formatName string) (string, error) { + format, ok := FormatMap[strings.TrimSpace(formatName)] + if !ok { + return "", fmt.Errorf("format Not Found: %v", format) + } + return format, nil +} + +func TimeMinRFC3339() time.Time { + t0, _ := time.Parse(time.RFC3339, RFC3339Min) + return t0 +} + +func TimeZeroRFC3339() time.Time { + t0, _ := time.Parse(time.RFC3339, RFC3339Zero) + return t0 +} + +func TimeZeroUnix() time.Time { + t0, _ := time.Parse(time.RFC3339, RFC3339ZeroUnix) + return t0 +} + +func isZeroAny(u time.Time) bool { + return u.Equal(TimeZeroRFC3339()) || + u.Equal(TimeMinRFC3339()) || + u.Equal(TimeZeroUnix()) +} + +type RFC3339YMDTime struct{ time.Time } + +type ISO8601NoTzMilliTime struct{ time.Time } + +func (t *RFC3339YMDTime) UnmarshalJSON(buf []byte) error { + tt, isNil, err := timeUnmarshalJSON(buf, RFC3339FullDate) + if err != nil || isNil { + return err + } + t.Time = tt + return nil +} + +func (t RFC3339YMDTime) MarshalJSON() ([]byte, error) { + return timeMarshalJSON(t.Time, RFC3339FullDate) +} + +func (t *ISO8601NoTzMilliTime) UnmarshalJSON(buf []byte) error { + tt, isNil, err := timeUnmarshalJSON(buf, ISO8601MilliNoTZ) + if err != nil || isNil { + return err + } + t.Time = tt + return nil +} + +func (t ISO8601NoTzMilliTime) MarshalJSON() ([]byte, error) { + return timeMarshalJSON(t.Time, ISO8601MilliNoTZ) +} + +func timeUnmarshalJSON(buf []byte, layout string) (time.Time, bool, error) { + str := string(buf) + isNil := true + if str == "null" || str == "\"\"" { + return time.Time{}, isNil, nil + } + tt, err := time.Parse(layout, strings.Trim(str, `"`)) + if err != nil { + return time.Time{}, false, err + } + return tt, false, nil +} + +func timeMarshalJSON(t time.Time, layout string) ([]byte, error) { + return []byte(`"` + t.Format(layout) + `"`), nil +} + +func ParseSlice(layout string, strings []string) ([]time.Time, error) { + times := []time.Time{} + for _, raw := range strings { + t, err := time.Parse(layout, raw) + if err != nil { + return times, err + } + times = append(times, t) + } + return times, nil +} + +// FormatTimeMulti formats a `time.Time` object or +// an epoch number. It is adapted from `github.com/wcharczuk/go-chart`. +func FormatTimeMulti(dateFormat string, v any) string { + if typed, isTyped := v.(time.Time); isTyped { + return typed.Format(dateFormat) + } + if typed, isTyped := v.(int64); isTyped { + return time.Unix(0, typed).Format(dateFormat) + } + if typed, isTyped := v.(float64); isTyped { + return time.Unix(0, int64(typed)).Format(dateFormat) + } + return "" +} + +func FormatTimeToString(format string) func(time.Time) string { + return func(dt time.Time) string { + return dt.Format(format) + } +} + +// OffsetFormat converts an integer offset value to a string +// value to use in string time formats. Note: RFC-3339 times +// use colons and the UTC "Z" offset. +func OffsetFormat(offset int, useColon, useZ bool) string { + offsetStr := "+0000" + if offset == 0 { + if useZ { + offsetStr = "Z" + } else if useColon { + offsetStr = "+00:00" + } + } else if offset > 0 { + if useColon { + hr := offset / 100 + mn := offset - (hr * 100) + offsetStr = "+" + fmt.Sprintf("%02d:%02d", hr, mn) + } else { + offsetStr = "+" + fmt.Sprintf("%04d", offset) + } + } else if offset < 0 { + if useColon { + offsetPositive := -1 * offset + hr := offsetPositive / 100 + mn := offsetPositive - (hr * 100) + offsetStr = "-" + fmt.Sprintf("%02d:%02d", hr, mn) + } else { + offsetStr = "-" + fmt.Sprintf("%04d", -1*offset) + } + } + return offsetStr +} + +// var rxSQLTimestamp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$`) + +func ParseTimeUsingOffset(format, raw, sep string, offset int, useColon, useZ bool) (time.Time, error) { + return time.Parse(format, raw+sep+OffsetFormat(offset, useColon, useZ)) +} + +// ParseTimeSQLTimestampUsingOffset converts a SQL timestamp without timezone +// adding in a manual integer timezone. +func ParseTimeSQLTimestampUsingOffset(timeStr string, offset int) (time.Time, error) { + return ParseTimeUsingOffset(Ruby, timeStr, " ", offset, false, false) + /* + timeStr = strings.TrimSpace(timeStr) + if !rxSQLTimestamp.MatchString(timeStr) { + return time.Now(), fmt.Errorf("E_INVALID_SQL_TIMESTAMP [%v]", timeStr) + } + offsetStr := OffsetFormat(offset, useColon, useZ) + timeStr += " " + offsetStr + dt, err := time.Parse(Ruby, timeStr) + return dt, err + */ +} + +const ( + LayoutNameDT4 = "dt4" + LayoutNameDT6 = "dt6" + LayoutNameDT8 = "dt8" + LayoutNameDT14 = "dt14" +) + +// IsDTX returns the dtx format if conformant to various DTX values (dt4, dt6, dt8, dt14). +func IsDTX(d int) (string, error) { + switch len(strconv.Itoa(d)) { + case 4: + return LayoutNameDT4, nil + case 6: + m := d - ((d / 100) * 100) + if m < 1 || m > 12 { + return LayoutNameDT6, errors.New("dt6 length month value is out of bounds") + } + return LayoutNameDT6, nil + case 8: + dy := d - ((d / 100) * 100) + if dy < 1 || dy > 31 { + return LayoutNameDT6, errors.New("dt8 day value is out of bounds") + } + return LayoutNameDT8, nil + case 14: + return LayoutNameDT14, nil + default: + return "", errors.New("length mismatch") + } +} diff --git a/timeutil/format_test.go b/timeutil/format_test.go new file mode 100644 index 0000000..8ef71de --- /dev/null +++ b/timeutil/format_test.go @@ -0,0 +1,132 @@ +package timeutil + +import ( + "encoding/json" + "testing" + "time" +) + +var dmyhm2ParseTests = []struct { + v string + want string +}{ + {"02:01:06 15:04", "2006-01-02T15:04:00Z"}, +} + +// TestDMYHM2ParseTime ensures timeutil.DateDMYHM2 is parsed to GMT timezone. +func TestDMYHM2ParseTime(t *testing.T) { + for _, tt := range dmyhm2ParseTests { + got, err := FromTo(tt.v, DateDMYHM2, time.RFC3339) + if err != nil { + t.Errorf("time.Parse(DateDMYHM2) Error: with %v, want %v, err %v", tt.v, tt.want, err) + } + if got != tt.want { + t.Errorf("time.Parse(\"%v\", DateDMYHM2) Mismatch: want %v, got %v", tt.v, tt.want, got) + } + } +} + +var rfc3339YMDTimeTests = []struct { + v string + want string +}{ + {`{"MyTime":"2001-02-03"}`, `{"MyTime":"2001-02-03"}`}, + {`{"MyTime":"0001-01-01"}`, `{"MyTime":"0001-01-01"}`}, + {`{"MyTime":""}`, `{"MyTime":"0001-01-01"}`}, + {`{}`, `{"MyTime":"0001-01-01"}`}} + +type myStruct struct { + MyTime RFC3339YMDTime +} + +func TestRfc3339YMDTime(t *testing.T) { + for _, tt := range rfc3339YMDTimeTests { + my := myStruct{} + //fmt.Println(tt.v) + err := json.Unmarshal([]byte(tt.v), &my) + if err != nil { + t.Errorf("Rfc3339YMDTime.Unmarshal: with %v, want %v, err %v", tt.v, tt.want, err) + } + + bytes, err := json.Marshal(my) + if err != nil { + t.Errorf("Rfc3339YMDTime.Marshal: with %v, want %v, err %v", tt.v, tt.want, err) + } + + got := string(bytes) + + if got != tt.want { + t.Errorf("Rfc3339YMDTime(%v): want %v, got %v", tt.v, tt.want, got) + } + } +} + +var offsetFormatTests = []struct { + input int + useColon bool + useZ bool + want string +}{ + {0, false, false, "+0000"}, + {0, true, false, "+00:00"}, + {0, true, true, "Z"}, + {400, false, false, "+0400"}, + {-400, false, false, "-0400"}, + {530, false, false, "+0530"}, + {-530, false, false, "-0530"}, + {700, true, false, "+07:00"}, + {-700, true, false, "-07:00"}, + {845, true, false, "+08:45"}, + {-845, true, false, "-08:45"}, +} + +func TestOffsetFormat(t *testing.T) { + for _, tt := range offsetFormatTests { + got := OffsetFormat(tt.input, tt.useColon, tt.useZ) + if got != tt.want { + t.Errorf("time.OffsetFormat(\"%v\",%v,%v) Mismatch: want [%v], got [%v]", tt.input, tt.useColon, tt.useZ, tt.want, got) + } + } +} + +var isDTXTests = []struct { + v int + want string +}{ + {2004, LayoutNameDT4}, + {200401, LayoutNameDT6}, +} + +func TestIsDTX(t *testing.T) { + for _, tt := range isDTXTests { + got, err := IsDTX(tt.v) + if err != nil { + t.Errorf("time.IsDTX(%d) Error: want (%s), err (%v)", tt.v, tt.want, err) + } + if got != tt.want { + t.Errorf("time.IsDTX(%d) Mismatch: want (%s), got (%s)", tt.v, tt.want, got) + } + } +} + +var formatsTests = []struct { + v string + format string + want string +}{ + {"2023-06-18T00:00:00Z", DateTextUS, "June 18, 2023"}, + {"2023-06-18T00:00:00Z", DateTextUSAbbr3, "Jun 18, 2023"}, +} + +func TestFormats(t *testing.T) { + for _, tt := range formatsTests { + dt, err := time.Parse(time.RFC3339, tt.v) + if err != nil { + t.Errorf("time.Parse(time.RFC3339, %s) Error: err (%s)", tt.v, err.Error()) + } + got := dt.Format(tt.format) + if got != tt.want { + t.Errorf("time.Format(%s) dt (%s) Mismatch: want (%s), got (%s)", tt.format, tt.v, tt.want, got) + } + } +} diff --git a/timeutil/trans.go b/timeutil/trans.go index fc2c3c4..045eb5d 100644 --- a/timeutil/trans.go +++ b/timeutil/trans.go @@ -26,11 +26,17 @@ func GetDefaultTimeLocation() *time.Location { } // UnixMilliToStringPtr 毫秒时间戳 -> 字符串 -func UnixMilliToStringPtr(tm *int64) *string { - if tm == nil { +func UnixMilliToStringPtr(milli *int64) *string { + if milli == nil { return nil } - str := time.UnixMilli(*tm).Format(TimeLayout) + + tm := time.UnixMilli(*milli) + + // 设置默认时区 + tm.In(GetDefaultTimeLocation()) + + str := tm.Format(TimeLayout) return &str } @@ -44,17 +50,18 @@ func StringToUnixMilliInt64Ptr(tm *string) *int64 { if theTime == nil { return nil } + unixTime := theTime.UnixMilli() return &unixTime } // UnixMilliToTimePtr 毫秒时间戳 -> 时间 -func UnixMilliToTimePtr(tm *int64) *time.Time { - if tm == nil { +func UnixMilliToTimePtr(milli *int64) *time.Time { + if milli == nil { return nil } - unixMilli := time.UnixMilli(*tm) + unixMilli := time.UnixMilli(*milli) return &unixMilli } @@ -69,12 +76,13 @@ func TimeToUnixMilliInt64Ptr(tm *time.Time) *int64 { } // UnixSecondToTimePtr 秒时间戳 -> 时间 -func UnixSecondToTimePtr(tm *int64) *time.Time { - if tm == nil { +func UnixSecondToTimePtr(second *int64) *time.Time { + if second == nil { return nil } - unixMilli := time.Unix(*tm, 0) + unixMilli := time.Unix(*second, 0) + return &unixMilli } @@ -100,18 +108,19 @@ func StringTimeToTime(str *string) *time.Time { var err error var theTime time.Time - theTime, err = time.ParseInLocation(TimeLayout, *str, GetDefaultTimeLocation()) - if err == nil { + if theTime, err = time.ParseInLocation(TimeLayout, *str, GetDefaultTimeLocation()); err == nil { return &theTime } - theTime, err = time.ParseInLocation(DateLayout, *str, GetDefaultTimeLocation()) - if err == nil { + if theTime, err = time.ParseInLocation(DateLayout, *str, GetDefaultTimeLocation()); err == nil { return &theTime } - theTime, err = time.ParseInLocation(ClockLayout, *str, GetDefaultTimeLocation()) - if err == nil { + if theTime, err = time.ParseInLocation(ClockLayout, *str, GetDefaultTimeLocation()); err == nil { + return &theTime + } + + if theTime, err = time.ParseInLocation(ISO9075MicroTZ, *str, GetDefaultTimeLocation()); err == nil { return &theTime } @@ -123,6 +132,10 @@ func TimeToTimeString(tm *time.Time) *string { if tm == nil { return nil } + + // 设置默认时区 + tm.In(GetDefaultTimeLocation()) + return trans.String(tm.Format(TimeLayout)) } @@ -161,13 +174,17 @@ func TimeToDateString(tm *time.Time) *string { if tm == nil { return nil } + + // 设置默认时区 + tm.In(GetDefaultTimeLocation()) + return trans.String(tm.Format(DateLayout)) } // TimestamppbToTime timestamppb.Timestamp -> time.Time -func TimestamppbToTime(tm *timestamppb.Timestamp) *time.Time { - if tm != nil { - return trans.Ptr(tm.AsTime()) +func TimestamppbToTime(timestamp *timestamppb.Timestamp) *time.Time { + if timestamp != nil { + return trans.Ptr(timestamp.AsTime()) } return nil } diff --git a/timeutil/trans_test.go b/timeutil/trans_test.go index 9de678b..33e4c4e 100644 --- a/timeutil/trans_test.go +++ b/timeutil/trans_test.go @@ -2,13 +2,14 @@ package timeutil import ( "fmt" - "google.golang.org/protobuf/types/known/timestamppb" "testing" "time" "github.com/stretchr/testify/assert" "github.com/tx7do/go-utils/trans" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestUnixMilliToStringPtr(t *testing.T) {