lib/timeutil/timeutil.go (284 lines of code) (raw):
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package timeutil provides utilities for working with time related objects.
package timeutil
import (
"encoding/json"
"fmt"
"io/ioutil"
"regexp"
"strconv"
"strings"
"time"
"golang.org/x/text/language" /* copybara-comment */
"github.com/golang/protobuf/ptypes" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/srcutil" /* copybara-comment: srcutil */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/strutil" /* copybara-comment: strutil */
glog "github.com/golang/glog" /* copybara-comment */
dpb "github.com/golang/protobuf/ptypes/duration" /* copybara-comment */
tspb "github.com/golang/protobuf/ptypes/timestamp" /* copybara-comment */
)
const (
// DurationREStr is a regexp for a duration string consiting of
// days, hours, minutess, and seconds (each one is optional).
DurationREStr = "^([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?$"
)
var (
dayRE = regexp.MustCompile(`^(.*[dhms])?([\d\.]+)d(.*)$`)
hourRE = regexp.MustCompile(`^(.*[dhms])?([\d\.]+)h(.*)$`)
// For performance reasons, initialize these structures as part of the startup sequence
// so that they are always available.
localeMap = generateLocales()
timeZoneMap = generateTimeZones()
)
// LocaleInfo is the descriptor for a locale.
type LocaleInfo struct {
// Base is the base language as defined by BCP47.
Base string `json:"base"`
// Script is the script character set used as defined by BCP47.
Script string `json:"script"`
// Region is the geolocation as defined by BCP47.
Region string `json:"region"`
// UI contains human-friendly labels for UIs to display.
UI map[string]string `json:"ui"`
}
// TimezoneInfo is the descriptor for a time zone.
type TimezoneInfo struct {
// UI contains human-friendly labels for UIs to display.
UI map[string]string `json:"ui"`
}
// ParseDuration parses the given duration string to time.Duration.
// It supports "d" for days which time.ParseDuration does not.
// Each day is 24 hours.
func ParseDuration(d string) (time.Duration, error) {
if len(d) == 0 {
return 0, nil
}
neg := time.Duration(1)
if d[0] == '-' {
neg = -1
d = d[1:]
}
h := float64(0)
if days := dayRE.FindStringSubmatch(d); len(days) > 3 {
n, err := strconv.ParseFloat(days[2], 64)
if err != nil {
return 0, err
}
h += n * 24
d = days[1] + days[3]
}
if hours := hourRE.FindStringSubmatch(d); len(hours) > 3 {
n, err := strconv.ParseFloat(hours[2], 64)
if err != nil {
return 0, err
}
h += n
d = hours[1] + hours[3]
}
if h > 0 {
d = fmt.Sprintf("%0.3fh%s", h, d)
}
out, err := time.ParseDuration(d)
if err != nil {
return 0, err
}
return neg * out, nil
}
// ParseDurationWithDefault parses the given duration string.
// Returns the provided default if the string is empty or on error.
func ParseDurationWithDefault(d string, def time.Duration) time.Duration {
if len(d) == 0 {
return def
}
v, err := ParseDuration(d)
if err != nil {
return def
}
return v
}
// ParseSeconds returns a duration from a numeric string in seconds.
func ParseSeconds(d string) (time.Duration, error) {
n, err := strconv.ParseInt(d, 10, 64)
if err != nil {
return 0, err
}
return time.Duration(n) * time.Second, nil
}
// TTLString removes tailing 0s, 0m, 0h for human readable.
func TTLString(ttl time.Duration) string {
str := ttl.String()
str = strings.TrimSuffix(str, "0s")
str = strings.TrimSuffix(str, "0m")
str = strings.TrimSuffix(str, "0h")
return str
}
// KeyTTL retuns the ttl for a number of keys.
func KeyTTL(maxRequestedTTL time.Duration, numKeys int) time.Duration {
offset := int(maxRequestedTTL.Seconds()) / numKeys
return maxRequestedTTL + time.Second*time.Duration(offset+1)
}
// IsTimeZone returns true if the "name" provided is an IANA Time Zone name.
func IsTimeZone(name string) bool {
if name == "" {
return false
}
if _, ok := timeZoneMap[name]; ok {
return true
}
// Fallback to environment check.
if _, err := time.LoadLocation(name); err != nil {
return false
}
return true
}
// GetTimeZones returns a map of canonical timezone names to region names.
func GetTimeZones() map[string]*TimezoneInfo {
return timeZoneMap
}
// generateTimeZones returns a map of canonical timezone names to regional information.
// Example: {
// "America/Los_Angeles": {
// "ui" {
// "label": "Los Angeles (America)",
// "region": "America",
// "city": "Los Angeles"
// }
// }
// }
// TODO: use standard time functions for other platforms if https://github.com/golang/go/issues/20629 is implemented.
func generateTimeZones() map[string]*TimezoneInfo {
zoneDirs := []string{
"/usr/share/zoneinfo/",
"/usr/share/lib/zoneinfo/",
"/usr/lib/locale/TZ/",
}
out := make(map[string]*TimezoneInfo)
for _, dir := range zoneDirs {
genZones(dir, "", out)
}
data, err := srcutil.LoadFile("deploy/metadata/standard_timezones.json")
if err != nil {
glog.Errorf("failed to load time zones: %v", err)
return out
}
loaded := make(map[string]string)
if err := json.Unmarshal([]byte(data), &loaded); err != nil {
glog.Errorf("failed to unmarshal time zone data: %v", err)
return out
}
for k := range loaded {
out[k] = genZoneInfo(k)
}
return out
}
func genZoneInfo(key string) *TimezoneInfo {
info := &TimezoneInfo{UI: map[string]string{}}
p := strings.Split(key, "/")
plen := len(p)
switch plen {
case 1:
// Example: "EST"
info.UI["label"] = key
case 2:
// Example: "America/Los_Angeles"
info.UI["label"] = fmt.Sprintf("%s (%s)", strutil.ToTitle(p[1]), p[0])
info.UI["region"] = p[0]
info.UI["city"] = strutil.ToTitle(p[1])
default:
// Example: "America/Indiana/Indianapolis"
info.UI["label"] = fmt.Sprintf("%s (%s, %s)", strutil.ToTitle(p[plen-1]), strutil.ToTitle(p[plen-2]), p[0])
info.UI["region"] = p[0]
info.UI["subregion"] = strutil.ToTitle(p[1])
if plen > 3 {
// Found a deep path, just dump the middle levels in "part".
// There are no current examples of paths this deep.
info.UI["part"] = strings.Join(p[2:plen-1], ", ")
}
info.UI["city"] = strutil.ToTitle(p[plen-1])
}
return info
}
func genZones(dir, path string, out map[string]*TimezoneInfo) {
files, err := ioutil.ReadDir(dir + path)
if err != nil {
return // not all files need to be present
}
for _, f := range files {
if f.Name() != strings.ToUpper(f.Name()[:1])+f.Name()[1:] {
continue
}
if f.IsDir() {
genZones(dir, path+"/"+f.Name(), out)
} else {
zone := (path + "/" + f.Name())[1:]
out[zone] = genZoneInfo(zone)
}
}
}
// IsLocale returns true if the "name" provided is a locale name as per
// https://tools.ietf.org/html/bcp47.
func IsLocale(name string) bool {
if _, ok := localeMap[name]; ok {
return true
}
// Fallback to environment check.
if _, err := language.Parse(name); err != nil {
return false
}
return true
}
// GetLocales returns a map of locale identifiers to English labels.
func GetLocales() map[string]*LocaleInfo {
return localeMap
}
// generateLocales returns a map of canonical BCP47 locale identifiers to English labels. See IsLocale() for identifier details.
func generateLocales() map[string]*LocaleInfo {
out := make(map[string]*LocaleInfo)
loaded := make(map[string]string)
data, err := srcutil.LoadFile("deploy/metadata/standard_locales.json")
if err != nil {
glog.Errorf("failed to load locales: %v", err)
return out
}
if err := json.Unmarshal([]byte(data), &loaded); err != nil {
glog.Errorf("failed to unmarshal locale data: %v", err)
return out
}
for k, v := range loaded {
out[k] = genLocaleInfo(k, v)
}
return out
}
func genLocaleInfo(key, label string) *LocaleInfo {
info := &LocaleInfo{UI: map[string]string{"label": label}}
p := strings.Split(key, "-")
info.Base = p[0]
info.UI["language"] = strings.Trim(strings.SplitN(label, "(", 2)[0], " ")
switch len(p) {
case 1:
// Label Format: "<language>"
// Label Example: "English"
// Already captured above.
case 2:
// Label Format: "<language> (<region>)"
// Label Example: "English (Canada)"
info.Region = p[1]
i2 := strings.Trim(leftOf(rightOf(label, "("), ")"), " ")
switch {
case len(p[1]) > 3:
info.UI["script"] = i2
default:
info.UI["region"] = i2
}
case 3:
// Label Format: "<language> (<script>, <region>)"
// Label Example: "Serbian (Cyrillic, Serbia)"
info.Script = p[1]
info.Region = p[2]
i3 := strings.Split(leftOf(rightOf(label, "("), ")"), ",")
switch {
case len(i3) == 1:
// Found a misformatted string, as a safety check. Assume it is a region.
info.UI["region"] = strings.Trim(i3[0], " ")
default:
info.UI["script"] = strings.Trim(i3[0], " ")
info.UI["region"] = strings.Trim(i3[1], " ")
}
}
return info
}
func leftOf(str, split string) string {
return strings.SplitN(str, split, 2)[0]
}
func rightOf(str, split string) string {
parts := strings.Split(str, split)
return parts[len(parts)-1]
}
// TimestampString returns a RFC3339 date/time string for seconds sinc epoch.
func TimestampString(secs int64) string {
return time.Unix(secs, 0).UTC().Format(time.RFC3339)
}
// RFC3339 convers a Timestamp to RFC3339 string.
// Returns "" if the timestamp is invalid.
func RFC3339(ts *tspb.Timestamp) string {
t, err := ptypes.Timestamp(ts)
if err != nil {
return ""
}
return t.Format(time.RFC3339)
}
// ParseRFC3339 converts an RFC3339 string to time.Time.
// Returns default value of time.Time if the string is invalid.
func ParseRFC3339(s string) time.Time {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return time.Time{}
}
return t
}
// TimestampProto returns the timestamp proto for a given time.
// Returns empty if the time is invalid.
func TimestampProto(t time.Time) *tspb.Timestamp {
ts, err := ptypes.TimestampProto(t)
if err != nil {
return &tspb.Timestamp{}
}
return ts
}
// Time returns the time for a given timestamp.
// Returns 0 if the timestamp is invalid.
func Time(ts *tspb.Timestamp) time.Time {
t, err := ptypes.Timestamp(ts)
if err != nil {
return time.Time{}
}
return t
}
// DurationProto returns the duration proto for a given duration.
func DurationProto(d time.Duration) *dpb.Duration {
return ptypes.DurationProto(d)
}
// Duration returns the time for a given timestamp.
// Returns 0 if the timestamp is invalid.
func Duration(ds *dpb.Duration) time.Duration {
d, err := ptypes.Duration(ds)
if err != nil {
return 0
}
return d
}