Skip to content

Commit

Permalink
👔 feat: timex - update the ToDuration() for support unit d,w and long…
Browse files Browse the repository at this point in the history
… unit hour,min,sec
  • Loading branch information
inhere committed Jun 1, 2023
1 parent 90843e9 commit b8b9075
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 42 deletions.
60 changes: 56 additions & 4 deletions internal/comfunc/comfunc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -102,21 +103,72 @@ func FormatTplAndArgs(fmtAndArgs []any) string {
return fmt.Sprint(fmtAndArgs...)
}

var durStrReg = regexp.MustCompile(`^(-?\d+)(ns|us|µs|ms|s|m|h)$`)
var (
// TIP: extend unit d,w
// time.ParseDuration() is not supported. eg: "1d", "2w"
durStrReg = regexp.MustCompile(`^(-?\d+)(ns|us|µs|ms|s|m|h|d|w)$`)
// match long duration string, such as "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks"
// time.ParseDuration() is not supported.
durStrRegL = regexp.MustCompile(`^(-?\d+)([a-zA-Z]{3,})$`)
)

// IsDuration check the string is a duration string.
func IsDuration(s string) bool {
if s == "0" {
if s == "0" || durStrReg.MatchString(s) {
return true
}
return durStrReg.MatchString(s)
return durStrRegL.MatchString(s)
}

// ToDuration parses a duration string. such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
//
// Diff of time.ParseDuration:
// - support extend unit d, w at the end of string. such as "1d", "2w".
// - support long string unit at end. such as "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks".
//
// If the string is not a valid duration string, it will return an error.
func ToDuration(s string) (time.Duration, error) {
if s == "now" {
ln := len(s)
if ln == 0 {
return 0, fmt.Errorf("empty duration string")
}

s = strings.ToLower(s)
if s == "0" {
return 0, nil
}

// extend unit d,w, time.ParseDuration() is not supported. eg: "1d", "2w"
if lastUnit := s[ln-1]; lastUnit == 'd' {
s = s + "ay"
} else if lastUnit == 'w' {
s = s + "eek"
}

// long unit, time.ParseDuration() is not supported. eg: "-3sec" => [3sec -3 sec]
ss := durStrRegL.FindStringSubmatch(s)
if len(ss) == 3 {
num, unit := ss[1], ss[2]

// convert to short unit
switch unit {
case "week", "weeks":
// max unit is hour, so need convert by 24 * 7 * n
n, _ := strconv.Atoi(num)
s = strconv.Itoa(n*24*7) + "h"
case "day", "days":
// max unit is hour, so need convert by 24 * n
n, _ := strconv.Atoi(num)
s = strconv.Itoa(n*24) + "h"
case "hour", "hours":
s = num + "h"
case "min", "mins", "minute", "minutes":
s = num + "m"
case "sec", "secs", "second", "seconds":
s = num + "s"
}
}

return time.ParseDuration(s)
}
12 changes: 12 additions & 0 deletions strutil/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,22 @@ func TestToTime(t *testing.T) {
is.Panics(func() {
strutil.MustToTime("invalid")
})
}

func TestToDuration(t *testing.T) {
is := assert.New(t)

dur, err1 := strutil.ToDuration("3s")
is.NoErr(err1)
is.Eq(3*timex.Second, dur)

dur, err1 = strutil.ToDuration("3sec")
is.NoErr(err1)
is.Eq(3*timex.Second, dur)

dur, err1 = strutil.ToDuration("-3sec")
is.NoErr(err1)
is.Eq(-3*timex.Second, dur)
}

func TestParseSizeRange(t *testing.T) {
Expand Down
19 changes: 19 additions & 0 deletions timex/gotime.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@ package timex

import "time"

// some time layout or time
const (
DatetimeLayout = "2006-01-02 15:04:05"
LayoutWithMs3 = "2006-01-02 15:04:05.000"
LayoutWithMs6 = "2006-01-02 15:04:05.000000"
DateOnlyLayout = "2006-01-02"
TimeOnlyLayout = "15:04:05"

// ZeroUnix zero unix timestamp
ZeroUnix int64 = -62135596800
)

var (
// DefaultLayout template for format time
DefaultLayout = DatetimeLayout
// ZeroTime zero time instance
ZeroTime = time.Time{}
)

// SetLocalByName set local by tz name. eg: UTC, PRC
func SetLocalByName(tzName string) error {
location, err := time.LoadLocation(tzName)
Expand Down
19 changes: 5 additions & 14 deletions timex/timex.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,14 @@ const (
OneWeekSec = 7 * 86400

Second = time.Second
Minute = time.Minute
OneMin = time.Minute
Hour = time.Hour
OneHour = time.Hour
Day = 24 * time.Hour
OneDay = 24 * time.Hour
Week = 7 * 24 * time.Hour
OneWeek = 7 * 24 * time.Hour

DatetimeLayout = "2006-01-02 15:04:05"
DateOnlyLayout = "2006-01-02"
TimeOnlyLayout = "15:04:05"
)

var (
// DefaultLayout template for format time
DefaultLayout = "2006-01-02 15:04:05"
// ZeroTime zero time instance
ZeroTime = time.Time{}
)

// TimeX alias of Time
Expand Down Expand Up @@ -89,14 +82,12 @@ func FromDate(s string, template ...string) (*Time, error) {
return FromString(s)
}

// FromString create from datetime string.
// see strutil.ToTime()
// FromString create from datetime string. see strutil.ToTime()
func FromString(s string, layouts ...string) (*Time, error) {
t, err := strutil.ToTime(s, layouts...)
if err != nil {
return nil, err
}

return New(t), nil
}

Expand Down
31 changes: 18 additions & 13 deletions timex/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ func ToDuration(s string) (time.Duration, error) {
func IsDuration(s string) bool { return comfunc.IsDuration(s) }

// TryToTime parse a date string or duration string to time.Time.
//
// if s is empty, return zero time.
func TryToTime(s string, bt time.Time) (time.Time, error) {
if s == "" {
return ZeroTime, nil
}
if s == "now" {
return time.Now(), nil
}
Expand Down Expand Up @@ -151,17 +156,17 @@ func ensureOpt(opt *ParseRangeOpt) *ParseRangeOpt {
//
// Expression format:
//
// "-5h~-1h" => 5 hours ago to 1 hour ago
// "1h~5h" => 1 hour after to 5 hours after
// "-1h~1h" => 1 hour ago to 1 hour after
// "-1h" => 1 hour ago to feature. eq "-1h,"
// "-1h~0" => 1 hour ago to now.
// "< -1h" => 1 hour ago. eq ",-1h"
// "> 1h" OR "1h" => 1 hour after to feature
// // keyword: now, today, yesterday, tomorrow
// "today" => today start to today end
// "yesterday" => yesterday start to yesterday end
// "tomorrow" => tomorrow start to tomorrow end
// "-5h~-1h" => 5 hours ago to 1 hour ago
// "1h~5h" => 1 hour after to 5 hours after
// "-1h~1h" => 1 hour ago to 1 hour after
// "-1h" => 1 hour ago to feature. eq "-1h,"
// "-1h~0" => 1 hour ago to now.
// "< -1h" OR "~-1h" => 1 hour ago. eq ",-1h"
// "> 1h" OR "1h" => 1 hour after to feature
// // keyword: now, today, yesterday, tomorrow
// "today" => today start to today end
// "yesterday" => yesterday start to yesterday end
// "tomorrow" => tomorrow start to tomorrow end
//
// Usage:
//
Expand All @@ -172,7 +177,7 @@ func ensureOpt(opt *ParseRangeOpt) *ParseRangeOpt {
// fmt.Println(start, end)
func ParseRange(expr string, opt *ParseRangeOpt) (start, end time.Time, err error) {
opt = ensureOpt(opt)
expr = strings.Trim(expr, " "+string(opt.SepChar))
expr = strings.TrimSpace(expr)
if expr == "" {
err = fmt.Errorf("invalid time range expr %q", expr)
return
Expand All @@ -181,7 +186,7 @@ func ParseRange(expr string, opt *ParseRangeOpt) (start, end time.Time, err erro
// parse time range. eg: "5h~1h"
if strings.IndexByte(expr, opt.SepChar) > -1 {
s1, s2 := strutil.TrimCut(expr, string(opt.SepChar))
if s1 == "" || s2 == "" {
if s1 == "" && s2 == "" {
err = fmt.Errorf("invalid time range expr: %s", expr)
return
}
Expand Down
124 changes: 113 additions & 11 deletions timex/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,38 @@ func TestToLayout(t *testing.T) {
}

func TestToDur(t *testing.T) {
dur, err := timex.ToDur("3s")
assert.NoErr(t, err)
assert.Eq(t, 3*timex.Second, dur)
tests := []struct {
in string
out time.Duration
ok bool
}{
{"", time.Duration(0), false},
{"0", time.Duration(0), true},
{"now", time.Duration(0), false},
{"3s", 3 * timex.Second, true},
{"3sec", 3 * timex.Second, true},
{"3m", 3 * timex.Minute, true},
{"3min", 3 * timex.Minute, true},
{"3h", 3 * timex.Hour, true},
{"3hours", 3 * timex.Hour, true},
{"3d", 3 * timex.Day, true},
{"3day", 3 * timex.Day, true},
{"1w", 1 * timex.Week, true},
{"1week", 1 * timex.Week, true},
}

for _, item := range tests {
dur, err := timex.ToDur(item.in)
if item.ok {
assert.NoErr(t, err)
} else {
assert.Err(t, err)
}

assert.Eq(t, item.out, dur)
}

dur, err = timex.ToDur("now")
dur, err := timex.ToDur("now")
assert.NoErr(t, err)
assert.Eq(t, time.Duration(0), dur)

Expand Down Expand Up @@ -153,23 +180,98 @@ func TestTryToTime(t *testing.T) {
}
}

func TestInRange(t *testing.T) {
tests := []struct {
start, end string
out bool
}{
{"-5s", "5s", true},
{"", "-2s", false},
}

now := time.Now()
for _, item := range tests {
startT, err := timex.TryToTime(item.start, now)
assert.NoErr(t, err)
endT, err := timex.TryToTime(item.end, now)
assert.NoErr(t, err)
assert.Eq(t, item.out, timex.InRange(now, startT, endT))
}
}

func TestParseRange(t *testing.T) {
tests := []struct {
input string
start int64
end int64
ok bool
}{
// {"2020-01-02 15:04:05", 1577977445, 0},
// {"2020-01-02 15:04:05~2020-01-03 15:04:05", 1577977445, 1578063845},
{"2020-01-02 15:04:06~", 1577977446, 0},
{"~2020-01-02 15:04:07", 0, 1577946245},
{"~", 0, 0},
// date string
{"2020-01-02 15:04:05", 1577977445, timex.ZeroUnix, true},
{"2020-01-02 15:04:05~2020-01-03 15:04:05", 1577977445, 1578063845, true},
{"2020-01-02 15:04:06~", 1577977446, timex.ZeroUnix, true},
{"~2020-01-02 15:04:07", timex.ZeroUnix, 1577977447, true},
// duration string
{"-5s", 1672671840, timex.ZeroUnix, true},
{"> 5s", 1672671850, timex.ZeroUnix, true},
{"-5s~5s", 1672671840, 1672671850, true},
{"~5s", timex.ZeroUnix, 1672671850, true},
{"< 5s", timex.ZeroUnix, 1672671850, true},
{"1h", 1672675445, timex.ZeroUnix, true},
{"1hour", 1672675445, timex.ZeroUnix, true},
// invalid
{"~", timex.ZeroUnix, timex.ZeroUnix, false},
{" ", timex.ZeroUnix, timex.ZeroUnix, false},
}

bt, err := timex.FromDate("2023-01-02 15:04:05")
assert.NoError(t, err)
opt := &timex.ParseRangeOpt{
BaseTime: bt.T(),
}

for _, item := range tests {
start, end, err := timex.ParseRange(item.input, nil)
assert.NoErr(t, err)
start, end, err := timex.ParseRange(item.input, opt)
assert.Eq(t, item.ok, err == nil, "err for %q", item.input)
assert.Eq(t, item.start, start.Unix(), "start for %q", item.input)
assert.Eq(t, item.end, end.Unix(), "end for %q", item.input)
}

t.Run("keyword now", func(t *testing.T) {
start, end, err := timex.ParseRange("now", nil)
assert.NoError(t, err)
assert.Eq(t, timex.Now().Unix(), start.Unix())
assert.Eq(t, timex.ZeroUnix, end.Unix())

start, end, err = timex.ParseRange("~now", nil)
assert.NoError(t, err)
assert.Eq(t, timex.ZeroUnix, start.Unix())
assert.Eq(t, timex.Now().Unix(), end.Unix())
})

t.Run("keyword today", func(t *testing.T) {
now := timex.Now()
start, end, err := timex.ParseRange("today", nil)
assert.NoError(t, err)
assert.Eq(t, now.DayStart().Unix(), start.Unix())
assert.Eq(t, now.DayEnd().Unix(), end.Unix())

start, end, err = timex.ParseRange("~today", nil)
assert.Error(t, err)
assert.Eq(t, timex.ZeroUnix, start.Unix())
assert.Eq(t, timex.ZeroUnix, end.Unix())
})

t.Run("keyword yesterday", func(t *testing.T) {
yd := timex.Now().DayAgo(1)
start, end, err := timex.ParseRange("yesterday", nil)
assert.NoError(t, err)
assert.Eq(t, yd.DayStart().Unix(), start.Unix())
assert.Eq(t, yd.DayEnd().Unix(), end.Unix())

start, end, err = timex.ParseRange("~yesterday", nil)
assert.Error(t, err)
assert.Eq(t, timex.ZeroUnix, start.Unix())
assert.Eq(t, timex.ZeroUnix, end.Unix())
})
}

0 comments on commit b8b9075

Please sign in to comment.