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  }