diff --git a/date_utils/date_utils.go b/date_utils/date_utils.go index 5f0ad1b4d4ac543812913a8499a64fb3031eb40d..0fed0164883abd13881b054024c52adc149c0c73 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") }