From b43fec144b0f9404e98c9638f806f9b8102333a0 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 21 Dec 2023 17:12:34 -0800 Subject: [PATCH] Fix duration pretty printing and number shortening --- pkg/config/admin_scopes.go | 2 +- pkg/templates/template_funcs.go | 15 ++++ pkg/utility/number_format.go | 40 +++++++++ pkg/utility/number_format_test.go | 66 ++++++++++++++ pkg/utility/time.go | 13 ++- pkg/utility/time_test.go | 143 ++++++++++++++++++++++++++++++ web/templates/base.html | 36 ++++---- 7 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 pkg/utility/number_format.go create mode 100644 pkg/utility/number_format_test.go create mode 100644 pkg/utility/time_test.go diff --git a/pkg/config/admin_scopes.go b/pkg/config/admin_scopes.go index 21a0e4c..07c7508 100644 --- a/pkg/config/admin_scopes.go +++ b/pkg/config/admin_scopes.go @@ -46,7 +46,7 @@ const ( ) // Number of expected scopes for unit test and validation. -const QuantityAdminScopes = 14 +const QuantityAdminScopes = 16 // The specially named Superusers group. const AdminGroupSuperusers = "Superusers" diff --git a/pkg/templates/template_funcs.go b/pkg/templates/template_funcs.go index f653d48..c64aed6 100644 --- a/pkg/templates/template_funcs.go +++ b/pkg/templates/template_funcs.go @@ -29,6 +29,7 @@ func TemplateFuncs(r *http.Request) template.FuncMap { return template.FuncMap{ "InputCSRF": InputCSRF(r), "SincePrettyCoarse": SincePrettyCoarse(), + "FormatNumberShort": FormatNumberShort(), "ComputeAge": utility.Age, "Split": strings.Split, "ToMarkdown": ToMarkdown, @@ -109,6 +110,20 @@ func SincePrettyCoarse() func(time.Time) template.HTML { } } +// FormatNumberShort will format any integer number into a short representation. +func FormatNumberShort() func(v interface{}) template.HTML { + return func(v interface{}) template.HTML { + var number int64 + switch v.(type) { + case int, int64, uint, uint64: + number = v.(int64) + default: + return template.HTML("#INVALID#") + } + return template.HTML(utility.FormatNumberShort(number)) + } +} + // ToMarkdown renders input text as Markdown. func ToMarkdown(input string) template.HTML { return template.HTML(markdown.Render(input)) diff --git a/pkg/utility/number_format.go b/pkg/utility/number_format.go new file mode 100644 index 0000000..d6b8ef2 --- /dev/null +++ b/pkg/utility/number_format.go @@ -0,0 +1,40 @@ +package utility + +import ( + "fmt" + "strconv" + "strings" +) + +// FormatNumberShort compresses a number into as short a string as possible (e.g. "1.2K" when it gets into the thousands). +func FormatNumberShort(value int64) string { + // Under 1,000? + if value < 1000 { + return fmt.Sprintf("%d", value) + } + + // Start to bucket it. + var ( + thousands = float64(value) / 1000 + millions = float64(thousands) / 1000 + billions = float64(millions) / 1000 + ) + + formatFloat := func(v float64) string { + s := strconv.FormatFloat(v, 'f', 1, 64) + if strings.HasSuffix(s, ".0") { + return strings.Split(s, ".")[0] + } + return s + } + + if thousands < 1000 { + return fmt.Sprintf("%sK", formatFloat(thousands)) + } + + if millions < 1000 { + return fmt.Sprintf("%sM", formatFloat(millions)) + } + + return fmt.Sprintf("%sB", formatFloat(billions)) +} diff --git a/pkg/utility/number_format_test.go b/pkg/utility/number_format_test.go new file mode 100644 index 0000000..eb3a8e5 --- /dev/null +++ b/pkg/utility/number_format_test.go @@ -0,0 +1,66 @@ +package utility_test + +import ( + "testing" + + "code.nonshy.com/nonshy/website/pkg/utility" +) + +func TestNumberFormat(t *testing.T) { + var tests = []struct { + In int64 + Expect string + }{ + {0, "0"}, + {1, "1"}, + {10, "10"}, + {15, "15"}, + {92, "92"}, + {100, "100"}, + {101, "101"}, + {150, "150"}, + {200, "200"}, + {404, "404"}, + {867, "867"}, + {990, "990"}, + {999, "999"}, + {1000, "1K"}, + {1001, "1K"}, + {1010, "1K"}, + {1100, "1.1K"}, + {1111, "1.1K"}, + {1200, "1.2K"}, + {1500, "1.5K"}, + {1700, "1.7K"}, + {1849, "1.8K"}, + {1850, "1.9K"}, + {1899, "1.9K"}, + {1900, "1.9K"}, + {12000, "12K"}, + {12300, "12.3K"}, + {900000, "900K"}, + {900100, "900.1K"}, + {999100, "999.1K"}, + {999500, "999.5K"}, + {999999, "1000K"}, // TODO: not ideal + {1000000, "1M"}, + {1001000, "1M"}, + {1005000, "1M"}, + {1010000, "1M"}, + {1100000, "1.1M"}, + {1200000, "1.2M"}, + {1305000, "1.3M"}, + {1350000, "1.4M"}, + {1400000, "1.4M"}, + {100000000, "100M"}, + {990509000, "990.5M"}, + {1000000000, "1B"}, + } + + for _, test := range tests { + actual := utility.FormatNumberShort(test.In) + if actual != test.Expect { + t.Errorf("Expected %d to be '%s' but got '%s'", test.In, test.Expect, actual) + } + } +} diff --git a/pkg/utility/time.go b/pkg/utility/time.go index 4b402be..f474859 100644 --- a/pkg/utility/time.go +++ b/pkg/utility/time.go @@ -2,6 +2,7 @@ package utility import ( "fmt" + "strconv" "strings" "time" ) @@ -38,10 +39,16 @@ func FormatDurationCoarse(duration time.Duration) string { } months := int64(days / 30) - if months <= 12 { + if months < 12 { return result("%d months", months) } - years := int64(days / 365) - return result("%d years", years) + // Over one year: start to show it as a floating point number of years (e.g. "1.2 years") + years := float64(days) / 365 + s := strconv.FormatFloat(years, 'f', 1, 64) + if strings.HasSuffix(s, ".0") { + y, _ := strconv.Atoi(strings.Split(s, ".")[0]) + return result("%d years", int64(y)) + } + return fmt.Sprintf("%s years", s) } diff --git a/pkg/utility/time_test.go b/pkg/utility/time_test.go new file mode 100644 index 0000000..1ea28d7 --- /dev/null +++ b/pkg/utility/time_test.go @@ -0,0 +1,143 @@ +package utility_test + +import ( + "testing" + "time" + + "code.nonshy.com/nonshy/website/pkg/utility" +) + +func TestFormatDurationCoarse(t *testing.T) { + var tests = []struct { + In time.Duration + Expect string + }{ + { + In: 0, + Expect: "0 seconds", + }, + { + In: 1 * time.Second, + Expect: "1 second", + }, + { + In: 2 * time.Second, + Expect: "2 seconds", + }, + { + In: 25 * time.Second, + Expect: "25 seconds", + }, + { + In: 59 * time.Second, + Expect: "59 seconds", + }, + { + In: 60 * time.Second, + Expect: "1 minute", + }, + { + In: 90 * time.Second, + Expect: "1 minute", + }, + { + In: 119 * time.Second, + Expect: "1 minute", + }, + { + In: 120 * time.Second, + Expect: "2 minutes", + }, + { + In: 15 * time.Minute, + Expect: "15 minutes", + }, + { + In: 59 * time.Minute, + Expect: "59 minutes", + }, + { + In: 60 * time.Minute, + Expect: "1 hour", + }, + { + In: 75 * time.Minute, + Expect: "1 hour", + }, + { + In: 119 * time.Minute, + Expect: "1 hour", + }, + { + In: 120 * time.Minute, + Expect: "2 hours", + }, + { + In: 14 * time.Hour, + Expect: "14 hours", + }, + { + In: 23 * time.Hour, + Expect: "23 hours", + }, + { + In: 24 * time.Hour, + Expect: "1 day", + }, + { + In: 36 * time.Hour, + Expect: "1 day", + }, + { + In: 48 * time.Hour, + Expect: "2 days", + }, + { + In: time.Hour * 24 * 60, + Expect: "2 months", + }, + { + In: time.Hour * 24 * 365, + Expect: "1 year", + }, + { + In: time.Hour * 24 * 30 * 12, + Expect: "1 year", + }, + { + In: time.Hour*24*30*12 + time.Hour*12, + Expect: "1 year", + }, + { + In: time.Hour * 24 * 30 * 13, + Expect: "1.1 years", + }, + { + In: time.Hour * 24 * 30 * 18, + Expect: "1.5 years", + }, + { + In: time.Hour * 24 * 30 * 22, + Expect: "1.8 years", + }, + { + In: time.Hour * 24 * 30 * 24, + Expect: "2 years", + }, + { + In: time.Hour * 24 * 30 * 36, + Expect: "3 years", + }, + { + In: time.Hour * 24 * 30 * 49, + Expect: "4 years", + }, + } + + for _, test := range tests { + actual := utility.FormatDurationCoarse(test.In) + if actual != test.Expect { + t.Errorf("Expected %d to be '%s' but got '%s'", test.In, test.Expect, actual) + } + } +} diff --git a/web/templates/base.html b/web/templates/base.html index 2003775..f93f132 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -74,13 +74,13 @@ Friends - {{if .NavFriendRequests}}{{.NavFriendRequests}}{{end}} + {{if .NavFriendRequests}}{{FormatNumberShort .NavFriendRequests}}{{end}} Messages - {{if .NavUnreadMessages}}{{.NavUnreadMessages}}{{end}} + {{if .NavUnreadMessages}}{{FormatNumberShort .NavUnreadMessages}}{{end}} @@ -140,14 +140,16 @@
{{.CurrentUser.Username}} - {{if .NavUnreadNotifications}}{{.NavUnreadNotifications}}{{end}} + {{if .NavUnreadNotifications}} + {{FormatNumberShort .NavUnreadNotifications}} + {{end}} {{if .NavAdminNotifications}} {{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{else if and .NavCertificationPhotos .NavAdminFeedback}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{else}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{end}} {{end}}
@@ -161,7 +163,7 @@ {{if .NavUnreadNotifications}} - {{.NavUnreadNotifications}} + {{FormatNumberShort .NavUnreadNotifications}} {{end}}
@@ -202,11 +204,11 @@ {{if .NavAdminNotifications}} {{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{else if and .NavCertificationPhotos .NavAdminFeedback}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{else}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{end}} {{end}} @@ -247,7 +249,7 @@ href="/chat"> {{if gt .NavChatStatistics.UserCount 0}} - {{.NavChatStatistics.UserCount}} + {{FormatNumberShort .NavChatStatistics.UserCount}} {{end}} @@ -266,7 +268,7 @@ href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}"> {{if gt .NavFriendRequests 0}} - {{.NavFriendRequests}} + {{FormatNumberShort .NavFriendRequests}} {{end}} @@ -274,14 +276,14 @@ {{if gt .NavUnreadMessages 0}} - {{.NavUnreadMessages}} + {{FormatNumberShort .NavUnreadMessages}} {{end}} {{if gt .NavUnreadNotifications 0}} - {{.NavUnreadNotifications}} + {{FormatNumberShort .NavUnreadNotifications}} {{end}} @@ -290,17 +292,17 @@ {{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{else if and .NavCertificationPhotos .NavAdminFeedback}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{else}} - {{.NavAdminNotifications}} + {{FormatNumberShort .NavAdminNotifications}} {{end}} {{end}}