Skip to content
Snippets Groups Projects
date_utils.go 11.2 KiB
Newer Older
Francé Wilke's avatar
Francé Wilke committed
package date_utils

import (
Francé Wilke's avatar
Francé Wilke committed
	"strconv"
Francé Wilke's avatar
Francé Wilke committed
	"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"
Francé Wilke's avatar
Francé Wilke committed
)

const TimeZoneString = "Africa/Johannesburg"

Francé Wilke's avatar
Francé Wilke committed
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"
}

Francé Wilke's avatar
Francé Wilke committed
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"
Francé Wilke's avatar
Francé Wilke committed
	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
}

Francé Wilke's avatar
Francé Wilke committed
func DateLayoutTrimmed() string {
	layout := "20060102150405"
	return layout
}

func DateDBFormattedString(date time.Time) string {
	return date.Format(DateLayoutDB())
Francé Wilke's avatar
Francé Wilke committed
}

func DateDBFormattedStringDateOnly(date time.Time) string {
	return date.Format(DateLayoutYearMonthDay())
Francé Wilke's avatar
Francé Wilke committed
}

func CurrentLocation() *time.Location {
	if currentLocation == nil {
		currentLocation, _ = time.LoadLocation(TimeZoneString)
	}
	return currentLocation
Francé Wilke's avatar
Francé Wilke committed
}

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) {
Francé Wilke's avatar
Francé Wilke committed
	// Try using the defined formats in date_utils
	parsedTime, err := time.Parse(DateLayoutYearMonthDayTimeMillisecondTZ(), timeString)
	if err != nil {
Francé Wilke's avatar
Francé Wilke committed
		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)
						}
					}
				}
			}
		}
Francé Wilke's avatar
Francé Wilke committed
	if err != nil {
		// Try using other date formats from dateparse library
		parsedTime, err = dateparse.ParseAny(timeString)
	}


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.
	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")
		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, ":")
			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, ":")
			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")
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
	for i := 1; i < len(weekdays); i++ {
		currentDay := weekdays[i]
		nextDay := currentDay
		// Determine times
		var times string
		if currentDay.StartTime != "" && currentDay.EndTime != "" {
			startTime, err := time.Parse("15:04", currentDay.StartTime)
			endTime, err := time.Parse("15:04", currentDay.EndTime)
			times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm")
			if currentDay.StartTime == "00:00" && currentDay.EndTime == "23:59" {
		}

		// 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 {
				day := time.Weekday(rangeStartIndex).String()[:3]
				if rangeStartIndex == numberOfDaysInWeek {
				}
				result.WriteString(fmt.Sprintf("%s: %s", day, times))
				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()