mirror of https://github.com/go-gitea/gitea
Show download count info in release list (#10124)
* Show download count info in release list * Use go-humanizepull/10130/head^2
parent
ea50f60df2
commit
20c513be6e
@ -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,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...) |
||||
} |
Loading…
Reference in new issue