package date_utils import ( "fmt" "github.com/jinzhu/now" "reflect" "strconv" "strings" "time" "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" var currentLocation *time.Location func DateLayoutYearMonthDayTimeT() string { layout := "2006-01-02T15:04:05" return layout } func DateLayoutYearMonthDayTimeTZ() string { layout := "2006-01-02T15:04:05Z" return layout } func DateLayoutYearMonthDayTimeMillisecondTZ() string { layout := "2006-01-02T15:04:05.000Z" return layout } func DateLayoutDB() string { return "2006-01-02 15:04:05.000000-07" } func DateLayoutYearMonthDayTimeTimezone() string { layout := "2006-01-02 15:04:05-07:00" return layout } func DateLayoutForDB() string { layout := "2006-01-02 15:04:05-07" return layout } func DateLayoutYearMonthDayTime() string { layout := "2006-01-02 15:04:05" return layout } func DateLayoutFilenameSafe() string { layout := "02-Jan-2006-15h04" return layout } func DateLayoutYearMonthDay() string { layout := "2006-01-02" return layout } func DateLayoutTime() string { layout := "15:04:05" return layout } func DateLayoutHumanReadable() string { layout := "02 Jan 2006" return layout } func DateLayoutHumanReadableWithTime() string { layout := "02 Jan 2006 15:04" return layout } func DateLayoutTrimmed() string { layout := "20060102150405" return layout } func DateDBFormattedString(date time.Time) string { return date.Format(DateLayoutDB()) } func DateDBFormattedStringDateOnly(date time.Time) string { return date.Format(DateLayoutYearMonthDay()) } func CurrentLocation() *time.Location { if currentLocation == nil { currentLocation, _ = time.LoadLocation(TimeZoneString) } return currentLocation } func DateLocal(date *time.Time) { if date == nil { return } *date = (*date).In(CurrentLocation()) } func CurrentDate() time.Time { currentDate := time.Now().In(CurrentLocation()) return currentDate } func TodayStart() string { currentTime := CurrentDate() return DateDBFormattedString(now.With(currentTime).BeginningOfDay()) } func TodayEnd() string { currentTime := CurrentDate() return DateDBFormattedString(now.With(currentTime).EndOfDay()) } func StartOfDay(date time.Time) time.Time { day := now.With(date.In(CurrentLocation())).BeginningOfDay() return day } func EndOfDay(date time.Time) time.Time { // Subtract one second from the start of the next day so that we have 23:59:59 instead of 23:59:58.999999 day := now.With(date.AddDate(0, 0, 1).In(CurrentLocation())).BeginningOfDay().Add(-time.Second) return day } // BeginningOfNextDay is useful for specifying intervals where the beginning of the next day is excluded e.g. date < beginningOfNextDay func BeginningOfNextDay(date time.Time) time.Time { day := now.With(date.AddDate(0, 0, 1).In(CurrentLocation())).BeginningOfDay() return day } func DateEqual(date1, date2 time.Time) bool { y1, m1, d1 := date1.Date() y2, m2, d2 := date2.Date() return y1 == y2 && m1 == m2 && d1 == d2 } // TimeBefore determines whether a (string format HH:mm) is earlier than b (string format HH:mm) func TimeBefore(a string, b string) bool { if len(a) < 5 || len(b) < 5 { return false // can't detemrine before/after } hoursA, _ := strconv.Atoi(a[0:2]) hoursB, _ := strconv.Atoi(b[0:2]) minA, _ := strconv.Atoi(a[3:5]) minB, _ := strconv.Atoi(b[3:5]) if hoursA == hoursB { return minA < minB } return hoursA < hoursB } // DatePtrToString converts a time.Time pointer to a string in the format "2006-01-02 15:04:05". func DatePtrToString(date *time.Time) string { if date == nil { return "" } return date.In(CurrentLocation()).Format(DateLayoutYearMonthDayTime()) } // DatePtrToTimeString converts a time.Time pointer to a string in the format "15:04". If the pointer is nil, it returns "--:--". func DatePtrToTimeString(date *time.Time) string { if date != nil { // Convert to local time DateLocal(date) // Return formatted as time only return date.Format("15:04") } return "--:--" } // ConvertToNoDateTimeString - Converts a PSQL Time type to Go Time type func ConvertToNoDateTimeString(timeString *string) (*string, error) { parsedTime, err := time.Parse("15:04:05", *timeString) if err != nil { return nil, err } formattedTime := parsedTime.Format("15:04") return &formattedTime, nil } // ParseTimeString attempts to parse the string as the default date-time format, or as a date only format func ParseTimeString(timeString string) (time.Time, error) { // Try using the defined formats in date_utils parsedTime, err := time.Parse(DateLayoutYearMonthDayTimeMillisecondTZ(), timeString) if err != nil { parsedTime, err = time.Parse(DateLayoutYearMonthDayTimeT(), timeString) if err != nil { parsedTime, err = time.Parse(DateLayoutYearMonthDayTimeTZ(), timeString) if err != nil { parsedTime, err = time.Parse(DateLayoutYearMonthDay(), timeString) if err != nil { parsedTime, err = time.Parse(DateLayoutYearMonthDayTime(), timeString) if err != nil { parsedTime, err = time.Parse(DateLayoutYearMonthDayTimeTimezone(), timeString) if err != nil { parsedTime, err = time.Parse(DateLayoutDB(), timeString) } } } } } } if err != nil { // Try using other date formats from dateparse library parsedTime, err = dateparse.ParseAny(timeString) } return parsedTime, err } func FormatTimestampsWithTimeZoneOnStructRecursively(object any, location *time.Location) error { var objectValue reflect.Value objectValue = reflect.ValueOf(object) for objectValue.Kind() == reflect.Pointer || objectValue.Kind() == reflect.Interface { objectValue = objectValue.Elem() } err := formatTimestampsWithTimeZoneOnStruct(objectValue, location) return err } func formatTimestampsWithTimeZoneOnStruct(structValue reflect.Value, location *time.Location) error { numF := structValue.NumField() for i := 0; i < numF; i++ { fieldValue := structValue.Field(i) fieldType := fieldValue.Type() fieldKind := fieldValue.Type().Kind() timeType := reflect.TypeOf(time.Time{}) if fieldType == timeType && !fieldValue.IsZero() { fieldValue.Set(reflect.ValueOf(fieldValue.Interface().(time.Time).In(location))) continue } timePointerType := reflect.TypeOf(&time.Time{}) if fieldType == timePointerType && !fieldValue.IsNil() { timeInLocation := fieldValue.Interface().(*time.Time).In(location) timeInLocationPointer := reflect.ValueOf(timeInLocation) fieldPointer := fieldValue.Elem() fieldPointer.Set(timeInLocationPointer) continue } if fieldKind == reflect.Slice { // Loop over the slice items err := formatTimestampsWithTimeZoneInSlice(fieldValue, location) if err != nil { return err } continue } else if fieldKind == reflect.Struct { err := formatTimestampsWithTimeZoneOnStruct(fieldValue, location) if err != nil { return err } continue } else if fieldKind == reflect.Interface || fieldKind == reflect.Pointer { var objectValue reflect.Value objectValue = fieldValue.Elem() if objectValue.Kind() == reflect.Slice { err := formatTimestampsWithTimeZoneInSlice(objectValue, location) if err != nil { return err } } else if objectValue.Kind() == reflect.Struct { err := formatTimestampsWithTimeZoneOnStruct(objectValue, location) if err != nil { return err } } continue } } return nil } func formatTimestampsWithTimeZoneInSlice(fieldValue reflect.Value, location *time.Location) error { for j := 0; j < fieldValue.Len(); j++ { sliceItem := fieldValue.Index(j) if sliceItem.IsValid() { var sliceItemValue reflect.Value if sliceItem.Kind() == reflect.Ptr && sliceItem.IsValid() { // Dereference the pointer sliceItemValue = sliceItem.Elem() } else { sliceItemValue = sliceItem } if sliceItemValue.IsValid() { sliceItemKind := sliceItemValue.Kind() sliceItemType := sliceItemValue.Type() // Check whether we have a slice of time.Time, and set the location if we do. if sliceItemType == reflect.TypeOf(time.Time{}) { sliceItemValue.Set(reflect.ValueOf(sliceItemValue.Interface().(time.Time).In(location))) continue } if sliceItemKind == reflect.Struct { err := formatTimestampsWithTimeZoneOnStruct(sliceItemValue, location) if err != nil { return err } } } } } return nil } // 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"` } 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") } for _, day := range t { if day.StartTime == "" || day.EndTime == "" { // Allow empty trading hours for a day to represent closed continue } if !TimeBefore(day.StartTime, day.EndTime) { return errors.Error("Start time must be before end time") } if len(day.StartTime) != 5 || len(day.EndTime) != 5 { return errors.Error("Time must be in the format HH:MM") } 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] } // Determine times var times string if currentDay.StartTime != "" && currentDay.EndTime != "" { startTime, err := time.Parse("15:04", currentDay.StartTime) if err != nil { return "" } endTime, err := time.Parse("15:04", currentDay.EndTime) if err != nil { return "" } times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm") if currentDay.StartTime == "00:00" && currentDay.EndTime == "23:59" { times = "All day" } } else { times = "Closed" } // 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)) } if i < len(weekdays)-1 { result.WriteString(", ") } rangeStartIndex = i + 1 } } // Public holidays var times string if publicHolidays.StartTime != "" && publicHolidays.EndTime != "" { startTime, err := time.Parse("15:04", publicHolidays.StartTime) if err != nil { return "" } endTime, err := time.Parse("15:04", publicHolidays.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" } result.WriteString(fmt.Sprintf(", Public holidays: %s", times)) return result.String() }