add humanize package
This commit is contained in:
parent
4114e5fcb0
commit
4199b81b5f
|
@ -6,6 +6,7 @@ import (
|
||||||
_ "git.nspix.com/golang/kos/pkg/request"
|
_ "git.nspix.com/golang/kos/pkg/request"
|
||||||
_ "git.nspix.com/golang/kos/util/bs"
|
_ "git.nspix.com/golang/kos/util/bs"
|
||||||
_ "git.nspix.com/golang/kos/util/fetch"
|
_ "git.nspix.com/golang/kos/util/fetch"
|
||||||
|
_ "git.nspix.com/golang/kos/util/humanize"
|
||||||
_ "git.nspix.com/golang/kos/util/random"
|
_ "git.nspix.com/golang/kos/util/random"
|
||||||
_ "git.nspix.com/golang/kos/util/reflection"
|
_ "git.nspix.com/golang/kos/util/reflection"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"git.nspix.com/golang/kos/util/bs"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Nanosecond Duration = 1
|
||||||
|
Microsecond = 1000 * Nanosecond
|
||||||
|
Millisecond = 1000 * Microsecond
|
||||||
|
Second = 1000 * Millisecond
|
||||||
|
Minute = 60 * Second
|
||||||
|
Hour = 60 * Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type Duration int64
|
||||||
|
|
||||||
|
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
|
||||||
|
// tail of buf, omitting trailing zeros. It omits the decimal
|
||||||
|
// point too when the fraction is 0. It returns the index where the
|
||||||
|
// output bytes begin and the value v/10**prec.
|
||||||
|
func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
|
||||||
|
// Omit trailing zeros up to and including decimal point.
|
||||||
|
w := len(buf)
|
||||||
|
print := false
|
||||||
|
for i := 0; i < prec; i++ {
|
||||||
|
digit := v % 10
|
||||||
|
print = print || digit != 0
|
||||||
|
if print {
|
||||||
|
w--
|
||||||
|
buf[w] = byte(digit) + '0'
|
||||||
|
}
|
||||||
|
v /= 10
|
||||||
|
}
|
||||||
|
if print {
|
||||||
|
w--
|
||||||
|
buf[w] = '.'
|
||||||
|
}
|
||||||
|
return w, v
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmtInt formats v into the tail of buf.
|
||||||
|
// It returns the index where the output begins.
|
||||||
|
func fmtInt(buf []byte, v uint64) int {
|
||||||
|
w := len(buf)
|
||||||
|
if v == 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = '0'
|
||||||
|
} else {
|
||||||
|
for v > 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = byte(v%10) + '0'
|
||||||
|
v /= 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) String() string {
|
||||||
|
// Largest time is 2540400h10m10.000000000s
|
||||||
|
var buf [32]byte
|
||||||
|
w := len(buf)
|
||||||
|
|
||||||
|
u := uint64(d)
|
||||||
|
neg := d < 0
|
||||||
|
if neg {
|
||||||
|
u = -u
|
||||||
|
}
|
||||||
|
|
||||||
|
if u < uint64(time.Second) {
|
||||||
|
// Special case: if duration is smaller than a second,
|
||||||
|
// use smaller units, like 1.2ms
|
||||||
|
var prec int
|
||||||
|
w--
|
||||||
|
buf[w] = 's'
|
||||||
|
w--
|
||||||
|
switch {
|
||||||
|
case u == 0:
|
||||||
|
return "0s"
|
||||||
|
case u < uint64(time.Microsecond):
|
||||||
|
// print nanoseconds
|
||||||
|
prec = 0
|
||||||
|
buf[w] = 'n'
|
||||||
|
case u < uint64(time.Millisecond):
|
||||||
|
// print microseconds
|
||||||
|
prec = 3
|
||||||
|
// U+00B5 'µ' micro sign == 0xC2 0xB5
|
||||||
|
w-- // Need room for two bytes.
|
||||||
|
copy(buf[w:], "µ")
|
||||||
|
default:
|
||||||
|
// print milliseconds
|
||||||
|
prec = 6
|
||||||
|
buf[w] = 'm'
|
||||||
|
}
|
||||||
|
w, u = fmtFrac(buf[:w], u, prec)
|
||||||
|
w = fmtInt(buf[:w], u)
|
||||||
|
} else {
|
||||||
|
w--
|
||||||
|
buf[w] = 's'
|
||||||
|
|
||||||
|
w, u = fmtFrac(buf[:w], u, 9)
|
||||||
|
|
||||||
|
// u is now integer seconds
|
||||||
|
w = fmtInt(buf[:w], u%60)
|
||||||
|
u /= 60
|
||||||
|
|
||||||
|
// u is now integer minutes
|
||||||
|
if u > 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = 'm'
|
||||||
|
w = fmtInt(buf[:w], u%60)
|
||||||
|
u /= 60
|
||||||
|
|
||||||
|
// u is now integer hours
|
||||||
|
// Stop at hours because days can be different lengths.
|
||||||
|
if u > 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = 'h'
|
||||||
|
w = fmtInt(buf[:w], u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if neg {
|
||||||
|
w--
|
||||||
|
buf[w] = '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf[w:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
var n time.Duration
|
||||||
|
b = bytes.TrimFunc(b, func(r rune) bool {
|
||||||
|
if r == '"' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if n, err = time.ParseDuration(bs.BytesToString(b)); err == nil {
|
||||||
|
*d = Duration(n)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
|
return bs.StringToBytes(`"` + d.String() + `"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) Duration() time.Duration {
|
||||||
|
return time.Duration(d)
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
renderFloatPrecisionMultipliers = [...]float64{
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
100,
|
||||||
|
1000,
|
||||||
|
10000,
|
||||||
|
100000,
|
||||||
|
1000000,
|
||||||
|
10000000,
|
||||||
|
100000000,
|
||||||
|
1000000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFloatPrecisionRounders = [...]float64{
|
||||||
|
0.5,
|
||||||
|
0.05,
|
||||||
|
0.005,
|
||||||
|
0.0005,
|
||||||
|
0.00005,
|
||||||
|
0.000005,
|
||||||
|
0.0000005,
|
||||||
|
0.00000005,
|
||||||
|
0.000000005,
|
||||||
|
0.0000000005,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||||
|
// * thousands separator
|
||||||
|
// * decimal separator
|
||||||
|
// * decimal precision
|
||||||
|
//
|
||||||
|
// Usage: s := RenderFloat(format, n)
|
||||||
|
// The format parameter tells how to render the number n.
|
||||||
|
//
|
||||||
|
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||||
|
//
|
||||||
|
// Examples of format strings, given n = 12345.6789:
|
||||||
|
// "#,###.##" => "12,345.67"
|
||||||
|
// "#,###." => "12,345"
|
||||||
|
// "#,###" => "12345,678"
|
||||||
|
// "#\u202F###,##" => "12 345,68"
|
||||||
|
// "#.###,###### => 12.345,678900
|
||||||
|
// "" (aka default format) => 12,345.67
|
||||||
|
//
|
||||||
|
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||||
|
// There is also a version for integer number, FormatInteger(),
|
||||||
|
// which is convenient for calls within template.
|
||||||
|
func FormatFloat(format string, n float64) string {
|
||||||
|
// Special cases:
|
||||||
|
// NaN = "NaN"
|
||||||
|
// +Inf = "+Infinity"
|
||||||
|
// -Inf = "-Infinity"
|
||||||
|
if math.IsNaN(n) {
|
||||||
|
return "NaN"
|
||||||
|
}
|
||||||
|
if n > math.MaxFloat64 {
|
||||||
|
return "Infinity"
|
||||||
|
}
|
||||||
|
if n < (0.0 - math.MaxFloat64) {
|
||||||
|
return "-Infinity"
|
||||||
|
}
|
||||||
|
|
||||||
|
// default format
|
||||||
|
precision := 2
|
||||||
|
decimalStr := "."
|
||||||
|
thousandStr := ","
|
||||||
|
positiveStr := ""
|
||||||
|
negativeStr := "-"
|
||||||
|
|
||||||
|
if len(format) > 0 {
|
||||||
|
format := []rune(format)
|
||||||
|
|
||||||
|
// If there is an explicit format directive,
|
||||||
|
// then default values are these:
|
||||||
|
precision = 9
|
||||||
|
thousandStr = ""
|
||||||
|
|
||||||
|
// collect indices of meaningful formatting directives
|
||||||
|
formatIndx := []int{}
|
||||||
|
for i, char := range format {
|
||||||
|
if char != '#' && char != '0' {
|
||||||
|
formatIndx = append(formatIndx, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(formatIndx) > 0 {
|
||||||
|
// Directive at index 0:
|
||||||
|
// Must be a '+'
|
||||||
|
// Raise an error if not the case
|
||||||
|
// index: 0123456789
|
||||||
|
// +0.000,000
|
||||||
|
// +000,000.0
|
||||||
|
// +0000.00
|
||||||
|
// +0000
|
||||||
|
if formatIndx[0] == 0 {
|
||||||
|
if format[formatIndx[0]] != '+' {
|
||||||
|
panic("RenderFloat(): invalid positive sign directive")
|
||||||
|
}
|
||||||
|
positiveStr = "+"
|
||||||
|
formatIndx = formatIndx[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two directives:
|
||||||
|
// First is thousands separator
|
||||||
|
// Raise an error if not followed by 3-digit
|
||||||
|
// 0123456789
|
||||||
|
// 0.000,000
|
||||||
|
// 000,000.00
|
||||||
|
if len(formatIndx) == 2 {
|
||||||
|
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||||
|
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||||
|
}
|
||||||
|
thousandStr = string(format[formatIndx[0]])
|
||||||
|
formatIndx = formatIndx[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// One directive:
|
||||||
|
// Directive is decimal separator
|
||||||
|
// The number of digit-specifier following the separator indicates wanted precision
|
||||||
|
// 0123456789
|
||||||
|
// 0.00
|
||||||
|
// 000,0000
|
||||||
|
if len(formatIndx) == 1 {
|
||||||
|
decimalStr = string(format[formatIndx[0]])
|
||||||
|
precision = len(format) - formatIndx[0] - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate sign part
|
||||||
|
var signStr string
|
||||||
|
if n >= 0.000000001 {
|
||||||
|
signStr = positiveStr
|
||||||
|
} else if n <= -0.000000001 {
|
||||||
|
signStr = negativeStr
|
||||||
|
n = -n
|
||||||
|
} else {
|
||||||
|
signStr = ""
|
||||||
|
n = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// split number into integer and fractional parts
|
||||||
|
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||||
|
|
||||||
|
// generate integer part string
|
||||||
|
intStr := strconv.FormatInt(int64(intf), 10)
|
||||||
|
|
||||||
|
// add thousand separator if required
|
||||||
|
if len(thousandStr) > 0 {
|
||||||
|
for i := len(intStr); i > 3; {
|
||||||
|
i -= 3
|
||||||
|
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no fractional part, we can leave now
|
||||||
|
if precision == 0 {
|
||||||
|
return signStr + intStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate fractional part
|
||||||
|
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||||
|
// may need padding
|
||||||
|
if len(fracStr) < precision {
|
||||||
|
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||||
|
}
|
||||||
|
|
||||||
|
return signStr + intStr + decimalStr + fracStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatInteger produces a formatted number as string.
|
||||||
|
// See FormatFloat.
|
||||||
|
func FormatInteger(format string, n int) string {
|
||||||
|
return FormatFloat(format, float64(n))
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"git.nspix.com/golang/kos/util/bs"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Size uint64
|
||||||
|
|
||||||
|
// IEC Sizes.
|
||||||
|
// kibis of bits
|
||||||
|
const (
|
||||||
|
Byte = 1 << (iota * 10)
|
||||||
|
KiByte
|
||||||
|
MiByte
|
||||||
|
GiByte
|
||||||
|
TiByte
|
||||||
|
PiByte
|
||||||
|
EiByte
|
||||||
|
)
|
||||||
|
|
||||||
|
// SI Sizes.
|
||||||
|
const (
|
||||||
|
IByte = 1
|
||||||
|
KByte = IByte * 1000
|
||||||
|
MByte = KByte * 1000
|
||||||
|
GByte = MByte * 1000
|
||||||
|
TByte = GByte * 1000
|
||||||
|
PByte = TByte * 1000
|
||||||
|
EByte = PByte * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
var bytesSizeTable = map[string]uint64{
|
||||||
|
"b": Byte,
|
||||||
|
"kib": KiByte,
|
||||||
|
"kb": KByte,
|
||||||
|
"mib": MiByte,
|
||||||
|
"mb": MByte,
|
||||||
|
"gib": GiByte,
|
||||||
|
"gb": GByte,
|
||||||
|
"tib": TiByte,
|
||||||
|
"tb": TByte,
|
||||||
|
"pib": PiByte,
|
||||||
|
"pb": PByte,
|
||||||
|
"eib": EiByte,
|
||||||
|
"eb": EByte,
|
||||||
|
// Without suffix
|
||||||
|
"": Byte,
|
||||||
|
"ki": KiByte,
|
||||||
|
"k": KByte,
|
||||||
|
"mi": MiByte,
|
||||||
|
"m": MByte,
|
||||||
|
"gi": GiByte,
|
||||||
|
"g": GByte,
|
||||||
|
"ti": TiByte,
|
||||||
|
"t": TByte,
|
||||||
|
"pi": PiByte,
|
||||||
|
"p": PByte,
|
||||||
|
"ei": EiByte,
|
||||||
|
"e": EByte,
|
||||||
|
}
|
||||||
|
|
||||||
|
func logn(n, b float64) float64 {
|
||||||
|
return math.Log(n) / math.Log(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||||
|
if s < 10 {
|
||||||
|
return fmt.Sprintf("%d B", s)
|
||||||
|
}
|
||||||
|
e := math.Floor(logn(float64(s), base))
|
||||||
|
suffix := sizes[int(e)]
|
||||||
|
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||||
|
f := "%.0f %s"
|
||||||
|
if val < 10 {
|
||||||
|
f = "%.1f %s"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(f, val, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes produces a human readable representation of an SI size.
|
||||||
|
//
|
||||||
|
// See also: ParseBytes.
|
||||||
|
//
|
||||||
|
// Bytes(82854982) -> 83 MB
|
||||||
|
func Bytes(s uint64) string {
|
||||||
|
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||||
|
return humanateBytes(s, 1000, sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IBytes produces a human readable representation of an IEC size.
|
||||||
|
//
|
||||||
|
// See also: ParseBytes.
|
||||||
|
//
|
||||||
|
// IBytes(82854982) -> 79 MiB
|
||||||
|
func IBytes(s uint64) string {
|
||||||
|
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||||
|
return humanateBytes(s, 1024, sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytes parses a string representation of bytes into the number
|
||||||
|
// of bytes it represents.
|
||||||
|
//
|
||||||
|
// See Also: Bytes, IBytes.
|
||||||
|
//
|
||||||
|
// ParseBytes("42 MB") -> 42000000, nil
|
||||||
|
// ParseBytes("42 mib") -> 44040192, nil
|
||||||
|
func ParseBytes(s string) (uint64, error) {
|
||||||
|
lastDigit := 0
|
||||||
|
hasComma := false
|
||||||
|
for _, r := range s {
|
||||||
|
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if r == ',' {
|
||||||
|
hasComma = true
|
||||||
|
}
|
||||||
|
lastDigit++
|
||||||
|
}
|
||||||
|
|
||||||
|
num := s[:lastDigit]
|
||||||
|
if hasComma {
|
||||||
|
num = strings.Replace(num, ",", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := strconv.ParseFloat(num, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||||
|
if m, ok := bytesSizeTable[extra]; ok {
|
||||||
|
f *= float64(m)
|
||||||
|
if f >= math.MaxUint64 {
|
||||||
|
return 0, fmt.Errorf("too large: %v", s)
|
||||||
|
}
|
||||||
|
return uint64(f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Size) UnmarshalJSON(buf []byte) error {
|
||||||
|
var (
|
||||||
|
n uint64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
buf = bytes.TrimFunc(buf, func(r rune) bool {
|
||||||
|
if r == '"' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if n, err = ParseBytes(bs.BytesToString(buf)); err == nil {
|
||||||
|
*b = Size(n)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Size) MarshalJSON() ([]byte, error) {
|
||||||
|
s := `"` + IBytes(uint64(b)) + `"`
|
||||||
|
return bs.StringToBytes(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Size) String() string {
|
||||||
|
return IBytes(uint64(b))
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"git.nspix.com/golang/kos/util/bs"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Day = 24 * time.Hour
|
||||||
|
Week = 7 * Day
|
||||||
|
Month = 30 * Day
|
||||||
|
Year = 12 * Month
|
||||||
|
LongTime = 37 * Year
|
||||||
|
)
|
||||||
|
|
||||||
|
// A RelTimeMagnitude struct contains a relative time point at which
|
||||||
|
// the relative format of time will switch to a new format string. A
|
||||||
|
// slice of these in ascending order by their "D" field is passed to
|
||||||
|
// CustomRelTime to format durations.
|
||||||
|
//
|
||||||
|
// The Format field is a string that may contain a "%s" which will be
|
||||||
|
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||||
|
// now") and a "%d" that will be replaced by the quantity.
|
||||||
|
//
|
||||||
|
// The DivBy field is the amount of time the time difference must be
|
||||||
|
// divided by in order to display correctly.
|
||||||
|
//
|
||||||
|
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||||
|
// DivBy should be time.Minute so whatever the duration is will be
|
||||||
|
// expressed in minutes.
|
||||||
|
type RelTimeMagnitude struct {
|
||||||
|
D time.Duration
|
||||||
|
Format string
|
||||||
|
DivBy time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultMagnitudes = []RelTimeMagnitude{
|
||||||
|
{time.Second, "now", time.Second},
|
||||||
|
{2 * time.Second, "1 second %s", 1},
|
||||||
|
{time.Minute, "%d seconds %s", time.Second},
|
||||||
|
{2 * time.Minute, "1 minute %s", 1},
|
||||||
|
{time.Hour, "%d minutes %s", time.Minute},
|
||||||
|
{2 * time.Hour, "1 hour %s", 1},
|
||||||
|
{Day, "%d hours %s", time.Hour},
|
||||||
|
{2 * Day, "1 day %s", 1},
|
||||||
|
{Week, "%d days %s", Day},
|
||||||
|
{2 * Week, "1 week %s", 1},
|
||||||
|
{Month, "%d weeks %s", Week},
|
||||||
|
{2 * Month, "1 month %s", 1},
|
||||||
|
{Year, "%d months %s", Month},
|
||||||
|
{18 * Month, "1 year %s", 1},
|
||||||
|
{2 * Year, "2 years %s", 1},
|
||||||
|
{LongTime, "%d years %s", Year},
|
||||||
|
{math.MaxInt64, "a long while %s", 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelTime formats a time into a relative string.
|
||||||
|
//
|
||||||
|
// It takes two times and two labels. In addition to the generic time
|
||||||
|
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||||
|
// the label corresponding to the smaller time is applied.
|
||||||
|
//
|
||||||
|
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||||
|
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||||
|
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomRelTime formats a time into a relative string.
|
||||||
|
//
|
||||||
|
// It takes two times two labels and a table of relative time formats.
|
||||||
|
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||||
|
// labels are used applied so that the label corresponding to the
|
||||||
|
// smaller time is applied.
|
||||||
|
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||||
|
lbl := albl
|
||||||
|
diff := b.Sub(a)
|
||||||
|
|
||||||
|
if a.After(b) {
|
||||||
|
lbl = blbl
|
||||||
|
diff = a.Sub(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||||
|
return magnitudes[i].D > diff
|
||||||
|
})
|
||||||
|
|
||||||
|
if n >= len(magnitudes) {
|
||||||
|
n = len(magnitudes) - 1
|
||||||
|
}
|
||||||
|
mag := magnitudes[n]
|
||||||
|
args := []interface{}{}
|
||||||
|
escaped := false
|
||||||
|
for _, ch := range mag.Format {
|
||||||
|
if escaped {
|
||||||
|
switch ch {
|
||||||
|
case 's':
|
||||||
|
args = append(args, lbl)
|
||||||
|
case 'd':
|
||||||
|
args = append(args, diff/mag.DivBy)
|
||||||
|
}
|
||||||
|
escaped = false
|
||||||
|
} else {
|
||||||
|
escaped = ch == '%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(mag.Format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Time struct {
|
||||||
|
tm time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func Now() Time {
|
||||||
|
return Time{tm: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapTime(t time.Time) Time {
|
||||||
|
return Time{tm: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Add(d Duration) Time {
|
||||||
|
t.tm = t.tm.Add(d.Duration())
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) AddDuration(d time.Duration) Time {
|
||||||
|
t.tm = t.tm.Add(d)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) After(u Time) bool {
|
||||||
|
return t.tm.After(u.tm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) AfterTime(u time.Time) bool {
|
||||||
|
return t.tm.After(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Sub(u Time) Duration {
|
||||||
|
return Duration(t.tm.Sub(u.tm))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) SubTime(u time.Time) Duration {
|
||||||
|
return Duration(t.tm.Sub(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Time() time.Time {
|
||||||
|
return t.tm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) String() string {
|
||||||
|
return t.tm.Format(time.DateTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) MarshalJSON() ([]byte, error) {
|
||||||
|
s := `"` + t.tm.Format(time.DateTime) + `"`
|
||||||
|
return bs.StringToBytes(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Ago() string {
|
||||||
|
return RelTime(t.tm, time.Now(), "ago", "from now")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) UnmarshalJSON(buf []byte) (err error) {
|
||||||
|
buf = bytes.TrimFunc(buf, func(r rune) bool {
|
||||||
|
if r == '"' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
t.tm, err = time.ParseInLocation(time.DateTime, bs.BytesToString(buf), time.Local)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
Time Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNow(t *testing.T) {
|
||||||
|
tm := Now().Add(-1 * Hour * 223)
|
||||||
|
t.Log(tm.Ago())
|
||||||
|
ts := &test{Time: Now()}
|
||||||
|
buf, err := json.Marshal(ts)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
t.Log(string(buf))
|
||||||
|
vv := &test{}
|
||||||
|
if err = json.Unmarshal(buf, vv); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
t.Log(vv.Time)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package reflect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSet(t *testing.T) {
|
||||||
|
type hack struct {
|
||||||
|
Duration time.Duration
|
||||||
|
}
|
||||||
|
h := &hack{}
|
||||||
|
Set(h, "Duration", "5s")
|
||||||
|
t.Log(h.Duration)
|
||||||
|
}
|
Loading…
Reference in New Issue