From daab1fd296551d9505e9ff2e524b44b1291918cf Mon Sep 17 00:00:00 2001 From: "daniel.naude" <danieln@bob.co.za> Date: Thu, 5 Sep 2024 10:27:20 +0200 Subject: [PATCH] Refactor TradingHours struct and validation Refactor the TradingHours struct to have separate fields for each day of the week and holidays. Also, update the validation logic to validate each day individually. --- date_utils/date_utils.go | 240 ++++++++++++++++++++------------------- 1 file changed, 126 insertions(+), 114 deletions(-) diff --git a/date_utils/date_utils.go b/date_utils/date_utils.go index 5f0ad1b..0fed016 100644 --- a/date_utils/date_utils.go +++ b/date_utils/date_utils.go @@ -2,15 +2,15 @@ package date_utils import ( "fmt" - "github.com/jinzhu/now" "reflect" "strconv" "strings" "time" + "github.com/jinzhu/now" + "github.com/araddon/dateparse" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" - "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils" ) const TimeZoneString = "Africa/Johannesburg" @@ -327,144 +327,156 @@ func formatTimestampsWithTimeZoneInSlice(fieldValue reflect.Value, location *tim // TradingHours represents an array of (StartTime,EndTime) pairs, one for each day of the week. // The array is 0 indexed, with 0 being Sunday and 6 being Saturday and 7 being public holidays. -type TradingHours []struct { - StartTime string `json:"start_time"` - EndTime string `json:"end_time"` +type TradingHours struct { + Monday TradingHoursDay `json:"monday"` + Tuesday TradingHoursDay `json:"tuesday"` + Wednesday TradingHoursDay `json:"wednesday"` + Thursday TradingHoursDay `json:"thursday"` + Friday TradingHoursDay `json:"friday"` + Saturday TradingHoursDay `json:"saturday"` + Sunday TradingHoursDay `json:"sunday"` + Holidays TradingHoursDay `json:"holidays"` } func (t TradingHours) Validate() error { - if len(t) != 8 { - return errors.Error("Trading hours must have 8 days, 7 for every day of the week and 1 for public holidays") + if err := t.Monday.Validate(); err != nil { + return errors.Wrapf(err, "Monday failed validation: %v", err.Error()) + } + if err := t.Tuesday.Validate(); err != nil { + return errors.Wrapf(err, "Tuesday failed validation: %v", err.Error()) + } + if err := t.Wednesday.Validate(); err != nil { + return errors.Wrapf(err, "Wednesday failed validation: %v", err.Error()) + } + if err := t.Thursday.Validate(); err != nil { + return errors.Wrapf(err, "Thursday failed validation: %v", err.Error()) + } + if err := t.Friday.Validate(); err != nil { + return errors.Wrapf(err, "Friday failed validation: %v", err.Error()) + } + if err := t.Saturday.Validate(); err != nil { + return errors.Wrapf(err, "Saturday failed validation: %v", err.Error()) + } + if err := t.Sunday.Validate(); err != nil { + return errors.Wrapf(err, "Sunday failed validation: %v", err.Error()) + } + if err := t.Holidays.Validate(); err != nil { + return errors.Wrapf(err, "Holidays failed validation: %v", err.Error()) } - for _, day := range t { - if day.StartTime == "" || day.EndTime == "" { - // Allow empty trading hours for a day to represent closed - continue - } + return nil +} - if !TimeBefore(day.StartTime, day.EndTime) { - return errors.Error("Start time must be before end time") - } +func (day TradingHoursDay) Validate() error { + if day.StartTime == "" || day.EndTime == "" { + // Allow empty trading hours for a day to represent closed + return nil + } - if len(day.StartTime) != 5 || len(day.EndTime) != 5 { - return errors.Error("Time must be in the format HH:MM") - } + if !TimeBefore(day.StartTime, day.EndTime) { + return errors.Error("start time must be before end time") + } - startHourMinSlice := strings.Split(day.StartTime, ":") - if len(startHourMinSlice) != 2 { - return errors.Error("Time must be in the format HH:MM") - } - startHour, startMin := startHourMinSlice[0], startHourMinSlice[1] - startHourInt, err := strconv.Atoi(startHour) - if err != nil || startHourInt < 0 || startHourInt > 23 { - return errors.Error("Start hour must be between 0 and 23") - } - startMinInt, err := strconv.Atoi(startMin) - if err != nil || !(startMinInt == 0 || startMinInt == 30) { - return errors.Error("Start minute must be 0 or 30") - } + if len(day.StartTime) != 5 || len(day.EndTime) != 5 { + return errors.Error("time must be in the format HH:MM") + } - endHourMinSlice := strings.Split(day.EndTime, ":") - if len(endHourMinSlice) != 2 { - return errors.Error("Time must be in the format HH:MM") - } - endHour, endMin := endHourMinSlice[0], endHourMinSlice[1] - endHourInt, err := strconv.Atoi(endHour) - if err != nil || endHourInt < 0 || endHourInt > 23 { - return errors.Error("End hour must be between 0 and 23") - } - endMinInt, err := strconv.Atoi(endMin) - if err != nil || !(endMinInt == 0 || endMinInt == 30 || endMinInt == 59) { - return errors.Error("End minute must be 0, 30 or 59") - } + startHourMinSlice := strings.Split(day.StartTime, ":") + if len(startHourMinSlice) != 2 { + return errors.Error("time must be in the format HH:MM") + } + startHour, startMin := startHourMinSlice[0], startHourMinSlice[1] + startHourInt, err := strconv.Atoi(startHour) + if err != nil || startHourInt < 0 || startHourInt > 23 { + return errors.Error("start hour must be between 0 and 23") + } + startMinInt, err := strconv.Atoi(startMin) + if err != nil || !(startMinInt == 0 || startMinInt == 30) { + return errors.Error("start minute must be 0 or 30") + } + + endHourMinSlice := strings.Split(day.EndTime, ":") + if len(endHourMinSlice) != 2 { + return errors.Error("time must be in the format HH:MM") + } + endHour, endMin := endHourMinSlice[0], endHourMinSlice[1] + endHourInt, err := strconv.Atoi(endHour) + if err != nil || endHourInt < 0 || endHourInt > 23 { + return errors.Error("end hour must be between 0 and 23") + } + endMinInt, err := strconv.Atoi(endMin) + if err != nil || !(endMinInt == 0 || endMinInt == 30 || endMinInt == 59) { + return errors.Error("end minute must be 0, 30 or 59") } return nil } func (t TradingHours) String() string { - var result strings.Builder - const numberOfDaysInWeek = 7 - copyOfT := utils.DeepCopy(t).(TradingHours) - weekdays, publicHolidays := copyOfT[:numberOfDaysInWeek], copyOfT[numberOfDaysInWeek] - weekdays = append(weekdays, weekdays[0]) // Add the first day (Sunday) to the end because we want Monday to be first in the string - - rangeStartIndex := 1 - for i := 1; i < len(weekdays); i++ { - currentDay := weekdays[i] - nextDay := currentDay - if i+1 < len(weekdays) { - nextDay = weekdays[i+1] + var ( + result strings.Builder + weekdays = []TradingHoursDay{t.Monday, t.Tuesday, t.Wednesday, t.Thursday, t.Friday, t.Saturday, t.Sunday} + rangeStartWeekday = time.Monday + ) + + for i := range weekdays { + currentTradingHoursDay := weekdays[i] + + // Here the index is wrapped so we will never go out of bounds. The next day after Sunday is Monday at index 0 because (6+1)%7 = 0 + nextTradingHoursDay := weekdays[(i+1)%len(weekdays)] + + // Here we use the same value, as above, but for a different purpose of being compatible with the time package + // This is because the time package uses 0 for Sunday and 1 for Monday. This means when we get to index 6 (Sunday), + // to get the time.Weekday value for Sunday we need to use 0, and as before (6+1)%7 = 0 + currentWeekday := time.Weekday((i + 1) % len(weekdays)) + nextWeekday := time.Weekday((i + 2) % len(weekdays)) + + // Determine range description + var rangeDescription string + if currentWeekday == rangeStartWeekday { + rangeDescription = currentWeekday.String()[:3] + } else { + rangeDescription = fmt.Sprintf("%s – %s", rangeStartWeekday.String()[:3], currentWeekday.String()[:3]) } - // Determine times - var times string - if currentDay.StartTime != "" && currentDay.EndTime != "" { - startTime, err := time.Parse("15:04", currentDay.StartTime) - if err != nil { - return "" - } + // If the next day has the same times and we're not at the last element, we continue the current range + if nextTradingHoursDay.StartTime == currentTradingHoursDay.StartTime && nextTradingHoursDay.EndTime == currentTradingHoursDay.EndTime && i < len(weekdays)-1 { + continue + } - endTime, err := time.Parse("15:04", currentDay.EndTime) - if err != nil { - return "" - } + result.WriteString(fmt.Sprintf("%s: %s, ", rangeDescription, currentTradingHoursDay.String())) + rangeStartWeekday = nextWeekday + } - times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm") - if currentDay.StartTime == "00:00" && currentDay.EndTime == "23:59" { - times = "All day" - } - } else { - times = "Closed" - } + // Public holidays + result.WriteString(fmt.Sprintf("Public holidays: %s", t.Holidays.String())) - // If we're at the last element or the next day doesn't have the same times, we end the current range - if i == len(weekdays)-1 || currentDay.StartTime != nextDay.StartTime || currentDay.EndTime != nextDay.EndTime { - if rangeStartIndex == i { - day := time.Weekday(rangeStartIndex).String()[:3] - if rangeStartIndex == numberOfDaysInWeek { - day = time.Sunday.String()[:3] - } - result.WriteString(fmt.Sprintf("%s: %s", day, times)) - } else { - rangeStartDay := time.Weekday(rangeStartIndex).String()[:3] - rangeEndDay := time.Weekday(i).String()[:3] - if i == numberOfDaysInWeek { - rangeEndDay = time.Sunday.String()[:3] - } - result.WriteString(fmt.Sprintf("%s – %s: %s", rangeStartDay, rangeEndDay, times)) - } + return result.String() +} - if i < len(weekdays)-1 { - result.WriteString(", ") - } +type TradingHoursDay struct { + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` +} - rangeStartIndex = i + 1 - } +func (day TradingHoursDay) String() string { + if day.StartTime == "" && day.EndTime == "" { + return "Closed" } - // Public holidays - var times string - if publicHolidays.StartTime != "" && publicHolidays.EndTime != "" { - startTime, err := time.Parse("15:04", publicHolidays.StartTime) - if err != nil { - return "" - } + startTime, err := time.Parse("15:04", day.StartTime) + if err != nil { + return "" + } - endTime, err := time.Parse("15:04", publicHolidays.EndTime) - if err != nil { - return "" - } + endTime, err := time.Parse("15:04", day.EndTime) + if err != nil { + return "" + } - times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm") - if publicHolidays.StartTime == "00:00" && publicHolidays.EndTime == "23:59" { - times = "All day" - } - } else { - times = "Closed" + if day.StartTime == "00:00" && day.EndTime == "23:59" { + return "All day" } - result.WriteString(fmt.Sprintf(", Public holidays: %s", times)) - return result.String() + return startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm") } -- GitLab