Fix duration pretty printing and number shortening
This commit is contained in:
parent
8863daac60
commit
b43fec144b
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
40
pkg/utility/number_format.go
Normal file
40
pkg/utility/number_format.go
Normal file
|
@ -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))
|
||||
}
|
66
pkg/utility/number_format_test.go
Normal file
66
pkg/utility/number_format_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
143
pkg/utility/time_test.go
Normal file
143
pkg/utility/time_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -74,13 +74,13 @@
|
|||
<a class="navbar-item px-1" href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}">
|
||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||
<span>Friends</span>
|
||||
{{if .NavFriendRequests}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavFriendRequests}}</span>{{end}}
|
||||
{{if .NavFriendRequests}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{FormatNumberShort .NavFriendRequests}}</span>{{end}}
|
||||
</a>
|
||||
|
||||
<a class="navbar-item px-1" href="/messages">
|
||||
<span class="icon"><i class="fa fa-envelope"></i></span>
|
||||
<span>Messages</span>
|
||||
{{if .NavUnreadMessages}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavUnreadMessages}}</span>{{end}}
|
||||
{{if .NavUnreadMessages}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{FormatNumberShort .NavUnreadMessages}}</span>{{end}}
|
||||
</a>
|
||||
|
||||
<a class="navbar-item px-1" href="/members">
|
||||
|
@ -140,14 +140,16 @@
|
|||
</div>
|
||||
<div class="column">
|
||||
{{.CurrentUser.Username}}
|
||||
{{if .NavUnreadNotifications}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavUnreadNotifications}}</span>{{end}}
|
||||
{{if .NavUnreadNotifications}}
|
||||
<span class="nonshy-navbar-notification-tag is-warning ml-1">{{FormatNumberShort .NavUnreadNotifications}}</span>
|
||||
{{end}}
|
||||
{{if .NavAdminNotifications}}
|
||||
{{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}}
|
||||
<span class="nonshy-navbar-notification-tag is-success ml-1">{{.NavAdminNotifications}}</span>
|
||||
<span class="nonshy-navbar-notification-tag is-success ml-1">{{FormatNumberShort .NavAdminNotifications}}</span>
|
||||
{{else if and .NavCertificationPhotos .NavAdminFeedback}}
|
||||
<span class="nonshy-navbar-notification-tag is-mixed ml-1">{{.NavAdminNotifications}}</span>
|
||||
<span class="nonshy-navbar-notification-tag is-mixed ml-1">{{FormatNumberShort .NavAdminNotifications}}</span>
|
||||
{{else}}
|
||||
<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>
|
||||
<span class="nonshy-navbar-notification-tag is-danger ml-1">{{FormatNumberShort .NavAdminNotifications}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -161,7 +163,7 @@
|
|||
{{if .NavUnreadNotifications}}
|
||||
<span class="nonshy-navbar-notification-tag is-warning ml-1">
|
||||
<span class="icon"><i class="fa fa-bell"></i></span>
|
||||
<span>{{.NavUnreadNotifications}}</span>
|
||||
<span>{{FormatNumberShort .NavUnreadNotifications}}</span>
|
||||
</span>
|
||||
{{end}}
|
||||
</a>
|
||||
|
@ -202,11 +204,11 @@
|
|||
{{if .NavAdminNotifications}}
|
||||
<!-- Color code them by the type -->
|
||||
{{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}}
|
||||
<span class="nonshy-navbar-notification-tag is-success ml-1">{{.NavAdminNotifications}}</span>
|
||||
<span class="nonshy-navbar-notification-tag is-success ml-1">{{FormatNumberShort .NavAdminNotifications}}</span>
|
||||
{{else if and .NavCertificationPhotos .NavAdminFeedback}}
|
||||
<span class="nonshy-navbar-notification-tag is-mixed ml-1">{{.NavAdminNotifications}}</span>
|
||||
<span class="nonshy-navbar-notification-tag is-mixed ml-1">{{FormatNumberShort .NavAdminNotifications}}</span>
|
||||
{{else}}
|
||||
<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>
|
||||
<span class="nonshy-navbar-notification-tag is-danger ml-1">{{FormatNumberShort .NavAdminNotifications}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</a>
|
||||
|
@ -247,7 +249,7 @@
|
|||
href="/chat">
|
||||
<span class="icon"><i class="fa fa-message"></i></span>
|
||||
{{if gt .NavChatStatistics.UserCount 0}}
|
||||
<small class="nonshy-navbar-notification-count">{{.NavChatStatistics.UserCount}}</small>
|
||||
<small class="nonshy-navbar-notification-count">{{FormatNumberShort .NavChatStatistics.UserCount}}</small>
|
||||
{{end}}
|
||||
</a>
|
||||
|
||||
|
@ -266,7 +268,7 @@
|
|||
href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}">
|
||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||
{{if gt .NavFriendRequests 0}}
|
||||
<small class="nonshy-navbar-notification-count">{{.NavFriendRequests}}</small>
|
||||
<small class="nonshy-navbar-notification-count">{{FormatNumberShort .NavFriendRequests}}</small>
|
||||
{{end}}
|
||||
</a>
|
||||
|
||||
|
@ -274,14 +276,14 @@
|
|||
<a class="tag {{if gt .NavUnreadMessages 0}}is-warning{{else}}is-grey{{end}} py-4" href="/messages">
|
||||
<span class="icon"><i class="fa fa-envelope"></i></span>
|
||||
{{if gt .NavUnreadMessages 0}}
|
||||
<small class="nonshy-navbar-notification-count">{{.NavUnreadMessages}}</small>
|
||||
<small class="nonshy-navbar-notification-count">{{FormatNumberShort .NavUnreadMessages}}</small>
|
||||
{{end}}
|
||||
</a>
|
||||
|
||||
{{if gt .NavUnreadNotifications 0}}
|
||||
<a class="tag is-warning py-4" href="/me#notifications">
|
||||
<span class="icon"><i class="fa fa-bell"></i></span>
|
||||
<small class="nonshy-navbar-notification-count">{{.NavUnreadNotifications}}</small>
|
||||
<small class="nonshy-navbar-notification-count">{{FormatNumberShort .NavUnreadNotifications}}</small>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
|
@ -290,17 +292,17 @@
|
|||
{{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}}
|
||||
<a class="tag is-success py-4" href="/admin">
|
||||
<span class="icon"><i class="fa fa-peace"></i></span>
|
||||
<small class="nonshy-navbar-notification-count">{{.NavAdminNotifications}}</small>
|
||||
<small class="nonshy-navbar-notification-count">{{FormatNumberShort .NavAdminNotifications}}</small>
|
||||
</a>
|
||||
{{else if and .NavCertificationPhotos .NavAdminFeedback}}
|
||||
<a class="tag is-mixed py-4" href="/admin">
|
||||
<span class="icon"><i class="fa fa-peace"></i></span>
|
||||
<small class="nonshy-navbar-notification-count">{{.NavAdminNotifications}}</small>
|
||||
<small class="nonshy-navbar-notification-count">{{FormatNumberShort .NavAdminNotifications}}</small>
|
||||
</a>
|
||||
{{else}}
|
||||
<a class="tag is-danger py-4" href="/admin">
|
||||
<span class="icon"><i class="fa fa-peace"></i></span>
|
||||
<small class="nonshy-navbar-notification-count">{{.NavAdminNotifications}}</small>
|
||||
<small class="nonshy-navbar-notification-count">{{FormatNumberShort .NavAdminNotifications}}</small>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in New Issue
Block a user