Skip to content

Commit

Permalink
support time zone listing
Browse files Browse the repository at this point in the history
  • Loading branch information
rogpeppe committed Sep 11, 2020
1 parent 359f758 commit 93ce1f8
Show file tree
Hide file tree
Showing 4 changed files with 816 additions and 13 deletions.
66 changes: 56 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,56 @@

A simple command to print dates with Go-style formatting

Usage: godate [flags] [time...]
Usage:

## Flags
godate [flags] [[time [+-]duration...]...]

or:

godate [-alias] tz [name...]

- -f *string*
## Flags
- -alias
when printing time zone matches, also print time zone aliases
- -f string
read times from named file, one per line; - means stdin
- -i string
interpret argument times as this Go-style format (or name) (default "unix")
- -itz string
interpret argument times in this time zone location (default local)
- -o string
use Go-style time format string (or name) (default "rfc3339nano")
- -in *string*
interpret argument time as this Go-style format (or name) (default "unix")
- -l
print local time (default is UTC)
- -otz string
print times in this time zone location (default local)
- -u default to UTC time zone rather than local

This command parses and prints times in arbitrary formats and time zones.
Each argument is a time followed by an arbitrary number of offset
arguments adjusting the time. Godate reads all the times
according to the format specified by the -i flag, adjusts them by
the offsets, and prints them in the format specified by the -o flag.
The special time "now" is recognized as the current time.

As a special case, if the first argument is "tz", then godate prints all
the available time zones (note: this uses an internal list and may not
exactly match the system-provided time zones). If any arguments are
provided after "tz", only time zones matching those arguments (see below
for timezone matching behavior) are printed.

The format for a duration is either as accepted by Go's ParseDuration
function (see https://golang.org/pkg/time/#Time.ParseDuration for details)
or a similar format that specifies years (year, y), months (month, mo),
weeks (week, w) or days (day, d). For example, this would print
the local time 1 month and 3 days hence and 20 minutes before the
current time:

godate now +1month3days -20m

Note that year, month, and week durations cannot be mixed with
other duration kinds in the same argument.

By default godate prints the current time in RFC3339 format in
the UTC time zone. The -f flag can be used to change the format
the local time zone. The -o flag can be used to change the format
that is printed (see https://golang.org/pkg/time/#Time.Format
for details). The reference date is:

Expand All @@ -25,6 +62,7 @@ constants in the time package (case-insensitive), in which case that format will
The supported formats are these:

ansic Mon Jan _2 15:04:05 2006
git Mon Jan _2 15:04:05 2006 -0700
go 2006-01-02 15:04:05.999999999 -0700 MST
kitchen 3:04PM
rfc1123 Mon, 02 Jan 2006 15:04:05 MST
Expand All @@ -41,13 +79,21 @@ The supported formats are these:
stampnano Jan _2 15:04:05.000000000
unix custom
unixdate Mon Jan _2 15:04:05 MST 2006
unixmilli custom
unixnano custom

The unix and unixnano formats are special cases that print the number of seconds
or nanoseconds since the Unix epoch (Jan 1st 1970). The "go" format is the
The unix, unixmilla and unixnano formats are special cases that print the number of seconds,
milliseconds or nanoseconds since the Unix epoch (Jan 1st 1970). The "go" format is the
format used by the time package to print times by default.

When one or more arguments are provided, they will be used as the time
to print instead of the current time. The -in flag can be used to specify
what format to interpret these arguments in. Again, unix and unixnano
can be used to specify input in seconds or nanoseconds since the Unix epoch.

Time zones can be specified with the -itz and -otz flags. As a convenience,
if the specified zone does not exactly match one of the known zones,
a case-insensitive match is tried, and then a substring match.
If the result is unambiguous, the matching time zone is used
(for example "-otz london" can be used to select the "Europe/London"
time zone).
35 changes: 35 additions & 0 deletions getzones.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash

# copied from $GOROOT/lib/time/update.bash

CODE=2020a
DATA=2020a

set -e
WORK="$(pwd)/work"
rm -rf work
mkdir work
cd ./work
trap 'rm -r $WORK' 0
curl -sSLO https://www.iana.org/time-zones/repository/releases/tzcode$CODE.tar.gz
curl -sSLO https://www.iana.org/time-zones/repository/releases/tzdata$DATA.tar.gz
tar xzf tzdata$DATA.tar.gz
tar xzf tzcode$CODE.tar.gz
make --silent tzdata.zi
{
cat << "EOF"
// Code xx generated by getzones.bash. DO NOT EDIT.
package main
var zoneNames = map[string]string{
EOF
awk '
/^Z/ {
printf("\t\"%s\": \"\",\n", $2)
}
/^L/ {
printf("\t\"%s\": \"%s\",\n", $3, $2)
}' tzdata.zi
echo '}'
} | gofmt > ../zonenames.go
128 changes: 125 additions & 3 deletions godate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ import (
"time"
)

//go:generate bash getzones.bash

// Possible TODOs:
// - support for rounding and truncation (how would that work syntactically?).
//
// godate now +5m round:1h trunc:1day
//
// rounding for date durations might be hard.

var (
outFormat = flag.String("o", "rfc3339nano", "use Go-style time format string (or name)")
inFormat = flag.String("i", "unix", "interpret argument times as this Go-style format (or name)")
file = flag.String("f", "", "read times from named file, one per line; - means stdin")
tzIn = flag.String("itz", "", "interpret argument times in this time zone location (default local)")
tzOut = flag.String("otz", "", "print times in this time zone location (default local)")
alias = flag.Bool("alias", false, "when printing time zone matches, also print time zone aliases")
utc = flag.Bool("u", false, "default to UTC time zone rather than local")
)

Expand Down Expand Up @@ -83,12 +93,16 @@ func main() {
if len(args) == 0 {
args = []string{"now"}
}
if args[0] == "tz" {
printZones(args[1:])
return
}
i := 0
for i < len(args) {
arg := args[i]
t, err := parseTime(arg)
if err != nil {
fatalf("parse error on %q: %v\n", arg, err)
fatalf("parse error on %q: %v", arg, err)
}
i++
for i < len(args) {
Expand Down Expand Up @@ -205,7 +219,10 @@ var errLeadingInt = errors.New("bad [0-9]*") // never printed

func usage() {
fmt.Fprintf(os.Stderr, `
Usage: godate [flags] [[time [+-]duration...]...]
Usage:
godate [flags] [[time [+-]duration...]...]
or:
godate tz [name...]
Flags:
`[1:])
flag.PrintDefaults()
Expand All @@ -219,6 +236,12 @@ according to the format specified by the -i flag, adjusts them by
the offsets, and prints them in the format specified by the -o flag.
The special time "now" is recognized as the current time.
As a special case, if the first argument is "tz", then godate prints all
the available time zones (note: this uses an internal list and may not
exactly match the system-provided time zones). If any arguments are
provided after "tz", only time zones matching those arguments (see below
for timezone matching behavior) are printed.
The format for a duration is either as accepted by Go's ParseDuration
function (see https://golang.org/pkg/time/#Time.ParseDuration for details)
or a similar format that specifies years (year, y), months (month, mo),
Expand Down Expand Up @@ -270,6 +293,13 @@ When one or more arguments are provided, they will be used as the time
to print instead of the current time. The -in flag can be used to specify
what format to interpret these arguments in. Again, unix and unixnano
can be used to specify input in seconds or nanoseconds since the Unix epoch.
Time zones can be specified with the -itz and -otz flags. As a convenience,
if the specified zone does not exactly match one of the known zones,
a case-insensitive match is tried, and then a substring match.
If the result is unambiguous, the matching time zone is used
(for example "-otz london" can be used to select the "Europe/London"
time zone).
`[1:])
os.Exit(2)
}
Expand Down Expand Up @@ -362,8 +392,24 @@ func loadLocation(loc string) (*time.Location, error) {
return nil, nil
}
tz, err := time.LoadLocation(loc)
if err == nil {
return tz, nil
}
available := zoneMatch(loc)
if len(available) > 1 {
// If the zones are actually all referring to the same underlying time zone, then
// allow it (for example, "samoa" could match both "US/Samoa" and "Pacific/Samoa"
// but they're actually both the same)
if !allIdenticalZones(available) {
return nil, fmt.Errorf("ambiguous time zone %q (%d matches; use 'godate tz %s' to see them)", loc, len(available), loc)
}
}
if len(available) == 0 {
return nil, err
}
tz, err = time.LoadLocation(available[0])
if err != nil {
return nil, fmt.Errorf("cannot load location %q: %v", loc, err)
return nil, fmt.Errorf("time zone %s not available in system time zone database: %v", available[0], err)
}
return tz, nil
}
Expand All @@ -381,6 +427,82 @@ func formatCustom(t time.Time, format string) string {
}
}

func printZones(args []string) {
if len(args) == 0 {
args = []string{""}
}
var tzs []string
zones := make(map[string]bool)
for _, arg := range args {
for _, tz := range zoneMatch(arg) {
zones[tz] = true
}
}
if len(zones) == 0 {
fatalf("no matching time zones found")
}
tzs = make([]string, 0, len(zones))
for zone := range zones {
tzs = append(tzs, zone)
}
sort.Strings(tzs)
w := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0)
for _, tz := range tzs {
linked := zoneNames[tz]
if !*alias || linked == "" {
fmt.Fprintf(w, "%s\n", tz)
} else {
fmt.Fprintf(w, "%s\t%s\n", tz, linked)
}
}
w.Flush()
}

func zoneMatch(tz string) []string {
if _, ok := zoneNames[tz]; ok {
return []string{tz}
}
var matches []string
for name := range zoneNames {
if strings.EqualFold(name, tz) {
matches = append(matches, name)
}
}
if len(matches) > 0 {
return matches
}
tz = strings.ToLower(tz)
for name := range zoneNames {
if strings.Contains(strings.ToLower(name), tz) {
matches = append(matches, name)
}
}
return matches
}

func allIdenticalZones(tzs []string) bool {
if len(tzs) < 2 {
return true
}
ctz := canonicalTimezone(tzs[0])
for _, tz := range tzs[1:] {
if canonicalTimezone(tz) != ctz {
return false
}
}
return true
}

func canonicalTimezone(tz string) string {
for {
link := zoneNames[tz]
if link == "" {
return tz
}
tz = link
}
}

func fatalf(f string, a ...interface{}) {
fmt.Fprintf(os.Stderr, "%s\n", fmt.Sprintf(f, a...))
os.Exit(1)
Expand Down
Loading

0 comments on commit 93ce1f8

Please sign in to comment.