Improve duration humanization. (#4071)

This commit is contained in:
Bala FA
2017-04-26 16:08:35 +05:30
committed by Harshavardhana
parent 64c1c0f37d
commit cf1fc45142
13 changed files with 167 additions and 260 deletions

View File

@@ -3,7 +3,7 @@
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`
`"github.com/dustin/go-humanize"`, use it as `humanize`.
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
complete documentation.
@@ -11,12 +11,12 @@ complete documentation.
## Sizes
This lets you take numbers like `82854982` and convert them to useful
strings like, `83MB` or `79MiB` (whichever you prefer).
strings like, `83 MB` or `79 MiB` (whichever you prefer).
Example:
```go
fmt.Printf("That file is %s.", humanize.Bytes(82854982))
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
```
## Times
@@ -27,11 +27,11 @@ For example, `12 seconds ago` or `3 days from now`.
Example:
```go
fmt.Printf("This was touched %s", humanize.Time(someTimeInstance))
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.
conversation one day. It's pretty neat.
## Ordinals
@@ -48,12 +48,12 @@ to label ordinals.
Example:
```go
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193))
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.
Want to shove commas into numbers? Be my guest.
0 -> 0
100 -> 100
@@ -64,7 +64,7 @@ Want to shove commas into numbers? Be my guest.
Example:
```go
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491))
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
```
## Ftoa
@@ -72,10 +72,10 @@ fmt.Printf("You owe $%s.\n", humanize.Comma(6582491))
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
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
@@ -85,7 +85,7 @@ Format numbers with [SI notation][sinotation].
Example:
```go
humanize.SI(0.00000000223, "M") // 2.23nM
humanize.SI(0.00000000223, "M") // 2.23 nM
```
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion

View File

@@ -113,7 +113,7 @@ func humanateBigBytes(s, base *big.Int, sizes []string) string {
//
// See also: ParseBigBytes.
//
// BigBytes(82854982) -> 83MB
// 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)
@@ -123,7 +123,7 @@ func BigBytes(s *big.Int) string {
//
// See also: ParseBigBytes.
//
// BigIBytes(82854982) -> 79MiB
// 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)
@@ -134,19 +134,28 @@ func BigIBytes(s *big.Int) string {
//
// See also: BigBytes, BigIBytes.
//
// ParseBigBytes("42MB") -> 42000000, nil
// ParseBigBytes("42mib") -> 44040192, nil
// 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 == '.') {
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(s[:lastDigit], "%f", val)
_, err := fmt.Sscanf(num, "%f", val)
if err != nil {
return nil, err
}

View File

@@ -84,7 +84,7 @@ func humanateBytes(s uint64, base float64, sizes []string) string {
//
// See also: ParseBytes.
//
// Bytes(82854982) -> 83MB
// Bytes(82854982) -> 83 MB
func Bytes(s uint64) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
return humanateBytes(s, 1000, sizes)
@@ -94,7 +94,7 @@ func Bytes(s uint64) string {
//
// See also: ParseBytes.
//
// IBytes(82854982) -> 79MiB
// IBytes(82854982) -> 79 MiB
func IBytes(s uint64) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
return humanateBytes(s, 1024, sizes)
@@ -105,18 +105,27 @@ func IBytes(s uint64) string {
//
// See Also: Bytes, IBytes.
//
// ParseBytes("42MB") -> 42000000, nil
// ParseBytes("42mib") -> 44040192, nil
// 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 == '.') {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
f, err := strconv.ParseFloat(s[:lastDigit], 64)
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}

View File

@@ -2,6 +2,7 @@ package humanize
import (
"bytes"
"math"
"math/big"
"strconv"
"strings"
@@ -13,6 +14,12 @@ import (
// e.g. Comma(834142) -> 834,142
func Comma(v int64) string {
sign := ""
// minin64 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

View File

@@ -2,7 +2,7 @@
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, "83MB" or
"79MiB" (whichever you prefer).
representing sizes like 82854982 into useful strings like, "83 MB" or
"79 MiB" (whichever you prefer).
*/
package humanize

View File

@@ -160,7 +160,7 @@ func FormatFloat(format string, n float64) string {
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
// generate integer part string
intStr := strconv.Itoa(int(intf))
intStr := strconv.FormatInt(int64(intf), 10)
// add thousand separator if required
if len(thousandStr) > 0 {

View File

@@ -68,7 +68,7 @@ func ComputeSI(input float64) (float64, string) {
value := mag / math.Pow(10, exponent)
// Handle special case where value is exactly 1000.0
// Should return 1M instead of 1000k
// Should return 1 M instead of 1000 k
if value == 1000.0 {
exponent += 3
value = mag / math.Pow(10, exponent)
@@ -86,8 +86,8 @@ func ComputeSI(input float64) (float64, string) {
//
// See also: ComputeSI, ParseSI.
//
// e.g. SI(1000000, B) -> 1MB
// e.g. SI(2.2345e-12, "F") -> 2.2345pF
// 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
@@ -99,7 +99,7 @@ var errInvalid = errors.New("invalid input")
//
// See also: SI, ComputeSI.
//
// e.g. ParseSI(2.2345pF) -> (2.2345e-12, "F", nil)
// 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 {

View File

@@ -9,9 +9,7 @@ import (
// Seconds-based time units
const (
Minute = 60
Hour = 60 * Minute
Day = 24 * Hour
Day = 24 * time.Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
@@ -25,18 +23,35 @@ func Time(then time.Time) string {
return RelTime(then, time.Now(), "ago", "from now")
}
var magnitudes = []struct {
d int64
format string
divby int64
}{
{1, "now", 1},
{2, "1 second %s", 1},
{Minute, "%d seconds %s", 1},
{2 * Minute, "1 minute %s", 1},
{Hour, "%d minutes %s", Minute},
{2 * Hour, "1 hour %s", 1},
{Day, "%d hours %s", Hour},
// 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},
@@ -57,34 +72,46 @@ var magnitudes = []struct {
//
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func RelTime(a, b time.Time, albl, blbl string) string {
lbl := albl
diff := b.Unix() - a.Unix()
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
}
after := a.After(b)
if after {
// 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.Unix() - b.Unix()
diff = a.Sub(b)
}
n := sort.Search(len(magnitudes), func(i int) bool {
return magnitudes[i].d > diff
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 {
for _, ch := range mag.Format {
if escaped {
switch ch {
case 's':
args = append(args, lbl)
case 'd':
args = append(args, diff/mag.divby)
args = append(args, diff/mag.DivBy)
}
escaped = false
} else {
escaped = ch == '%'
}
}
return fmt.Sprintf(mag.format, args...)
return fmt.Sprintf(mag.Format, args...)
}