github.com/dolthub/go-mysql-server@v0.18.0/internal/time/time.go (about) 1 // Copyright 2023 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package time contains low-level utility functions for working with time.Time values and timezones. 16 package time 17 18 import ( 19 "errors" 20 "fmt" 21 "math" 22 "regexp" 23 "time" 24 ) 25 26 // offsetRegex is a regex for matching MySQL offsets (e.g. +01:00). 27 var offsetRegex = regexp.MustCompile(`(?m)^([+\-])(\d{2}):(\d{2})$`) 28 29 // ConvertTimeZone converts |datetime| from one timezone to another. |fromLocation| and |toLocation| can be either 30 // the name of a timezone (e.g. "UTC") or a MySQL-formatted timezone offset (e.g. "+01:00"). If the time was converted 31 // successfully, then the second return value will be true, otherwise the time was not able to be converted. 32 func ConvertTimeZone(datetime time.Time, fromLocation string, toLocation string) (time.Time, bool) { 33 if fromLocation == toLocation { 34 return datetime, true 35 } 36 convertedFromTime, err := ConvertTimeToLocation(datetime, fromLocation) 37 if err != nil { 38 return time.Time{}, false 39 } 40 convertedToTime, err := ConvertTimeToLocation(datetime, toLocation) 41 if err != nil { 42 return time.Time{}, false 43 } 44 45 delta := convertedFromTime.Sub(convertedToTime) 46 return datetime.Add(delta), true 47 } 48 49 // MySQLOffsetToDuration takes in a MySQL timezone offset (e.g. "+01:00") and returns it as a time.Duration. 50 // If any problems are encountered, an error is returned. 51 func MySQLOffsetToDuration(d string) (time.Duration, error) { 52 matches := offsetRegex.FindStringSubmatch(d) 53 if len(matches) == 4 { 54 symbol := matches[1] 55 hours := matches[2] 56 mins := matches[3] 57 return time.ParseDuration(symbol + hours + "h" + mins + "m") 58 } else { 59 return -1, errors.New("error: unable to process time") 60 } 61 } 62 63 // SystemTimezoneOffset returns the current system timezone offset as a MySQL timezone offset (e.g. "+01:00"). 64 func SystemTimezoneOffset() string { 65 t := time.Now() 66 _, offset := t.Zone() 67 68 return SecondsToMySQLOffset(offset) 69 } 70 71 // SystemTimezoneName returns the current system timezone name. 72 func SystemTimezoneName() string { 73 t := time.Now() 74 name, _ := t.Zone() 75 76 return name 77 } 78 79 // SecondsToMySQLOffset takes in a timezone offset in seconds (as returned by time.Time.Zone()) and returns it as a 80 // MySQL timezone offset (e.g. "+01:00"). 81 func SecondsToMySQLOffset(offset int) string { 82 seconds := offset % (60 * 60 * 24) 83 hours := math.Floor(float64(seconds) / 60 / 60) 84 seconds = offset % (60 * 60) 85 minutes := math.Floor(float64(seconds) / 60) 86 87 result := fmt.Sprintf("%02d:%02d", int(math.Abs(hours)), int(math.Abs(minutes))) 88 if offset >= 0 { 89 result = fmt.Sprintf("+%s", result) 90 } else { 91 result = fmt.Sprintf("-%s", result) 92 } 93 94 return result 95 } 96 97 // ConvertTimeToLocation converts |datetime| to the given |location|. |location| can be either the name of a timezone 98 // (e.g. "UTC") or a MySQL-formatted timezone offset (e.g. "+01:00"). If the time was converted successfully, then 99 // the converted time is returned, otherwise an error is returned. 100 func ConvertTimeToLocation(datetime time.Time, location string) (time.Time, error) { 101 // Try to load the timezone location string first 102 loc, err := time.LoadLocation(location) 103 if err == nil { 104 return getCopy(datetime, loc), nil 105 } 106 107 // If we can't parse a timezone location string, then try to parse a MySQL location offset 108 duration, err := MySQLOffsetToDuration(location) 109 if err == nil { 110 return datetime.Add(-1 * duration), nil 111 } 112 113 return time.Time{}, errors.New(fmt.Sprintf("error: unable to parse timezone '%s'", location)) 114 } 115 116 // getCopy recreates the time t in the wanted timezone. 117 func getCopy(t time.Time, loc *time.Location) time.Time { 118 return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc).UTC() 119 }