From 05affbcb555c41eb058952f6cb2c75b930dce172 Mon Sep 17 00:00:00 2001 From: Mathias Gumz Date: Sat, 22 Apr 2023 16:21:08 +0200 Subject: [PATCH 1/5] Add -dtfmt flag to customize output date/time format --- cmd/astral/main.go | 66 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/cmd/astral/main.go b/cmd/astral/main.go index db62520..fd5c8ec 100644 --- a/cmd/astral/main.go +++ b/cmd/astral/main.go @@ -23,7 +23,7 @@ var ( func main() { var ( - dateTimeFormat = "Jan _2 15:04" + dateTimeFormat = TimeFormatAstral timeFormat = "15:04" timeFlag = flag.String("time", time.Now().Format(time.RFC3339), "day/time used for the calculation") @@ -32,6 +32,7 @@ func main() { elevationFlag = flag.Float64("elev", 0, "elevation of the observer") versionFlag = flag.Bool("version", false, fmt.Sprintf("print version information of this release (%v)", version)) ) + flag.StringVar(&dateTimeFormat, "dtfmt", dateTimeFormat, "date/time format to use for output") flag.Parse() if *versionFlag { @@ -41,6 +42,12 @@ func main() { os.Exit(0) } + dateTimeFormat, err := FormatName(dateTimeFormat) + if err != nil { + fmt.Fprintf(os.Stderr, "error '-dtfmt': %s", err) + return + } + observer := astral.Observer{Latitude: *latFlag, Longitude: *longFlag, Elevation: *elevationFlag} t, err := time.Parse(time.RFC3339, *timeFlag) @@ -195,3 +202,60 @@ func (p timeSlice) Less(i, j int) bool { func (p timeSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +const ( + // astrals default time format + TimeFormatAstral = "Jan _2 15:04" + // TimeFormatGo handles Go's default time.Now() format + // (e.g. 2019-01-26 09:43:57.377055 +0100 CET m=+0.644739467) + TimeFormatGo = "2006-01-02 15:04:05.999999999 -0700 MST" + // TimeFormatSimple handles "2019-01-25 21:51:38" + TimeFormatSimple = "2006-01-02 15:04:05.999999999" + // TimeFormatHTTP instead of importing main with http.TimeFormat + // which would increase the binary size significantly. + TimeFormatHTTP = "Mon, 02 Jan 2006 15:04:05 GMT" +) + +func FormatName(format string) (string, error) { + + if format == TimeFormatAstral { + return TimeFormatAstral, nil + } + + switch strings.ToLower(format) { + case "": + return TimeFormatGo, nil + case "unix": + return time.UnixDate, nil + case "ruby": + return time.RubyDate, nil + case "ansic": + return time.ANSIC, nil + case "rfc822": + return time.RFC822, nil + case "rfc822z": + return time.RFC822Z, nil + case "rfc850": + return time.RFC850, nil + case "rfc1123": + return time.RFC1123, nil + case "rfc1123z": + return time.RFC1123Z, nil + case "rfc3339": + return time.RFC3339, nil + case "rfc3339nano": + return time.RFC3339Nano, nil + case "stamp": + return time.Stamp, nil + case "stampmilli": + return time.StampMilli, nil + case "stampmicro": + return time.StampMicro, nil + case "stampnano": + return time.StampNano, nil + case "http": + return TimeFormatHTTP, nil + default: + return TimeFormatGo, fmt.Errorf("failed to parse format %q", format) + } +} From a92aefa504d6231b289e205325254e46d524f104 Mon Sep 17 00:00:00 2001 From: Mathias Gumz Date: Sun, 23 Apr 2023 10:41:07 +0200 Subject: [PATCH 2/5] Algorithmic approach to separator construction --- cmd/astral/main.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/cmd/astral/main.go b/cmd/astral/main.go index fd5c8ec..2f1618e 100644 --- a/cmd/astral/main.go +++ b/cmd/astral/main.go @@ -21,6 +21,8 @@ var ( date = "undefined" ) +const dashes = "┈" + func main() { var ( dateTimeFormat = TimeFormatAstral @@ -116,10 +118,8 @@ func main() { log.Fatalf("failed parsing moon phase: %v", err) } - dashes := "┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈" - dates := make(map[time.Time]colorDesc) - dates[t] = colorDesc{desc: dashes} + dates[t] = colorDesc{special: true} dates[dawnAstronomical] = colorDesc{color: aurora.BgGray(8, " "), desc: "Dawn (Astronomical)"} dates[dawnNautical] = colorDesc{color: aurora.BgGray(15, " "), desc: "Dawn (Nautical)"} dates[dawnCivil] = colorDesc{color: aurora.BgIndex(111, " "), desc: "Dawn (Civil) Twilight Start Blue Hour Start"} @@ -150,6 +150,14 @@ func main() { fmt.Printf("Moon Phase\t%v (%v)\n", moonDesc, moonPhase) fmt.Println() + longestDesc := 0 + for _, key := range sortedTimes { + ld := len(dates[key].desc) + if ld > longestDesc { + longestDesc = ld + } + } + lastColor := aurora.BgBlack(" ") for _, key := range sortedTimes { @@ -165,17 +173,18 @@ func main() { } // edge case for the given time - if dates[key].desc == dashes { + if dates[key].special { prefixDashesCount := len(dateTimeFormat) - len(timeFormat) - 1 if prefixDashesCount < 0 { prefixDashesCount = 0 } - prefixDashes := key.Format(strings.Repeat("┈", prefixDashesCount)) - midDashes := strings.Repeat("┈", len(agoOrUntil)+2) + prefixDashes := key.Format(strings.Repeat(dashes, prefixDashesCount)) + midDashes := strings.Repeat(dashes, len(agoOrUntil)+2) + endDashes := strings.Repeat(dashes, longestDesc) t := key.Truncate(1 * time.Minute).Format(timeFormat) - fmt.Printf("%v %v %v %v %v\n", prefixDashes, t, midDashes, lastColor, dates[key].desc) + fmt.Printf("%v %v %v %v %v\n", prefixDashes, t, midDashes, lastColor, endDashes) continue } @@ -185,8 +194,9 @@ func main() { } type colorDesc struct { - color aurora.Value - desc string + color aurora.Value + desc string + special bool } type timeSlice []time.Time From c7d7fcf3c90f6cb47ff15bbe1305c3cb747193c4 Mon Sep 17 00:00:00 2001 From: Mathias Gumz Date: Sun, 23 Apr 2023 10:42:08 +0200 Subject: [PATCH 3/5] Cosmetic time formatting --- cmd/astral/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/astral/main.go b/cmd/astral/main.go index 2f1618e..ee7f08e 100644 --- a/cmd/astral/main.go +++ b/cmd/astral/main.go @@ -142,12 +142,12 @@ func main() { } sort.Sort(sortedTimes) - fmt.Printf("Date/Time\t%v\n", t.Format(time.UnixDate)) + fmt.Printf("Date/Time\t%v\n", t.Format(dateTimeFormat)) fmt.Printf("Latitude\t%v\nLongitude\t%v\nElevation\t%v\n", *latFlag, *longFlag, *elevationFlag) fmt.Println() fmt.Printf("Daylight\t%v\n", sunset.Sub(sunrise).Truncate(1*time.Second)) fmt.Printf("Night-Time\t%v\n", sunriseNextDay.Sub(sunset).Truncate(1*time.Second)) - fmt.Printf("Moon Phase\t%v (%v)\n", moonDesc, moonPhase) + fmt.Printf("Moon Phase\t%v (%.2f)\n", moonDesc, moonPhase) fmt.Println() longestDesc := 0 From d0ab59a7562db444c887d80db2d0e9e67fc3bd73 Mon Sep 17 00:00:00 2001 From: Mathias Gumz Date: Sun, 23 Apr 2023 11:31:41 +0200 Subject: [PATCH 4/5] More robust rendering of time --- cmd/astral/main.go | 67 +++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/cmd/astral/main.go b/cmd/astral/main.go index ee7f08e..41be730 100644 --- a/cmd/astral/main.go +++ b/cmd/astral/main.go @@ -9,6 +9,7 @@ import ( "sort" "strings" "time" + "unicode/utf8" "github.com/logrusorgru/aurora/v3" "github.com/sj14/astral/pkg/astral" @@ -118,22 +119,22 @@ func main() { log.Fatalf("failed parsing moon phase: %v", err) } - dates := make(map[time.Time]colorDesc) - dates[t] = colorDesc{special: true} - dates[dawnAstronomical] = colorDesc{color: aurora.BgGray(8, " "), desc: "Dawn (Astronomical)"} - dates[dawnNautical] = colorDesc{color: aurora.BgGray(15, " "), desc: "Dawn (Nautical)"} - dates[dawnCivil] = colorDesc{color: aurora.BgIndex(111, " "), desc: "Dawn (Civil) Twilight Start Blue Hour Start"} - dates[goldenRisingStart] = colorDesc{color: aurora.BgIndex(208, " "), desc: "Golden Hour Start Blue Hour End"} - dates[sunrise] = colorDesc{color: aurora.BgIndex(214, " "), desc: "Sunrise Twilight End"} - dates[goldenRisingEnd] = colorDesc{color: aurora.BgIndex(226, " "), desc: "Golden Hour End"} - dates[noon] = colorDesc{color: aurora.BgIndex(226, " "), desc: "Noon"} - dates[goldenSettingStart] = colorDesc{color: aurora.BgIndex(214, " "), desc: "Golden Hour Start"} - dates[sunset] = colorDesc{color: aurora.BgIndex(208, " "), desc: "Sunset Twilight Start"} - dates[goldenSettingEnd] = colorDesc{color: aurora.BgIndex(111, " "), desc: "Golden Hour End Blue Hour Start"} - dates[duskCivil] = colorDesc{color: aurora.BgGray(18, " "), desc: "Dusk (Civil) Twilight End Blue Hour End "} - dates[duskNautical] = colorDesc{color: aurora.BgGray(15, " "), desc: "Dusk (Nautical)"} - dates[duskAstronomical] = colorDesc{color: aurora.BgGray(8, " "), desc: "Dusk (Astronomical)"} - dates[midnight] = colorDesc{color: aurora.BgBlack(" "), desc: "Midnight"} + dates := make(map[time.Time]*colorDesc) + dates[t] = &colorDesc{special: true} + dates[dawnAstronomical] = &colorDesc{color: aurora.BgGray(8, " "), desc: "Dawn (Astronomical)"} + dates[dawnNautical] = &colorDesc{color: aurora.BgGray(15, " "), desc: "Dawn (Nautical)"} + dates[dawnCivil] = &colorDesc{color: aurora.BgIndex(111, " "), desc: "Dawn (Civil) Twilight Start Blue Hour Start"} + dates[goldenRisingStart] = &colorDesc{color: aurora.BgIndex(208, " "), desc: "Golden Hour Start Blue Hour End"} + dates[sunrise] = &colorDesc{color: aurora.BgIndex(214, " "), desc: "Sunrise Twilight End"} + dates[goldenRisingEnd] = &colorDesc{color: aurora.BgIndex(226, " "), desc: "Golden Hour End"} + dates[noon] = &colorDesc{color: aurora.BgIndex(226, " "), desc: "Noon"} + dates[goldenSettingStart] = &colorDesc{color: aurora.BgIndex(214, " "), desc: "Golden Hour Start"} + dates[sunset] = &colorDesc{color: aurora.BgIndex(208, " "), desc: "Sunset Twilight Start"} + dates[goldenSettingEnd] = &colorDesc{color: aurora.BgIndex(111, " "), desc: "Golden Hour End Blue Hour Start"} + dates[duskCivil] = &colorDesc{color: aurora.BgGray(18, " "), desc: "Dusk (Civil) Twilight End Blue Hour End "} + dates[duskNautical] = &colorDesc{color: aurora.BgGray(15, " "), desc: "Dusk (Nautical)"} + dates[duskAstronomical] = &colorDesc{color: aurora.BgGray(8, " "), desc: "Dusk (Astronomical)"} + dates[midnight] = &colorDesc{color: aurora.BgBlack(" "), desc: "Midnight"} var sortedTimes timeSlice @@ -165,31 +166,31 @@ func main() { inHours := math.Abs(key.Sub(t).Truncate(1 * time.Hour).Hours()) inMinutes := int(math.Abs((key.Sub(t).Truncate(1 * time.Minute).Minutes()))) % 60 - agoOrUntil := fmt.Sprintf("%02.0f:%02d", inHours, inMinutes) + sign := "+" if key.Before(t) { - agoOrUntil = fmt.Sprintf("-%s", agoOrUntil) - } else { - agoOrUntil = fmt.Sprintf("+%s", agoOrUntil) + sign = "-" } + agoOrUntil := fmt.Sprintf("%s%02.0f:%02d", sign, inHours, inMinutes) + + prefix := fmt.Sprintf("%v (%v)", key.Format(dateTimeFormat), agoOrUntil) // edge case for the given time if dates[key].special { - prefixDashesCount := len(dateTimeFormat) - len(timeFormat) - 1 - if prefixDashesCount < 0 { - prefixDashesCount = 0 - } - - prefixDashes := key.Format(strings.Repeat(dashes, prefixDashesCount)) - midDashes := strings.Repeat(dashes, len(agoOrUntil)+2) - endDashes := strings.Repeat(dashes, longestDesc) - t := key.Truncate(1 * time.Minute).Format(timeFormat) - - fmt.Printf("%v %v %v %v %v\n", prefixDashes, t, midDashes, lastColor, endDashes) - continue + + t := fmt.Sprintf(" %v ", key.Truncate(1*time.Minute).Format(timeFormat)) + l := utf8.RuneCountInString(prefix) - utf8.RuneCountInString(t) + + preDashes := strings.Repeat(dashes, l/2) + postDashes := strings.Repeat(dashes, l-(l/2)) + + prefix = fmt.Sprintf("%v%v%v", preDashes, t, postDashes) + dates[key].color = lastColor + dates[key].desc = strings.Repeat(dashes, longestDesc) } lastColor = dates[key].color - fmt.Printf("%v (%v) %v %v\n", key.Format(dateTimeFormat), agoOrUntil, dates[key].color, dates[key].desc) + + fmt.Printf("%v %v %v\n", prefix, dates[key].color, dates[key].desc) } } From 3a41f1d3acbde81efc359692e27dc97825c7e131 Mon Sep 17 00:00:00 2001 From: Mathias Gumz Date: Sun, 23 Apr 2023 16:23:31 +0200 Subject: [PATCH 5/5] Refactor data structure Instead of a map and a slice, just use a slice, sort that once and then print it. --- cmd/astral/main.go | 81 +++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/cmd/astral/main.go b/cmd/astral/main.go index 41be730..6a7fe4f 100644 --- a/cmd/astral/main.go +++ b/cmd/astral/main.go @@ -119,29 +119,24 @@ func main() { log.Fatalf("failed parsing moon phase: %v", err) } - dates := make(map[time.Time]*colorDesc) - dates[t] = &colorDesc{special: true} - dates[dawnAstronomical] = &colorDesc{color: aurora.BgGray(8, " "), desc: "Dawn (Astronomical)"} - dates[dawnNautical] = &colorDesc{color: aurora.BgGray(15, " "), desc: "Dawn (Nautical)"} - dates[dawnCivil] = &colorDesc{color: aurora.BgIndex(111, " "), desc: "Dawn (Civil) Twilight Start Blue Hour Start"} - dates[goldenRisingStart] = &colorDesc{color: aurora.BgIndex(208, " "), desc: "Golden Hour Start Blue Hour End"} - dates[sunrise] = &colorDesc{color: aurora.BgIndex(214, " "), desc: "Sunrise Twilight End"} - dates[goldenRisingEnd] = &colorDesc{color: aurora.BgIndex(226, " "), desc: "Golden Hour End"} - dates[noon] = &colorDesc{color: aurora.BgIndex(226, " "), desc: "Noon"} - dates[goldenSettingStart] = &colorDesc{color: aurora.BgIndex(214, " "), desc: "Golden Hour Start"} - dates[sunset] = &colorDesc{color: aurora.BgIndex(208, " "), desc: "Sunset Twilight Start"} - dates[goldenSettingEnd] = &colorDesc{color: aurora.BgIndex(111, " "), desc: "Golden Hour End Blue Hour Start"} - dates[duskCivil] = &colorDesc{color: aurora.BgGray(18, " "), desc: "Dusk (Civil) Twilight End Blue Hour End "} - dates[duskNautical] = &colorDesc{color: aurora.BgGray(15, " "), desc: "Dusk (Nautical)"} - dates[duskAstronomical] = &colorDesc{color: aurora.BgGray(8, " "), desc: "Dusk (Astronomical)"} - dates[midnight] = &colorDesc{color: aurora.BgBlack(" "), desc: "Midnight"} - - var sortedTimes timeSlice - - for key := range dates { - sortedTimes = append(sortedTimes, key) + dates := eventTable{ + &eventEntry{t, colorDesc{special: true}}, + &eventEntry{dawnAstronomical, colorDesc{color: aurora.BgGray(8, " "), desc: "Dawn (Astronomical)"}}, + &eventEntry{dawnNautical, colorDesc{color: aurora.BgGray(15, " "), desc: "Dawn (Nautical)"}}, + &eventEntry{dawnCivil, colorDesc{color: aurora.BgIndex(111, " "), desc: "Dawn (Civil) Twilight Start Blue Hour Start"}}, + &eventEntry{goldenRisingStart, colorDesc{color: aurora.BgIndex(208, " "), desc: "Golden Hour Start Blue Hour End"}}, + &eventEntry{sunrise, colorDesc{color: aurora.BgIndex(214, " "), desc: "Sunrise Twilight End"}}, + &eventEntry{goldenRisingEnd, colorDesc{color: aurora.BgIndex(226, " "), desc: "Golden Hour End"}}, + &eventEntry{noon, colorDesc{color: aurora.BgIndex(226, " "), desc: "Noon"}}, + &eventEntry{goldenSettingStart, colorDesc{color: aurora.BgIndex(214, " "), desc: "Golden Hour Start"}}, + &eventEntry{sunset, colorDesc{color: aurora.BgIndex(208, " "), desc: "Sunset Twilight Start"}}, + &eventEntry{goldenSettingEnd, colorDesc{color: aurora.BgIndex(111, " "), desc: "Golden Hour End Blue Hour Start"}}, + &eventEntry{duskCivil, colorDesc{color: aurora.BgGray(18, " "), desc: "Dusk (Civil) Twilight End Blue Hour End "}}, + &eventEntry{duskNautical, colorDesc{color: aurora.BgGray(15, " "), desc: "Dusk (Nautical)"}}, + &eventEntry{duskAstronomical, colorDesc{color: aurora.BgGray(8, " "), desc: "Dusk (Astronomical)"}}, + &eventEntry{midnight, colorDesc{color: aurora.BgBlack(" "), desc: "Midnight"}}, } - sort.Sort(sortedTimes) + sort.Sort(dates) fmt.Printf("Date/Time\t%v\n", t.Format(dateTimeFormat)) fmt.Printf("Latitude\t%v\nLongitude\t%v\nElevation\t%v\n", *latFlag, *longFlag, *elevationFlag) @@ -152,45 +147,44 @@ func main() { fmt.Println() longestDesc := 0 - for _, key := range sortedTimes { - ld := len(dates[key].desc) - if ld > longestDesc { + for _, de := range dates { + if ld := utf8.RuneCountInString(de.desc); ld > longestDesc { longestDesc = ld } } lastColor := aurora.BgBlack(" ") - for _, key := range sortedTimes { + for _, de := range dates { // calculate when the particular phase happend or will happen - inHours := math.Abs(key.Sub(t).Truncate(1 * time.Hour).Hours()) - inMinutes := int(math.Abs((key.Sub(t).Truncate(1 * time.Minute).Minutes()))) % 60 + inHours := math.Abs(de.time.Sub(t).Truncate(1 * time.Hour).Hours()) + inMinutes := int(math.Abs((de.time.Sub(t).Truncate(1 * time.Minute).Minutes()))) % 60 sign := "+" - if key.Before(t) { + if de.time.Before(t) { sign = "-" } agoOrUntil := fmt.Sprintf("%s%02.0f:%02d", sign, inHours, inMinutes) - prefix := fmt.Sprintf("%v (%v)", key.Format(dateTimeFormat), agoOrUntil) + prefix := fmt.Sprintf("%v (%v)", de.time.Format(dateTimeFormat), agoOrUntil) // edge case for the given time - if dates[key].special { + if de.special { - t := fmt.Sprintf(" %v ", key.Truncate(1*time.Minute).Format(timeFormat)) + t := fmt.Sprintf(" %v ", de.time.Truncate(1*time.Minute).Format(timeFormat)) l := utf8.RuneCountInString(prefix) - utf8.RuneCountInString(t) preDashes := strings.Repeat(dashes, l/2) postDashes := strings.Repeat(dashes, l-(l/2)) prefix = fmt.Sprintf("%v%v%v", preDashes, t, postDashes) - dates[key].color = lastColor - dates[key].desc = strings.Repeat(dashes, longestDesc) + de.color = lastColor + de.desc = strings.Repeat(dashes, longestDesc) } - lastColor = dates[key].color + lastColor = de.color - fmt.Printf("%v %v %v\n", prefix, dates[key].color, dates[key].desc) + fmt.Printf("%v %v %v\n", prefix, de.color, de.desc) } } @@ -200,17 +194,22 @@ type colorDesc struct { special bool } -type timeSlice []time.Time +type eventEntry struct { + time time.Time + colorDesc +} + +type eventTable []*eventEntry -func (p timeSlice) Len() int { +func (p eventTable) Len() int { return len(p) } -func (p timeSlice) Less(i, j int) bool { - return p[i].Before(p[j]) +func (p eventTable) Less(i, j int) bool { + return p[i].time.Before(p[j].time) } -func (p timeSlice) Swap(i, j int) { +func (p eventTable) Swap(i, j int) { p[i], p[j] = p[j], p[i] }