diff --git a/date/date.go b/date/date.go index 1546528..8ee1122 100644 --- a/date/date.go +++ b/date/date.go @@ -24,6 +24,17 @@ type Date struct { DateTime time.Time } +func Now() *Date { + now := time.Now() + + return &Date{ + Year: now.Year(), + Month: int(now.Month()), + Day: now.Day(), + DateTime: now, + } +} + func New(year, month, day int) (datetime.Interface, error) { errs := validator.Validate(year, constraints.GreaterOrEqual{Value: 0}) @@ -95,5 +106,13 @@ func (d *Date) Equal(u datetime.Interface) bool { } func (d *Date) Between(start, end datetime.Interface) bool { - return d.Time().Before(end.Time()) && d.Time().After(start.Time()) + return d.Before(end) && d.After(start) +} + +func (d *Date) Before(u datetime.Interface) bool { + return d.Time().Before(u.Time()) +} + +func (d *Date) After(u datetime.Interface) bool { + return d.Time().After(u.Time()) } diff --git a/date/range.go b/date/range.go index 00e96e3..a67a5df 100644 --- a/date/range.go +++ b/date/range.go @@ -11,7 +11,8 @@ import ( ) const ( - RangeRegexp = `^([\[\(])(\d{4}-\d{2}-\d{2})?,(\d{4}-\d{2}-\d{2})?([\]\)])$` + RangeDateRegexp = `(` + datetime.YearRegexp + `-` + datetime.MonthRegexp + `-` + datetime.DayRegexp + `)` + RangeRegexp = `^([\[\(])` + RangeDateRegexp + `?\s*,\s*` + RangeDateRegexp + `?([\]\)])$` ) type Range struct { @@ -21,13 +22,46 @@ type Range struct { end datetime.RangeEnd } -func NewDateRange(from, to string, start datetime.RangeStart, end datetime.RangeEnd) *Range { +func NewRange(from, to string, start datetime.RangeStart, end datetime.RangeEnd) (*Range, error) { + + _, err := FromString(from) + + if from != "" && err != nil { + return nil, err + } + + _, err = FromString(to) + + if to != "" && err != nil { + return nil, err + } + + if to == "" && from == "" { + return nil, errors.New("from and to can not be both empty") + } + return &Range{ from: Value(from), to: Value(to), start: start, end: end, - } + }, nil +} + +func NewRangeOptional(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartOptional, datetime.RangeEndOptional) +} + +func NewRangeStrict(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartStrict, datetime.RangeEndStrict) +} + +func NewRangeStartStrict(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartStrict, datetime.RangeEndOptional) +} + +func NewRangeStartOptional(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartOptional, datetime.RangeEndStrict) } func RangeFromString(dateRange string) (*Range, error) { @@ -39,9 +73,9 @@ func RangeFromString(dateRange string) (*Range, error) { re := regexp.MustCompile(RangeRegexp) match := re.FindStringSubmatch(dateRange) - openBracket, date1, date2, closeBracket := match[1], match[2], match[3], match[4] + openBracket, date1, date2, closeBracket := match[1], match[2], match[6], match[10] - return NewDateRange(date1, date2, datetime.RangeStart(openBracket), datetime.RangeEnd(closeBracket)), nil + return NewRange(date1, date2, datetime.RangeStart(openBracket), datetime.RangeEnd(closeBracket)) } func (d *Range) Start() datetime.RangeStart { @@ -52,8 +86,16 @@ func (d *Range) End() datetime.RangeEnd { return d.end } +func (d *Range) From() Value { + return d.from +} + +func (d *Range) To() Value { + return d.to +} + func (d *Range) String() string { - return fmt.Sprintf("%s%s,%s%s", d.start, d.from, d.to, d.end) + return fmt.Sprintf("%s%s, %s%s", d.Start(), d.From(), d.To(), d.End()) } func (d *Range) Is(value any) bool { @@ -63,9 +105,18 @@ func (d *Range) Is(value any) bool { return false } - start, _ := FromString(string(d.start)) - end, _ := FromString(string(d.end)) - return date.Between(start, end) + from, _ := FromString(string(d.From())) + to, _ := FromString(string(d.To())) + + if from == nil { + return date.Before(to) + } + + if to == nil { + return date.After(from) + } + + return date.Between(from, to) } func (d *Range) format(date any) (datetime.Interface, error) { @@ -74,8 +125,6 @@ func (d *Range) format(date any) (datetime.Interface, error) { return New(i.Year(), int(i.Month()), i.Day()) case *Date: return i, nil - case Date: - return &i, nil case string: return FromString(i) default: diff --git a/dateTimeInterface.go b/dateTimeInterface.go index 23361bd..e6f5cff 100644 --- a/dateTimeInterface.go +++ b/dateTimeInterface.go @@ -8,5 +8,7 @@ type Interface interface { Time() time.Time Equal(u Interface) bool Between(start, end Interface) bool + Before(u Interface) bool + After(u Interface) bool Compare(u Interface) int } diff --git a/datetime.go b/datetime.go index 0c34c57..d7c44c3 100644 --- a/datetime.go +++ b/datetime.go @@ -12,7 +12,7 @@ import ( ) const ( - Regexp = `^(\d{4})-(\d{2})-(\d{2})( (\d{2}):(\d{2}):(\d{2}))?$` + Regexp = `^((\d+)-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]))\s(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$` ) type DateTime struct { @@ -96,19 +96,14 @@ func FromString(value string) (Interface, error) { re := regexp.MustCompile(Regexp) match := re.FindStringSubmatch(value) - year, _ := strconv.Atoi(match[1]) - month, _ := strconv.Atoi(match[2]) - day, _ := strconv.Atoi(match[3]) - - if match[4] != "" { - hour, _ := strconv.Atoi(match[5]) - minute, _ := strconv.Atoi(match[6]) - second, _ := strconv.Atoi(match[7]) - - return New(year, month, day, hour, minute, second) - } - - return nil, errors.New(fmt.Sprintf("unsupported format of datetime \"%s\". time is missing.", value)) + year, _ := strconv.Atoi(match[2]) + month, _ := strconv.Atoi(match[3]) + day, _ := strconv.Atoi(match[4]) + hour, _ := strconv.Atoi(match[5]) + minute, _ := strconv.Atoi(match[6]) + second, _ := strconv.Atoi(match[7]) + + return New(year, month, day, hour, minute, second) } func (d *DateTime) FromString(value string) (Interface, error) { @@ -143,7 +138,15 @@ func (d *DateTime) Equal(u Interface) bool { } func (d *DateTime) Between(start, end Interface) bool { - return d.Time().Before(end.Time()) && d.Time().After(start.Time()) + return d.Before(end) && d.After(start) +} + +func (d *DateTime) Before(u Interface) bool { + return d.Time().Before(u.Time()) +} + +func (d *DateTime) After(u Interface) bool { + return d.Time().After(u.Time()) } func DaysInMonthList(year int, month int) []int { diff --git a/range.go b/range.go index 4722bf4..296899d 100644 --- a/range.go +++ b/range.go @@ -1,43 +1,130 @@ package datetime -type RangeStart string -type RangeEnd string - -var ( - // RangeStartStrict can not be equal - RangeStartStrict RangeStart = "[" - // RangeStartOptional can be equal - RangeStartOptional RangeStart = "(" - // RangeEndStrict can not be equal - RangeEndStrict RangeEnd = "]" - // RangeEndOptional can be equal - RangeEndOptional RangeEnd = ")" +import ( + "errors" + "fmt" + "github.com/gouef/validator" + "github.com/gouef/validator/constraints" + "regexp" + "time" ) type Range struct { + from Value + to Value start RangeStart end RangeEnd } -func NewRange(start RangeStart, end RangeEnd) *Range { +func NewRange(from, to string, start RangeStart, end RangeEnd) (*Range, error) { + + _, err := FromString(from) + + if from != "" && err != nil { + return nil, err + } + + _, err = FromString(to) + + if to != "" && err != nil { + return nil, err + } + return &Range{ + from: Value(from), + to: Value(to), start: start, end: end, + }, nil +} + +func NewRangeOptional(from, to string) (*Range, error) { + return NewRange(from, to, RangeStartOptional, RangeEndOptional) +} + +func NewRangeStrict(from, to string) (*Range, error) { + return NewRange(from, to, RangeStartStrict, RangeEndStrict) +} + +func NewRangeStartStrict(from, to string) (*Range, error) { + return NewRange(from, to, RangeStartStrict, RangeEndOptional) +} + +func NewRangeStartOptional(from, to string) (*Range, error) { + return NewRange(from, to, RangeStartOptional, RangeEndStrict) +} + +func RangeFromString(value string) (*Range, error) { + errs := validator.Validate(value, constraints.RegularExpression{Regexp: RangeRegexp}) + + if len(errs) != 0 { + return nil, errors.New(fmt.Sprintf("unsupported format of datetime range \"%s\"", value)) } + + re := regexp.MustCompile(RangeRegexp) + match := re.FindStringSubmatch(value) + start := match[1] + from := match[2] + to := match[12] + end := match[22] + + return NewRange(from, to, RangeStart(start), RangeEnd(end)) +} + +func (r *Range) Start() RangeStart { + return r.start +} + +func (r *Range) End() RangeEnd { + return r.end } -func NewRangeOptional() *Range { - return NewRange(RangeStartOptional, RangeEndOptional) +func (r *Range) From() ValueInterface { + return r.from } -func NewRangeStrict() *Range { - return NewRange(RangeStartStrict, RangeEndStrict) +func (r *Range) To() ValueInterface { + return r.to } -func NewRangeStartStrict() *Range { - return NewRange(RangeStartStrict, RangeEndOptional) +func (r *Range) String() string { + return fmt.Sprintf("%s%s, %s%s", r.Start(), r.From(), r.To(), r.End()) +} + +func (r *Range) Is(value any) bool { + date, err := r.format(value) + + if err != nil { + return false + } + + from, _ := FromString(string(r.from)) + to, _ := FromString(string(r.to)) + + if from == nil && to == nil { + return false + } + + if from == nil { + return date.Before(to) + } + + if to == nil { + return date.After(from) + } + + return date.Between(from, to) } -func NewRangeStartOptional() *Range { - return NewRange(RangeStartOptional, RangeEndStrict) +func (r *Range) format(date any) (Interface, error) { + switch i := date.(type) { + case time.Time: + return New(i.Year(), int(i.Month()), i.Day(), i.Hour(), i.Minute(), i.Second()) + case *DateTime: + return i, nil + case string: + return FromString(i) + default: + return nil, errors.New("unsupported format of datetime") + } } diff --git a/rangeInterface.go b/rangeInterface.go new file mode 100644 index 0000000..4e0e43a --- /dev/null +++ b/rangeInterface.go @@ -0,0 +1,40 @@ +package datetime + +// RangeStart start bracket +type RangeStart string + +// RangeEnd end bracket +type RangeEnd string + +var ( + // RangeStartStrict can not be equal + RangeStartStrict RangeStart = "[" + // RangeStartOptional can be equal + RangeStartOptional RangeStart = "(" + // RangeEndStrict can not be equal + RangeEndStrict RangeEnd = "]" + // RangeEndOptional can be equal + RangeEndOptional RangeEnd = ")" +) + +const ( + YearRegexp = `(\d+)` + MonthRegexp = `(0[1-9]|1[0-2])` + DayRegexp = `(0[1-9]|[12][0-9]|3[01])` + HourRegexp = `(0[0-9]|1[0-9]|2[0-3])` + MinuteRegexp = `[0-5][0-9]` + SecondRegexp = `[0-5][0-9]` + DateRegexp = YearRegexp + `-` + MonthRegexp + `-` + DayRegexp + TimeRegexp = `(` + HourRegexp + `):(` + MinuteRegexp + `):(` + SecondRegexp + `)` + DateTimeRegexp = `((` + DateRegexp + `) (` + TimeRegexp + `))` + RangeRegexp = `^([\[\(])` + DateTimeRegexp + `?\s*,\s*` + DateTimeRegexp + `?([\]\)])$` +) + +type RangeInterface interface { + Start() RangeStart + End() RangeEnd + From() ValueInterface + To() ValueInterface + String() string + Is(value any) bool +} diff --git a/tests/date_test.go b/tests/date_test.go index afab41f..6b93bd8 100644 --- a/tests/date_test.go +++ b/tests/date_test.go @@ -1,6 +1,7 @@ package tests import ( + "fmt" "github.com/gouef/datetime" "github.com/gouef/datetime/date" "github.com/stretchr/testify/assert" @@ -200,3 +201,224 @@ func TestDateToString(t *testing.T) { }) } } + +func TestDateRange(t *testing.T) { + t.Run("New Date Range", func(t *testing.T) { + tests := []struct { + rangeStr string + testDate any + expectedErr bool + expectedValidErr bool + expected func() any + }{ + {"[2025-01-31, 2026-01-31]", "2025-05-06", false, false, func() any { + val, err := date.NewRangeStrict( + "2025-01-31", "2026-01-31") + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", true, false, func() any { + val, err := date.NewRange( + "2025-01-31 14:15:16", "2026-01-31 17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", true, false, func() any { + val, err := date.NewRange( + "", "2026-01-31 17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, ]", "2025-05-06 20:21:22", true, false, func() any { + val, err := date.NewRange( + "2025-01-31 14:15:16", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, ]", time.Date(2025, 5, 6, 20, 21, 22, 0, time.UTC), true, false, func() any { + val, err := date.NewRange( + "2025-01-31 14:15:16", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, 2026-01-31]", "2025-05-06", true, false, nil}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", 2025, true, true, func() any { + val, err := date.NewRange( + "2025-01-31 14:15:16", "2026-01-31 17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[, ]", date.Now(), true, true, func() any { + val, err := date.NewRange( + "", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.Error(t, err) + return val + }}, + {"[2025-01-31 14:15:16, 2026-02-31 14:15:16]", date.Now(), true, true, func() any { + val, err := date.NewRange( + "2025-01-31 14:15:16", "2026-02-31 14:15:16", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.Error(t, err) + return val + }}, + } + + for _, tt := range tests { + t.Run("New Range: "+tt.rangeStr+fmt.Sprintf(", valid date: %v", tt.testDate), func(t *testing.T) { + if tt.expectedErr { + r, err := date.RangeFromString(tt.rangeStr) + assert.Error(t, err) + assert.Nil(t, r) + + } else { + r, err := date.RangeFromString(tt.rangeStr) + assert.NoError(t, err) + assert.Equal(t, tt.expected(), r) + if tt.expectedValidErr { + assert.False(t, r.Is(tt.testDate)) + } else { + assert.True(t, r.Is(tt.testDate)) + assert.Equal(t, tt.rangeStr, r.String()) + } + } + }) + } + + t.Run("New range invalid from and to", func(t *testing.T) { + _, err := date.NewRange( + "invalid", "2026-01-31", datetime.RangeStartStrict, datetime.RangeEndStrict) + assert.Error(t, err) + _, err = date.NewRange( + "2025-01-31", "invalid", datetime.RangeStartStrict, datetime.RangeEndStrict) + assert.Error(t, err) + }) + + }) + + t.Run("Range Is", func(t *testing.T) { + d, _ := date.New(2025, 5, 6) + tests := []struct { + rangeStr string + testDate any + expectedErr bool + expected bool + }{ + {"[2025-01-31, 2026-01-31]", "2025-05-06", false, true}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", true, true}, + {"[, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", true, true}, + {"[2025-01-31 14:15:16, ]", "2025-05-06 20:21:22", true, true}, + {"[2025-01-31, ]", time.Date(2025, 5, 6, 20, 21, 22, 0, time.UTC), false, true}, + {"[2025-01-31, ]", d, false, true}, + {"[, 2029-01-31]", d, false, true}, + {"[, 2029-01-31]", 30, false, false}, + {"[2025-01-31 14:15:16, 2026-01-31]", "2025-05-06", true, false}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", 2025, true, false}, + {"[, ]", date.Now(), true, false}, + {"[2, ]", date.Now(), true, false}, + } + + for _, tt := range tests { + t.Run("New Range: "+tt.rangeStr, func(t *testing.T) { + if tt.expectedErr { + r, err := date.RangeFromString(tt.rangeStr) + assert.Error(t, err) + assert.Nil(t, r) + + } else { + r, err := date.RangeFromString(tt.rangeStr) + assert.NoError(t, err) + assert.Equal(t, tt.expected, r.Is(tt.testDate)) + } + }) + } + }) + + t.Run("Range brackets", func(t *testing.T) { + tests := []struct { + expectedErr bool + start string + from string + to string + end string + expected func() any + }{ + {false, "[", "2025-01-31", "2026-01-31", "]", func() any { + val, err := date.NewRangeStrict("2025-01-31", "2026-01-31") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "2026-01-31 17:18:19", "]", func() any { + val, err := date.NewRangeStrict("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "", "2026-01-31 17:18:19", "]", func() any { + val, err := date.NewRangeStrict("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "", "]", func() any { + val, err := date.NewRangeStrict("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "2026-01-31 17:18:19", "]", func() any { + val, err := date.NewRangeStartOptional("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "", "2026-01-31 17:18:19", "]", func() any { + val, err := date.NewRangeStartOptional("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "", "]", func() any { + val, err := date.NewRangeStartOptional("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "2026-01-31 17:18:19", ")", func() any { + val, err := date.NewRangeOptional("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "", "2026-01-31 17:18:19", ")", func() any { + val, err := date.NewRangeOptional("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "", ")", func() any { + val, err := date.NewRangeOptional("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "2026-01-31 17:18:19", ")", func() any { + val, err := date.NewRangeStartStrict("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "", "2026-01-31 17:18:19", ")", func() any { + val, err := date.NewRangeStartStrict("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "", ")", func() any { + val, err := date.NewRangeStartStrict("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + } + + for _, tt := range tests { + t.Run("New Range brackets: "+fmt.Sprintf("From: %s%s, To: %s%s", tt.start, tt.from, tt.to, tt.end), func(t *testing.T) { + r, err := date.NewRange(tt.from, tt.to, datetime.RangeStart(tt.start), datetime.RangeEnd(tt.end)) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected(), r) + } + }) + } + }) +} diff --git a/tests/datetime_test.go b/tests/datetime_test.go index 35069df..d7cbb5d 100644 --- a/tests/datetime_test.go +++ b/tests/datetime_test.go @@ -1,6 +1,7 @@ package tests import ( + "fmt" "github.com/gouef/datetime" "github.com/gouef/datetime/date" "github.com/stretchr/testify/assert" @@ -20,10 +21,13 @@ func TestNewDateTime(t *testing.T) { err bool }{ {2024, 12, 25, 0, 0, 0, &datetime.DateTime{Year: 2024, Month: 12, Day: 25, DateTime: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC)}, false}, - {2024, 2, 30, 0, 0, 0, nil, true}, // Invalid day for February - {2024, 13, 10, 0, 0, 0, nil, true}, // Invalid month (13) - {2024, 4, 31, 0, 0, 0, nil, true}, // April has only 30 days - {-2024, 12, 25, 0, 0, 0, nil, true}, // April has only 30 days + {2024, 2, 30, 0, 0, 0, nil, true}, + {2024, 13, 10, 0, 0, 0, nil, true}, + {2024, 4, 31, 0, 0, 0, nil, true}, + {-2024, 12, 25, 0, 0, 0, nil, true}, + {2024, 1, 31, 24, 0, 0, nil, true}, + {2024, 1, 31, 23, 60, 0, nil, true}, + {2024, 1, 31, 23, 59, 60, nil, true}, } for _, tt := range tests { @@ -207,6 +211,9 @@ func TestDateTimeFromString(t *testing.T) { }{ {"2025-01-31", true, nil}, {"2025-01-31 23:27:15", false, validDate}, + {"2025-01-31 24:27:15", true, nil}, + {"2025-01-31 23:60:15", true, nil}, + {"2025-01-31 23:27:65", true, nil}, {"2025-02-31", true, nil}, {"2025-13-32", true, nil}, {"-2025-06-31", true, nil}, @@ -230,3 +237,209 @@ func TestDateTimeFromString(t *testing.T) { }) } } + +func TestRange(t *testing.T) { + t.Run("New Range", func(t *testing.T) { + tests := []struct { + rangeStr string + testDate any + expectedErr bool + expectedValidErr bool + expected func() any + }{ + {"[2025-01-31, 2026-01-31]", "2025-05-06", true, false, func() any { + val, err := datetime.NewRangeStrict( + "2025-01-31", "2026-01-31") + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", false, false, func() any { + val, err := datetime.NewRange( + "2025-01-31 14:15:16", "2026-01-31 17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", false, false, func() any { + val, err := datetime.NewRange( + "", "2026-01-31 17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, ]", "2025-05-06 20:21:22", false, false, func() any { + val, err := datetime.NewRange( + "2025-01-31 14:15:16", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, ]", time.Date(2025, 5, 6, 20, 21, 22, 0, time.UTC), false, false, func() any { + val, err := datetime.NewRange( + "2025-01-31 14:15:16", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[2025-01-31, 2026-01-31]", "2025-05-06", true, false, nil}, + {"[2025-01-31 14:15:16, 2026-01-31]", "2025-05-06", true, false, nil}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", 2025, false, true, func() any { + val, err := datetime.NewRange( + "2025-01-31 14:15:16", "2026-01-31 17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[, ]", datetime.Now(), false, true, func() any { + val, err := datetime.NewRange( + "", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[2025-01-31 14:15:16, 2026-02-31 14:15:16]", datetime.Now(), true, true, func() any { + val, err := datetime.NewRange( + "2025-01-31 14:15:16", "2026-02-31 14:15:16", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.Error(t, err) + return val + }}, + } + + for _, tt := range tests { + t.Run("New Range: "+tt.rangeStr+fmt.Sprintf(", valid date: %v", tt.testDate), func(t *testing.T) { + if tt.expectedErr { + r, err := datetime.RangeFromString(tt.rangeStr) + assert.Error(t, err) + assert.Nil(t, r) + + } else { + r, err := datetime.RangeFromString(tt.rangeStr) + assert.NoError(t, err) + assert.Equal(t, tt.expected(), r) + if tt.expectedValidErr { + assert.False(t, r.Is(tt.testDate)) + } else { + assert.True(t, r.Is(tt.testDate)) + assert.Equal(t, tt.rangeStr, r.String()) + } + } + }) + } + + }) + + t.Run("Range Is", func(t *testing.T) { + tests := []struct { + rangeStr string + testDate any + expectedErr bool + expected bool + }{ + {"[2025-01-31, 2026-01-31]", "2025-05-06", true, false}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", false, true}, + {"[, 2026-01-31 17:18:19]", "2025-05-06 20:21:22", false, true}, + {"[2025-01-31 14:15:16, ]", "2025-05-06 20:21:22", false, true}, + {"[2025-01-31 14:15:16, ]", time.Date(2025, 5, 6, 20, 21, 22, 0, time.UTC), false, true}, + {"[2025-01-31, 2026-01-31]", "2025-05-06", true, false}, + {"[2025-01-31 14:15:16, 2026-01-31]", "2025-05-06", true, false}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", 2025, false, false}, + {"[, ]", datetime.Now(), false, false}, + {"[2, ]", datetime.Now(), true, false}, + } + + for _, tt := range tests { + t.Run("New Range: "+tt.rangeStr+fmt.Sprintf(", valid date: %v", tt.testDate), func(t *testing.T) { + if tt.expectedErr { + r, err := datetime.RangeFromString(tt.rangeStr) + assert.Error(t, err) + assert.Nil(t, r) + + } else { + r, err := datetime.RangeFromString(tt.rangeStr) + assert.NoError(t, err) + assert.Equal(t, tt.expected, r.Is(tt.testDate)) + } + }) + } + }) + + t.Run("Range brackets", func(t *testing.T) { + tests := []struct { + expectedErr bool + start string + from string + to string + end string + expected func() any + }{ + {true, "[", "2025-01-31", "2026-01-31", "]", nil}, + {false, "[", "2025-01-31 14:15:16", "2026-01-31 17:18:19", "]", func() any { + val, err := datetime.NewRangeStrict("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "", "2026-01-31 17:18:19", "]", func() any { + val, err := datetime.NewRangeStrict("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "", "]", func() any { + val, err := datetime.NewRangeStrict("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "2026-01-31 17:18:19", "]", func() any { + val, err := datetime.NewRangeStartOptional("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "", "2026-01-31 17:18:19", "]", func() any { + val, err := datetime.NewRangeStartOptional("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "", "]", func() any { + val, err := datetime.NewRangeStartOptional("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "2026-01-31 17:18:19", ")", func() any { + val, err := datetime.NewRangeOptional("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "", "2026-01-31 17:18:19", ")", func() any { + val, err := datetime.NewRangeOptional("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "", ")", func() any { + val, err := datetime.NewRangeOptional("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "2026-01-31 17:18:19", ")", func() any { + val, err := datetime.NewRangeStartStrict("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "", "2026-01-31 17:18:19", ")", func() any { + val, err := datetime.NewRangeStartStrict("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "", ")", func() any { + val, err := datetime.NewRangeStartStrict("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + } + + for _, tt := range tests { + t.Run("New Range brackets: "+fmt.Sprintf("From: %s%s, To: %s%s", tt.start, tt.from, tt.to, tt.end), func(t *testing.T) { + r, err := datetime.NewRange(tt.from, tt.to, datetime.RangeStart(tt.start), datetime.RangeEnd(tt.end)) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected(), r) + } + }) + } + }) +} diff --git a/tests/time_test.go b/tests/time_test.go index 89fda88..33b3e0d 100644 --- a/tests/time_test.go +++ b/tests/time_test.go @@ -1,6 +1,7 @@ package tests import ( + "fmt" "github.com/gouef/datetime" "github.com/gouef/datetime/time" "github.com/stretchr/testify/assert" @@ -152,3 +153,226 @@ func TestTimeFromString(t *testing.T) { }) } } + +func TestTimeRange(t *testing.T) { + t.Run("New Range", func(t *testing.T) { + + val, err := time.NewRange("", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.Error(t, err) + assert.Nil(t, val) + + val, err = time.NewRange("", "invalid", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.Error(t, err) + assert.Nil(t, val) + + tests := []struct { + rangeStr string + testDate any + expectedErr bool + expectedValidErr bool + expected func() any + }{ + {"[2025-01-31, 2026-01-31]", "2025-05-06", true, false, func() any { + val, err := time.NewRangeStrict( + "2025-01-31", "2026-01-31") + assert.NoError(t, err) + return val + }}, + {"[14:15:16, 17:18:19]", "2025-05-06 15:21:22", false, false, func() any { + val, err := time.NewRange( + "14:15:16", "17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[, 17:18:19]", "2025-05-06 15:21:22", false, false, func() any { + val, err := time.NewRange( + "", "17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[14:15:16, ]", "2025-05-06 15:21:22", false, false, func() any { + val, err := time.NewRange( + "14:15:16", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[14:15:16, ]", goTime.Date(2025, 5, 6, 20, 21, 22, 0, goTime.UTC), false, false, func() any { + val, err := time.NewRange( + "14:15:16", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[14:15:16, 17:18:19]", 2025, false, true, func() any { + val, err := time.NewRange( + "14:15:16", "17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[invalid, 17:18:19]", 2025, true, true, func() any { + val, err := time.NewRange( + "14:15:16", "17:18:19", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[14:15:16, invalid]", 2025, true, true, func() any { + val, err := time.NewRange( + "14:15:16", "invalid", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.NoError(t, err) + return val + }}, + {"[, ]", time.Now(), true, true, func() any { + val, err := time.NewRange( + "", "", datetime.RangeStart("["), datetime.RangeEnd("]")) + assert.Error(t, err) + return val + }}, + } + + for _, tt := range tests { + t.Run("New Range: "+tt.rangeStr+fmt.Sprintf(", valid date: %v", tt.testDate), func(t *testing.T) { + if tt.expectedErr { + r, err := time.RangeFromString(tt.rangeStr) + assert.Error(t, err) + assert.Nil(t, r) + + } else { + r, err := time.RangeFromString(tt.rangeStr) + assert.NoError(t, err) + assert.Equal(t, tt.expected(), r) + if tt.expectedValidErr { + assert.False(t, r.Is(tt.testDate)) + } else { + assert.True(t, r.Is(tt.testDate)) + assert.Equal(t, tt.rangeStr, r.String()) + } + } + }) + } + + }) + + t.Run("Range Is", func(t *testing.T) { + ttTime, err := time.New(20, 21, 22) + assert.Nil(t, err) + + tests := []struct { + rangeStr string + testDate any + expectedErr bool + expected bool + }{ + {"[2025-01-31, 2026-01-31]", "2025-05-06", true, false}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", "2025-05-06 15:21:22", false, true}, + {"[, 2026-01-31 17:18:19]", "2025-05-06 16:21:22", false, true}, + {"[2025-01-31 14:15:16, ]", "2025-05-06 15:21:22", false, true}, + {"[2025-01-31 14:15:16, ]", goTime.Date(2025, 5, 6, 20, 21, 22, 0, goTime.UTC), false, true}, + {"[2025-01-31 14:15:16, ]", ttTime, false, true}, + {"[2025-01-31, 2026-01-31]", "2025-05-06", true, false}, + {"[2025-01-31 14:15:16, 2026-01-31]", "2025-05-06", true, false}, + {"[2025-01-31 14:15:16, 2026-01-31 17:18:19]", 2025, false, false}, + {"[, ]", time.Now(), true, false}, + {"[2, ]", time.Now(), true, false}, + } + + for _, tt := range tests { + t.Run("New Range: "+tt.rangeStr+fmt.Sprintf(", valid date: %v", tt.testDate), func(t *testing.T) { + if tt.expectedErr { + r, err := time.RangeFromString(tt.rangeStr) + assert.Error(t, err) + assert.Nil(t, r) + + } else { + r, err := time.RangeFromString(tt.rangeStr) + assert.NoError(t, err) + assert.Equal(t, tt.expected, r.Is(tt.testDate)) + } + }) + } + }) + + t.Run("Range brackets", func(t *testing.T) { + tests := []struct { + expectedErr bool + start string + from string + to string + end string + expected func() any + }{ + {true, "[", "2025-01-31", "2026-01-31", "]", nil}, + {false, "[", "2025-01-31 14:15:16", "2026-01-31 17:18:19", "]", func() any { + val, err := time.NewRangeStrict("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "", "2026-01-31 17:18:19", "]", func() any { + val, err := time.NewRangeStrict("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "", "]", func() any { + val, err := time.NewRangeStrict("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "2026-01-31 17:18:19", "]", func() any { + val, err := time.NewRangeStartOptional("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "", "2026-01-31 17:18:19", "]", func() any { + val, err := time.NewRangeStartOptional("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "", "]", func() any { + val, err := time.NewRangeStartOptional("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "2026-01-31 17:18:19", ")", func() any { + val, err := time.NewRangeOptional("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "", "2026-01-31 17:18:19", ")", func() any { + val, err := time.NewRangeOptional("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "(", "2025-01-31 14:15:16", "", ")", func() any { + val, err := time.NewRangeOptional("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "2026-01-31 17:18:19", ")", func() any { + val, err := time.NewRangeStartStrict("2025-01-31 14:15:16", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "", "2026-01-31 17:18:19", ")", func() any { + val, err := time.NewRangeStartStrict("", "2026-01-31 17:18:19") + assert.NoError(t, err) + return val + }}, + {false, "[", "2025-01-31 14:15:16", "", ")", func() any { + val, err := time.NewRangeStartStrict("2025-01-31 14:15:16", "") + assert.NoError(t, err) + return val + }}, + } + + for _, tt := range tests { + t.Run("New Range brackets: "+fmt.Sprintf("From: %s%s, To: %s%s", tt.start, tt.from, tt.to, tt.end), func(t *testing.T) { + r, err := time.NewRange(tt.from, tt.to, datetime.RangeStart(tt.start), datetime.RangeEnd(tt.end)) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected(), r) + } + }) + } + }) +} diff --git a/time/range.go b/time/range.go index 1989258..704c638 100644 --- a/time/range.go +++ b/time/range.go @@ -11,7 +11,7 @@ import ( ) const ( - RangeRegexp = `^([\[\(])(\d{4}-\d{2}-\d{2})?,(\d{4}-\d{2}-\d{2})?([\]\)])$` + RangeRegexp = `^([\[\(])` + DateTimeRegexp + `?\s*,\s*` + DateTimeRegexp + `?([\]\)])$` ) type Range struct { @@ -21,13 +21,58 @@ type Range struct { end datetime.RangeEnd } -func NewDateRange(from, to string, start datetime.RangeStart, end datetime.RangeEnd) *Range { +func NewRange(from, to string, start datetime.RangeStart, end datetime.RangeEnd) (*Range, error) { + + if from == "" && to == "" { + return nil, errors.New("from and to (both) can not be empty") + } + + validFrom, err := getTimeFromDateTime(from) + + if err != nil && from != "" { + return nil, errors.New(fmt.Sprintf("from (%s) is not valid", from)) + } + + validTo, err := getTimeFromDateTime(to) + + if err != nil && to != "" { + return nil, errors.New(fmt.Sprintf("to (%s) is not valid", to)) + } + + if validFrom == nil { + from = "" + } else { + from = validFrom.ToString() + } + + if validTo == nil { + to = "" + } else { + to = validTo.ToString() + } + return &Range{ from: Value(from), to: Value(to), start: start, end: end, - } + }, nil +} + +func NewRangeOptional(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartOptional, datetime.RangeEndOptional) +} + +func NewRangeStrict(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartStrict, datetime.RangeEndStrict) +} + +func NewRangeStartStrict(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartStrict, datetime.RangeEndOptional) +} + +func NewRangeStartOptional(from, to string) (*Range, error) { + return NewRange(from, to, datetime.RangeStartOptional, datetime.RangeEndStrict) } func RangeFromString(dateRange string) (*Range, error) { @@ -39,9 +84,13 @@ func RangeFromString(dateRange string) (*Range, error) { re := regexp.MustCompile(RangeRegexp) match := re.FindStringSubmatch(dateRange) - openBracket, date1, date2, closeBracket := match[1], match[2], match[3], match[4] + openBracket, date1, date2, closeBracket := match[1], match[7], match[17], match[22] - return NewDateRange(date1, date2, datetime.RangeStart(openBracket), datetime.RangeEnd(closeBracket)), nil + if date1 == "" && date2 == "" { + return nil, errors.New(fmt.Sprintf("time range from and to can not be both empty \"%s\"", dateRange)) + } + + return NewRange(date1, date2, datetime.RangeStart(openBracket), datetime.RangeEnd(closeBracket)) } func (d *Range) Start() datetime.RangeStart { @@ -52,8 +101,16 @@ func (d *Range) End() datetime.RangeEnd { return d.end } +func (d *Range) From() datetime.ValueInterface { + return d.from +} + +func (d *Range) To() datetime.ValueInterface { + return d.to +} + func (d *Range) String() string { - return fmt.Sprintf("%s%s,%s%s", d.start, d.from, d.to, d.end) + return fmt.Sprintf("%s%s, %s%s", d.Start(), d.From(), d.To(), d.End()) } func (d *Range) Is(value any) bool { @@ -63,19 +120,30 @@ func (d *Range) Is(value any) bool { return false } - start, _ := FromString(string(d.start)) - end, _ := FromString(string(d.end)) - return date.Between(start, end) + from, _ := FromString(string(d.from)) + to, _ := FromString(string(d.to)) + + if from == nil { + return date.Before(to) + } + + if to == nil { + return date.After(from) + } + + return date.Between(from, to) +} + +func getTimeFromDateTime(date string) (datetime.Interface, error) { + return FromString(date) } func (d *Range) format(date any) (datetime.Interface, error) { switch i := date.(type) { case time.Time: - return New(i.Year(), int(i.Month()), i.Day()) + return New(i.Hour(), i.Minute(), i.Second()) case *Time: return i, nil - case Time: - return &i, nil case string: return FromString(i) default: diff --git a/time/time.go b/time/time.go index 99f4dac..f325011 100644 --- a/time/time.go +++ b/time/time.go @@ -13,7 +13,10 @@ import ( const ( Regexp = `^(\d{2}):(\d{2}):(\d{2})?$` - DateTimeRegexp = `^(((\d{4})-(\d{2})-(\d{2}))( ))?((\d{2}):(\d{2}):(\d{2}))$` + DateRegexp = datetime.YearRegexp + `-` + datetime.MonthRegexp + `-` + datetime.DayRegexp + TimeRegexp = `(` + datetime.HourRegexp + `):(` + datetime.MinuteRegexp + `):(` + datetime.SecondRegexp + `)` + DateTimeRegexp = `((` + DateRegexp + `)?\s*(` + TimeRegexp + `))` + //DateTimeRegexp = `^((` + datetime.DateRegexp + `)?\s*((` + datetime.HourRegexp + `):(` + datetime.MinuteRegexp + `):(` + datetime.SecondRegexp + `)))$` ) type Time struct { @@ -23,6 +26,17 @@ type Time struct { DateTime goTime.Time } +func Now() *Time { + now := goTime.Now() + + return &Time{ + Hour: now.Hour(), + Minute: now.Minute(), + Second: now.Second(), + DateTime: now, + } +} + func New(hour, minute, second int) (datetime.Interface, error) { errs := validator.Validate(hour, constraints.Range{Min: 0, Max: 23}) @@ -54,7 +68,7 @@ func FromString(value string) (datetime.Interface, error) { errs := validator.Validate(value, constraints.RegularExpression{Regexp: DateTimeRegexp}) if len(errs) != 0 { - return nil, errors.New(fmt.Sprintf("unsupported format of date \"%s\"", value)) + return nil, errors.New(fmt.Sprintf("unsupported format of time \"%s\"", value)) } re := regexp.MustCompile(DateTimeRegexp) @@ -89,5 +103,13 @@ func (t *Time) Equal(u datetime.Interface) bool { } func (t *Time) Between(start, end datetime.Interface) bool { - return t.Time().Before(end.Time()) && t.Time().After(start.Time()) + return t.Before(end) && t.After(start) +} + +func (t *Time) Before(u datetime.Interface) bool { + return t.Time().Before(u.Time()) +} + +func (t *Time) After(u datetime.Interface) bool { + return t.Time().After(u.Time()) } diff --git a/valueInterface.go b/valueInterface.go new file mode 100644 index 0000000..32c4274 --- /dev/null +++ b/valueInterface.go @@ -0,0 +1,5 @@ +package datetime + +type ValueInterface interface { + Date() Interface +}