├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .travis.yml ├── LICENSE ├── README.markdown ├── SECURITY.md ├── big.go ├── bigbytes.go ├── bigbytes_test.go ├── bytes.go ├── bytes_test.go ├── comma.go ├── comma_fuzz_test.go ├── comma_test.go ├── commaf.go ├── commaf_test.go ├── common_test.go ├── english ├── words.go └── words_test.go ├── ftoa.go ├── ftoa_test.go ├── go.mod ├── humanize.go ├── number.go ├── number_test.go ├── ordinals.go ├── ordinals_test.go ├── si.go ├── si_test.go ├── times.go └── times_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 24 | with: 25 | go-version: 1.19 26 | 27 | - name: Build 28 | run: go build -v ./... 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go_import_path: github.com/dustin/go-humanize 4 | go: 5 | - 1.13.x 6 | - 1.14.x 7 | - 1.15.x 8 | - 1.16.x 9 | - stable 10 | - master 11 | matrix: 12 | allow_failures: 13 | - go: master 14 | fast_finish: true 15 | install: 16 | - # 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). 17 | script: 18 | - diff -u <(echo -n) <(gofmt -d -s .) 19 | - go vet . 20 | - go install -v -race ./... 21 | - go test -v -race ./... 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2008 Dustin Sallings 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | Just a few functions for helping humanize times and sizes. 4 | 5 | `go get` it as `github.com/dustin/go-humanize`, import it as 6 | `"github.com/dustin/go-humanize"`, use it as `humanize`. 7 | 8 | See [godoc](https://pkg.go.dev/github.com/dustin/go-humanize) for 9 | complete documentation. 10 | 11 | ## Sizes 12 | 13 | This lets you take numbers like `82854982` and convert them to useful 14 | strings like, `83 MB` or `79 MiB` (whichever you prefer). 15 | 16 | Example: 17 | 18 | ```go 19 | fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB. 20 | ``` 21 | 22 | ## Times 23 | 24 | This lets you take a `time.Time` and spit it out in relative terms. 25 | For example, `12 seconds ago` or `3 days from now`. 26 | 27 | Example: 28 | 29 | ```go 30 | fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago. 31 | ``` 32 | 33 | Thanks to Kyle Lemons for the time implementation from an IRC 34 | conversation one day. It's pretty neat. 35 | 36 | ## Ordinals 37 | 38 | From a [mailing list discussion][odisc] where a user wanted to be able 39 | to label ordinals. 40 | 41 | 0 -> 0th 42 | 1 -> 1st 43 | 2 -> 2nd 44 | 3 -> 3rd 45 | 4 -> 4th 46 | [...] 47 | 48 | Example: 49 | 50 | ```go 51 | fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend. 52 | ``` 53 | 54 | ## Commas 55 | 56 | Want to shove commas into numbers? Be my guest. 57 | 58 | 0 -> 0 59 | 100 -> 100 60 | 1000 -> 1,000 61 | 1000000000 -> 1,000,000,000 62 | -100000 -> -100,000 63 | 64 | Example: 65 | 66 | ```go 67 | fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491. 68 | ``` 69 | 70 | ## Ftoa 71 | 72 | Nicer float64 formatter that removes trailing zeros. 73 | 74 | ```go 75 | fmt.Printf("%f", 2.24) // 2.240000 76 | fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24 77 | fmt.Printf("%f", 2.0) // 2.000000 78 | fmt.Printf("%s", humanize.Ftoa(2.0)) // 2 79 | ``` 80 | 81 | ## SI notation 82 | 83 | Format numbers with [SI notation][sinotation]. 84 | 85 | Example: 86 | 87 | ```go 88 | humanize.SI(0.00000000223, "M") // 2.23 nM 89 | ``` 90 | 91 | ## English-specific functions 92 | 93 | The following functions are in the `humanize/english` subpackage. 94 | 95 | ### Plurals 96 | 97 | Simple English pluralization 98 | 99 | ```go 100 | english.PluralWord(1, "object", "") // object 101 | english.PluralWord(42, "object", "") // objects 102 | english.PluralWord(2, "bus", "") // buses 103 | english.PluralWord(99, "locus", "loci") // loci 104 | 105 | english.Plural(1, "object", "") // 1 object 106 | english.Plural(42, "object", "") // 42 objects 107 | english.Plural(2, "bus", "") // 2 buses 108 | english.Plural(99, "locus", "loci") // 99 loci 109 | ``` 110 | 111 | ### Word series 112 | 113 | Format comma-separated words lists with conjuctions: 114 | 115 | ```go 116 | english.WordSeries([]string{"foo"}, "and") // foo 117 | english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar 118 | english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz 119 | 120 | english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz 121 | ``` 122 | 123 | [odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion 124 | [sinotation]: http://en.wikipedia.org/wiki/Metric_prefix 125 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please disclose it at [security advisory](https://github.com/dustin/go-humanize/security/advisories/new). 4 | 5 | We will respond within 7 working days of your submission. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline. 6 | -------------------------------------------------------------------------------- /big.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "math/big" 5 | ) 6 | 7 | // order of magnitude (to a max order) 8 | func oomm(n, b *big.Int, maxmag int) (float64, int) { 9 | mag := 0 10 | m := &big.Int{} 11 | for n.Cmp(b) >= 0 { 12 | n.DivMod(n, b, m) 13 | mag++ 14 | if mag == maxmag && maxmag >= 0 { 15 | break 16 | } 17 | } 18 | return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag 19 | } 20 | 21 | // total order of magnitude 22 | // (same as above, but with no upper limit) 23 | func oom(n, b *big.Int) (float64, int) { 24 | mag := 0 25 | m := &big.Int{} 26 | for n.Cmp(b) >= 0 { 27 | n.DivMod(n, b, m) 28 | mag++ 29 | } 30 | return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag 31 | } 32 | -------------------------------------------------------------------------------- /bigbytes.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | var ( 11 | bigIECExp = big.NewInt(1024) 12 | 13 | // BigByte is one byte in bit.Ints 14 | BigByte = big.NewInt(1) 15 | // BigKiByte is 1,024 bytes in bit.Ints 16 | BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp) 17 | // BigMiByte is 1,024 k bytes in bit.Ints 18 | BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp) 19 | // BigGiByte is 1,024 m bytes in bit.Ints 20 | BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp) 21 | // BigTiByte is 1,024 g bytes in bit.Ints 22 | BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp) 23 | // BigPiByte is 1,024 t bytes in bit.Ints 24 | BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp) 25 | // BigEiByte is 1,024 p bytes in bit.Ints 26 | BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp) 27 | // BigZiByte is 1,024 e bytes in bit.Ints 28 | BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp) 29 | // BigYiByte is 1,024 z bytes in bit.Ints 30 | BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp) 31 | // BigRiByte is 1,024 y bytes in bit.Ints 32 | BigRiByte = (&big.Int{}).Mul(BigYiByte, bigIECExp) 33 | // BigQiByte is 1,024 r bytes in bit.Ints 34 | BigQiByte = (&big.Int{}).Mul(BigRiByte, bigIECExp) 35 | ) 36 | 37 | var ( 38 | bigSIExp = big.NewInt(1000) 39 | 40 | // BigSIByte is one SI byte in big.Ints 41 | BigSIByte = big.NewInt(1) 42 | // BigKByte is 1,000 SI bytes in big.Ints 43 | BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp) 44 | // BigMByte is 1,000 SI k bytes in big.Ints 45 | BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp) 46 | // BigGByte is 1,000 SI m bytes in big.Ints 47 | BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp) 48 | // BigTByte is 1,000 SI g bytes in big.Ints 49 | BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp) 50 | // BigPByte is 1,000 SI t bytes in big.Ints 51 | BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp) 52 | // BigEByte is 1,000 SI p bytes in big.Ints 53 | BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp) 54 | // BigZByte is 1,000 SI e bytes in big.Ints 55 | BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp) 56 | // BigYByte is 1,000 SI z bytes in big.Ints 57 | BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp) 58 | // BigRByte is 1,000 SI y bytes in big.Ints 59 | BigRByte = (&big.Int{}).Mul(BigYByte, bigSIExp) 60 | // BigQByte is 1,000 SI r bytes in big.Ints 61 | BigQByte = (&big.Int{}).Mul(BigRByte, bigSIExp) 62 | ) 63 | 64 | var bigBytesSizeTable = map[string]*big.Int{ 65 | "b": BigByte, 66 | "kib": BigKiByte, 67 | "kb": BigKByte, 68 | "mib": BigMiByte, 69 | "mb": BigMByte, 70 | "gib": BigGiByte, 71 | "gb": BigGByte, 72 | "tib": BigTiByte, 73 | "tb": BigTByte, 74 | "pib": BigPiByte, 75 | "pb": BigPByte, 76 | "eib": BigEiByte, 77 | "eb": BigEByte, 78 | "zib": BigZiByte, 79 | "zb": BigZByte, 80 | "yib": BigYiByte, 81 | "yb": BigYByte, 82 | "rib": BigRiByte, 83 | "rb": BigRByte, 84 | "qib": BigQiByte, 85 | "qb": BigQByte, 86 | // Without suffix 87 | "": BigByte, 88 | "ki": BigKiByte, 89 | "k": BigKByte, 90 | "mi": BigMiByte, 91 | "m": BigMByte, 92 | "gi": BigGiByte, 93 | "g": BigGByte, 94 | "ti": BigTiByte, 95 | "t": BigTByte, 96 | "pi": BigPiByte, 97 | "p": BigPByte, 98 | "ei": BigEiByte, 99 | "e": BigEByte, 100 | "z": BigZByte, 101 | "zi": BigZiByte, 102 | "y": BigYByte, 103 | "yi": BigYiByte, 104 | "r": BigRByte, 105 | "ri": BigRiByte, 106 | "q": BigQByte, 107 | "qi": BigQiByte, 108 | } 109 | 110 | var ten = big.NewInt(10) 111 | 112 | func humanateBigBytes(s, base *big.Int, sizes []string) string { 113 | if s.Cmp(ten) < 0 { 114 | return fmt.Sprintf("%d B", s) 115 | } 116 | c := (&big.Int{}).Set(s) 117 | val, mag := oomm(c, base, len(sizes)-1) 118 | suffix := sizes[mag] 119 | f := "%.0f %s" 120 | if val < 10 { 121 | f = "%.1f %s" 122 | } 123 | 124 | return fmt.Sprintf(f, val, suffix) 125 | 126 | } 127 | 128 | // BigBytes produces a human readable representation of an SI size. 129 | // 130 | // See also: ParseBigBytes. 131 | // 132 | // BigBytes(82854982) -> 83 MB 133 | func BigBytes(s *big.Int) string { 134 | sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "RB", "QB"} 135 | return humanateBigBytes(s, bigSIExp, sizes) 136 | } 137 | 138 | // BigIBytes produces a human readable representation of an IEC size. 139 | // 140 | // See also: ParseBigBytes. 141 | // 142 | // BigIBytes(82854982) -> 79 MiB 143 | func BigIBytes(s *big.Int) string { 144 | sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB"} 145 | return humanateBigBytes(s, bigIECExp, sizes) 146 | } 147 | 148 | // ParseBigBytes parses a string representation of bytes into the number 149 | // of bytes it represents. 150 | // 151 | // See also: BigBytes, BigIBytes. 152 | // 153 | // ParseBigBytes("42 MB") -> 42000000, nil 154 | // ParseBigBytes("42 mib") -> 44040192, nil 155 | func ParseBigBytes(s string) (*big.Int, error) { 156 | lastDigit := 0 157 | hasComma := false 158 | for _, r := range s { 159 | if !(unicode.IsDigit(r) || r == '.' || r == ',') { 160 | break 161 | } 162 | if r == ',' { 163 | hasComma = true 164 | } 165 | lastDigit++ 166 | } 167 | 168 | num := s[:lastDigit] 169 | if hasComma { 170 | num = strings.Replace(num, ",", "", -1) 171 | } 172 | 173 | val := &big.Rat{} 174 | _, err := fmt.Sscanf(num, "%f", val) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) 180 | if m, ok := bigBytesSizeTable[extra]; ok { 181 | mv := (&big.Rat{}).SetInt(m) 182 | val.Mul(val, mv) 183 | rv := &big.Int{} 184 | rv.Div(val.Num(), val.Denom()) 185 | return rv, nil 186 | } 187 | 188 | return nil, fmt.Errorf("unhandled size name: %v", extra) 189 | } 190 | -------------------------------------------------------------------------------- /bigbytes_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | ) 7 | 8 | func TestBigByteParsing(t *testing.T) { 9 | tests := []struct { 10 | in string 11 | exp uint64 12 | }{ 13 | {"42", 42}, 14 | {"42MB", 42000000}, 15 | {"42MiB", 44040192}, 16 | {"42mb", 42000000}, 17 | {"42mib", 44040192}, 18 | {"42MIB", 44040192}, 19 | {"42 MB", 42000000}, 20 | {"42 MiB", 44040192}, 21 | {"42 mb", 42000000}, 22 | {"42 mib", 44040192}, 23 | {"42 MIB", 44040192}, 24 | {"42.5MB", 42500000}, 25 | {"42.5MiB", 44564480}, 26 | {"42.5 MB", 42500000}, 27 | {"42.5 MiB", 44564480}, 28 | // No need to say B 29 | {"42M", 42000000}, 30 | {"42Mi", 44040192}, 31 | {"42m", 42000000}, 32 | {"42mi", 44040192}, 33 | {"42MI", 44040192}, 34 | {"42 M", 42000000}, 35 | {"42 Mi", 44040192}, 36 | {"42 m", 42000000}, 37 | {"42 mi", 44040192}, 38 | {"42 MI", 44040192}, 39 | {"42.5M", 42500000}, 40 | {"42.5Mi", 44564480}, 41 | {"42.5 M", 42500000}, 42 | {"42.5 Mi", 44564480}, 43 | {"1,005.03 MB", 1005030000}, 44 | // Large testing, breaks when too much larger than 45 | // this. 46 | {"12.5 EB", uint64(12.5 * float64(EByte))}, 47 | {"12.5 E", uint64(12.5 * float64(EByte))}, 48 | {"12.5 EiB", uint64(12.5 * float64(EiByte))}, 49 | } 50 | 51 | for _, p := range tests { 52 | got, err := ParseBigBytes(p.in) 53 | if err != nil { 54 | t.Errorf("Couldn't parse %v: %v", p.in, err) 55 | } else { 56 | if got.Uint64() != p.exp { 57 | t.Errorf("Expected %v for %v, got %v", 58 | p.exp, p.in, got) 59 | } 60 | } 61 | } 62 | } 63 | 64 | func TestBigByteErrors(t *testing.T) { 65 | got, err := ParseBigBytes("84 JB") 66 | if err == nil { 67 | t.Errorf("Expected error, got %v", got) 68 | } 69 | _, err = ParseBigBytes("") 70 | if err == nil { 71 | t.Errorf("Expected error parsing nothing") 72 | } 73 | } 74 | 75 | func bbyte(in uint64) string { 76 | return BigBytes((&big.Int{}).SetUint64(in)) 77 | } 78 | 79 | func bibyte(in uint64) string { 80 | return BigIBytes((&big.Int{}).SetUint64(in)) 81 | } 82 | 83 | func TestBigBytes(t *testing.T) { 84 | testList{ 85 | {"bytes(0)", bbyte(0), "0 B"}, 86 | {"bytes(1)", bbyte(1), "1 B"}, 87 | {"bytes(803)", bbyte(803), "803 B"}, 88 | {"bytes(999)", bbyte(999), "999 B"}, 89 | 90 | {"bytes(1024)", bbyte(1024), "1.0 kB"}, 91 | {"bytes(1MB - 1)", bbyte(MByte - Byte), "1000 kB"}, 92 | 93 | {"bytes(1MB)", bbyte(1024 * 1024), "1.0 MB"}, 94 | {"bytes(1GB - 1K)", bbyte(GByte - KByte), "1000 MB"}, 95 | 96 | {"bytes(1GB)", bbyte(GByte), "1.0 GB"}, 97 | {"bytes(1TB - 1M)", bbyte(TByte - MByte), "1000 GB"}, 98 | 99 | {"bytes(1TB)", bbyte(TByte), "1.0 TB"}, 100 | {"bytes(1PB - 1T)", bbyte(PByte - TByte), "999 TB"}, 101 | 102 | {"bytes(1PB)", bbyte(PByte), "1.0 PB"}, 103 | {"bytes(1PB - 1T)", bbyte(EByte - PByte), "999 PB"}, 104 | 105 | {"bytes(1EB)", bbyte(EByte), "1.0 EB"}, 106 | // Overflows. 107 | // {"bytes(1EB - 1P)", Bytes((KByte*EByte)-PByte), "1023EB"}, 108 | 109 | {"bytes(0)", bibyte(0), "0 B"}, 110 | {"bytes(1)", bibyte(1), "1 B"}, 111 | {"bytes(803)", bibyte(803), "803 B"}, 112 | {"bytes(1023)", bibyte(1023), "1023 B"}, 113 | 114 | {"bytes(1024)", bibyte(1024), "1.0 KiB"}, 115 | {"bytes(1MB - 1)", bibyte(MiByte - IByte), "1024 KiB"}, 116 | 117 | {"bytes(1MB)", bibyte(1024 * 1024), "1.0 MiB"}, 118 | {"bytes(1GB - 1K)", bibyte(GiByte - KiByte), "1024 MiB"}, 119 | 120 | {"bytes(1GB)", bibyte(GiByte), "1.0 GiB"}, 121 | {"bytes(1TB - 1M)", bibyte(TiByte - MiByte), "1024 GiB"}, 122 | 123 | {"bytes(1TB)", bibyte(TiByte), "1.0 TiB"}, 124 | {"bytes(1PB - 1T)", bibyte(PiByte - TiByte), "1023 TiB"}, 125 | 126 | {"bytes(1PB)", bibyte(PiByte), "1.0 PiB"}, 127 | {"bytes(1PB - 1T)", bibyte(EiByte - PiByte), "1023 PiB"}, 128 | 129 | {"bytes(1EiB)", bibyte(EiByte), "1.0 EiB"}, 130 | // Overflows. 131 | // {"bytes(1EB - 1P)", bibyte((KIByte*EIByte)-PiByte), "1023EB"}, 132 | 133 | {"bytes(5.5GiB)", bibyte(5.5 * GiByte), "5.5 GiB"}, 134 | 135 | {"bytes(5.5GB)", bbyte(5.5 * GByte), "5.5 GB"}, 136 | }.validate(t) 137 | } 138 | 139 | func TestVeryBigBytes(t *testing.T) { 140 | b, _ := (&big.Int{}).SetString("15347691069326346944512", 10) 141 | s := BigBytes(b) 142 | if s != "15 ZB" { 143 | t.Errorf("Expected 15 ZB, got %v", s) 144 | } 145 | s = BigIBytes(b) 146 | if s != "13 ZiB" { 147 | t.Errorf("Expected 13 ZiB, got %v", s) 148 | } 149 | 150 | b, _ = (&big.Int{}).SetString("15716035654990179271180288", 10) 151 | s = BigBytes(b) 152 | if s != "16 YB" { 153 | t.Errorf("Expected 16 YB, got %v", s) 154 | } 155 | s = BigIBytes(b) 156 | if s != "13 YiB" { 157 | t.Errorf("Expected 13 YiB, got %v", s) 158 | } 159 | } 160 | 161 | func TestVeryVeryBigBytes(t *testing.T) { 162 | b, _ := (&big.Int{}).SetString("16093220510709943573688614912", 10) 163 | s := BigBytes(b) 164 | if s != "16 RB" { 165 | t.Errorf("Expected 16 RB, got %v", s) 166 | } 167 | s = BigIBytes(b) 168 | if s != "13 RiB" { 169 | t.Errorf("Expected 13 RiB, got %v", s) 170 | } 171 | } 172 | 173 | func TestParseVeryBig(t *testing.T) { 174 | tests := []struct { 175 | in string 176 | out string 177 | }{ 178 | {"16 ZB", "16000000000000000000000"}, 179 | {"16 ZiB", "18889465931478580854784"}, 180 | {"16.5 ZB", "16500000000000000000000"}, 181 | {"16.5 ZiB", "19479761741837286506496"}, 182 | {"16 Z", "16000000000000000000000"}, 183 | {"16 Zi", "18889465931478580854784"}, 184 | {"16.5 Z", "16500000000000000000000"}, 185 | {"16.5 Zi", "19479761741837286506496"}, 186 | 187 | {"16 YB", "16000000000000000000000000"}, 188 | {"16 YiB", "19342813113834066795298816"}, 189 | {"16.5 YB", "16500000000000000000000000"}, 190 | {"16.5 YiB", "19947276023641381382651904"}, 191 | {"16 Y", "16000000000000000000000000"}, 192 | {"16 Yi", "19342813113834066795298816"}, 193 | {"16.5 Y", "16500000000000000000000000"}, 194 | {"16.5 Yi", "19947276023641381382651904"}, 195 | } 196 | 197 | for _, test := range tests { 198 | x, err := ParseBigBytes(test.in) 199 | if err != nil { 200 | t.Errorf("Error parsing %q: %v", test.in, err) 201 | continue 202 | } 203 | 204 | if x.String() != test.out { 205 | t.Errorf("Expected %q for %q, got %v", test.out, test.in, x) 206 | } 207 | } 208 | } 209 | 210 | func BenchmarkParseBigBytes(b *testing.B) { 211 | for i := 0; i < b.N; i++ { 212 | ParseBigBytes("16.5 Z") 213 | } 214 | } 215 | 216 | func BenchmarkBigBytes(b *testing.B) { 217 | for i := 0; i < b.N; i++ { 218 | bibyte(16.5 * GByte) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | // IEC Sizes. 12 | // kibis of bits 13 | const ( 14 | Byte = 1 << (iota * 10) 15 | KiByte 16 | MiByte 17 | GiByte 18 | TiByte 19 | PiByte 20 | EiByte 21 | ) 22 | 23 | // SI Sizes. 24 | const ( 25 | IByte = 1 26 | KByte = IByte * 1000 27 | MByte = KByte * 1000 28 | GByte = MByte * 1000 29 | TByte = GByte * 1000 30 | PByte = TByte * 1000 31 | EByte = PByte * 1000 32 | ) 33 | 34 | var bytesSizeTable = map[string]uint64{ 35 | "b": Byte, 36 | "kib": KiByte, 37 | "kb": KByte, 38 | "mib": MiByte, 39 | "mb": MByte, 40 | "gib": GiByte, 41 | "gb": GByte, 42 | "tib": TiByte, 43 | "tb": TByte, 44 | "pib": PiByte, 45 | "pb": PByte, 46 | "eib": EiByte, 47 | "eb": EByte, 48 | // Without suffix 49 | "": Byte, 50 | "ki": KiByte, 51 | "k": KByte, 52 | "mi": MiByte, 53 | "m": MByte, 54 | "gi": GiByte, 55 | "g": GByte, 56 | "ti": TiByte, 57 | "t": TByte, 58 | "pi": PiByte, 59 | "p": PByte, 60 | "ei": EiByte, 61 | "e": EByte, 62 | } 63 | 64 | func logn(n, b float64) float64 { 65 | return math.Log(n) / math.Log(b) 66 | } 67 | 68 | func countDigits(n int64) int { 69 | digits := 0 70 | for n != 0 { 71 | n /= 10 72 | digits += 1 73 | } 74 | return digits 75 | } 76 | 77 | func humanateBytes(s uint64, base float64, minDigits int, sizes []string) string { 78 | if s < 10 { 79 | return fmt.Sprintf("%d B", s) 80 | } 81 | e := math.Floor(logn(float64(s), base)) 82 | suffix := sizes[int(e)] 83 | rounding := math.Pow10(minDigits - 1) 84 | val := math.Floor(float64(s)/math.Pow(base, e)*rounding+0.5) / rounding 85 | ff := "%%.%df %%s" 86 | digits := minDigits - countDigits(int64(val)) 87 | if digits < 0 { 88 | digits = 0 89 | } 90 | f := fmt.Sprintf(ff, digits) 91 | return fmt.Sprintf(f, val, suffix) 92 | } 93 | 94 | // Bytes produces a human-readable representation of an SI size. 95 | // 96 | // See also: ParseBytes. 97 | // 98 | // Bytes(82854982) -> 83 MB 99 | func Bytes(s uint64) string { 100 | sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} 101 | return humanateBytes(s, 1000, 2, sizes) 102 | } 103 | 104 | // BytesN produces a human-readable representation of an SI size. 105 | // n specifies the total number of digits to output, including the decimal part. 106 | // If n is less than or equal to the number of digits in the integer part, the decimal part will be omitted. 107 | // 108 | // See also: ParseBytes. 109 | // 110 | // BytesN(82854982, 3) -> 82.9 MB 111 | // BytesN(82854982, 4) -> 82.85 MB 112 | func BytesN(s uint64, n int) string { 113 | sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} 114 | return humanateBytes(s, 1000, n, sizes) 115 | } 116 | 117 | // IBytes produces a human-readable representation of an IEC size. 118 | // 119 | // See also: ParseBytes. 120 | // 121 | // IBytes(82854982) -> 79 MiB 122 | func IBytes(s uint64) string { 123 | sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} 124 | return humanateBytes(s, 1024, 2, sizes) 125 | } 126 | 127 | // IBytesN produces a human-readable representation of an IEC size. 128 | // n specifies the total number of digits to output, including the decimal part. 129 | // If n is less than or equal to the number of digits in the integer part, the decimal part will be omitted. 130 | // 131 | // See also: ParseBytes. 132 | // 133 | // IBytesN(82854982, 4) -> 79.02 MiB 134 | // IBytesN(123456789, 3) -> 118 MiB 135 | // IBytesN(123456789, 6) -> 117.738 MiB 136 | func IBytesN(s uint64, n int) string { 137 | sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} 138 | return humanateBytes(s, 1024, n, sizes) 139 | } 140 | 141 | // ParseBytes parses a string representation of bytes into the number 142 | // of bytes it represents. 143 | // 144 | // See Also: Bytes, IBytes. 145 | // 146 | // ParseBytes("42 MB") -> 42000000, nil 147 | // ParseBytes("42 mib") -> 44040192, nil 148 | func ParseBytes(s string) (uint64, error) { 149 | lastDigit := 0 150 | hasComma := false 151 | for _, r := range s { 152 | if !(unicode.IsDigit(r) || r == '.' || r == ',') { 153 | break 154 | } 155 | if r == ',' { 156 | hasComma = true 157 | } 158 | lastDigit++ 159 | } 160 | 161 | num := s[:lastDigit] 162 | if hasComma { 163 | num = strings.Replace(num, ",", "", -1) 164 | } 165 | 166 | f, err := strconv.ParseFloat(num, 64) 167 | if err != nil { 168 | return 0, err 169 | } 170 | 171 | extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) 172 | if m, ok := bytesSizeTable[extra]; ok { 173 | f *= float64(m) 174 | if f >= math.MaxUint64 { 175 | return 0, fmt.Errorf("too large: %v", s) 176 | } 177 | return uint64(f), nil 178 | } 179 | 180 | return 0, fmt.Errorf("unhandled size name: %v", extra) 181 | } 182 | -------------------------------------------------------------------------------- /bytes_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestByteParsing(t *testing.T) { 8 | tests := []struct { 9 | in string 10 | exp uint64 11 | }{ 12 | {"42", 42}, 13 | {"42MB", 42000000}, 14 | {"42MiB", 44040192}, 15 | {"42mb", 42000000}, 16 | {"42mib", 44040192}, 17 | {"42MIB", 44040192}, 18 | {"42 MB", 42000000}, 19 | {"42 MiB", 44040192}, 20 | {"42 mb", 42000000}, 21 | {"42 mib", 44040192}, 22 | {"42 MIB", 44040192}, 23 | {"42.5MB", 42500000}, 24 | {"42.5MiB", 44564480}, 25 | {"42.5 MB", 42500000}, 26 | {"42.5 MiB", 44564480}, 27 | // No need to say B 28 | {"42M", 42000000}, 29 | {"42Mi", 44040192}, 30 | {"42m", 42000000}, 31 | {"42mi", 44040192}, 32 | {"42MI", 44040192}, 33 | {"42 M", 42000000}, 34 | {"42 Mi", 44040192}, 35 | {"42 m", 42000000}, 36 | {"42 mi", 44040192}, 37 | {"42 MI", 44040192}, 38 | {"42.5M", 42500000}, 39 | {"42.5Mi", 44564480}, 40 | {"42.5 M", 42500000}, 41 | {"42.5 Mi", 44564480}, 42 | // Bug #42 43 | {"1,005.03 MB", 1005030000}, 44 | // Large testing, breaks when too much larger than 45 | // this. 46 | {"12.5 EB", uint64(12.5 * float64(EByte))}, 47 | {"12.5 E", uint64(12.5 * float64(EByte))}, 48 | {"12.5 EiB", uint64(12.5 * float64(EiByte))}, 49 | } 50 | 51 | for _, p := range tests { 52 | got, err := ParseBytes(p.in) 53 | if err != nil { 54 | t.Errorf("Couldn't parse %v: %v", p.in, err) 55 | } 56 | if got != p.exp { 57 | t.Errorf("Expected %v for %v, got %v", 58 | p.exp, p.in, got) 59 | } 60 | } 61 | } 62 | 63 | func TestByteErrors(t *testing.T) { 64 | got, err := ParseBytes("84 JB") 65 | if err == nil { 66 | t.Errorf("Expected error, got %v", got) 67 | } 68 | _, err = ParseBytes("") 69 | if err == nil { 70 | t.Errorf("Expected error parsing nothing") 71 | } 72 | got, err = ParseBytes("16 EiB") 73 | if err == nil { 74 | t.Errorf("Expected error, got %v", got) 75 | } 76 | } 77 | 78 | func TestBytes(t *testing.T) { 79 | testList{ 80 | {"bytes(0)", Bytes(0), "0 B"}, 81 | {"bytes(1)", Bytes(1), "1 B"}, 82 | {"bytes(803)", Bytes(803), "803 B"}, 83 | {"bytes(999)", Bytes(999), "999 B"}, 84 | 85 | {"bytes(1024)", Bytes(1024), "1.0 kB"}, 86 | {"bytes(9999)", Bytes(9999), "10 kB"}, 87 | {"bytes(1MB - 1)", Bytes(MByte - Byte), "1000 kB"}, 88 | 89 | {"bytes(1MB)", Bytes(1024 * 1024), "1.0 MB"}, 90 | {"bytes(1GB - 1K)", Bytes(GByte - KByte), "1000 MB"}, 91 | 92 | {"bytes(1GB)", Bytes(GByte), "1.0 GB"}, 93 | {"bytes(1TB - 1M)", Bytes(TByte - MByte), "1000 GB"}, 94 | {"bytes(10MB)", Bytes(9999 * 1000), "10 MB"}, 95 | 96 | {"bytes(1TB)", Bytes(TByte), "1.0 TB"}, 97 | {"bytes(1PB - 1T)", Bytes(PByte - TByte), "999 TB"}, 98 | 99 | {"bytes(1PB)", Bytes(PByte), "1.0 PB"}, 100 | {"bytes(1PB - 1T)", Bytes(EByte - PByte), "999 PB"}, 101 | 102 | {"bytes(1EB)", Bytes(EByte), "1.0 EB"}, 103 | // Overflows. 104 | // {"bytes(1EB - 1P)", Bytes((KByte*EByte)-PByte), "1023EB"}, 105 | 106 | {"bytesN(1234, 3)", BytesN(1234, 3), "1.23 kB"}, 107 | 108 | {"bytes(0)", IBytes(0), "0 B"}, 109 | {"bytes(1)", IBytes(1), "1 B"}, 110 | {"bytes(803)", IBytes(803), "803 B"}, 111 | {"bytes(1023)", IBytes(1023), "1023 B"}, 112 | 113 | {"bytes(1024)", IBytes(1024), "1.0 KiB"}, 114 | {"bytes(1MB - 1)", IBytes(MiByte - IByte), "1024 KiB"}, 115 | 116 | {"bytes(1MB)", IBytes(1024 * 1024), "1.0 MiB"}, 117 | {"bytes(1GB - 1K)", IBytes(GiByte - KiByte), "1024 MiB"}, 118 | 119 | {"bytes(1GB)", IBytes(GiByte), "1.0 GiB"}, 120 | {"bytes(1TB - 1M)", IBytes(TiByte - MiByte), "1024 GiB"}, 121 | 122 | {"bytes(1TB)", IBytes(TiByte), "1.0 TiB"}, 123 | {"bytes(1PB - 1T)", IBytes(PiByte - TiByte), "1023 TiB"}, 124 | 125 | {"bytes(1PB)", IBytes(PiByte), "1.0 PiB"}, 126 | {"bytes(1PB - 1T)", IBytes(EiByte - PiByte), "1023 PiB"}, 127 | 128 | {"bytes(1EiB)", IBytes(EiByte), "1.0 EiB"}, 129 | // Overflows. 130 | // {"bytes(1EB - 1P)", IBytes((KIByte*EIByte)-PiByte), "1023EB"}, 131 | 132 | {"bytes(5.5GiB)", IBytes(5.5 * GiByte), "5.5 GiB"}, 133 | 134 | {"bytes(5.5GB)", Bytes(5.5 * GByte), "5.5 GB"}, 135 | 136 | {"bytes(123456789, 3)", IBytesN(123456789, 3), "118 MiB"}, 137 | {"bytes(123456789, 6)", IBytesN(123456789, 6), "117.738 MiB"}, 138 | }.validate(t) 139 | } 140 | 141 | func BenchmarkParseBytes(b *testing.B) { 142 | for i := 0; i < b.N; i++ { 143 | ParseBytes("16.5 GB") 144 | } 145 | } 146 | 147 | func BenchmarkBytes(b *testing.B) { 148 | for i := 0; i < b.N; i++ { 149 | Bytes(16.5 * GByte) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /comma.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "math/big" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Comma produces a string form of the given number in base 10 with 12 | // commas after every three orders of magnitude. 13 | // 14 | // e.g. Comma(834142) -> 834,142 15 | func Comma(v int64) string { 16 | // Shortcut for [0, 7] 17 | if v&^0b111 == 0 { 18 | return string([]byte{byte(v) + 48}) 19 | } 20 | 21 | // Min int64 can't be negated to a usable value, so it has to be special cased. 22 | if v == math.MinInt64 { 23 | return "-9,223,372,036,854,775,808" 24 | } 25 | // Counting the number of digits. 26 | var count byte = 0 27 | for n := v; n != 0; n = n / 10 { 28 | count++ 29 | } 30 | 31 | count += (count - 1) / 3 32 | if v < 0 { 33 | v = 0 - v 34 | count++ 35 | } 36 | output := make([]byte, count) 37 | j := len(output) - 1 38 | 39 | var counter byte = 0 40 | for v > 9 { 41 | output[j] = byte(v%10) + 48 42 | v = v / 10 43 | j-- 44 | if counter == 2 { 45 | counter = 0 46 | output[j] = ',' 47 | j-- 48 | } else { 49 | counter++ 50 | } 51 | } 52 | 53 | output[j] = byte(v) + 48 54 | if j == 1 { 55 | output[0] = '-' 56 | } 57 | return string(output) 58 | } 59 | 60 | // Commaf produces a string form of the given number in base 10 with 61 | // commas after every three orders of magnitude. 62 | // 63 | // e.g. Commaf(834142.32) -> 834,142.32 64 | func Commaf(v float64) string { 65 | buf := &bytes.Buffer{} 66 | if v < 0 { 67 | buf.Write([]byte{'-'}) 68 | v = 0 - v 69 | } 70 | 71 | comma := []byte{','} 72 | 73 | parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".") 74 | pos := 0 75 | if len(parts[0])%3 != 0 { 76 | pos += len(parts[0]) % 3 77 | buf.WriteString(parts[0][:pos]) 78 | buf.Write(comma) 79 | } 80 | for ; pos < len(parts[0]); pos += 3 { 81 | buf.WriteString(parts[0][pos : pos+3]) 82 | buf.Write(comma) 83 | } 84 | buf.Truncate(buf.Len() - 1) 85 | 86 | if len(parts) > 1 { 87 | buf.Write([]byte{'.'}) 88 | buf.WriteString(parts[1]) 89 | } 90 | return buf.String() 91 | } 92 | 93 | // CommafWithDigits works like the Commaf but limits the resulting 94 | // string to the given number of decimal places. 95 | // 96 | // e.g. CommafWithDigits(834142.32, 1) -> 834,142.3 97 | func CommafWithDigits(f float64, decimals int) string { 98 | return stripTrailingDigits(Commaf(f), decimals) 99 | } 100 | 101 | // BigComma produces a string form of the given big.Int in base 10 102 | // with commas after every three orders of magnitude. 103 | func BigComma(b *big.Int) string { 104 | sign := "" 105 | if b.Sign() < 0 { 106 | sign = "-" 107 | b.Abs(b) 108 | } 109 | 110 | athousand := big.NewInt(1000) 111 | c := (&big.Int{}).Set(b) 112 | _, m := oom(c, athousand) 113 | parts := make([]string, m+1) 114 | j := len(parts) - 1 115 | 116 | mod := &big.Int{} 117 | for b.Cmp(athousand) >= 0 { 118 | b.DivMod(b, athousand, mod) 119 | parts[j] = strconv.FormatInt(mod.Int64(), 10) 120 | switch len(parts[j]) { 121 | case 2: 122 | parts[j] = "0" + parts[j] 123 | case 1: 124 | parts[j] = "00" + parts[j] 125 | } 126 | j-- 127 | } 128 | parts[j] = strconv.Itoa(int(b.Int64())) 129 | return sign + strings.Join(parts[j:], ",") 130 | } 131 | -------------------------------------------------------------------------------- /comma_fuzz_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package humanize_test 5 | 6 | import ( 7 | "math" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/dustin/go-humanize" 13 | ) 14 | 15 | func FuzzComma(f *testing.F) { 16 | f.Add(int64(0)) 17 | f.Add(int64(-1)) 18 | f.Add(int64(10)) 19 | f.Add(int64(100)) 20 | f.Add(int64(1000)) 21 | f.Add(int64(-1000)) 22 | f.Add(int64(math.MaxInt64)) 23 | f.Add(int64(math.MaxInt64) - 1) 24 | f.Add(int64(math.MinInt64)) 25 | f.Add(int64(math.MinInt64) + 1) 26 | 27 | f.Fuzz(func(t *testing.T, v int64) { 28 | got := humanize.Comma(v) 29 | gotNoCommas := strings.ReplaceAll(got, ",", "") 30 | expected := strconv.FormatInt(v, 10) 31 | if gotNoCommas != expected { 32 | t.Fatalf("%d: got %q, expected %q", v, got, expected) 33 | } 34 | 35 | if v < 0 { 36 | if got[0] != '-' { 37 | t.Fatalf("%d: got: %q", v, got) 38 | } 39 | // Remove sign 40 | got = got[1:] 41 | } 42 | // Check that commas are located every 3 digits 43 | l := len(got) 44 | for i := l - 1; i >= 0; i-- { 45 | var ok bool 46 | if (l-1-i)%4 == 3 { 47 | ok = got[i] == ',' 48 | } else { 49 | ok = got[i] >= '0' && got[i] <= '9' 50 | } 51 | if !ok { 52 | t.Log(l - 1 - i) 53 | t.Log((l - 1 - i) % 4) 54 | t.Fatalf("%d: got: %q", v, got) 55 | } 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /comma_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "math" 5 | "math/big" 6 | "testing" 7 | ) 8 | 9 | func TestCommas(t *testing.T) { 10 | testList{ 11 | {"0", Comma(0), "0"}, 12 | {"10", Comma(10), "10"}, 13 | {"100", Comma(100), "100"}, 14 | {"1,000", Comma(1000), "1,000"}, 15 | {"10,000", Comma(10000), "10,000"}, 16 | {"100,000", Comma(100000), "100,000"}, 17 | {"10,000,000", Comma(10000000), "10,000,000"}, 18 | {"10,100,000", Comma(10100000), "10,100,000"}, 19 | {"10,010,000", Comma(10010000), "10,010,000"}, 20 | {"10,001,000", Comma(10001000), "10,001,000"}, 21 | {"123,456,789", Comma(123456789), "123,456,789"}, 22 | {"maxint", Comma(9.223372e+18), "9,223,372,000,000,000,000"}, 23 | {"math.maxint", Comma(math.MaxInt64), "9,223,372,036,854,775,807"}, 24 | {"math.minint", Comma(math.MinInt64), "-9,223,372,036,854,775,808"}, 25 | {"minint", Comma(-9.223372e+18), "-9,223,372,000,000,000,000"}, 26 | {"-123,456,789", Comma(-123456789), "-123,456,789"}, 27 | {"-10,100,000", Comma(-10100000), "-10,100,000"}, 28 | {"-10,010,000", Comma(-10010000), "-10,010,000"}, 29 | {"-10,001,000", Comma(-10001000), "-10,001,000"}, 30 | {"-10,000,000", Comma(-10000000), "-10,000,000"}, 31 | {"-100,000", Comma(-100000), "-100,000"}, 32 | {"-10,000", Comma(-10000), "-10,000"}, 33 | {"-1,000", Comma(-1000), "-1,000"}, 34 | {"-100", Comma(-100), "-100"}, 35 | {"-10", Comma(-10), "-10"}, 36 | }.validate(t) 37 | } 38 | 39 | func TestCommafWithDigits(t *testing.T) { 40 | testList{ 41 | {"1.23, 0", CommafWithDigits(1.23, 0), "1"}, 42 | {"1.23, 1", CommafWithDigits(1.23, 1), "1.2"}, 43 | {"1.23, 2", CommafWithDigits(1.23, 2), "1.23"}, 44 | {"1.23, 3", CommafWithDigits(1.23, 3), "1.23"}, 45 | }.validate(t) 46 | } 47 | 48 | func TestCommafs(t *testing.T) { 49 | testList{ 50 | {"0", Commaf(0), "0"}, 51 | {"10.11", Commaf(10.11), "10.11"}, 52 | {"100", Commaf(100), "100"}, 53 | {"1,000", Commaf(1000), "1,000"}, 54 | {"10,000", Commaf(10000), "10,000"}, 55 | {"100,000", Commaf(100000), "100,000"}, 56 | {"834,142.32", Commaf(834142.32), "834,142.32"}, 57 | {"10,000,000", Commaf(10000000), "10,000,000"}, 58 | {"10,100,000", Commaf(10100000), "10,100,000"}, 59 | {"10,010,000", Commaf(10010000), "10,010,000"}, 60 | {"10,001,000", Commaf(10001000), "10,001,000"}, 61 | {"123,456,789", Commaf(123456789), "123,456,789"}, 62 | {"maxf64", Commaf(math.MaxFloat64), "179,769,313,486,231,570,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000"}, 63 | {"minf64", Commaf(math.SmallestNonzeroFloat64), "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005"}, 64 | {"-123,456,789", Commaf(-123456789), "-123,456,789"}, 65 | {"-10,100,000", Commaf(-10100000), "-10,100,000"}, 66 | {"-10,010,000", Commaf(-10010000), "-10,010,000"}, 67 | {"-10,001,000", Commaf(-10001000), "-10,001,000"}, 68 | {"-10,000,000", Commaf(-10000000), "-10,000,000"}, 69 | {"-100,000", Commaf(-100000), "-100,000"}, 70 | {"-10,000", Commaf(-10000), "-10,000"}, 71 | {"-1,000", Commaf(-1000), "-1,000"}, 72 | {"-100.11", Commaf(-100.11), "-100.11"}, 73 | {"-10", Commaf(-10), "-10"}, 74 | }.validate(t) 75 | } 76 | 77 | func BenchmarkCommas(b *testing.B) { 78 | for i := 0; i < b.N; i++ { 79 | Comma(1234567890) 80 | } 81 | } 82 | 83 | func BenchmarkCommaf(b *testing.B) { 84 | for i := 0; i < b.N; i++ { 85 | Commaf(1234567890.83584) 86 | } 87 | } 88 | 89 | func BenchmarkBigCommas(b *testing.B) { 90 | for i := 0; i < b.N; i++ { 91 | BigComma(big.NewInt(1234567890)) 92 | } 93 | } 94 | 95 | func bigComma(i int64) string { 96 | return BigComma(big.NewInt(i)) 97 | } 98 | 99 | func TestBigCommas(t *testing.T) { 100 | testList{ 101 | {"0", bigComma(0), "0"}, 102 | {"10", bigComma(10), "10"}, 103 | {"100", bigComma(100), "100"}, 104 | {"1,000", bigComma(1000), "1,000"}, 105 | {"10,000", bigComma(10000), "10,000"}, 106 | {"100,000", bigComma(100000), "100,000"}, 107 | {"10,000,000", bigComma(10000000), "10,000,000"}, 108 | {"10,100,000", bigComma(10100000), "10,100,000"}, 109 | {"10,010,000", bigComma(10010000), "10,010,000"}, 110 | {"10,001,000", bigComma(10001000), "10,001,000"}, 111 | {"123,456,789", bigComma(123456789), "123,456,789"}, 112 | {"maxint", bigComma(9.223372e+18), "9,223,372,000,000,000,000"}, 113 | {"minint", bigComma(-9.223372e+18), "-9,223,372,000,000,000,000"}, 114 | {"-123,456,789", bigComma(-123456789), "-123,456,789"}, 115 | {"-10,100,000", bigComma(-10100000), "-10,100,000"}, 116 | {"-10,010,000", bigComma(-10010000), "-10,010,000"}, 117 | {"-10,001,000", bigComma(-10001000), "-10,001,000"}, 118 | {"-10,000,000", bigComma(-10000000), "-10,000,000"}, 119 | {"-100,000", bigComma(-100000), "-100,000"}, 120 | {"-10,000", bigComma(-10000), "-10,000"}, 121 | {"-1,000", bigComma(-1000), "-1,000"}, 122 | {"-100", bigComma(-100), "-100"}, 123 | {"-10", bigComma(-10), "-10"}, 124 | }.validate(t) 125 | } 126 | 127 | func TestVeryBigCommas(t *testing.T) { 128 | tests := []struct{ in, exp string }{ 129 | { 130 | "84889279597249724975972597249849757294578485", 131 | "84,889,279,597,249,724,975,972,597,249,849,757,294,578,485", 132 | }, 133 | { 134 | "-84889279597249724975972597249849757294578485", 135 | "-84,889,279,597,249,724,975,972,597,249,849,757,294,578,485", 136 | }, 137 | } 138 | for _, test := range tests { 139 | n, _ := (&big.Int{}).SetString(test.in, 10) 140 | got := BigComma(n) 141 | if test.exp != got { 142 | t.Errorf("Expected %q, got %q", test.exp, got) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /commaf.go: -------------------------------------------------------------------------------- 1 | //go:build go1.6 2 | // +build go1.6 3 | 4 | package humanize 5 | 6 | import ( 7 | "bytes" 8 | "math/big" 9 | "strings" 10 | ) 11 | 12 | // BigCommaf produces a string form of the given big.Float in base 10 13 | // with commas after every three orders of magnitude. 14 | func BigCommaf(v *big.Float) string { 15 | buf := &bytes.Buffer{} 16 | if v.Sign() < 0 { 17 | buf.Write([]byte{'-'}) 18 | v.Abs(v) 19 | } 20 | 21 | comma := []byte{','} 22 | 23 | parts := strings.Split(v.Text('f', -1), ".") 24 | pos := 0 25 | if len(parts[0])%3 != 0 { 26 | pos += len(parts[0]) % 3 27 | buf.WriteString(parts[0][:pos]) 28 | buf.Write(comma) 29 | } 30 | for ; pos < len(parts[0]); pos += 3 { 31 | buf.WriteString(parts[0][pos : pos+3]) 32 | buf.Write(comma) 33 | } 34 | buf.Truncate(buf.Len() - 1) 35 | 36 | if len(parts) > 1 { 37 | buf.Write([]byte{'.'}) 38 | buf.WriteString(parts[1]) 39 | } 40 | return buf.String() 41 | } 42 | -------------------------------------------------------------------------------- /commaf_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.6 2 | // +build go1.6 3 | 4 | package humanize 5 | 6 | import ( 7 | "math" 8 | "math/big" 9 | "testing" 10 | ) 11 | 12 | func BenchmarkBigCommaf(b *testing.B) { 13 | for i := 0; i < b.N; i++ { 14 | Commaf(1234567890.83584) 15 | } 16 | } 17 | 18 | func TestBigCommafs(t *testing.T) { 19 | testList{ 20 | {"0", BigCommaf(big.NewFloat(0)), "0"}, 21 | {"10.11", BigCommaf(big.NewFloat(10.11)), "10.11"}, 22 | {"100", BigCommaf(big.NewFloat(100)), "100"}, 23 | {"1,000", BigCommaf(big.NewFloat(1000)), "1,000"}, 24 | {"10,000", BigCommaf(big.NewFloat(10000)), "10,000"}, 25 | {"100,000", BigCommaf(big.NewFloat(100000)), "100,000"}, 26 | {"834,142.32", BigCommaf(big.NewFloat(834142.32)), "834,142.32"}, 27 | {"10,000,000", BigCommaf(big.NewFloat(10000000)), "10,000,000"}, 28 | {"10,100,000", BigCommaf(big.NewFloat(10100000)), "10,100,000"}, 29 | {"10,010,000", BigCommaf(big.NewFloat(10010000)), "10,010,000"}, 30 | {"10,001,000", BigCommaf(big.NewFloat(10001000)), "10,001,000"}, 31 | {"123,456,789", BigCommaf(big.NewFloat(123456789)), "123,456,789"}, 32 | {"maxf64", BigCommaf(big.NewFloat(math.MaxFloat64)), "179,769,313,486,231,570,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000"}, 33 | {"minf64", BigCommaf(big.NewFloat(math.SmallestNonzeroFloat64)), "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004940656458412465"}, 34 | {"-123,456,789", BigCommaf(big.NewFloat(-123456789)), "-123,456,789"}, 35 | {"-10,100,000", BigCommaf(big.NewFloat(-10100000)), "-10,100,000"}, 36 | {"-10,010,000", BigCommaf(big.NewFloat(-10010000)), "-10,010,000"}, 37 | {"-10,001,000", BigCommaf(big.NewFloat(-10001000)), "-10,001,000"}, 38 | {"-10,000,000", BigCommaf(big.NewFloat(-10000000)), "-10,000,000"}, 39 | {"-100,000", BigCommaf(big.NewFloat(-100000)), "-100,000"}, 40 | {"-10,000", BigCommaf(big.NewFloat(-10000)), "-10,000"}, 41 | {"-1,000", BigCommaf(big.NewFloat(-1000)), "-1,000"}, 42 | {"-100.11", BigCommaf(big.NewFloat(-100.11)), "-100.11"}, 43 | {"-10", BigCommaf(big.NewFloat(-10)), "-10"}, 44 | }.validate(t) 45 | } 46 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type testList []struct { 8 | name, got, exp string 9 | } 10 | 11 | func (tl testList) validate(t *testing.T) { 12 | for _, test := range tl { 13 | if test.got != test.exp { 14 | t.Errorf("On %v, expected '%v', but got '%v'", 15 | test.name, test.exp, test.got) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /english/words.go: -------------------------------------------------------------------------------- 1 | // Package english provides utilities to generate more user-friendly English output. 2 | package english 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | humanize "github.com/dustin/go-humanize" 9 | ) 10 | 11 | // These are included because they are common technical terms. 12 | var specialPlurals = map[string]string{ 13 | "index": "indices", 14 | "matrix": "matrices", 15 | "vertex": "vertices", 16 | } 17 | 18 | var sibilantEndings = []string{"s", "sh", "tch", "x"} 19 | 20 | var isVowel = map[byte]bool{ 21 | 'A': true, 'E': true, 'I': true, 'O': true, 'U': true, 22 | 'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 23 | } 24 | 25 | // PluralWord builds the plural form of an English word. 26 | // The simple English rules of regular pluralization will be used 27 | // if the plural form is an empty string (i.e. not explicitly given). 28 | // The special cases are not guaranteed to work for strings outside ASCII. 29 | func PluralWord(quantity int, singular, plural string) string { 30 | if quantity == 1 { 31 | return singular 32 | } 33 | if plural != "" { 34 | return plural 35 | } 36 | if plural = specialPlurals[singular]; plural != "" { 37 | return plural 38 | } 39 | 40 | // We need to guess what the English plural might be. Keep this 41 | // function simple! It doesn't need to know about every possiblity; 42 | // only regular rules and the most common special cases. 43 | // 44 | // Reference: http://en.wikipedia.org/wiki/English_plural 45 | 46 | for _, ending := range sibilantEndings { 47 | if strings.HasSuffix(singular, ending) { 48 | return singular + "es" 49 | } 50 | } 51 | l := len(singular) 52 | if l >= 2 && singular[l-1] == 'o' && !isVowel[singular[l-2]] { 53 | return singular + "es" 54 | } 55 | if l >= 2 && singular[l-1] == 'y' && !isVowel[singular[l-2]] { 56 | return singular[:l-1] + "ies" 57 | } 58 | 59 | return singular + "s" 60 | } 61 | 62 | // Plural formats an integer and a string into a single pluralized string. 63 | // The simple English rules of regular pluralization will be used 64 | // if the plural form is an empty string (i.e. not explicitly given). 65 | func Plural(quantity int, singular, plural string) string { 66 | return fmt.Sprintf("%s %s", humanize.Comma(int64(quantity)), PluralWord(quantity, singular, plural)) 67 | } 68 | 69 | // WordSeries converts a list of words into a word series in English. 70 | // It returns a string containing all the given words separated by commas, 71 | // the coordinating conjunction, and a serial comma, as appropriate. 72 | func WordSeries(words []string, conjunction string) string { 73 | switch len(words) { 74 | case 0: 75 | return "" 76 | case 1: 77 | return words[0] 78 | default: 79 | return fmt.Sprintf("%s %s %s", strings.Join(words[:len(words)-1], ", "), conjunction, words[len(words)-1]) 80 | } 81 | } 82 | 83 | // OxfordWordSeries converts a list of words into a word series in English, 84 | // using an Oxford comma (https://en.wikipedia.org/wiki/Serial_comma). It 85 | // returns a string containing all the given words separated by commas, the 86 | // coordinating conjunction, and a serial comma, as appropriate. 87 | func OxfordWordSeries(words []string, conjunction string) string { 88 | switch len(words) { 89 | case 0: 90 | return "" 91 | case 1: 92 | return words[0] 93 | case 2: 94 | return strings.Join(words, fmt.Sprintf(" %s ", conjunction)) 95 | default: 96 | return fmt.Sprintf("%s, %s %s", strings.Join(words[:len(words)-1], ", "), conjunction, words[len(words)-1]) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /english/words_test.go: -------------------------------------------------------------------------------- 1 | package english 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPluralWord(t *testing.T) { 8 | tests := []struct { 9 | n int 10 | singular, plural string 11 | want string 12 | }{ 13 | {0, "object", "", "objects"}, 14 | {1, "object", "", "object"}, 15 | {-1, "object", "", "objects"}, 16 | {42, "object", "", "objects"}, 17 | {2, "vax", "vaxen", "vaxen"}, 18 | 19 | // special cases 20 | {2, "index", "", "indices"}, 21 | 22 | // ending in a sibilant sound 23 | {2, "bus", "", "buses"}, 24 | {2, "bush", "", "bushes"}, 25 | {2, "watch", "", "watches"}, 26 | {2, "box", "", "boxes"}, 27 | 28 | // ending with 'o' preceded by a consonant 29 | {2, "hero", "", "heroes"}, 30 | 31 | // ending with 'y' preceded by a consonant 32 | {2, "lady", "", "ladies"}, 33 | {2, "day", "", "days"}, 34 | } 35 | for _, tt := range tests { 36 | if got := PluralWord(tt.n, tt.singular, tt.plural); got != tt.want { 37 | t.Errorf("PluralWord(%d, %q, %q)=%q; want: %q", tt.n, tt.singular, tt.plural, got, tt.want) 38 | } 39 | } 40 | } 41 | 42 | func TestPlural(t *testing.T) { 43 | tests := []struct { 44 | n int 45 | singular, plural string 46 | want string 47 | }{ 48 | {1, "object", "", "1 object"}, 49 | {42, "object", "", "42 objects"}, 50 | {1234567, "object", "", "1,234,567 objects"}, 51 | } 52 | for _, tt := range tests { 53 | if got := Plural(tt.n, tt.singular, tt.plural); got != tt.want { 54 | t.Errorf("Plural(%d, %q, %q)=%q; want: %q", tt.n, tt.singular, tt.plural, got, tt.want) 55 | } 56 | } 57 | } 58 | 59 | func TestWordSeries(t *testing.T) { 60 | tests := []struct { 61 | words []string 62 | conjunction string 63 | want string 64 | }{ 65 | {[]string{}, "and", ""}, 66 | {[]string{"foo"}, "and", "foo"}, 67 | {[]string{"foo", "bar"}, "and", "foo and bar"}, 68 | {[]string{"foo", "bar", "baz"}, "and", "foo, bar and baz"}, 69 | {[]string{"foo", "bar", "baz"}, "or", "foo, bar or baz"}, 70 | } 71 | for _, tt := range tests { 72 | if got := WordSeries(tt.words, tt.conjunction); got != tt.want { 73 | t.Errorf("WordSeries(%q, %q)=%q; want: %q", tt.words, tt.conjunction, got, tt.want) 74 | } 75 | } 76 | } 77 | 78 | func TestOxfordWordSeries(t *testing.T) { 79 | tests := []struct { 80 | words []string 81 | conjunction string 82 | want string 83 | }{ 84 | {[]string{}, "and", ""}, 85 | {[]string{"foo"}, "and", "foo"}, 86 | {[]string{"foo", "bar"}, "and", "foo and bar"}, 87 | {[]string{"foo", "bar", "baz"}, "and", "foo, bar, and baz"}, 88 | {[]string{"foo", "bar", "baz"}, "or", "foo, bar, or baz"}, 89 | } 90 | for _, tt := range tests { 91 | if got := OxfordWordSeries(tt.words, tt.conjunction); got != tt.want { 92 | t.Errorf("OxfordWordSeries(%q, %q)=%q; want: %q", tt.words, tt.conjunction, got, tt.want) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ftoa.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func stripTrailingZeros(s string) string { 9 | if !strings.ContainsRune(s, '.') { 10 | return s 11 | } 12 | offset := len(s) - 1 13 | for offset > 0 { 14 | if s[offset] == '.' { 15 | offset-- 16 | break 17 | } 18 | if s[offset] != '0' { 19 | break 20 | } 21 | offset-- 22 | } 23 | return s[:offset+1] 24 | } 25 | 26 | func stripTrailingDigits(s string, digits int) string { 27 | if i := strings.Index(s, "."); i >= 0 { 28 | if digits <= 0 { 29 | return s[:i] 30 | } 31 | i++ 32 | if i+digits >= len(s) { 33 | return s 34 | } 35 | return s[:i+digits] 36 | } 37 | return s 38 | } 39 | 40 | // Ftoa converts a float to a string with no trailing zeros. 41 | func Ftoa(num float64) string { 42 | return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64)) 43 | } 44 | 45 | // FtoaWithDigits converts a float to a string but limits the resulting string 46 | // to the given number of decimal places, and no trailing zeros. 47 | func FtoaWithDigits(num float64, digits int) string { 48 | return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits)) 49 | } 50 | -------------------------------------------------------------------------------- /ftoa_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "reflect" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "testing/quick" 12 | ) 13 | 14 | func TestFtoa(t *testing.T) { 15 | testList{ 16 | {"200", Ftoa(200), "200"}, 17 | {"20", Ftoa(20.0), "20"}, 18 | {"2", Ftoa(2), "2"}, 19 | {"2.2", Ftoa(2.2), "2.2"}, 20 | {"2.02", Ftoa(2.02), "2.02"}, 21 | {"200.02", Ftoa(200.02), "200.02"}, 22 | }.validate(t) 23 | } 24 | 25 | func TestFtoaWithDigits(t *testing.T) { 26 | testList{ 27 | {"1.23, 0", FtoaWithDigits(1.23, 0), "1"}, 28 | {"20, 0", FtoaWithDigits(20.0, 0), "20"}, 29 | {"1.23, 1", FtoaWithDigits(1.23, 1), "1.2"}, 30 | {"1.23, 2", FtoaWithDigits(1.23, 2), "1.23"}, 31 | {"1.23, 3", FtoaWithDigits(1.23, 3), "1.23"}, 32 | }.validate(t) 33 | } 34 | 35 | func TestStripTrailingDigits(t *testing.T) { 36 | err := quick.Check(func(s string, digits int) bool { 37 | stripped := stripTrailingDigits(s, digits) 38 | 39 | // A stripped string will always be a prefix of its original string 40 | if !strings.HasPrefix(s, stripped) { 41 | return false 42 | } 43 | 44 | if strings.ContainsRune(s, '.') { 45 | // If there is a dot, the part on the left of the dot will never change 46 | a := strings.Split(s, ".") 47 | b := strings.Split(stripped, ".") 48 | if a[0] != b[0] { 49 | return false 50 | } 51 | } else { 52 | // If there's no dot in the input, the output will always be the same as the input. 53 | if stripped != s { 54 | return false 55 | } 56 | } 57 | 58 | return true 59 | }, &quick.Config{ 60 | MaxCount: 10000, 61 | Values: func(v []reflect.Value, r *rand.Rand) { 62 | rdigs := func(n int) string { 63 | digs := []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} 64 | var rv []rune 65 | for i := 0; i < n; i++ { 66 | rv = append(rv, digs[r.Intn(len(digs))]) 67 | } 68 | return string(rv) 69 | } 70 | 71 | ls := r.Intn(20) 72 | rs := r.Intn(20) 73 | jc := "." 74 | if rs == 0 { 75 | jc = "" 76 | } 77 | s := rdigs(ls) + jc + rdigs(rs) 78 | digits := r.Intn(len(s) + 1) 79 | 80 | v[0] = reflect.ValueOf(s) 81 | v[1] = reflect.ValueOf(digits) 82 | }, 83 | }) 84 | 85 | if err != nil { 86 | t.Error(err) 87 | } 88 | } 89 | 90 | func BenchmarkFtoaRegexTrailing(b *testing.B) { 91 | trailingZerosRegex := regexp.MustCompile(`\.?0+$`) 92 | 93 | b.ResetTimer() 94 | for i := 0; i < b.N; i++ { 95 | trailingZerosRegex.ReplaceAllString("2.00000", "") 96 | trailingZerosRegex.ReplaceAllString("2.0000", "") 97 | trailingZerosRegex.ReplaceAllString("2.000", "") 98 | trailingZerosRegex.ReplaceAllString("2.00", "") 99 | trailingZerosRegex.ReplaceAllString("2.0", "") 100 | trailingZerosRegex.ReplaceAllString("2", "") 101 | } 102 | } 103 | 104 | func BenchmarkFtoaFunc(b *testing.B) { 105 | for i := 0; i < b.N; i++ { 106 | stripTrailingZeros("2.00000") 107 | stripTrailingZeros("2.0000") 108 | stripTrailingZeros("2.000") 109 | stripTrailingZeros("2.00") 110 | stripTrailingZeros("2.0") 111 | stripTrailingZeros("2") 112 | } 113 | } 114 | 115 | func BenchmarkFmtF(b *testing.B) { 116 | for i := 0; i < b.N; i++ { 117 | _ = fmt.Sprintf("%f", 2.03584) 118 | } 119 | } 120 | 121 | func BenchmarkStrconvF(b *testing.B) { 122 | for i := 0; i < b.N; i++ { 123 | strconv.FormatFloat(2.03584, 'f', 6, 64) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dustin/go-humanize 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /humanize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package humanize converts boring ugly numbers to human-friendly strings and back. 3 | 4 | Durations can be turned into strings such as "3 days ago", numbers 5 | representing sizes like 82854982 into useful strings like, "83 MB" or 6 | "79 MiB" (whichever you prefer). 7 | */ 8 | package humanize 9 | -------------------------------------------------------------------------------- /number.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | /* 4 | Slightly adapted from the source to fit go-humanize. 5 | 6 | Author: https://github.com/gorhill 7 | Source: https://gist.github.com/gorhill/5285193 8 | 9 | */ 10 | 11 | import ( 12 | "math" 13 | "strconv" 14 | ) 15 | 16 | var ( 17 | renderFloatPrecisionMultipliers = [...]float64{ 18 | 1, 19 | 10, 20 | 100, 21 | 1000, 22 | 10000, 23 | 100000, 24 | 1000000, 25 | 10000000, 26 | 100000000, 27 | 1000000000, 28 | } 29 | 30 | renderFloatPrecisionRounders = [...]float64{ 31 | 0.5, 32 | 0.05, 33 | 0.005, 34 | 0.0005, 35 | 0.00005, 36 | 0.000005, 37 | 0.0000005, 38 | 0.00000005, 39 | 0.000000005, 40 | 0.0000000005, 41 | } 42 | ) 43 | 44 | // FormatFloat produces a formatted number as string based on the following user-specified criteria: 45 | // 46 | // * thousands separator 47 | // * decimal separator 48 | // * decimal precision 49 | // 50 | // Usage: s := FormatFloat(format, n) 51 | // The format parameter tells how to render the number n. 52 | // 53 | // See examples: http://play.golang.org/p/LXc1Ddm1lJ 54 | // 55 | // Examples of format strings, given n = 12345.6789: 56 | // "#,###.##" => "12,345.67" 57 | // "#,###." => "12,345" 58 | // "#,###" => "12345,678" 59 | // "#\u202F###,##" => "12 345,68" 60 | // "#.###,###### => 12.345,678900 61 | // "" (aka default format) => 12,345.67 62 | // 63 | // The highest precision allowed is 9 digits after the decimal symbol. 64 | // There is also a version for integer number, FormatInteger(), 65 | // which is convenient for calls within template. 66 | func FormatFloat(format string, n float64) string { 67 | // Special cases: 68 | // NaN = "NaN" 69 | // +Inf = "+Infinity" 70 | // -Inf = "-Infinity" 71 | if math.IsNaN(n) { 72 | return "NaN" 73 | } 74 | if n > math.MaxFloat64 { 75 | return "Infinity" 76 | } 77 | if n < (0.0 - math.MaxFloat64) { 78 | return "-Infinity" 79 | } 80 | 81 | // default format 82 | precision := 2 83 | decimalStr := "." 84 | thousandStr := "," 85 | positiveStr := "" 86 | negativeStr := "-" 87 | 88 | if len(format) > 0 { 89 | format := []rune(format) 90 | 91 | // If there is an explicit format directive, 92 | // then default values are these: 93 | precision = 9 94 | thousandStr = "" 95 | 96 | // collect indices of meaningful formatting directives 97 | formatIndx := []int{} 98 | for i, char := range format { 99 | if char != '#' && char != '0' { 100 | formatIndx = append(formatIndx, i) 101 | } 102 | } 103 | 104 | if len(formatIndx) > 0 { 105 | // Directive at index 0: 106 | // Must be a '+' 107 | // Raise an error if not the case 108 | // index: 0123456789 109 | // +0.000,000 110 | // +000,000.0 111 | // +0000.00 112 | // +0000 113 | if formatIndx[0] == 0 { 114 | if format[formatIndx[0]] != '+' { 115 | panic("FormatFloat(): invalid positive sign directive") 116 | } 117 | positiveStr = "+" 118 | formatIndx = formatIndx[1:] 119 | } 120 | 121 | // Two directives: 122 | // First is thousands separator 123 | // Raise an error if not followed by 3-digit 124 | // 0123456789 125 | // 0.000,000 126 | // 000,000.00 127 | if len(formatIndx) == 2 { 128 | if (formatIndx[1] - formatIndx[0]) != 4 { 129 | panic("FormatFloat(): thousands separator directive must be followed by 3 digit-specifiers") 130 | } 131 | thousandStr = string(format[formatIndx[0]]) 132 | formatIndx = formatIndx[1:] 133 | } 134 | 135 | // One directive: 136 | // Directive is decimal separator 137 | // The number of digit-specifier following the separator indicates wanted precision 138 | // 0123456789 139 | // 0.00 140 | // 000,0000 141 | if len(formatIndx) == 1 { 142 | decimalStr = string(format[formatIndx[0]]) 143 | precision = len(format) - formatIndx[0] - 1 144 | } 145 | } 146 | } 147 | 148 | // generate sign part 149 | var signStr string 150 | if n >= 0.000000001 { 151 | signStr = positiveStr 152 | } else if n <= -0.000000001 { 153 | signStr = negativeStr 154 | n = -n 155 | } else { 156 | signStr = "" 157 | n = 0.0 158 | } 159 | 160 | // split number into integer and fractional parts 161 | intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) 162 | 163 | // generate integer part string 164 | intStr := strconv.FormatInt(int64(intf), 10) 165 | 166 | // add thousand separator if required 167 | if len(thousandStr) > 0 { 168 | for i := len(intStr); i > 3; { 169 | i -= 3 170 | intStr = intStr[:i] + thousandStr + intStr[i:] 171 | } 172 | } 173 | 174 | // no fractional part, we can leave now 175 | if precision == 0 { 176 | return signStr + intStr 177 | } 178 | 179 | // generate fractional part 180 | fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) 181 | // may need padding 182 | if len(fracStr) < precision { 183 | fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr 184 | } 185 | 186 | return signStr + intStr + decimalStr + fracStr 187 | } 188 | 189 | // FormatInteger produces a formatted number as string. 190 | // See FormatFloat. 191 | func FormatInteger(format string, n int) string { 192 | return FormatFloat(format, float64(n)) 193 | } 194 | -------------------------------------------------------------------------------- /number_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | type TestStruct struct { 9 | name string 10 | format string 11 | num float64 12 | formatted string 13 | } 14 | 15 | func TestFormatFloat(t *testing.T) { 16 | tests := []TestStruct{ 17 | {"default", "", 12345.6789, "12,345.68"}, 18 | {"#", "#", 12345.6789, "12345.678900000"}, 19 | {"#.", "#.", 12345.6789, "12346"}, 20 | {"#,#", "#,#", 12345.6789, "12345,7"}, 21 | {"#,##", "#,##", 12345.6789, "12345,68"}, 22 | {"#,###", "#,###", 12345.6789, "12345,679"}, 23 | {"#,###.", "#,###.", 12345.6789, "12,346"}, 24 | {"#,###.##", "#,###.##", 12345.6789, "12,345.68"}, 25 | {"#,###.###", "#,###.###", 12345.6789, "12,345.679"}, 26 | {"#,###.####", "#,###.####", 12345.6789, "12,345.6789"}, 27 | {"#.###,######", "#.###,######", 12345.6789, "12.345,678900"}, 28 | {"bug46", "#,###.##", 52746220055.92342, "52,746,220,055.92"}, 29 | {"#\u202f###,##", "#\u202f###,##", 12345.6789, "12 345,68"}, 30 | 31 | // special cases 32 | {"NaN", "#", math.NaN(), "NaN"}, 33 | {"+Inf", "#", math.Inf(1), "Infinity"}, 34 | {"-Inf", "#", math.Inf(-1), "-Infinity"}, 35 | {"signStr <= -0.000000001", "", -0.000000002, "-0.00"}, 36 | {"signStr = 0", "", 0, "0.00"}, 37 | {"Format directive must start with +", "+000", 12345.6789, "+12345.678900000"}, 38 | } 39 | 40 | for _, test := range tests { 41 | got := FormatFloat(test.format, test.num) 42 | if got != test.formatted { 43 | t.Errorf("On %v (%v, %v), got %v, wanted %v", 44 | test.name, test.format, test.num, got, test.formatted) 45 | } 46 | } 47 | // Test a single integer 48 | got := FormatInteger("#", 12345) 49 | if got != "12345.000000000" { 50 | t.Errorf("On %v (%v, %v), got %v, wanted %v", 51 | "integerTest", "#", 12345, got, "12345.000000000") 52 | } 53 | // Test the things that could panic 54 | panictests := []TestStruct{ 55 | {"FormatFloat(): invalid positive sign directive", "-", 12345.6789, "12,345.68"}, 56 | {"FormatFloat(): thousands separator directive must be followed by 3 digit-specifiers", "0.01", 12345.6789, "12,345.68"}, 57 | } 58 | for _, test := range panictests { 59 | didPanic := false 60 | var message interface{} 61 | func() { 62 | 63 | defer func() { 64 | if message = recover(); message != nil { 65 | didPanic = true 66 | } 67 | }() 68 | 69 | // call the target function 70 | _ = FormatFloat(test.format, test.num) 71 | 72 | }() 73 | if didPanic != true { 74 | t.Errorf("On %v, should have panic and did not.", 75 | test.name) 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /ordinals.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import "strconv" 4 | 5 | // Ordinal gives you the input number in a rank/ordinal format. 6 | // 7 | // Ordinal(3) -> 3rd 8 | func Ordinal(x int) string { 9 | suffix := "th" 10 | switch x % 10 { 11 | case 1: 12 | if x%100 != 11 { 13 | suffix = "st" 14 | } 15 | case 2: 16 | if x%100 != 12 { 17 | suffix = "nd" 18 | } 19 | case 3: 20 | if x%100 != 13 { 21 | suffix = "rd" 22 | } 23 | } 24 | return strconv.Itoa(x) + suffix 25 | } 26 | -------------------------------------------------------------------------------- /ordinals_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestOrdinals(t *testing.T) { 8 | testList{ 9 | {"0", Ordinal(0), "0th"}, 10 | {"1", Ordinal(1), "1st"}, 11 | {"2", Ordinal(2), "2nd"}, 12 | {"3", Ordinal(3), "3rd"}, 13 | {"4", Ordinal(4), "4th"}, 14 | {"10", Ordinal(10), "10th"}, 15 | {"11", Ordinal(11), "11th"}, 16 | {"12", Ordinal(12), "12th"}, 17 | {"13", Ordinal(13), "13th"}, 18 | {"21", Ordinal(21), "21st"}, 19 | {"32", Ordinal(32), "32nd"}, 20 | {"43", Ordinal(43), "43rd"}, 21 | {"101", Ordinal(101), "101st"}, 22 | {"102", Ordinal(102), "102nd"}, 23 | {"103", Ordinal(103), "103rd"}, 24 | {"211", Ordinal(211), "211th"}, 25 | {"212", Ordinal(212), "212th"}, 26 | {"213", Ordinal(213), "213th"}, 27 | }.validate(t) 28 | } 29 | -------------------------------------------------------------------------------- /si.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | var siPrefixTable = map[float64]string{ 11 | -30: "q", // quecto 12 | -27: "r", // ronto 13 | -24: "y", // yocto 14 | -21: "z", // zepto 15 | -18: "a", // atto 16 | -15: "f", // femto 17 | -12: "p", // pico 18 | -9: "n", // nano 19 | -6: "µ", // micro 20 | -3: "m", // milli 21 | 0: "", 22 | 3: "k", // kilo 23 | 6: "M", // mega 24 | 9: "G", // giga 25 | 12: "T", // tera 26 | 15: "P", // peta 27 | 18: "E", // exa 28 | 21: "Z", // zetta 29 | 24: "Y", // yotta 30 | 27: "R", // ronna 31 | 30: "Q", // quetta 32 | } 33 | 34 | var revSIPrefixTable = revfmap(siPrefixTable) 35 | 36 | // revfmap reverses the map and precomputes the power multiplier 37 | func revfmap(in map[float64]string) map[string]float64 { 38 | rv := map[string]float64{} 39 | for k, v := range in { 40 | rv[v] = math.Pow(10, k) 41 | } 42 | return rv 43 | } 44 | 45 | var riParseRegex *regexp.Regexp 46 | 47 | func init() { 48 | ri := `^([\-0-9.]+)\s?([` 49 | for _, v := range siPrefixTable { 50 | ri += v 51 | } 52 | ri += `]?)(.*)` 53 | 54 | riParseRegex = regexp.MustCompile(ri) 55 | } 56 | 57 | // ComputeSI finds the most appropriate SI prefix for the given number 58 | // and returns the prefix along with the value adjusted to be within 59 | // that prefix. 60 | // 61 | // See also: SI, ParseSI. 62 | // 63 | // e.g. ComputeSI(2.2345e-12) -> (2.2345, "p") 64 | func ComputeSI(input float64) (float64, string) { 65 | if input == 0 { 66 | return 0, "" 67 | } 68 | mag := math.Abs(input) 69 | exponent := math.Floor(logn(mag, 10)) 70 | exponent = math.Floor(exponent/3) * 3 71 | 72 | value := mag / math.Pow(10, exponent) 73 | 74 | // Handle special case where value is exactly 1000.0 75 | // Should return 1 M instead of 1000 k 76 | if value == 1000.0 { 77 | exponent += 3 78 | value = mag / math.Pow(10, exponent) 79 | } 80 | 81 | value = math.Copysign(value, input) 82 | 83 | prefix := siPrefixTable[exponent] 84 | return value, prefix 85 | } 86 | 87 | // SI returns a string with default formatting. 88 | // 89 | // SI uses Ftoa to format float value, removing trailing zeros. 90 | // 91 | // See also: ComputeSI, ParseSI. 92 | // 93 | // e.g. SI(1000000, "B") -> 1 MB 94 | // e.g. SI(2.2345e-12, "F") -> 2.2345 pF 95 | func SI(input float64, unit string) string { 96 | value, prefix := ComputeSI(input) 97 | return Ftoa(value) + " " + prefix + unit 98 | } 99 | 100 | // SIWithDigits works like SI but limits the resulting string to the 101 | // given number of decimal places. 102 | // 103 | // e.g. SIWithDigits(1000000, 0, "B") -> 1 MB 104 | // e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF 105 | func SIWithDigits(input float64, decimals int, unit string) string { 106 | value, prefix := ComputeSI(input) 107 | return FtoaWithDigits(value, decimals) + " " + prefix + unit 108 | } 109 | 110 | var errInvalid = errors.New("invalid input") 111 | 112 | // ParseSI parses an SI string back into the number and unit. 113 | // 114 | // See also: SI, ComputeSI. 115 | // 116 | // e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil) 117 | func ParseSI(input string) (float64, string, error) { 118 | found := riParseRegex.FindStringSubmatch(input) 119 | if len(found) != 4 { 120 | return 0, "", errInvalid 121 | } 122 | mag := revSIPrefixTable[found[2]] 123 | unit := found[3] 124 | 125 | base, err := strconv.ParseFloat(found[1], 64) 126 | return base * mag, unit, err 127 | } 128 | -------------------------------------------------------------------------------- /si_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestSI(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | num float64 12 | formatted string 13 | }{ 14 | {"e-30", 1e-30, "1 qF"}, 15 | {"e-27", 1e-27, "1 rF"}, 16 | {"e-24", 1e-24, "1 yF"}, 17 | {"e-21", 1e-21, "1 zF"}, 18 | {"e-18", 1e-18, "1 aF"}, 19 | {"e-15", 1e-15, "1 fF"}, 20 | {"e-12", 1e-12, "1 pF"}, 21 | {"e-12", 2.2345e-12, "2.2345 pF"}, 22 | {"e-12", 2.23e-12, "2.23 pF"}, 23 | {"e-11", 2.23e-11, "22.3 pF"}, 24 | {"e-10", 2.2e-10, "220 pF"}, 25 | {"e-9", 2.2e-9, "2.2 nF"}, 26 | {"e-8", 2.2e-8, "22 nF"}, 27 | {"e-7", 2.2e-7, "220 nF"}, 28 | {"e-6", 2.2e-6, "2.2 µF"}, 29 | {"e-6", 1e-6, "1 µF"}, 30 | {"e-5", 2.2e-5, "22 µF"}, 31 | {"e-4", 2.2e-4, "220 µF"}, 32 | {"e-3", 2.2e-3, "2.2 mF"}, 33 | {"e-2", 2.2e-2, "22 mF"}, 34 | {"e-1", 2.2e-1, "220 mF"}, 35 | {"e+0", 2.2e-0, "2.2 F"}, 36 | {"e+0", 2.2, "2.2 F"}, 37 | {"e+1", 2.2e+1, "22 F"}, 38 | {"0", 0, "0 F"}, 39 | {"e+1", 22, "22 F"}, 40 | {"e+2", 2.2e+2, "220 F"}, 41 | {"e+2", 220, "220 F"}, 42 | {"e+3", 2.2e+3, "2.2 kF"}, 43 | {"e+3", 2200, "2.2 kF"}, 44 | {"e+4", 2.2e+4, "22 kF"}, 45 | {"e+4", 22000, "22 kF"}, 46 | {"e+5", 2.2e+5, "220 kF"}, 47 | {"e+6", 2.2e+6, "2.2 MF"}, 48 | {"e+6", 1e+6, "1 MF"}, 49 | {"e+7", 2.2e+7, "22 MF"}, 50 | {"e+8", 2.2e+8, "220 MF"}, 51 | {"e+9", 2.2e+9, "2.2 GF"}, 52 | {"e+10", 2.2e+10, "22 GF"}, 53 | {"e+11", 2.2e+11, "220 GF"}, 54 | {"e+12", 2.2e+12, "2.2 TF"}, 55 | {"e+15", 2.2e+15, "2.2 PF"}, 56 | {"e+18", 2.2e+18, "2.2 EF"}, 57 | {"e+21", 2.2e+21, "2.2 ZF"}, 58 | {"e+24", 2.2e+24, "2.2 YF"}, 59 | {"e+27", 2.2e+27, "2.2 RF"}, 60 | {"e+30", 2.2e+30, "2.2 QF"}, 61 | 62 | // special case 63 | {"1F", 1000 * 1000, "1 MF"}, 64 | {"1F", 1e6, "1 MF"}, 65 | 66 | // negative number 67 | {"-100 F", -100, "-100 F"}, 68 | } 69 | 70 | for _, test := range tests { 71 | got := SI(test.num, "F") 72 | if got != test.formatted { 73 | t.Errorf("On %v (%v), got %v, wanted %v", 74 | test.name, test.num, got, test.formatted) 75 | } 76 | 77 | gotf, gotu, err := ParseSI(test.formatted) 78 | if err != nil { 79 | t.Errorf("Error parsing %v (%v): %v", test.name, test.formatted, err) 80 | continue 81 | } 82 | 83 | if math.Abs(1-(gotf/test.num)) > 0.01 { 84 | t.Errorf("On %v (%v), got %v, wanted %v (±%v)", 85 | test.name, test.formatted, gotf, test.num, 86 | math.Abs(1-(gotf/test.num))) 87 | } 88 | if gotu != "F" { 89 | t.Errorf("On %v (%v), expected unit F, got %v", 90 | test.name, test.formatted, gotu) 91 | } 92 | } 93 | 94 | // Parse error 95 | gotf, gotu, err := ParseSI("x1.21JW") // 1.21 jigga whats 96 | if err == nil { 97 | t.Errorf("Expected error on x1.21JW, got %v %v", gotf, gotu) 98 | } 99 | } 100 | 101 | func TestSIWithDigits(t *testing.T) { 102 | tests := []struct { 103 | name string 104 | num float64 105 | digits int 106 | formatted string 107 | }{ 108 | {"e-12", 2.234e-12, 0, "2 pF"}, 109 | {"e-12", 2.234e-12, 1, "2.2 pF"}, 110 | {"e-12", 2.234e-12, 2, "2.23 pF"}, 111 | {"e-12", 2.234e-12, 3, "2.234 pF"}, 112 | {"e-12", 2.234e-12, 4, "2.234 pF"}, 113 | } 114 | 115 | for _, test := range tests { 116 | got := SIWithDigits(test.num, test.digits, "F") 117 | if got != test.formatted { 118 | t.Errorf("On %v (%v), got %v, wanted %v", 119 | test.name, test.num, got, test.formatted) 120 | } 121 | } 122 | } 123 | 124 | func BenchmarkParseSI(b *testing.B) { 125 | for i := 0; i < b.N; i++ { 126 | ParseSI("2.2346ZB") 127 | } 128 | } 129 | 130 | // There was a report that zeroes were being truncated incorrectly 131 | func TestBug106(t *testing.T) { 132 | tests := []struct{ 133 | in float64 134 | want string 135 | }{ 136 | {20.0, "20 U"}, 137 | {200.0, "200 U"}, 138 | } 139 | 140 | for _, test := range tests { 141 | if got :=SIWithDigits(test.in, 0, "U") ; got != test.want { 142 | t.Errorf("on %f got %v, want %v", test.in, got, test.want); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /times.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | "time" 8 | ) 9 | 10 | // Seconds-based time units 11 | const ( 12 | Day = 24 * time.Hour 13 | Week = 7 * Day 14 | Month = 30 * Day 15 | Year = 12 * Month 16 | LongTime = 37 * Year 17 | ) 18 | 19 | // Time formats a time into a relative string. 20 | // 21 | // Time(someT) -> "3 weeks ago" 22 | func Time(then time.Time) string { 23 | return RelTime(then, time.Now(), "ago", "from now") 24 | } 25 | 26 | // A RelTimeMagnitude struct contains a relative time point at which 27 | // the relative format of time will switch to a new format string. A 28 | // slice of these in ascending order by their "D" field is passed to 29 | // CustomRelTime to format durations. 30 | // 31 | // The Format field is a string that may contain a "%s" which will be 32 | // replaced with the appropriate signed label (e.g. "ago" or "from 33 | // now") and a "%d" that will be replaced by the quantity. 34 | // 35 | // The DivBy field is the amount of time the time difference must be 36 | // divided by in order to display correctly. 37 | // 38 | // e.g. if D is 2*time.Minute and you want to display "%d minutes %s" 39 | // DivBy should be time.Minute so whatever the duration is will be 40 | // expressed in minutes. 41 | type RelTimeMagnitude struct { 42 | D time.Duration 43 | Format string 44 | DivBy time.Duration 45 | } 46 | 47 | var defaultMagnitudes = []RelTimeMagnitude{ 48 | {time.Second, "now", time.Second}, 49 | {2 * time.Second, "1 second %s", 1}, 50 | {time.Minute, "%d seconds %s", time.Second}, 51 | {2 * time.Minute, "1 minute %s", 1}, 52 | {time.Hour, "%d minutes %s", time.Minute}, 53 | {2 * time.Hour, "1 hour %s", 1}, 54 | {Day, "%d hours %s", time.Hour}, 55 | {2 * Day, "1 day %s", 1}, 56 | {Week, "%d days %s", Day}, 57 | {2 * Week, "1 week %s", 1}, 58 | {Month, "%d weeks %s", Week}, 59 | {2 * Month, "1 month %s", 1}, 60 | {Year, "%d months %s", Month}, 61 | {18 * Month, "1 year %s", 1}, 62 | {2 * Year, "2 years %s", 1}, 63 | {LongTime, "%d years %s", Year}, 64 | {math.MaxInt64, "a long while %s", 1}, 65 | } 66 | 67 | // RelTime formats a time into a relative string. 68 | // 69 | // It takes two times and two labels. In addition to the generic time 70 | // delta string (e.g. 5 minutes), the labels are used applied so that 71 | // the label corresponding to the smaller time is applied. 72 | // 73 | // RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" 74 | func RelTime(a, b time.Time, albl, blbl string) string { 75 | return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) 76 | } 77 | 78 | // CustomRelTime formats a time into a relative string. 79 | // 80 | // It takes two times two labels and a table of relative time formats. 81 | // In addition to the generic time delta string (e.g. 5 minutes), the 82 | // labels are used applied so that the label corresponding to the 83 | // smaller time is applied. 84 | func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { 85 | lbl := albl 86 | diff := b.Sub(a) 87 | 88 | if a.After(b) { 89 | lbl = blbl 90 | diff = a.Sub(b) 91 | } 92 | 93 | n := sort.Search(len(magnitudes), func(i int) bool { 94 | return magnitudes[i].D > diff 95 | }) 96 | 97 | if n >= len(magnitudes) { 98 | n = len(magnitudes) - 1 99 | } 100 | mag := magnitudes[n] 101 | args := []interface{}{} 102 | escaped := false 103 | for _, ch := range mag.Format { 104 | if escaped { 105 | switch ch { 106 | case 's': 107 | args = append(args, lbl) 108 | case 'd': 109 | args = append(args, diff/mag.DivBy) 110 | } 111 | escaped = false 112 | } else { 113 | escaped = ch == '%' 114 | } 115 | } 116 | return fmt.Sprintf(mag.Format, args...) 117 | } 118 | -------------------------------------------------------------------------------- /times_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestPast(t *testing.T) { 10 | now := time.Now() 11 | testList{ 12 | {"now", Time(now), "now"}, 13 | {"1 second ago", Time(now.Add(-1 * time.Second)), "1 second ago"}, 14 | {"12 seconds ago", Time(now.Add(-12 * time.Second)), "12 seconds ago"}, 15 | {"30 seconds ago", Time(now.Add(-30 * time.Second)), "30 seconds ago"}, 16 | {"45 seconds ago", Time(now.Add(-45 * time.Second)), "45 seconds ago"}, 17 | {"1 minute ago", Time(now.Add(-63 * time.Second)), "1 minute ago"}, 18 | {"15 minutes ago", Time(now.Add(-15 * time.Minute)), "15 minutes ago"}, 19 | {"1 hour ago", Time(now.Add(-63 * time.Minute)), "1 hour ago"}, 20 | {"2 hours ago", Time(now.Add(-2 * time.Hour)), "2 hours ago"}, 21 | {"21 hours ago", Time(now.Add(-21 * time.Hour)), "21 hours ago"}, 22 | {"1 day ago", Time(now.Add(-26 * time.Hour)), "1 day ago"}, 23 | {"2 days ago", Time(now.Add(-49 * time.Hour)), "2 days ago"}, 24 | {"3 days ago", Time(now.Add(-3 * Day)), "3 days ago"}, 25 | {"1 week ago (1)", Time(now.Add(-7 * Day)), "1 week ago"}, 26 | {"1 week ago (2)", Time(now.Add(-12 * Day)), "1 week ago"}, 27 | {"2 weeks ago", Time(now.Add(-15 * Day)), "2 weeks ago"}, 28 | {"1 month ago", Time(now.Add(-39 * Day)), "1 month ago"}, 29 | {"3 months ago", Time(now.Add(-99 * Day)), "3 months ago"}, 30 | {"1 year ago (1)", Time(now.Add(-365 * Day)), "1 year ago"}, 31 | {"1 year ago (1)", Time(now.Add(-400 * Day)), "1 year ago"}, 32 | {"2 years ago (1)", Time(now.Add(-548 * Day)), "2 years ago"}, 33 | {"2 years ago (2)", Time(now.Add(-725 * Day)), "2 years ago"}, 34 | {"2 years ago (3)", Time(now.Add(-800 * Day)), "2 years ago"}, 35 | {"3 years ago", Time(now.Add(-3 * Year)), "3 years ago"}, 36 | {"long ago", Time(now.Add(-LongTime)), "a long while ago"}, 37 | }.validate(t) 38 | } 39 | 40 | func TestReltimeOffbyone(t *testing.T) { 41 | testList{ 42 | {"1w-1", RelTime(time.Unix(0, 0), time.Unix(7*24*60*60, -1), "ago", ""), "6 days ago"}, 43 | {"1w±0", RelTime(time.Unix(0, 0), time.Unix(7*24*60*60, 0), "ago", ""), "1 week ago"}, 44 | {"1w+1", RelTime(time.Unix(0, 0), time.Unix(7*24*60*60, 1), "ago", ""), "1 week ago"}, 45 | {"2w-1", RelTime(time.Unix(0, 0), time.Unix(14*24*60*60, -1), "ago", ""), "1 week ago"}, 46 | {"2w±0", RelTime(time.Unix(0, 0), time.Unix(14*24*60*60, 0), "ago", ""), "2 weeks ago"}, 47 | {"2w+1", RelTime(time.Unix(0, 0), time.Unix(14*24*60*60, 1), "ago", ""), "2 weeks ago"}, 48 | }.validate(t) 49 | } 50 | 51 | func TestFuture(t *testing.T) { 52 | // Add a little time so that these things properly line up in 53 | // the future. 54 | now := time.Now().Add(time.Millisecond * 250) 55 | testList{ 56 | {"now", Time(now), "now"}, 57 | {"1 second from now", Time(now.Add(+1 * time.Second)), "1 second from now"}, 58 | {"12 seconds from now", Time(now.Add(+12 * time.Second)), "12 seconds from now"}, 59 | {"30 seconds from now", Time(now.Add(+30 * time.Second)), "30 seconds from now"}, 60 | {"45 seconds from now", Time(now.Add(+45 * time.Second)), "45 seconds from now"}, 61 | {"15 minutes from now", Time(now.Add(+15 * time.Minute)), "15 minutes from now"}, 62 | {"2 hours from now", Time(now.Add(+2 * time.Hour)), "2 hours from now"}, 63 | {"21 hours from now", Time(now.Add(+21 * time.Hour)), "21 hours from now"}, 64 | {"1 day from now", Time(now.Add(+26 * time.Hour)), "1 day from now"}, 65 | {"2 days from now", Time(now.Add(+49 * time.Hour)), "2 days from now"}, 66 | {"3 days from now", Time(now.Add(+3 * Day)), "3 days from now"}, 67 | {"1 week from now (1)", Time(now.Add(+7 * Day)), "1 week from now"}, 68 | {"1 week from now (2)", Time(now.Add(+12 * Day)), "1 week from now"}, 69 | {"2 weeks from now", Time(now.Add(+15 * Day)), "2 weeks from now"}, 70 | {"1 month from now", Time(now.Add(+30 * Day)), "1 month from now"}, 71 | {"1 year from now", Time(now.Add(+365 * Day)), "1 year from now"}, 72 | {"2 years from now", Time(now.Add(+2 * Year)), "2 years from now"}, 73 | {"a while from now", Time(now.Add(+LongTime)), "a long while from now"}, 74 | }.validate(t) 75 | } 76 | 77 | func TestRange(t *testing.T) { 78 | start := time.Time{} 79 | end := time.Unix(math.MaxInt64, math.MaxInt64) 80 | x := RelTime(start, end, "ago", "from now") 81 | if x != "a long while from now" { 82 | t.Errorf("Expected a long while from now, got %q", x) 83 | } 84 | } 85 | 86 | func TestCustomRelTime(t *testing.T) { 87 | now := time.Now().Add(time.Millisecond * 250) 88 | magnitudes := []RelTimeMagnitude{ 89 | {time.Second, "now", time.Second}, 90 | {2 * time.Second, "1 second %s", 1}, 91 | {time.Minute, "%d seconds %s", time.Second}, 92 | {Day - time.Second, "%d minutes %s", time.Minute}, 93 | {Day, "%d hours %s", time.Hour}, 94 | {2 * Day, "1 day %s", 1}, 95 | {Week, "%d days %s", Day}, 96 | {2 * Week, "1 week %s", 1}, 97 | {6 * Month, "%d weeks %s", Week}, 98 | {Year, "%d months %s", Month}, 99 | } 100 | customRelTime := func(then time.Time) string { 101 | return CustomRelTime(then, time.Now(), "ago", "from now", magnitudes) 102 | } 103 | testList{ 104 | {"now", customRelTime(now), "now"}, 105 | {"1 second from now", customRelTime(now.Add(+1 * time.Second)), "1 second from now"}, 106 | {"12 seconds from now", customRelTime(now.Add(+12 * time.Second)), "12 seconds from now"}, 107 | {"30 seconds from now", customRelTime(now.Add(+30 * time.Second)), "30 seconds from now"}, 108 | {"45 seconds from now", customRelTime(now.Add(+45 * time.Second)), "45 seconds from now"}, 109 | {"15 minutes from now", customRelTime(now.Add(+15 * time.Minute)), "15 minutes from now"}, 110 | {"2 hours from now", customRelTime(now.Add(+2 * time.Hour)), "120 minutes from now"}, 111 | {"21 hours from now", customRelTime(now.Add(+21 * time.Hour)), "1260 minutes from now"}, 112 | {"1 day from now", customRelTime(now.Add(+26 * time.Hour)), "1 day from now"}, 113 | {"2 days from now", customRelTime(now.Add(+49 * time.Hour)), "2 days from now"}, 114 | {"3 days from now", customRelTime(now.Add(+3 * Day)), "3 days from now"}, 115 | {"1 week from now (1)", customRelTime(now.Add(+7 * Day)), "1 week from now"}, 116 | {"1 week from now (2)", customRelTime(now.Add(+12 * Day)), "1 week from now"}, 117 | {"2 weeks from now", customRelTime(now.Add(+15 * Day)), "2 weeks from now"}, 118 | {"1 month from now", customRelTime(now.Add(+30 * Day)), "4 weeks from now"}, 119 | {"6 months from now", customRelTime(now.Add(+6*Month - time.Second)), "25 weeks from now"}, 120 | {"1 year from now", customRelTime(now.Add(+365 * Day)), "12 months from now"}, 121 | {"2 years from now", customRelTime(now.Add(+2 * Year)), "24 months from now"}, 122 | {"a while from now", customRelTime(now.Add(+LongTime)), "444 months from now"}, 123 | }.validate(t) 124 | } 125 | --------------------------------------------------------------------------------