package date_utils import ( "reflect" "strconv" "strings" "time" "github.com/araddon/dateparse" ) 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 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 } // 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 Monday and 6 being Sunday. type TradingHours []struct { StartTime string `json:"start_time"` EndTime string `json:"end_time"` } func (t TradingHours) Validate() bool { if t == nil { return false } for _, day := range t { if !TimeBefore(day.StartTime, day.EndTime) { return false } if len(day.StartTime) != 5 || len(day.EndTime) != 5 { return false } startHourMinSlice := strings.Split(day.StartTime, ":") if len(startHourMinSlice) != 2 { return false } startHour, startMin := startHourMinSlice[0], startHourMinSlice[1] startHourInt, err := strconv.Atoi(startHour) if err != nil || startHourInt < 0 || startHourInt > 23 { return false } startMinInt, err := strconv.Atoi(startMin) if err != nil || !(startMinInt == 0 || startMinInt == 30) { return false } endHourMinSlice := strings.Split(day.EndTime, ":") if len(endHourMinSlice) != 2 { return false } endHour, endMin := endHourMinSlice[0], endHourMinSlice[1] endHourInt, err := strconv.Atoi(endHour) if err != nil || endHourInt < 0 || endHourInt > 23 { return false } endMinInt, err := strconv.Atoi(endMin) if err != nil || !(endMinInt == 0 || endMinInt == 30 || endMinInt == 59) { return false } } return true } func (t TradingHours) String() string { var dayMap = map[int]string{ 0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun", } var timeMap = map[string]string{ "00:00": "12am", "00:30": "12:30am", "01:00": "1am", "01:30": "1:30am", "02:00": "2am", "02:30": "2:30am", "03:00": "3am", "03:30": "3:30am", "04:00": "4am", "04:30": "4:30am", "05:00": "5am", "05:30": "5:30am", "06:00": "6am", "06:30": "6:30am", "07:00": "7am", "07:30": "7:30am", "08:00": "8am", "08:30": "8:30am", "09:00": "9am", "09:30": "9:30am", "10:00": "10am", "10:30": "10:30am", "11:00": "11am", "11:30": "11:30am", "12:00": "12pm", "12:30": "12:30pm", "13:00": "1pm", "13:30": "1:30pm", "14:00": "2pm", "14:30": "2:30pm", "15:00": "3pm", "15:30": "3:30pm", "16:00": "4pm", "16:30": "4:30pm", "17:00": "5pm", "17:30": "5:30pm", "18:00": "6pm", "18:30": "6:30pm", "19:00": "7pm", "19:30": "7:30pm", "20:00": "8pm", "20:30": "8:30pm", "21:00": "9pm", "21:30": "9:30pm", "22:00": "10pm", "22:30": "10:30pm", "23:00": "11pm", "23:30": "11:30pm", "23:59": "11:59pm", } result := "" rangeStartIndex := 0 for i := 0; i < len(t); i++ { times := timeMap[t[i].StartTime] + "-" + timeMap[t[i].EndTime] if t[i].StartTime == "00:00" && t[i].EndTime == "23:59" { times = "All day" } // 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(t)-1 || t[i].StartTime != t[i+1].StartTime || t[i].EndTime != t[i+1].EndTime { if rangeStartIndex == i { result += dayMap[i] + ": " + times } else { result += dayMap[rangeStartIndex] + "-" + dayMap[i] + ": " + times } if i < len(t)-1 { result += ", " } rangeStartIndex++ } } return result }