* 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...) | |||
} |