* Show download count info in release list * Use go-humanizetags/v1.21.12.1
| @@ -32,6 +32,7 @@ require ( | |||
| github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect | |||
| github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 | |||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible | |||
| github.com/dustin/go-humanize v1.0.0 | |||
| github.com/editorconfig/editorconfig-core-go/v2 v2.1.1 | |||
| github.com/emirpasic/gods v1.12.0 | |||
| github.com/etcd-io/bbolt v1.3.3 // indirect | |||
| @@ -133,6 +133,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm | |||
| github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | |||
| github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= | |||
| github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= | |||
| github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= | |||
| github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= | |||
| github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= | |||
| github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= | |||
| github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= | |||
| @@ -13,7 +13,6 @@ import ( | |||
| "encoding/hex" | |||
| "fmt" | |||
| "io" | |||
| "math" | |||
| "net/http" | |||
| "net/url" | |||
| "os" | |||
| @@ -29,6 +28,7 @@ import ( | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "github.com/dustin/go-humanize" | |||
| "github.com/unknwon/com" | |||
| ) | |||
| @@ -214,40 +214,15 @@ func AvatarLink(email string) string { | |||
| return SizedAvatarLink(email, DefaultAvatarSize) | |||
| } | |||
| // Storage space size types | |||
| const ( | |||
| Byte = 1 | |||
| KByte = Byte * 1024 | |||
| MByte = KByte * 1024 | |||
| GByte = MByte * 1024 | |||
| TByte = GByte * 1024 | |||
| PByte = TByte * 1024 | |||
| EByte = PByte * 1024 | |||
| ) | |||
| 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("%dB", s) | |||
| } | |||
| e := math.Floor(logn(float64(s), base)) | |||
| suffix := sizes[int(e)] | |||
| val := float64(s) / math.Pow(base, math.Floor(e)) | |||
| f := "%.0f" | |||
| if val < 10 { | |||
| f = "%.1f" | |||
| } | |||
| return fmt.Sprintf(f+"%s", val, suffix) | |||
| } | |||
| // FileSize calculates the file size and generate user-friendly string. | |||
| func FileSize(s int64) string { | |||
| sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} | |||
| return humanateBytes(uint64(s), 1024, sizes) | |||
| return humanize.IBytes(uint64(s)) | |||
| } | |||
| // PrettyNumber produces a string form of the given number in base 10 with | |||
| // commas after every three orders of magnitud | |||
| func PrettyNumber(v int64) string { | |||
| return humanize.Comma(v) | |||
| } | |||
| // Subtract deals with subtraction of all types of number. | |||
| @@ -103,19 +103,19 @@ func TestAvatarLink(t *testing.T) { | |||
| func TestFileSize(t *testing.T) { | |||
| var size int64 = 512 | |||
| assert.Equal(t, "512B", FileSize(size)) | |||
| assert.Equal(t, "512 B", FileSize(size)) | |||
| size *= 1024 | |||
| assert.Equal(t, "512KB", FileSize(size)) | |||
| assert.Equal(t, "512 KiB", FileSize(size)) | |||
| size *= 1024 | |||
| assert.Equal(t, "512MB", FileSize(size)) | |||
| assert.Equal(t, "512 MiB", FileSize(size)) | |||
| size *= 1024 | |||
| assert.Equal(t, "512GB", FileSize(size)) | |||
| assert.Equal(t, "512 GiB", FileSize(size)) | |||
| size *= 1024 | |||
| assert.Equal(t, "512TB", FileSize(size)) | |||
| assert.Equal(t, "512 TiB", FileSize(size)) | |||
| size *= 1024 | |||
| assert.Equal(t, "512PB", FileSize(size)) | |||
| assert.Equal(t, "512 PiB", FileSize(size)) | |||
| size *= 4 | |||
| assert.Equal(t, "2.0EB", FileSize(size)) | |||
| assert.Equal(t, "2.0 EiB", FileSize(size)) | |||
| } | |||
| func TestSubtract(t *testing.T) { | |||
| @@ -93,6 +93,7 @@ func NewFuncMap() []template.FuncMap { | |||
| "TimeSinceUnix": timeutil.TimeSinceUnix, | |||
| "RawTimeSince": timeutil.RawTimeSince, | |||
| "FileSize": base.FileSize, | |||
| "PrettyNumber": base.PrettyNumber, | |||
| "Subtract": base.Subtract, | |||
| "EntryIcon": base.EntryIcon, | |||
| "MigrationIcon": MigrationIcon, | |||
| @@ -1565,6 +1565,7 @@ release.deletion_success = The release has been deleted. | |||
| release.tag_name_already_exist = A release with this tag name already exists. | |||
| release.tag_name_invalid = The tag name is not valid. | |||
| release.downloads = Downloads | |||
| release.download_count = Downloads: %s | |||
| branch.name = Branch Name | |||
| branch.search = Search branches | |||
| @@ -84,6 +84,7 @@ | |||
| {{if .Attachments}} | |||
| {{range .Attachments}} | |||
| <li> | |||
| <span class="ui text right" data-tooltip="{{$.i18n.Tr "repo.release.download_count" (.DownloadCount | PrettyNumber)}}" data-position="bottom right"><i class="ui octicon octicon-info"></i></span> | |||
| <a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}"> | |||
| <strong><span class="ui image octicon octicon-package" title='{{.Name}}'></span> {{.Name}}</strong> | |||
| <span class="ui text grey right">{{.Size | FileSize}}</span> | |||
| @@ -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 [](https://travis-ci.org/dustin/go-humanize) [](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...) | |||
| } | |||
| @@ -121,6 +121,8 @@ github.com/denisenkom/go-mssqldb/internal/decimal | |||
| github.com/denisenkom/go-mssqldb/internal/querytext | |||
| # github.com/dgrijalva/jwt-go v3.2.0+incompatible | |||
| github.com/dgrijalva/jwt-go | |||
| # github.com/dustin/go-humanize v1.0.0 | |||
| github.com/dustin/go-humanize | |||
| # github.com/editorconfig/editorconfig-core-go/v2 v2.1.1 | |||
| github.com/editorconfig/editorconfig-core-go/v2 | |||
| # github.com/edsrzf/mmap-go v1.0.0 | |||
| @@ -1631,6 +1631,10 @@ | |||
| padding-top: 8px; | |||
| padding-bottom: 8px; | |||
| border-bottom: 1px solid #eeeeee; | |||
| a > .text.right { | |||
| margin-right: 5px; | |||
| } | |||
| } | |||
| } | |||
| } | |||