diff --git a/date.go b/date/date.go similarity index 67% rename from date.go rename to date/date.go index a1f8a56..1546528 100644 --- a/date.go +++ b/date/date.go @@ -1,8 +1,9 @@ -package datetime +package date import ( "errors" "fmt" + "github.com/gouef/datetime" "github.com/gouef/utils" "github.com/gouef/validator" "github.com/gouef/validator/constraints" @@ -12,7 +13,8 @@ import ( ) const ( - dateRegexp = `^(\d{4})-(\d{2})-(\d{2})?$` + Regexp = `^(\d{4})-(\d{2})-(\d{2})?$` + DateTimeRegexp = `^(((\d{4})-(\d{2})-(\d{2}))( )?)((\d{2}):(\d{2}):(\d{2}))?$` ) type Date struct { @@ -22,7 +24,7 @@ type Date struct { DateTime time.Time } -func NewDate(year, month, day int) (*Date, error) { +func New(year, month, day int) (datetime.Interface, error) { errs := validator.Validate(year, constraints.GreaterOrEqual{Value: 0}) if len(errs) > 0 { @@ -34,7 +36,7 @@ func NewDate(year, month, day int) (*Date, error) { if len(errs) > 0 { return nil, errors.New(fmt.Sprintf("month must be between 1-12 get \"%d\"", month)) } - daysInMonth := DaysInMonth(year, month) + daysInMonth := datetime.DaysInMonth(year, month) errs = validator.Validate(day, constraints.Range{Min: 1, Max: float64(daysInMonth)}) if len(errs) > 0 { @@ -49,20 +51,28 @@ func NewDate(year, month, day int) (*Date, error) { }, nil } -func DateFromString(value string) (*Date, error) { - errs := validator.Validate(value, constraints.RegularExpression{Regexp: dateTimeRegexp}) +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)) } - re := regexp.MustCompile(dateTimeRegexp) + re := regexp.MustCompile(DateTimeRegexp) match := re.FindStringSubmatch(value) - year, _ := strconv.Atoi(match[1]) - month, _ := strconv.Atoi(match[2]) - day, _ := strconv.Atoi(match[3]) + year, _ := strconv.Atoi(match[3]) + month, _ := strconv.Atoi(match[4]) + day, _ := strconv.Atoi(match[5]) - return NewDate(year, month, day) + return New(year, month, day) +} + +func (d *Date) FromString(value string) (datetime.Interface, error) { + return FromString(value) +} + +func (d *Date) ToString() string { + return d.Time().Format(time.DateOnly) } func (d *Date) IsWeekend() bool { @@ -76,14 +86,14 @@ func (d *Date) Time() time.Time { // Compare compares the date instant d with u. If d is before u, it returns -1; // if d is after u, it returns +1; if they're the same, it returns 0. -func (d *Date) Compare(u *Date) int { +func (d *Date) Compare(u datetime.Interface) int { return d.Time().Compare(u.Time()) } -func (d *Date) Equal(u *Date) bool { +func (d *Date) Equal(u datetime.Interface) bool { return d.Time().Equal(u.Time()) } -func (d *Date) Between(start, end *Date) bool { +func (d *Date) Between(start, end datetime.Interface) bool { return d.Time().Before(end.Time()) && d.Time().After(start.Time()) } diff --git a/date/range.go b/date/range.go new file mode 100644 index 0000000..00e96e3 --- /dev/null +++ b/date/range.go @@ -0,0 +1,84 @@ +package date + +import ( + "errors" + "fmt" + "github.com/gouef/datetime" + "github.com/gouef/validator" + "github.com/gouef/validator/constraints" + "regexp" + "time" +) + +const ( + RangeRegexp = `^([\[\(])(\d{4}-\d{2}-\d{2})?,(\d{4}-\d{2}-\d{2})?([\]\)])$` +) + +type Range struct { + from Value + to Value + start datetime.RangeStart + end datetime.RangeEnd +} + +func NewDateRange(from, to string, start datetime.RangeStart, end datetime.RangeEnd) *Range { + return &Range{ + from: Value(from), + to: Value(to), + start: start, + end: end, + } +} + +func RangeFromString(dateRange string) (*Range, error) { + errs := validator.Validate(dateRange, constraints.RegularExpression{Regexp: RangeRegexp}) + + if len(errs) != 0 { + return nil, errors.New(fmt.Sprintf("unsupported format of date range \"%s\"", dateRange)) + } + + re := regexp.MustCompile(RangeRegexp) + match := re.FindStringSubmatch(dateRange) + openBracket, date1, date2, closeBracket := match[1], match[2], match[3], match[4] + + return NewDateRange(date1, date2, datetime.RangeStart(openBracket), datetime.RangeEnd(closeBracket)), nil +} + +func (d *Range) Start() datetime.RangeStart { + return d.start +} + +func (d *Range) End() datetime.RangeEnd { + return d.end +} + +func (d *Range) String() string { + return fmt.Sprintf("%s%s,%s%s", d.start, d.from, d.to, d.end) +} + +func (d *Range) Is(value any) bool { + date, err := d.format(value) + + if err != nil { + return false + } + + start, _ := FromString(string(d.start)) + end, _ := FromString(string(d.end)) + return date.Between(start, end) +} + +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()) + case *Date: + return i, nil + case Date: + return &i, nil + case string: + return FromString(i) + default: + return nil, errors.New("unsupported format of date") + } +} diff --git a/date/value.go b/date/value.go new file mode 100644 index 0000000..9b4d3e3 --- /dev/null +++ b/date/value.go @@ -0,0 +1,35 @@ +package date + +import ( + "errors" + "fmt" + "github.com/gouef/datetime" + "github.com/gouef/validator" + "github.com/gouef/validator/constraints" +) + +type Value string + +func StringToValue(value string) (Value, error) { + errs := validator.Validate(value, constraints.RegularExpression{Regexp: DateTimeRegexp}) + + d, err := FromString(value) + + if len(errs) != 0 || err != nil { + return "", errors.New(fmt.Sprintf("unsupported format of date \"%s\"", value)) + } + + str := d.ToString() + + return Value(str), nil +} + +func (d Value) Date() datetime.Interface { + date, err := FromString(string(d)) + + if err != nil { + return nil + } + + return date +} diff --git a/dateRange.go b/dateRange.go deleted file mode 100644 index 1f3facc..0000000 --- a/dateRange.go +++ /dev/null @@ -1,83 +0,0 @@ -package datetime - -import ( - "errors" - "fmt" - "github.com/gouef/validator" - "github.com/gouef/validator/constraints" - "regexp" - "time" -) - -const ( - dateRangeRegexp = `^([\[\(])(\d{4}-\d{2}-\d{2})?,(\d{4}-\d{2}-\d{2})?([\]\)])$` -) - -type DateRange struct { - from DateValue - to DateValue - start RangeStart - end RangeEnd -} - -func NewDateRange(from, to string, start RangeStart, end RangeEnd) *DateRange { - return &DateRange{ - from: DateValue(from), - to: DateValue(to), - start: start, - end: end, - } -} - -func DateRangeFromString(dateRange string) (*DateRange, error) { - errs := validator.Validate(dateRange, constraints.RegularExpression{Regexp: dateRangeRegexp}) - - if len(errs) != 0 { - return nil, errors.New(fmt.Sprintf("unsupported format of date range \"%s\"", dateRange)) - } - - re := regexp.MustCompile(dateRangeRegexp) - match := re.FindStringSubmatch(dateRange) - openBracket, date1, date2, closeBracket := match[1], match[2], match[3], match[4] - - return NewDateRange(date1, date2, RangeStart(openBracket), RangeEnd(closeBracket)), nil -} - -func (d *DateRange) Start() RangeStart { - return d.start -} - -func (d *DateRange) End() RangeEnd { - return d.end -} - -func (d *DateRange) String() string { - return fmt.Sprintf("%s%s,%s%s", d.start, d.from, d.to, d.end) -} - -func (d *DateRange) Is(value any) bool { - date, err := d.format(value) - - if err != nil { - return false - } - - start, _ := DateFromString(string(d.start)) - end, _ := DateFromString(string(d.end)) - return date.Between(start, end) -} - -func (d *DateRange) format(date any) (*Date, error) { - switch i := date.(type) { - case time.Time: - return NewDate(i.Year(), int(i.Month()), i.Day()) - case *Date: - return i, nil - case Date: - return &i, nil - case string: - return DateFromString(i) - default: - return nil, errors.New("unsupported format of date") - } -} diff --git a/dateTimeInterface.go b/dateTimeInterface.go new file mode 100644 index 0000000..23361bd --- /dev/null +++ b/dateTimeInterface.go @@ -0,0 +1,12 @@ +package datetime + +import "time" + +type Interface interface { + ToString() string + FromString(value string) (Interface, error) + Time() time.Time + Equal(u Interface) bool + Between(start, end Interface) bool + Compare(u Interface) int +} diff --git a/dateValue.go b/dateValue.go deleted file mode 100644 index 5573bd3..0000000 --- a/dateValue.go +++ /dev/null @@ -1,30 +0,0 @@ -package datetime - -import ( - "errors" - "fmt" - "github.com/gouef/validator" - "github.com/gouef/validator/constraints" -) - -type DateValue string - -func StringToDateValue(value string) (DateValue, error) { - errs := validator.Validate(value, constraints.RegularExpression{Regexp: dateRegexp}) - - if len(errs) != 0 { - return "", errors.New(fmt.Sprintf("unsupported format of date \"%s\"", value)) - } - - return DateValue(value), nil -} - -func (d DateValue) Date() *Date { - date, err := DateFromString(string(d)) - - if err != nil { - return nil - } - - return date -} diff --git a/datetime.go b/datetime.go index 4225835..0c34c57 100644 --- a/datetime.go +++ b/datetime.go @@ -12,7 +12,7 @@ import ( ) const ( - dateTimeRegexp = `^(\d{4})-(\d{2})-(\d{2})( (\d{2}):(\d{2}):(\d{2}))?$` + Regexp = `^(\d{4})-(\d{2})-(\d{2})( (\d{2}):(\d{2}):(\d{2}))?$` ) type DateTime struct { @@ -39,7 +39,7 @@ func Now() *DateTime { } } -func NewDateTime(year, month, day, hour, minute, second int) (*DateTime, error) { +func New(year, month, day, hour, minute, second int) (*DateTime, error) { errs := validator.Validate(year, constraints.GreaterOrEqual{Value: 0}) if len(errs) > 0 { @@ -87,14 +87,14 @@ func NewDateTime(year, month, day, hour, minute, second int) (*DateTime, error) }, nil } -func DateTimeFromString(value string) (*DateTime, error) { - errs := validator.Validate(value, constraints.RegularExpression{Regexp: dateTimeRegexp}) +func FromString(value string) (Interface, error) { + errs := validator.Validate(value, constraints.RegularExpression{Regexp: Regexp}) if len(errs) != 0 { return nil, errors.New(fmt.Sprintf("unsupported format of date \"%s\"", value)) } - re := regexp.MustCompile(dateTimeRegexp) + re := regexp.MustCompile(Regexp) match := re.FindStringSubmatch(value) year, _ := strconv.Atoi(match[1]) month, _ := strconv.Atoi(match[2]) @@ -105,12 +105,20 @@ func DateTimeFromString(value string) (*DateTime, error) { minute, _ := strconv.Atoi(match[6]) second, _ := strconv.Atoi(match[7]) - return NewDateTime(year, month, day, hour, minute, second) + return New(year, month, day, hour, minute, second) } return nil, errors.New(fmt.Sprintf("unsupported format of datetime \"%s\". time is missing.", value)) } +func (d *DateTime) FromString(value string) (Interface, error) { + return FromString(value) +} + +func (d *DateTime) ToString() string { + return d.Time().Format(time.DateTime) +} + func GetDate(year, month, day int) time.Time { return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) } @@ -121,20 +129,20 @@ func (d *DateTime) IsWeekend() bool { } func (d *DateTime) Time() time.Time { - return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, time.UTC) + return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, 0, time.UTC) } // Compare compares the date instant d with u. If d is before u, it returns -1; // if d is after u, it returns +1; if they're the same, it returns 0. -func (d *DateTime) Compare(u *DateTime) int { +func (d *DateTime) Compare(u Interface) int { return d.Time().Compare(u.Time()) } -func (d *DateTime) Equal(u *DateTime) bool { +func (d *DateTime) Equal(u Interface) bool { return d.Time().Equal(u.Time()) } -func (d *DateTime) Between(start, end *DateTime) bool { +func (d *DateTime) Between(start, end Interface) bool { return d.Time().Before(end.Time()) && d.Time().After(start.Time()) } diff --git a/go.mod b/go.mod index 30e4215..f25b943 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,11 @@ require ( github.com/stretchr/testify v1.10.0 ) +replace ( + github.com/gouef/datetime/date => ./date + github.com/gouef/datetime/time => ./time +) + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/gouef/country v1.0.2 // indirect diff --git a/tests/date_test.go b/tests/date_test.go new file mode 100644 index 0000000..afab41f --- /dev/null +++ b/tests/date_test.go @@ -0,0 +1,202 @@ +package tests + +import ( + "github.com/gouef/datetime" + "github.com/gouef/datetime/date" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestNewDate(t *testing.T) { + tests := []struct { + year int + month int + day int + expected *date.Date + err bool + }{ + {2024, 12, 25, &date.Date{Year: 2024, Month: 12, Day: 25, DateTime: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC)}, false}, + {2024, 2, 30, nil, true}, // Invalid day for February + {2024, 13, 10, nil, true}, // Invalid month (13) + {2024, 4, 31, nil, true}, // April has only 30 days + {-2024, 12, 25, nil, true}, // April has only 30 days + } + + for _, tt := range tests { + t.Run("TestNewDate", func(t *testing.T) { + d, err := date.New(tt.year, tt.month, tt.day) + if tt.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, d) + } + }) + } +} + +func TestDateIsWeekend(t *testing.T) { + tests := []struct { + date *date.Date + expected bool + }{ + {&date.Date{Year: 2024, Month: 12, Day: 21, DateTime: time.Date(2024, 12, 21, 0, 0, 0, 0, time.UTC)}, true}, // Saturday + {&date.Date{Year: 2024, Month: 12, Day: 22, DateTime: time.Date(2024, 12, 22, 0, 0, 0, 0, time.UTC)}, true}, // Sunday + {&date.Date{Year: 2024, Month: 12, Day: 23, DateTime: time.Date(2024, 12, 23, 0, 0, 0, 0, time.UTC)}, false}, // Monday + } + + for _, tt := range tests { + t.Run("TestIsWeekend", func(t *testing.T) { + assert.Equal(t, tt.expected, tt.date.IsWeekend()) + }) + } +} + +func TestDateCompare(t *testing.T) { + tests := []struct { + date1 *date.Date + date2 *date.Date + expected int + }{ + {&date.Date{Year: 2024, Month: 12, Day: 25, DateTime: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC)}, + &date.Date{Year: 2024, Month: 12, Day: 25, DateTime: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC)}, 0}, + {&date.Date{Year: 2024, Month: 12, Day: 25, DateTime: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC)}, + &date.Date{Year: 2024, Month: 12, Day: 26, DateTime: time.Date(2024, 12, 26, 0, 0, 0, 0, time.UTC)}, -1}, // 25th < 26th + {&date.Date{Year: 2024, Month: 12, Day: 26, DateTime: time.Date(2024, 12, 26, 0, 0, 0, 0, time.UTC)}, + &date.Date{Year: 2024, Month: 12, Day: 25, DateTime: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC)}, 1}, // 26th > 25th + } + + for _, tt := range tests { + t.Run("TestCompare", func(t *testing.T) { + assert.Equal(t, tt.expected, tt.date1.Compare(tt.date2)) + }) + } +} + +func TestDateEqual(t *testing.T) { + tests := []struct { + date1 *date.Date + date2 *date.Date + expected bool + }{ + // Test 1: Equal DateTime instances + { + date1: &date.Date{Year: 2024, Month: 3, Day: 31}, + date2: &date.Date{Year: 2024, Month: 3, Day: 31}, + expected: true, + }, + // Test 2: Different DateTime instances (different day) + { + date1: &date.Date{Year: 2024, Month: 3, Day: 31}, + date2: &date.Date{Year: 2024, Month: 3, Day: 30}, + expected: false, + }, + // Test 3: Different DateTime instances (different month) + { + date1: &date.Date{Year: 2024, Month: 3, Day: 31}, + date2: &date.Date{Year: 2024, Month: 4, Day: 1}, + expected: false, + }, + // Test 4: Different DateTime instances (different year) + { + date1: &date.Date{Year: 2024, Month: 3, Day: 31}, + date2: &date.Date{Year: 2025, Month: 3, Day: 31}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run("TestEqual", func(t *testing.T) { + result := tt.date1.Equal(tt.date2) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestDateBetween(t *testing.T) { + date1, _ := date.New(2025, 2, 1) + date2, _ := date.New(2024, 2, 1) + date3, _ := date.New(2026, 2, 1) + + tests := []struct { + date datetime.Interface + start datetime.Interface + end datetime.Interface + expected bool + }{ + {date1, date2, date3, true}, + {date2, date1, date3, false}, + {date3, date2, date3, false}, + } + + for _, tt := range tests { + t.Run("TestDaysInMonth", func(t *testing.T) { + assert.Equal(t, tt.expected, tt.date.Between(tt.start, tt.end)) + }) + } +} + +func TestDateFromString(t *testing.T) { + validDate, _ := date.New(2025, 1, 31) + tests := []struct { + date string + expectedErr bool + expectedDate datetime.Interface + }{ + {"2025-01-31", false, validDate}, + {"2025-01-31 23:27:15", false, validDate}, + {"2025-02-31", true, nil}, + {"2025-13-32", true, nil}, + {"-2025-06-31", true, nil}, + {"invalid", true, nil}, + } + + for _, tt := range tests { + t.Run("TestDateFromString: "+tt.date, func(t *testing.T) { + d, err := date.FromString(tt.date) + if tt.expectedErr { + assert.Nil(t, d) + assert.Error(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, tt.expectedDate, d) + + d2, err := tt.expectedDate.FromString(tt.date) + assert.Nil(t, err) + assert.Equal(t, d2, d) + } + }) + } +} + +func TestDateToString(t *testing.T) { + validDate, _ := date.New(2025, 1, 31) + tests := []struct { + expectedString string + date string + expectedErr bool + expectedDate datetime.Interface + }{ + {"2025-01-31", "2025-01-31", false, validDate}, + {"2025-01-31", "2025-01-31 23:27:15", false, validDate}, + {"2025-02-31", "2025-02-31", true, nil}, + {"2025-13-32", "2025-13-32", true, nil}, + {"-2025-06-31", "-2025-06-31", true, nil}, + {"invalid", "invalid", true, nil}, + } + + for _, tt := range tests { + t.Run("TestDateToString: "+tt.date, func(t *testing.T) { + d, err := date.FromString(tt.date) + if tt.expectedErr { + assert.Nil(t, d) + assert.Error(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, tt.expectedDate, d) + assert.Equal(t, tt.expectedString, d.ToString()) + } + }) + } +} diff --git a/tests/datetime_test.go b/tests/datetime_test.go index c7cb06e..35069df 100644 --- a/tests/datetime_test.go +++ b/tests/datetime_test.go @@ -2,37 +2,46 @@ package tests import ( "github.com/gouef/datetime" + "github.com/gouef/datetime/date" "github.com/stretchr/testify/assert" "testing" "time" ) -func TestNewDate(t *testing.T) { +func TestNewDateTime(t *testing.T) { tests := []struct { year int month int day int - expected *datetime.Date + hour int + minute int + second int + expected *datetime.DateTime err bool }{ - {2024, 12, 25, &datetime.Date{Year: 2024, Month: 12, Day: 25, DateTime: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC)}, false}, - {2024, 2, 30, nil, true}, // Invalid day for February - {2024, 13, 10, nil, true}, // Invalid month (13) - {2024, 4, 31, nil, true}, // April has only 30 days - {-2024, 12, 25, nil, true}, // April has only 30 days + {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 } for _, tt := range tests { t.Run("TestNewDate", func(t *testing.T) { - date, err := datetime.NewDate(tt.year, tt.month, tt.day) + d, err := datetime.New(tt.year, tt.month, tt.day, tt.hour, tt.minute, tt.second) if tt.err { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected, date) + assert.Equal(t, tt.expected, d) } }) } + + t.Run("Test Now", func(t *testing.T) { + d := datetime.Now() + assert.NotNil(t, d) + }) } func TestIsWeekend(t *testing.T) { @@ -73,7 +82,7 @@ func TestCompare(t *testing.T) { } } -func TestDateEqual(t *testing.T) { +func TestDateTimeEqual(t *testing.T) { tests := []struct { date1 *datetime.DateTime date2 *datetime.DateTime @@ -150,14 +159,14 @@ func TestDaysInMonth(t *testing.T) { } func TestBetween(t *testing.T) { - date1, _ := datetime.NewDate(2025, 2, 1) - date2, _ := datetime.NewDate(2024, 2, 1) - date3, _ := datetime.NewDate(2026, 2, 1) + date1, _ := date.New(2025, 2, 1) + date2, _ := date.New(2024, 2, 1) + date3, _ := date.New(2026, 2, 1) tests := []struct { - date *datetime.Date - start *datetime.Date - end *datetime.Date + date datetime.Interface + start datetime.Interface + end datetime.Interface expected bool }{ {date1, date2, date3, true}, @@ -189,30 +198,34 @@ func TestDaysInMonthByDate(t *testing.T) { } } -func TestDateFromString(t *testing.T) { - validDate, _ := datetime.NewDate(2025, 1, 31) +func TestDateTimeFromString(t *testing.T) { + validDate, _ := datetime.New(2025, 1, 31, 23, 27, 15) tests := []struct { date string expectedErr bool - expectedDate *datetime.Date + expectedDate datetime.Interface }{ - {"2025-01-31", false, validDate}, + {"2025-01-31", true, nil}, {"2025-01-31 23:27:15", false, validDate}, {"2025-02-31", true, nil}, {"2025-13-32", true, nil}, - {"-2025-01-31", true, nil}, + {"-2025-06-31", true, nil}, {"invalid", true, nil}, } for _, tt := range tests { - t.Run("TestDaysInMonthByDate", func(t *testing.T) { - date, err := datetime.DateFromString(tt.date) + t.Run("TestDaysInMonthByDate: "+tt.date, func(t *testing.T) { + d, err := datetime.FromString(tt.date) if tt.expectedErr { - assert.Nil(t, date) + assert.Nil(t, d) assert.Error(t, err) } else { assert.Nil(t, err) - assert.Equal(t, tt.expectedDate, date) + assert.Equal(t, tt.expectedDate, d) + + d2, err := tt.expectedDate.FromString(tt.date) + assert.Nil(t, err) + assert.Equal(t, d2, d) } }) } diff --git a/tests/time_test.go b/tests/time_test.go new file mode 100644 index 0000000..89fda88 --- /dev/null +++ b/tests/time_test.go @@ -0,0 +1,154 @@ +package tests + +import ( + "github.com/gouef/datetime" + "github.com/gouef/datetime/time" + "github.com/stretchr/testify/assert" + "testing" + goTime "time" +) + +func TestNewTime(t *testing.T) { + tests := []struct { + hour int + minute int + second int + expected *time.Time + err bool + }{ + {20, 12, 25, &time.Time{Hour: 20, Minute: 12, Second: 25, DateTime: goTime.Date(0, 1, 1, 20, 12, 25, 0, goTime.UTC)}, false}, + {24, 2, 30, nil, true}, // Invalid day for February + {20, 60, 10, nil, true}, // Invalid month (13) + {20, 4, 60, nil, true}, // April has only 30 days + {-20, 12, 25, nil, true}, // April has only 30 days + } + + for _, tt := range tests { + t.Run("TestNewDate", func(t *testing.T) { + d, err := time.New(tt.hour, tt.minute, tt.second) + if tt.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, d) + } + }) + } +} + +func TestTimeCompare(t *testing.T) { + tests := []struct { + date1 *time.Time + date2 *time.Time + expected int + }{ + {&time.Time{Hour: 2024, Minute: 12, Second: 25, DateTime: goTime.Date(2024, 12, 25, 0, 0, 0, 0, goTime.UTC)}, + &time.Time{Hour: 2024, Minute: 12, Second: 25, DateTime: goTime.Date(2024, 12, 25, 0, 0, 0, 0, goTime.UTC)}, 0}, + {&time.Time{Hour: 2024, Minute: 12, Second: 25, DateTime: goTime.Date(2024, 12, 25, 0, 0, 0, 0, goTime.UTC)}, + &time.Time{Hour: 2024, Minute: 12, Second: 26, DateTime: goTime.Date(2024, 12, 26, 0, 0, 0, 0, goTime.UTC)}, -1}, // 25th < 26th + {&time.Time{Hour: 2024, Minute: 12, Second: 26, DateTime: goTime.Date(2024, 12, 26, 0, 0, 0, 0, goTime.UTC)}, + &time.Time{Hour: 2024, Minute: 12, Second: 25, DateTime: goTime.Date(2024, 12, 25, 0, 0, 0, 0, goTime.UTC)}, 1}, // 26th > 25th + } + + for _, tt := range tests { + t.Run("TestCompare", func(t *testing.T) { + assert.Equal(t, tt.expected, tt.date1.Compare(tt.date2)) + }) + } +} + +func TestTimeEqual(t *testing.T) { + tests := []struct { + date1 *time.Time + date2 *time.Time + expected bool + }{ + // Test 1: Equal DateTime instances + { + date1: &time.Time{Hour: 20, Minute: 3, Second: 31}, + date2: &time.Time{Hour: 20, Minute: 3, Second: 31}, + expected: true, + }, + // Test 2: Different DateTime instances (different day) + { + date1: &time.Time{Hour: 20, Minute: 3, Second: 31}, + date2: &time.Time{Hour: 2024, Minute: 3, Second: 30}, + expected: false, + }, + // Test 3: Different DateTime instances (different month) + { + date1: &time.Time{Hour: 20, Minute: 3, Second: 31}, + date2: &time.Time{Hour: 2024, Minute: 4, Second: 1}, + expected: false, + }, + // Test 4: Different DateTime instances (different year) + { + date1: &time.Time{Hour: 20, Minute: 3, Second: 31}, + date2: &time.Time{Hour: 2025, Minute: 3, Second: 31}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run("TestEqual", func(t *testing.T) { + result := tt.date1.Equal(tt.date2) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTimeBetween(t *testing.T) { + date1, _ := time.New(20, 25, 1) + date2, _ := time.New(20, 24, 1) + date3, _ := time.New(20, 26, 1) + + tests := []struct { + date datetime.Interface + start datetime.Interface + end datetime.Interface + expected bool + }{ + {date1, date2, date3, true}, + {date2, date1, date3, false}, + {date3, date2, date3, false}, + } + + for _, tt := range tests { + t.Run("TestDaysInMonth", func(t *testing.T) { + assert.Equal(t, tt.expected, tt.date.Between(tt.start, tt.end)) + }) + } +} + +func TestTimeFromString(t *testing.T) { + validDate, _ := time.New(23, 27, 15) + tests := []struct { + date string + expectedErr bool + expectedDate datetime.Interface + }{ + {"2025-01-31", true, nil}, + {"2025-01-31 23:27:15", false, validDate}, + {"2025-02-31", true, nil}, + {"2025-13-32", true, nil}, + {"-2025-06-31", true, nil}, + {"invalid", true, nil}, + } + + for _, tt := range tests { + t.Run("TestTimeFromString: "+tt.date, func(t *testing.T) { + d, err := time.FromString(tt.date) + if tt.expectedErr { + assert.Nil(t, d) + assert.Error(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, tt.expectedDate, d) + + d2, err := tt.expectedDate.FromString(tt.date) + assert.Nil(t, err) + assert.Equal(t, d2, d) + } + }) + } +} diff --git a/tests/value_test.go b/tests/value_test.go index 24a1c31..8c310ab 100644 --- a/tests/value_test.go +++ b/tests/value_test.go @@ -2,6 +2,8 @@ package tests import ( "github.com/gouef/datetime" + "github.com/gouef/datetime/date" + "github.com/gouef/datetime/time" "github.com/stretchr/testify/assert" "testing" ) @@ -10,38 +12,56 @@ func TestValue(t *testing.T) { t.Run("DateValue", func(t *testing.T) { str := "2025-02-02" - dateValue, err := datetime.StringToDateValue(str) + dateValue, err := date.StringToValue(str) assert.Nil(t, err) - expected, _ := datetime.DateFromString(str) + expected, _ := date.FromString(str) assert.Equal(t, expected, dateValue.Date()) }) + t.Run("DateValue invalid", func(t *testing.T) { + str := "2025-02-31" + dateValue, err := date.StringToValue(str) + timeValueDate := dateValue.Date() + + assert.Error(t, err) + assert.Nil(t, timeValueDate) + }) + t.Run("TimeValue", func(t *testing.T) { str := "18:30:05" - timeValue, err := datetime.StringToTimeValue(str) + timeValue, err := time.StringToValue(str) assert.Nil(t, err) - expected, _ := datetime.TimeFromString(str) + expected, _ := time.FromString(str) assert.Equal(t, expected, timeValue.Date()) }) t.Run("TimeValue invalid", func(t *testing.T) { str := "18:60:05" - timeValue, err := datetime.StringToTimeValue(str) + timeValue, err := time.StringToValue(str) timeValueDate := timeValue.Date() - assert.Nil(t, err) + assert.Error(t, err) assert.Nil(t, timeValueDate) }) - t.Run("DateTimeValue", func(t *testing.T) { + t.Run("Value", func(t *testing.T) { str := "2025-02-02 18:30:05" - dateTimeValue, err := datetime.StringToDateTimeValue(str) + dateTimeValue, err := datetime.StringToValue(str) assert.Nil(t, err) - expected, _ := datetime.DateTimeFromString(str) + expected, _ := datetime.FromString(str) assert.Equal(t, expected, dateTimeValue.Date()) }) + t.Run("Value invalid", func(t *testing.T) { + str := "2025-02-31 18:30:05" + dateTimeValue, err := datetime.StringToValue(str) + timeValueDate := dateTimeValue.Date() + + assert.Error(t, err) + assert.Nil(t, timeValueDate) + }) + } diff --git a/time/range.go b/time/range.go new file mode 100644 index 0000000..1989258 --- /dev/null +++ b/time/range.go @@ -0,0 +1,84 @@ +package time + +import ( + "errors" + "fmt" + "github.com/gouef/datetime" + "github.com/gouef/validator" + "github.com/gouef/validator/constraints" + "regexp" + "time" +) + +const ( + RangeRegexp = `^([\[\(])(\d{4}-\d{2}-\d{2})?,(\d{4}-\d{2}-\d{2})?([\]\)])$` +) + +type Range struct { + from Value + to Value + start datetime.RangeStart + end datetime.RangeEnd +} + +func NewDateRange(from, to string, start datetime.RangeStart, end datetime.RangeEnd) *Range { + return &Range{ + from: Value(from), + to: Value(to), + start: start, + end: end, + } +} + +func RangeFromString(dateRange string) (*Range, error) { + errs := validator.Validate(dateRange, constraints.RegularExpression{Regexp: RangeRegexp}) + + if len(errs) != 0 { + return nil, errors.New(fmt.Sprintf("unsupported format of date range \"%s\"", dateRange)) + } + + re := regexp.MustCompile(RangeRegexp) + match := re.FindStringSubmatch(dateRange) + openBracket, date1, date2, closeBracket := match[1], match[2], match[3], match[4] + + return NewDateRange(date1, date2, datetime.RangeStart(openBracket), datetime.RangeEnd(closeBracket)), nil +} + +func (d *Range) Start() datetime.RangeStart { + return d.start +} + +func (d *Range) End() datetime.RangeEnd { + return d.end +} + +func (d *Range) String() string { + return fmt.Sprintf("%s%s,%s%s", d.start, d.from, d.to, d.end) +} + +func (d *Range) Is(value any) bool { + date, err := d.format(value) + + if err != nil { + return false + } + + start, _ := FromString(string(d.start)) + end, _ := FromString(string(d.end)) + return date.Between(start, end) +} + +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()) + case *Time: + return i, nil + case Time: + return &i, nil + case string: + return FromString(i) + default: + return nil, errors.New("unsupported format of date") + } +} diff --git a/time.go b/time/time.go similarity index 51% rename from time.go rename to time/time.go index d3b3ec7..99f4dac 100644 --- a/time.go +++ b/time/time.go @@ -1,27 +1,29 @@ -package datetime +package time import ( "errors" "fmt" + "github.com/gouef/datetime" "github.com/gouef/validator" "github.com/gouef/validator/constraints" "regexp" "strconv" - "time" + goTime "time" ) const ( - timeRegexp = `^(\d{2}):(\d{2}):(\d{2})?$` + Regexp = `^(\d{2}):(\d{2}):(\d{2})?$` + DateTimeRegexp = `^(((\d{4})-(\d{2})-(\d{2}))( ))?((\d{2}):(\d{2}):(\d{2}))$` ) type Time struct { Hour int `validate:"min=0,max=23"` Minute int `validate:"min=0,max=59"` Second int `validate:"min=0,max=59"` - DateTime time.Time + DateTime goTime.Time } -func NewTime(hour, minute, second int) (*Time, error) { +func New(hour, minute, second int) (datetime.Interface, error) { errs := validator.Validate(hour, constraints.Range{Min: 0, Max: 23}) if len(errs) > 0 { @@ -44,40 +46,48 @@ func NewTime(hour, minute, second int) (*Time, error) { Hour: hour, Minute: minute, Second: second, - DateTime: time.Date(0, time.Month(0), 0, hour, minute, second, 0, time.UTC), + DateTime: goTime.Date(0, goTime.Month(1), 1, hour, minute, second, 0, goTime.UTC), }, nil } -func TimeFromString(value string) (*Time, error) { - errs := validator.Validate(value, constraints.RegularExpression{Regexp: dateTimeRegexp}) +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)) } - re := regexp.MustCompile(dateTimeRegexp) + re := regexp.MustCompile(DateTimeRegexp) match := re.FindStringSubmatch(value) - hour, _ := strconv.Atoi(match[1]) - minute, _ := strconv.Atoi(match[2]) - second, _ := strconv.Atoi(match[3]) + hour, _ := strconv.Atoi(match[8]) + minute, _ := strconv.Atoi(match[9]) + second, _ := strconv.Atoi(match[10]) - return NewTime(hour, minute, second) + return New(hour, minute, second) } -func (d *Time) Time() time.Time { - return time.Date(0, time.Month(0), 0, d.Hour, d.Minute, d.Second, 0, time.UTC) +func (t *Time) FromString(value string) (datetime.Interface, error) { + return FromString(value) +} + +func (t *Time) ToString() string { + return t.Time().Format(goTime.TimeOnly) +} + +func (t *Time) Time() goTime.Time { + return goTime.Date(0, goTime.Month(1), 1, t.Hour, t.Minute, t.Second, 0, goTime.UTC) } // Compare compares the date instant d with u. If d is before u, it returns -1; // if d is after u, it returns +1; if they're the same, it returns 0. -func (d *Time) Compare(u *Time) int { - return d.Time().Compare(u.Time()) +func (t *Time) Compare(u datetime.Interface) int { + return t.Time().Compare(u.Time()) } -func (d *Time) Equal(u *Time) bool { - return d.Time().Equal(u.Time()) +func (t *Time) Equal(u datetime.Interface) bool { + return t.Time().Equal(u.Time()) } -func (d *Time) Between(start, end *Time) bool { - return d.Time().Before(end.Time()) && d.Time().After(start.Time()) +func (t *Time) Between(start, end datetime.Interface) bool { + return t.Time().Before(end.Time()) && t.Time().After(start.Time()) } diff --git a/time/value.go b/time/value.go new file mode 100644 index 0000000..98cb0f0 --- /dev/null +++ b/time/value.go @@ -0,0 +1,39 @@ +package time + +import ( + "errors" + "fmt" + "github.com/gouef/datetime" + "github.com/gouef/validator" + "github.com/gouef/validator/constraints" +) + +type Value string + +func StringToValue(value string) (Value, error) { + errs := validator.Validate(value, constraints.RegularExpression{Regexp: DateTimeRegexp}) + + time, err := FromString(value) + + if len(errs) != 0 || err != nil { + return "", errors.New(fmt.Sprintf("unsupported format of time \"%s\"", value)) + } + + str := time.ToString() + + return Value(str), nil +} + +func (d Value) String() string { + return string(d) +} + +func (d Value) Date() datetime.Interface { + time, err := FromString(d.String()) + + if err != nil { + return nil + } + + return time +} diff --git a/timeValue.go b/timeValue.go deleted file mode 100644 index 6ba32dc..0000000 --- a/timeValue.go +++ /dev/null @@ -1,30 +0,0 @@ -package datetime - -import ( - "errors" - "fmt" - "github.com/gouef/validator" - "github.com/gouef/validator/constraints" -) - -type TimeValue string - -func StringToTimeValue(value string) (TimeValue, error) { - errs := validator.Validate(value, constraints.RegularExpression{Regexp: timeRegexp}) - - if len(errs) != 0 { - return "", errors.New(fmt.Sprintf("unsupported format of time \"%s\"", value)) - } - - return TimeValue(value), nil -} - -func (d TimeValue) Date() *Time { - date, err := TimeFromString(string(d)) - - if err != nil { - return nil - } - - return date -} diff --git a/dateTimeValue.go b/value.go similarity index 55% rename from dateTimeValue.go rename to value.go index 87dffd3..916d3c5 100644 --- a/dateTimeValue.go +++ b/value.go @@ -7,20 +7,24 @@ import ( "github.com/gouef/validator/constraints" ) -type DateTimeValue string +type Value string -func StringToDateTimeValue(value string) (DateTimeValue, error) { - errs := validator.Validate(value, constraints.RegularExpression{Regexp: dateTimeRegexp}) +func StringToValue(value string) (Value, error) { + errs := validator.Validate(value, constraints.RegularExpression{Regexp: Regexp}) - if len(errs) != 0 { + d, err := FromString(value) + + if len(errs) != 0 || err != nil { return "", errors.New(fmt.Sprintf("unsupported format of date time \"%s\"", value)) } - return DateTimeValue(value), nil + str := d.ToString() + + return Value(str), nil } -func (d DateTimeValue) Date() *DateTime { - date, err := DateTimeFromString(string(d)) +func (d Value) Date() Interface { + date, err := FromString(string(d)) if err != nil { return nil