128 lines
2.5 KiB
Go
128 lines
2.5 KiB
Go
package web
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
var currencySymbols = map[string]string{
|
||
|
|
"USD": "$", "GBP": "£", "EUR": "€", "JPY": "¥",
|
||
|
|
}
|
||
|
|
|
||
|
|
func currencySymbol(currency string) string {
|
||
|
|
if sym, ok := currencySymbols[currency]; ok {
|
||
|
|
return sym
|
||
|
|
}
|
||
|
|
return currency + " "
|
||
|
|
}
|
||
|
|
|
||
|
|
// money formats an amount in the given currency with thousands separators,
|
||
|
|
// e.g. money(-1234.5, "GBP") -> "-£1,234.50".
|
||
|
|
func money(v float64, currency string) string {
|
||
|
|
neg := v < 0
|
||
|
|
if neg {
|
||
|
|
v = -v
|
||
|
|
}
|
||
|
|
s := strconv.FormatFloat(v, 'f', 2, 64)
|
||
|
|
intPart, decPart, _ := strings.Cut(s, ".")
|
||
|
|
var out []byte
|
||
|
|
n := len(intPart)
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
if i > 0 && (n-i)%3 == 0 {
|
||
|
|
out = append(out, ',')
|
||
|
|
}
|
||
|
|
out = append(out, intPart[i])
|
||
|
|
}
|
||
|
|
res := currencySymbol(currency) + string(out) + "." + decPart
|
||
|
|
if neg {
|
||
|
|
res = "-" + res
|
||
|
|
}
|
||
|
|
return res
|
||
|
|
}
|
||
|
|
|
||
|
|
// amt formats a crypto quantity, trimming trailing zeros.
|
||
|
|
func amt(v float64) string {
|
||
|
|
s := strconv.FormatFloat(v, 'f', 8, 64)
|
||
|
|
s = strings.TrimRight(s, "0")
|
||
|
|
s = strings.TrimRight(s, ".")
|
||
|
|
if s == "" || s == "-" {
|
||
|
|
s = "0"
|
||
|
|
}
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
func pctStr(v *float64) string {
|
||
|
|
if v == nil {
|
||
|
|
return "—"
|
||
|
|
}
|
||
|
|
sign := ""
|
||
|
|
if *v > 0 {
|
||
|
|
sign = "+"
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%s%.2f%%", sign, *v)
|
||
|
|
}
|
||
|
|
|
||
|
|
func pctClass(v *float64) string {
|
||
|
|
if v == nil {
|
||
|
|
return "flat"
|
||
|
|
}
|
||
|
|
return signClass(*v)
|
||
|
|
}
|
||
|
|
|
||
|
|
func signClass(v float64) string {
|
||
|
|
if v > 0 {
|
||
|
|
return "gain"
|
||
|
|
}
|
||
|
|
if v < 0 {
|
||
|
|
return "loss"
|
||
|
|
}
|
||
|
|
return "flat"
|
||
|
|
}
|
||
|
|
|
||
|
|
func dateStr(t time.Time) string {
|
||
|
|
return t.Format("Jan 2, 2006")
|
||
|
|
}
|
||
|
|
|
||
|
|
// capitalize upper-cases the first letter of an entry type ("withdrawal" -> "Withdrawal").
|
||
|
|
func capitalize(s string) string {
|
||
|
|
if s == "" {
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
return strings.ToUpper(s[:1]) + s[1:]
|
||
|
|
}
|
||
|
|
|
||
|
|
// sortVal renders a float64 or *float64 as a plain number string for a
|
||
|
|
// data-sort-value attribute, so the client-side table sort compares numbers
|
||
|
|
// rather than formatted display text ("£1,234.56", "+3.21%", "—").
|
||
|
|
func sortVal(v interface{}) string {
|
||
|
|
switch x := v.(type) {
|
||
|
|
case float64:
|
||
|
|
return strconv.FormatFloat(x, 'f', -1, 64)
|
||
|
|
case *float64:
|
||
|
|
if x == nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return strconv.FormatFloat(*x, 'f', -1, 64)
|
||
|
|
default:
|
||
|
|
return fmt.Sprint(v)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func holdDuration(t time.Time) string {
|
||
|
|
days := int(time.Since(t).Hours() / 24)
|
||
|
|
switch {
|
||
|
|
case days < 1:
|
||
|
|
return "<1 day"
|
||
|
|
case days == 1:
|
||
|
|
return "1 day"
|
||
|
|
case days < 30:
|
||
|
|
return fmt.Sprintf("%d days", days)
|
||
|
|
case days < 365:
|
||
|
|
return fmt.Sprintf("%d mo", days/30)
|
||
|
|
default:
|
||
|
|
return fmt.Sprintf("%.1f yr", float64(days)/365)
|
||
|
|
}
|
||
|
|
}
|