* Show download count info in release list * Use go-humanizefor-closed-social
@ -0,0 +1,21 @@ | |||||
sudo: false | |||||
language: go | |||||
go: | |||||
- 1.3.x | |||||
- 1.5.x | |||||
- 1.6.x | |||||
- 1.7.x | |||||
- 1.8.x | |||||
- 1.9.x | |||||
- master | |||||
matrix: | |||||
allow_failures: | |||||
- go: master | |||||
fast_finish: true | |||||
install: | |||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). | |||||
script: | |||||
- go get -t -v ./... | |||||
- diff -u <(echo -n) <(gofmt -d -s .) | |||||
- go tool vet . | |||||
- go test -v -race ./... |
@ -0,0 +1,21 @@ | |||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net> | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in | |||||
all copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. | |||||
<http://www.opensource.org/licenses/mit-license.php> |
@ -0,0 +1,124 @@ | |||||
# Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize) | |||||
Just a few functions for helping humanize times and sizes. | |||||
`go get` it as `github.com/dustin/go-humanize`, import it as | |||||
`"github.com/dustin/go-humanize"`, use it as `humanize`. | |||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for | |||||
complete documentation. | |||||
## Sizes | |||||
This lets you take numbers like `82854982` and convert them to useful | |||||
strings like, `83 MB` or `79 MiB` (whichever you prefer). | |||||
Example: | |||||
```go | |||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB. | |||||
``` | |||||
## Times | |||||
This lets you take a `time.Time` and spit it out in relative terms. | |||||
For example, `12 seconds ago` or `3 days from now`. | |||||
Example: | |||||
```go | |||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago. | |||||
``` | |||||
Thanks to Kyle Lemons for the time implementation from an IRC | |||||
conversation one day. It's pretty neat. | |||||
## Ordinals | |||||
From a [mailing list discussion][odisc] where a user wanted to be able | |||||
to label ordinals. | |||||
0 -> 0th | |||||
1 -> 1st | |||||
2 -> 2nd | |||||
3 -> 3rd | |||||
4 -> 4th | |||||
[...] | |||||
Example: | |||||
```go | |||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend. | |||||
``` | |||||
## Commas | |||||
Want to shove commas into numbers? Be my guest. | |||||
0 -> 0 | |||||
100 -> 100 | |||||
1000 -> 1,000 | |||||
1000000000 -> 1,000,000,000 | |||||
-100000 -> -100,000 | |||||
Example: | |||||
```go | |||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491. | |||||
``` | |||||
## Ftoa | |||||
Nicer float64 formatter that removes trailing zeros. | |||||
```go | |||||
fmt.Printf("%f", 2.24) // 2.240000 | |||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24 | |||||
fmt.Printf("%f", 2.0) // 2.000000 | |||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2 | |||||
``` | |||||
## SI notation | |||||
Format numbers with [SI notation][sinotation]. | |||||
Example: | |||||
```go | |||||
humanize.SI(0.00000000223, "M") // 2.23 nM | |||||
``` | |||||
## English-specific functions | |||||
The following functions are in the `humanize/english` subpackage. | |||||
### Plurals | |||||
Simple English pluralization | |||||
```go | |||||
english.PluralWord(1, "object", "") // object | |||||
english.PluralWord(42, "object", "") // objects | |||||
english.PluralWord(2, "bus", "") // buses | |||||
english.PluralWord(99, "locus", "loci") // loci | |||||
english.Plural(1, "object", "") // 1 object | |||||
english.Plural(42, "object", "") // 42 objects | |||||
english.Plural(2, "bus", "") // 2 buses | |||||
english.Plural(99, "locus", "loci") // 99 loci | |||||
``` | |||||
### Word series | |||||
Format comma-separated words lists with conjuctions: | |||||
```go | |||||
english.WordSeries([]string{"foo"}, "and") // foo | |||||
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar | |||||
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz | |||||
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz | |||||
``` | |||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion | |||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix |
@ -0,0 +1,31 @@ | |||||
package humanize | |||||
import ( | |||||
"math/big" | |||||
) | |||||
// order of magnitude (to a max order) | |||||
func oomm(n, b *big.Int, maxmag int) (float64, int) { | |||||
mag := 0 | |||||
m := &big.Int{} | |||||
for n.Cmp(b) >= 0 { | |||||
n.DivMod(n, b, m) | |||||
mag++ | |||||
if mag == maxmag && maxmag >= 0 { | |||||
break | |||||
} | |||||
} | |||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag | |||||
} | |||||
// total order of magnitude | |||||
// (same as above, but with no upper limit) | |||||
func oom(n, b *big.Int) (float64, int) { | |||||
mag := 0 | |||||
m := &big.Int{} | |||||
for n.Cmp(b) >= 0 { | |||||
n.DivMod(n, b, m) | |||||
mag++ | |||||
} | |||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag | |||||
} |
@ -0,0 +1,173 @@ | |||||
package humanize | |||||
import ( | |||||
"fmt" | |||||
"math/big" | |||||
"strings" | |||||
"unicode" | |||||
) | |||||
var ( | |||||
bigIECExp = big.NewInt(1024) | |||||
// BigByte is one byte in bit.Ints | |||||
BigByte = big.NewInt(1) | |||||
// BigKiByte is 1,024 bytes in bit.Ints | |||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp) | |||||
// BigMiByte is 1,024 k bytes in bit.Ints | |||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp) | |||||
// BigGiByte is 1,024 m bytes in bit.Ints | |||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp) | |||||
// BigTiByte is 1,024 g bytes in bit.Ints | |||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp) | |||||
// BigPiByte is 1,024 t bytes in bit.Ints | |||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp) | |||||
// BigEiByte is 1,024 p bytes in bit.Ints | |||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp) | |||||
// BigZiByte is 1,024 e bytes in bit.Ints | |||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp) | |||||
// BigYiByte is 1,024 z bytes in bit.Ints | |||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp) | |||||
) | |||||
var ( | |||||
bigSIExp = big.NewInt(1000) | |||||
// BigSIByte is one SI byte in big.Ints | |||||
BigSIByte = big.NewInt(1) | |||||
// BigKByte is 1,000 SI bytes in big.Ints | |||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp) | |||||
// BigMByte is 1,000 SI k bytes in big.Ints | |||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp) | |||||
// BigGByte is 1,000 SI m bytes in big.Ints | |||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp) | |||||
// BigTByte is 1,000 SI g bytes in big.Ints | |||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp) | |||||
// BigPByte is 1,000 SI t bytes in big.Ints | |||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp) | |||||
// BigEByte is 1,000 SI p bytes in big.Ints | |||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp) | |||||
// BigZByte is 1,000 SI e bytes in big.Ints | |||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp) | |||||
// BigYByte is 1,000 SI z bytes in big.Ints | |||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp) | |||||
) | |||||
var bigBytesSizeTable = map[string]*big.Int{ | |||||
"b": BigByte, | |||||
"kib": BigKiByte, | |||||
"kb": BigKByte, | |||||
"mib": BigMiByte, | |||||
"mb": BigMByte, | |||||
"gib": BigGiByte, | |||||
"gb": BigGByte, | |||||
"tib": BigTiByte, | |||||
"tb": BigTByte, | |||||
"pib": BigPiByte, | |||||
"pb": BigPByte, | |||||
"eib": BigEiByte, | |||||
"eb": BigEByte, | |||||
"zib": BigZiByte, | |||||
"zb": BigZByte, | |||||
"yib": BigYiByte, | |||||
"yb": BigYByte, | |||||
// Without suffix | |||||
"": BigByte, | |||||
"ki": BigKiByte, | |||||
"k": BigKByte, | |||||
"mi": BigMiByte, | |||||
"m": BigMByte, | |||||
"gi": BigGiByte, | |||||
"g": BigGByte, | |||||
"ti": BigTiByte, | |||||
"t": BigTByte, | |||||
"pi": BigPiByte, | |||||
"p": BigPByte, | |||||
"ei": BigEiByte, | |||||
"e": BigEByte, | |||||
"z": BigZByte, | |||||
"zi": BigZiByte, | |||||
"y": BigYByte, | |||||
"yi": BigYiByte, | |||||
} | |||||
var ten = big.NewInt(10) | |||||
func humanateBigBytes(s, base *big.Int, sizes []string) string { | |||||
if s.Cmp(ten) < 0 { | |||||
return fmt.Sprintf("%d B", s) | |||||
} | |||||
c := (&big.Int{}).Set(s) | |||||
val, mag := oomm(c, base, len(sizes)-1) | |||||
suffix := sizes[mag] | |||||
f := "%.0f %s" | |||||
if val < 10 { | |||||
f = "%.1f %s" | |||||
} | |||||
return fmt.Sprintf(f, val, suffix) | |||||
} | |||||
// BigBytes produces a human readable representation of an SI size. | |||||
// | |||||
// See also: ParseBigBytes. | |||||
// | |||||
// BigBytes(82854982) -> 83 MB | |||||
func BigBytes(s *big.Int) string { | |||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} | |||||
return humanateBigBytes(s, bigSIExp, sizes) | |||||
} | |||||
// BigIBytes produces a human readable representation of an IEC size. | |||||
// | |||||
// See also: ParseBigBytes. | |||||
// | |||||
// BigIBytes(82854982) -> 79 MiB | |||||
func BigIBytes(s *big.Int) string { | |||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} | |||||
return humanateBigBytes(s, bigIECExp, sizes) | |||||
} | |||||
// ParseBigBytes parses a string representation of bytes into the number | |||||
// of bytes it represents. | |||||
// | |||||
// See also: BigBytes, BigIBytes. | |||||
// | |||||
// ParseBigBytes("42 MB") -> 42000000, nil | |||||
// ParseBigBytes("42 mib") -> 44040192, nil | |||||
func ParseBigBytes(s string) (*big.Int, 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) | |||||
} | |||||
val := &big.Rat{} | |||||
_, err := fmt.Sscanf(num, "%f", val) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) | |||||
if m, ok := bigBytesSizeTable[extra]; ok { | |||||
mv := (&big.Rat{}).SetInt(m) | |||||
val.Mul(val, mv) | |||||
rv := &big.Int{} | |||||
rv.Div(val.Num(), val.Denom()) | |||||
return rv, nil | |||||
} | |||||
return nil, fmt.Errorf("unhandled size name: %v", extra) | |||||
} |
@ -0,0 +1,143 @@ | |||||
package humanize | |||||
import ( | |||||
"fmt" | |||||
"math" | |||||
"strconv" | |||||
"strings" | |||||
"unicode" | |||||
) | |||||
// 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) | |||||
} |
@ -0,0 +1,116 @@ | |||||
package humanize | |||||
import ( | |||||
"bytes" | |||||
"math" | |||||
"math/big" | |||||
"strconv" | |||||
"strings" | |||||
) | |||||
// Comma produces a string form of the given number in base 10 with | |||||
// commas after every three orders of magnitude. | |||||
// | |||||
// e.g. Comma(834142) -> 834,142 | |||||
func Comma(v int64) string { | |||||
sign := "" | |||||
// Min int64 can't be negated to a usable value, so it has to be special cased. | |||||
if v == math.MinInt64 { | |||||
return "-9,223,372,036,854,775,808" | |||||
} | |||||
if v < 0 { | |||||
sign = "-" | |||||
v = 0 - v | |||||
} | |||||
parts := []string{"", "", "", "", "", "", ""} | |||||
j := len(parts) - 1 | |||||
for v > 999 { | |||||
parts[j] = strconv.FormatInt(v%1000, 10) | |||||
switch len(parts[j]) { | |||||
case 2: | |||||
parts[j] = "0" + parts[j] | |||||
case 1: | |||||
parts[j] = "00" + parts[j] | |||||
} | |||||
v = v / 1000 | |||||
j-- | |||||
} | |||||
parts[j] = strconv.Itoa(int(v)) | |||||
return sign + strings.Join(parts[j:], ",") | |||||
} | |||||
// Commaf produces a string form of the given number in base 10 with | |||||
// commas after every three orders of magnitude. | |||||
// | |||||
// e.g. Commaf(834142.32) -> 834,142.32 | |||||
func Commaf(v float64) string { | |||||
buf := &bytes.Buffer{} | |||||
if v < 0 { | |||||
buf.Write([]byte{'-'}) | |||||
v = 0 - v | |||||
} | |||||
comma := []byte{','} | |||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".") | |||||
pos := 0 | |||||
if len(parts[0])%3 != 0 { | |||||
pos += len(parts[0]) % 3 | |||||
buf.WriteString(parts[0][:pos]) | |||||
buf.Write(comma) | |||||
} | |||||
for ; pos < len(parts[0]); pos += 3 { | |||||
buf.WriteString(parts[0][pos : pos+3]) | |||||
buf.Write(comma) | |||||
} | |||||
buf.Truncate(buf.Len() - 1) | |||||
if len(parts) > 1 { | |||||
buf.Write([]byte{'.'}) | |||||
buf.WriteString(parts[1]) | |||||
} | |||||
return buf.String() | |||||
} | |||||
// CommafWithDigits works like the Commaf but limits the resulting | |||||
// string to the given number of decimal places. | |||||
// | |||||
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3 | |||||
func CommafWithDigits(f float64, decimals int) string { | |||||
return stripTrailingDigits(Commaf(f), decimals) | |||||
} | |||||
// BigComma produces a string form of the given big.Int in base 10 | |||||
// with commas after every three orders of magnitude. | |||||
func BigComma(b *big.Int) string { | |||||
sign := "" | |||||
if b.Sign() < 0 { | |||||
sign = "-" | |||||
b.Abs(b) | |||||
} | |||||
athousand := big.NewInt(1000) | |||||
c := (&big.Int{}).Set(b) | |||||
_, m := oom(c, athousand) | |||||
parts := make([]string, m+1) | |||||
j := len(parts) - 1 | |||||
mod := &big.Int{} | |||||
for b.Cmp(athousand) >= 0 { | |||||
b.DivMod(b, athousand, mod) | |||||
parts[j] = strconv.FormatInt(mod.Int64(), 10) | |||||
switch len(parts[j]) { | |||||
case 2: | |||||
parts[j] = "0" + parts[j] | |||||
case 1: | |||||
parts[j] = "00" + parts[j] | |||||
} | |||||
j-- | |||||
} | |||||
parts[j] = strconv.Itoa(int(b.Int64())) | |||||
return sign + strings.Join(parts[j:], ",") | |||||
} |
@ -0,0 +1,40 @@ | |||||
// +build go1.6 | |||||
package humanize | |||||
import ( | |||||
"bytes" | |||||
"math/big" | |||||
"strings" | |||||
) | |||||
// BigCommaf produces a string form of the given big.Float in base 10 | |||||
// with commas after every three orders of magnitude. | |||||
func BigCommaf(v *big.Float) string { | |||||
buf := &bytes.Buffer{} | |||||
if v.Sign() < 0 { | |||||
buf.Write([]byte{'-'}) | |||||
v.Abs(v) | |||||
} | |||||
comma := []byte{','} | |||||
parts := strings.Split(v.Text('f', -1), ".") | |||||
pos := 0 | |||||
if len(parts[0])%3 != 0 { | |||||
pos += len(parts[0]) % 3 | |||||
buf.WriteString(parts[0][:pos]) | |||||
buf.Write(comma) | |||||
} | |||||
for ; pos < len(parts[0]); pos += 3 { | |||||
buf.WriteString(parts[0][pos : pos+3]) | |||||
buf.Write(comma) | |||||
} | |||||
buf.Truncate(buf.Len() - 1) | |||||
if len(parts) > 1 { | |||||
buf.Write([]byte{'.'}) | |||||
buf.WriteString(parts[1]) | |||||
} | |||||
return buf.String() | |||||
} |
@ -0,0 +1,46 @@ | |||||
package humanize | |||||
import ( | |||||
"strconv" | |||||
"strings" | |||||
) | |||||
func stripTrailingZeros(s string) string { | |||||
offset := len(s) - 1 | |||||
for offset > 0 { | |||||
if s[offset] == '.' { | |||||
offset-- | |||||
break | |||||
} | |||||
if s[offset] != '0' { | |||||
break | |||||
} | |||||
offset-- | |||||
} | |||||
return s[:offset+1] | |||||
} | |||||
func stripTrailingDigits(s string, digits int) string { | |||||
if i := strings.Index(s, "."); i >= 0 { | |||||
if digits <= 0 { | |||||
return s[:i] | |||||
} | |||||
i++ | |||||
if i+digits >= len(s) { | |||||
return s | |||||
} | |||||
return s[:i+digits] | |||||
} | |||||
return s | |||||
} | |||||
// Ftoa converts a float to a string with no trailing zeros. | |||||
func Ftoa(num float64) string { | |||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64)) | |||||
} | |||||
// FtoaWithDigits converts a float to a string but limits the resulting string | |||||
// to the given number of decimal places, and no trailing zeros. | |||||
func FtoaWithDigits(num float64, digits int) string { | |||||
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits)) | |||||
} |
@ -0,0 +1,8 @@ | |||||
/* | |||||
Package humanize converts boring ugly numbers to human-friendly strings and back. | |||||
Durations can be turned into strings such as "3 days ago", numbers | |||||
representing sizes like 82854982 into useful strings like, "83 MB" or | |||||
"79 MiB" (whichever you prefer). | |||||
*/ | |||||
package humanize |
@ -0,0 +1,192 @@ | |||||
package humanize | |||||
/* | |||||
Slightly adapted from the source to fit go-humanize. | |||||
Author: https://github.com/gorhill | |||||
Source: https://gist.github.com/gorhill/5285193 | |||||
*/ | |||||
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 < -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,25 @@ | |||||
package humanize | |||||
import "strconv" | |||||
// Ordinal gives you the input number in a rank/ordinal format. | |||||
// | |||||
// Ordinal(3) -> 3rd | |||||
func Ordinal(x int) string { | |||||
suffix := "th" | |||||
switch x % 10 { | |||||
case 1: | |||||
if x%100 != 11 { | |||||
suffix = "st" | |||||
} | |||||
case 2: | |||||
if x%100 != 12 { | |||||
suffix = "nd" | |||||
} | |||||
case 3: | |||||
if x%100 != 13 { | |||||
suffix = "rd" | |||||
} | |||||
} | |||||
return strconv.Itoa(x) + suffix | |||||
} |
@ -0,0 +1,123 @@ | |||||
package humanize | |||||
import ( | |||||
"errors" | |||||
"math" | |||||
"regexp" | |||||
"strconv" | |||||
) | |||||
var siPrefixTable = map[float64]string{ | |||||
-24: "y", // yocto | |||||
-21: "z", // zepto | |||||
-18: "a", // atto | |||||
-15: "f", // femto | |||||
-12: "p", // pico | |||||
-9: "n", // nano | |||||
-6: "µ", // micro | |||||
-3: "m", // milli | |||||
0: "", | |||||
3: "k", // kilo | |||||
6: "M", // mega | |||||
9: "G", // giga | |||||
12: "T", // tera | |||||
15: "P", // peta | |||||
18: "E", // exa | |||||
21: "Z", // zetta | |||||
24: "Y", // yotta | |||||
} | |||||
var revSIPrefixTable = revfmap(siPrefixTable) | |||||
// revfmap reverses the map and precomputes the power multiplier | |||||
func revfmap(in map[float64]string) map[string]float64 { | |||||
rv := map[string]float64{} | |||||
for k, v := range in { | |||||
rv[v] = math.Pow(10, k) | |||||
} | |||||
return rv | |||||
} | |||||
var riParseRegex *regexp.Regexp | |||||
func init() { | |||||
ri := `^([\-0-9.]+)\s?([` | |||||
for _, v := range siPrefixTable { | |||||
ri += v | |||||
} | |||||
ri += `]?)(.*)` | |||||
riParseRegex = regexp.MustCompile(ri) | |||||
} | |||||
// ComputeSI finds the most appropriate SI prefix for the given number | |||||
// and returns the prefix along with the value adjusted to be within | |||||
// that prefix. | |||||
// | |||||
// See also: SI, ParseSI. | |||||
// | |||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p") | |||||
func ComputeSI(input float64) (float64, string) { | |||||
if input == 0 { | |||||
return 0, "" | |||||
} | |||||
mag := math.Abs(input) | |||||
exponent := math.Floor(logn(mag, 10)) | |||||
exponent = math.Floor(exponent/3) * 3 | |||||
value := mag / math.Pow(10, exponent) | |||||
// Handle special case where value is exactly 1000.0 | |||||
// Should return 1 M instead of 1000 k | |||||
if value == 1000.0 { | |||||
exponent += 3 | |||||
value = mag / math.Pow(10, exponent) | |||||
} | |||||
value = math.Copysign(value, input) | |||||
prefix := siPrefixTable[exponent] | |||||
return value, prefix | |||||
} | |||||
// SI returns a string with default formatting. | |||||
// | |||||
// SI uses Ftoa to format float value, removing trailing zeros. | |||||
// | |||||
// See also: ComputeSI, ParseSI. | |||||
// | |||||
// e.g. SI(1000000, "B") -> 1 MB | |||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF | |||||
func SI(input float64, unit string) string { | |||||
value, prefix := ComputeSI(input) | |||||
return Ftoa(value) + " " + prefix + unit | |||||
} | |||||
// SIWithDigits works like SI but limits the resulting string to the | |||||
// given number of decimal places. | |||||
// | |||||
// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB | |||||
// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF | |||||
func SIWithDigits(input float64, decimals int, unit string) string { | |||||
value, prefix := ComputeSI(input) | |||||
return FtoaWithDigits(value, decimals) + " " + prefix + unit | |||||
} | |||||
var errInvalid = errors.New("invalid input") | |||||
// ParseSI parses an SI string back into the number and unit. | |||||
// | |||||
// See also: SI, ComputeSI. | |||||
// | |||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil) | |||||
func ParseSI(input string) (float64, string, error) { | |||||
found := riParseRegex.FindStringSubmatch(input) | |||||
if len(found) != 4 { | |||||
return 0, "", errInvalid | |||||
} | |||||
mag := revSIPrefixTable[found[2]] | |||||
unit := found[3] | |||||
base, err := strconv.ParseFloat(found[1], 64) | |||||
return base * mag, unit, err | |||||
} |
@ -0,0 +1,117 @@ | |||||
package humanize | |||||
import ( | |||||
"fmt" | |||||
"math" | |||||
"sort" | |||||
"time" | |||||
) | |||||
// Seconds-based time units | |||||
const ( | |||||
Day = 24 * time.Hour | |||||
Week = 7 * Day | |||||
Month = 30 * Day | |||||
Year = 12 * Month | |||||
LongTime = 37 * Year | |||||
) | |||||
// Time formats a time into a relative string. | |||||
// | |||||
// Time(someT) -> "3 weeks ago" | |||||
func Time(then time.Time) string { | |||||
return RelTime(then, time.Now(), "ago", "from now") | |||||
} | |||||
// 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...) | |||||
} |