package humanize import ( "math/big" ) // order of magnitude (to a max order) func oomm(n, b *big.Int, maxmag int) (float64, int) { mag := 0 m := &big.Int{} for n.Cmp(b) >= 0 { n.DivMod(n, b, m) mag++ if mag == maxmag && maxmag >= 0 { break } } return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag } // total order of magnitude // (same as above, but with no upper limit) func oom(n, b *big.Int) (float64, int) { mag := 0 m := &big.Int{} for n.Cmp(b) >= 0 { n.DivMod(n, b, m) mag++ } return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag }
package humanize import ( "fmt" "math/big" "strings" "unicode" ) var ( bigIECExp = big.NewInt(1024) // BigByte is one byte in bit.Ints BigByte = big.NewInt(1) // BigKiByte is 1,024 bytes in bit.Ints BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp) // BigMiByte is 1,024 k bytes in bit.Ints BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp) // BigGiByte is 1,024 m bytes in bit.Ints BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp) // BigTiByte is 1,024 g bytes in bit.Ints BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp) // BigPiByte is 1,024 t bytes in bit.Ints BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp) // BigEiByte is 1,024 p bytes in bit.Ints BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp) // BigZiByte is 1,024 e bytes in bit.Ints BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp) // BigYiByte is 1,024 z bytes in bit.Ints BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp) // BigRiByte is 1,024 y bytes in bit.Ints BigRiByte = (&big.Int{}).Mul(BigYiByte, bigIECExp) // BigQiByte is 1,024 r bytes in bit.Ints BigQiByte = (&big.Int{}).Mul(BigRiByte, bigIECExp) ) var ( bigSIExp = big.NewInt(1000) // BigSIByte is one SI byte in big.Ints BigSIByte = big.NewInt(1) // BigKByte is 1,000 SI bytes in big.Ints BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp) // BigMByte is 1,000 SI k bytes in big.Ints BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp) // BigGByte is 1,000 SI m bytes in big.Ints BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp) // BigTByte is 1,000 SI g bytes in big.Ints BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp) // BigPByte is 1,000 SI t bytes in big.Ints BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp) // BigEByte is 1,000 SI p bytes in big.Ints BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp) // BigZByte is 1,000 SI e bytes in big.Ints BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp) // BigYByte is 1,000 SI z bytes in big.Ints BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp) // BigRByte is 1,000 SI y bytes in big.Ints BigRByte = (&big.Int{}).Mul(BigYByte, bigSIExp) // BigQByte is 1,000 SI r bytes in big.Ints BigQByte = (&big.Int{}).Mul(BigRByte, bigSIExp) ) var bigBytesSizeTable = map[string]*big.Int{ "b": BigByte, "kib": BigKiByte, "kb": BigKByte, "mib": BigMiByte, "mb": BigMByte, "gib": BigGiByte, "gb": BigGByte, "tib": BigTiByte, "tb": BigTByte, "pib": BigPiByte, "pb": BigPByte, "eib": BigEiByte, "eb": BigEByte, "zib": BigZiByte, "zb": BigZByte, "yib": BigYiByte, "yb": BigYByte, "rib": BigRiByte, "rb": BigRByte, "qib": BigQiByte, "qb": BigQByte, // Without suffix "": BigByte, "ki": BigKiByte, "k": BigKByte, "mi": BigMiByte, "m": BigMByte, "gi": BigGiByte, "g": BigGByte, "ti": BigTiByte, "t": BigTByte, "pi": BigPiByte, "p": BigPByte, "ei": BigEiByte, "e": BigEByte, "z": BigZByte, "zi": BigZiByte, "y": BigYByte, "yi": BigYiByte, "r": BigRByte, "ri": BigRiByte, "q": BigQByte, "qi": BigQiByte, } var ten = big.NewInt(10) func humanateBigBytes(s, base *big.Int, sizes []string) string { if s.Cmp(ten) < 0 { return fmt.Sprintf("%d B", s) } c := (&big.Int{}).Set(s) val, mag := oomm(c, base, len(sizes)-1) suffix := sizes[mag] f := "%.0f %s" if val < 10 { f = "%.1f %s" } return fmt.Sprintf(f, val, suffix) } // BigBytes produces a human readable representation of an SI size. // // See also: ParseBigBytes. // // BigBytes(82854982) -> 83 MB func BigBytes(s *big.Int) string { sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "RB", "QB"} return humanateBigBytes(s, bigSIExp, sizes) } // BigIBytes produces a human readable representation of an IEC size. // // See also: ParseBigBytes. // // BigIBytes(82854982) -> 79 MiB func BigIBytes(s *big.Int) string { sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB"} return humanateBigBytes(s, bigIECExp, sizes) } // ParseBigBytes parses a string representation of bytes into the number // of bytes it represents. // // See also: BigBytes, BigIBytes. // // ParseBigBytes("42 MB") -> 42000000, nil // ParseBigBytes("42 mib") -> 44040192, nil func ParseBigBytes(s string) (*big.Int, error) { lastDigit := 0 hasComma := false for _, r := range s { if !(unicode.IsDigit(r) || r == '.' || r == ',') { break } if r == ',' { hasComma = true } lastDigit++ } num := s[:lastDigit] if hasComma { num = strings.Replace(num, ",", "", -1) } val := &big.Rat{} _, err := fmt.Sscanf(num, "%f", val) if err != nil { return nil, err } extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) if m, ok := bigBytesSizeTable[extra]; ok { mv := (&big.Rat{}).SetInt(m) val.Mul(val, mv) rv := &big.Int{} rv.Div(val.Num(), val.Denom()) return rv, nil } return nil, fmt.Errorf("unhandled size name: %v", extra) }
package humanize import ( "fmt" "math" "strconv" "strings" "unicode" ) // IEC Sizes. // kibis of bits const ( Byte = 1 << (iota * 10) KiByte MiByte GiByte TiByte PiByte EiByte ) // SI Sizes. const ( IByte = 1 KByte = IByte * 1000 MByte = KByte * 1000 GByte = MByte * 1000 TByte = GByte * 1000 PByte = TByte * 1000 EByte = PByte * 1000 ) var bytesSizeTable = map[string]uint64{ "b": Byte, "kib": KiByte, "kb": KByte, "mib": MiByte, "mb": MByte, "gib": GiByte, "gb": GByte, "tib": TiByte, "tb": TByte, "pib": PiByte, "pb": PByte, "eib": EiByte, "eb": EByte, // Without suffix "": Byte, "ki": KiByte, "k": KByte, "mi": MiByte, "m": MByte, "gi": GiByte, "g": GByte, "ti": TiByte, "t": TByte, "pi": PiByte, "p": PByte, "ei": EiByte, "e": EByte, } func logn(n, b float64) float64 { return math.Log(n) / math.Log(b) } func countDigits(n int64) int { digits := 0 for n != 0 { n /= 10 digits += 1 } return digits } func humanateBytes(s uint64, base float64, minDigits int, sizes []string) string { if s < 10 { return fmt.Sprintf("%d B", s) } e := math.Floor(logn(float64(s), base)) suffix := sizes[int(e)] rounding := math.Pow10(minDigits - 1) val := math.Floor(float64(s)/math.Pow(base, e)*rounding+0.5) / rounding ff := "%%.%df %%s" digits := minDigits - countDigits(int64(val)) if digits < 0 { digits = 0 } f := fmt.Sprintf(ff, digits) return fmt.Sprintf(f, val, suffix) } // Bytes produces a human-readable representation of an SI size. // // See also: ParseBytes. // // Bytes(82854982) -> 83 MB func Bytes(s uint64) string { sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} return humanateBytes(s, 1000, 2, sizes) } // BytesN produces a human-readable representation of an SI size. // n specifies the total number of digits to output, including the decimal part. // If n is less than or equal to the number of digits in the integer part, the decimal part will be omitted. // // See also: ParseBytes. // // BytesN(82854982, 3) -> 82.9 MB // BytesN(82854982, 4) -> 82.85 MB func BytesN(s uint64, n int) string { sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} return humanateBytes(s, 1000, n, sizes) } // IBytes produces a human-readable representation of an IEC size. // // See also: ParseBytes. // // IBytes(82854982) -> 79 MiB func IBytes(s uint64) string { sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} return humanateBytes(s, 1024, 2, sizes) } // IBytesN produces a human-readable representation of an IEC size. // n specifies the total number of digits to output, including the decimal part. // If n is less than or equal to the number of digits in the integer part, the decimal part will be omitted. // // See also: ParseBytes. // // IBytesN(82854982, 4) -> 79.02 MiB // IBytesN(123456789, 3) -> 118 MiB // IBytesN(123456789, 6) -> 117.738 MiB func IBytesN(s uint64, n int) string { sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} return humanateBytes(s, 1024, n, sizes) } // ParseBytes parses a string representation of bytes into the number // of bytes it represents. // // See Also: Bytes, IBytes. // // ParseBytes("42 MB") -> 42000000, nil // ParseBytes("42 mib") -> 44040192, nil func ParseBytes(s string) (uint64, error) { lastDigit := 0 hasComma := false for _, r := range s { if !(unicode.IsDigit(r) || r == '.' || r == ',') { break } if r == ',' { hasComma = true } lastDigit++ } num := s[:lastDigit] if hasComma { num = strings.Replace(num, ",", "", -1) } f, err := strconv.ParseFloat(num, 64) if err != nil { return 0, err } extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) if m, ok := bytesSizeTable[extra]; ok { f *= float64(m) if f >= math.MaxUint64 { return 0, fmt.Errorf("too large: %v", s) } return uint64(f), nil } return 0, fmt.Errorf("unhandled size name: %v", extra) }
package humanize import ( "bytes" "math" "math/big" "strconv" "strings" ) // Comma produces a string form of the given number in base 10 with // commas after every three orders of magnitude. // // e.g. Comma(834142) -> 834,142 func Comma(v int64) string { // Shortcut for [0, 7] if v&^0b111 == 0 { return string([]byte{byte(v) + 48}) } // Min int64 can't be negated to a usable value, so it has to be special cased. if v == math.MinInt64 { return "-9,223,372,036,854,775,808" } // Counting the number of digits. var count byte = 0 for n := v; n != 0; n = n / 10 { count++ } count += (count - 1) / 3 if v < 0 { v = 0 - v count++ } output := make([]byte, count) j := len(output) - 1 var counter byte = 0 for v > 9 { output[j] = byte(v%10) + 48 v = v / 10 j-- if counter == 2 { counter = 0 output[j] = ',' j-- } else { counter++ } } output[j] = byte(v) + 48 if j == 1 { output[0] = '-' } return string(output) } // Commaf produces a string form of the given number in base 10 with // commas after every three orders of magnitude. // // e.g. Commaf(834142.32) -> 834,142.32 func Commaf(v float64) string { buf := &bytes.Buffer{} if v < 0 { buf.Write([]byte{'-'}) v = 0 - v } comma := []byte{','} parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".") pos := 0 if len(parts[0])%3 != 0 { pos += len(parts[0]) % 3 buf.WriteString(parts[0][:pos]) buf.Write(comma) } for ; pos < len(parts[0]); pos += 3 { buf.WriteString(parts[0][pos : pos+3]) buf.Write(comma) } buf.Truncate(buf.Len() - 1) if len(parts) > 1 { buf.Write([]byte{'.'}) buf.WriteString(parts[1]) } return buf.String() } // CommafWithDigits works like the Commaf but limits the resulting // string to the given number of decimal places. // // e.g. CommafWithDigits(834142.32, 1) -> 834,142.3 func CommafWithDigits(f float64, decimals int) string { return stripTrailingDigits(Commaf(f), decimals) } // BigComma produces a string form of the given big.Int in base 10 // with commas after every three orders of magnitude. func BigComma(bin *big.Int) string { b := new(big.Int).Set(bin) sign := "" if b.Sign() < 0 { sign = "-" b.Abs(b) } athousand := big.NewInt(1000) c := (&big.Int{}).Set(b) _, m := oom(c, athousand) parts := make([]string, m+1) j := len(parts) - 1 mod := &big.Int{} for b.Cmp(athousand) >= 0 { b.DivMod(b, athousand, mod) parts[j] = strconv.FormatInt(mod.Int64(), 10) switch len(parts[j]) { case 2: parts[j] = "0" + parts[j] case 1: parts[j] = "00" + parts[j] } j-- } parts[j] = strconv.Itoa(int(b.Int64())) return sign + strings.Join(parts[j:], ",") }
//go:build go1.6 // +build go1.6 package humanize import ( "bytes" "math/big" "strings" ) // BigCommaf produces a string form of the given big.Float in base 10 // with commas after every three orders of magnitude. func BigCommaf(v *big.Float) string { buf := &bytes.Buffer{} if v.Sign() < 0 { buf.Write([]byte{'-'}) v.Abs(v) } comma := []byte{','} parts := strings.Split(v.Text('f', -1), ".") pos := 0 if len(parts[0])%3 != 0 { pos += len(parts[0]) % 3 buf.WriteString(parts[0][:pos]) buf.Write(comma) } for ; pos < len(parts[0]); pos += 3 { buf.WriteString(parts[0][pos : pos+3]) buf.Write(comma) } buf.Truncate(buf.Len() - 1) if len(parts) > 1 { buf.Write([]byte{'.'}) buf.WriteString(parts[1]) } return buf.String() }
package humanize import ( "strconv" "strings" ) func stripTrailingZeros(s string) string { if !strings.ContainsRune(s, '.') { return s } offset := len(s) - 1 for offset > 0 { if s[offset] == '.' { offset-- break } if s[offset] != '0' { break } offset-- } return s[:offset+1] } func stripTrailingDigits(s string, digits int) string { if i := strings.Index(s, "."); i >= 0 { if digits <= 0 { return s[:i] } i++ if i+digits >= len(s) { return s } return s[:i+digits] } return s } // Ftoa converts a float to a string with no trailing zeros. func Ftoa(num float64) string { return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64)) } // FtoaWithDigits converts a float to a string but limits the resulting string // to the given number of decimal places, and no trailing zeros. func FtoaWithDigits(num float64, digits int) string { return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits)) }
package humanize /* Slightly adapted from the source to fit go-humanize. Author: https://github.com/gorhill Source: https://gist.github.com/gorhill/5285193 */ import ( "math" "strconv" ) var ( renderFloatPrecisionMultipliers = [...]float64{ 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, } renderFloatPrecisionRounders = [...]float64{ 0.5, 0.05, 0.005, 0.0005, 0.00005, 0.000005, 0.0000005, 0.00000005, 0.000000005, 0.0000000005, } ) // FormatFloat produces a formatted number as string based on the following user-specified criteria: // // * thousands separator // * decimal separator // * decimal precision // // Usage: s := FormatFloat(format, n) // The format parameter tells how to render the number n. // // See examples: http://play.golang.org/p/LXc1Ddm1lJ // // Examples of format strings, given n = 12345.6789: // "#,###.##" => "12,345.67" // "#,###." => "12,345" // "#,###" => "12345,678" // "#\u202F###,##" => "12 345,68" // "#.###,###### => 12.345,678900 // "" (aka default format) => 12,345.67 // // The highest precision allowed is 9 digits after the decimal symbol. // There is also a version for integer number, FormatInteger(), // which is convenient for calls within template. func FormatFloat(format string, n float64) string { // Special cases: // NaN = "NaN" // +Inf = "+Infinity" // -Inf = "-Infinity" if math.IsNaN(n) { return "NaN" } if n > math.MaxFloat64 { return "Infinity" } if n < (0.0 - math.MaxFloat64) { return "-Infinity" } // default format precision := 2 decimalStr := "." thousandStr := "," positiveStr := "" negativeStr := "-" if len(format) > 0 { format := []rune(format) // If there is an explicit format directive, // then default values are these: precision = 9 thousandStr = "" // collect indices of meaningful formatting directives formatIndx := []int{} for i, char := range format { if char != '#' && char != '0' { formatIndx = append(formatIndx, i) } } if len(formatIndx) > 0 { // Directive at index 0: // Must be a '+' // Raise an error if not the case // index: 0123456789 // +0.000,000 // +000,000.0 // +0000.00 // +0000 if formatIndx[0] == 0 { if format[formatIndx[0]] != '+' { panic("FormatFloat(): invalid positive sign directive") } positiveStr = "+" formatIndx = formatIndx[1:] } // Two directives: // First is thousands separator // Raise an error if not followed by 3-digit // 0123456789 // 0.000,000 // 000,000.00 if len(formatIndx) == 2 { if (formatIndx[1] - formatIndx[0]) != 4 { panic("FormatFloat(): thousands separator directive must be followed by 3 digit-specifiers") } thousandStr = string(format[formatIndx[0]]) formatIndx = formatIndx[1:] } // One directive: // Directive is decimal separator // The number of digit-specifier following the separator indicates wanted precision // 0123456789 // 0.00 // 000,0000 if len(formatIndx) == 1 { decimalStr = string(format[formatIndx[0]]) precision = len(format) - formatIndx[0] - 1 } } } // generate sign part var signStr string if n >= 0.000000001 { signStr = positiveStr } else if n <= -0.000000001 { signStr = negativeStr n = -n } else { signStr = "" n = 0.0 } // split number into integer and fractional parts intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) // generate integer part string intStr := strconv.FormatInt(int64(intf), 10) // add thousand separator if required if len(thousandStr) > 0 { for i := len(intStr); i > 3; { i -= 3 intStr = intStr[:i] + thousandStr + intStr[i:] } } // no fractional part, we can leave now if precision == 0 { return signStr + intStr } // generate fractional part fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) // may need padding if len(fracStr) < precision { fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr } return signStr + intStr + decimalStr + fracStr } // FormatInteger produces a formatted number as string. // See FormatFloat. func FormatInteger(format string, n int) string { return FormatFloat(format, float64(n)) }
package humanize import "strconv" // Ordinal gives you the input number in a rank/ordinal format. // // Ordinal(3) -> 3rd func Ordinal(x int) string { suffix := "th" switch x % 10 { case 1: if x%100 != 11 { suffix = "st" } case 2: if x%100 != 12 { suffix = "nd" } case 3: if x%100 != 13 { suffix = "rd" } } return strconv.Itoa(x) + suffix }
package humanize import ( "errors" "math" "regexp" "strconv" ) var siPrefixTable = map[float64]string{ -30: "q", // quecto -27: "r", // ronto -24: "y", // yocto -21: "z", // zepto -18: "a", // atto -15: "f", // femto -12: "p", // pico -9: "n", // nano -6: "µ", // micro -3: "m", // milli 0: "", 3: "k", // kilo 6: "M", // mega 9: "G", // giga 12: "T", // tera 15: "P", // peta 18: "E", // exa 21: "Z", // zetta 24: "Y", // yotta 27: "R", // ronna 30: "Q", // quetta } var revSIPrefixTable = revfmap(siPrefixTable) // revfmap reverses the map and precomputes the power multiplier func revfmap(in map[float64]string) map[string]float64 { rv := map[string]float64{} for k, v := range in { rv[v] = math.Pow(10, k) } return rv } var riParseRegex *regexp.Regexp func init() { ri := `^([\-0-9.]+)\s?([` for _, v := range siPrefixTable { ri += v } ri += `]?)(.*)` riParseRegex = regexp.MustCompile(ri) } // ComputeSI finds the most appropriate SI prefix for the given number // and returns the prefix along with the value adjusted to be within // that prefix. // // See also: SI, ParseSI. // // e.g. ComputeSI(2.2345e-12) -> (2.2345, "p") func ComputeSI(input float64) (float64, string) { if input == 0 { return 0, "" } mag := math.Abs(input) exponent := math.Floor(logn(mag, 10)) exponent = math.Floor(exponent/3) * 3 value := mag / math.Pow(10, exponent) // Handle special case where value is exactly 1000.0 // Should return 1 M instead of 1000 k if value == 1000.0 { exponent += 3 value = mag / math.Pow(10, exponent) } value = math.Copysign(value, input) prefix := siPrefixTable[exponent] return value, prefix } // SI returns a string with default formatting. // // SI uses Ftoa to format float value, removing trailing zeros. // // See also: ComputeSI, ParseSI. // // e.g. SI(1000000, "B") -> 1 MB // e.g. SI(2.2345e-12, "F") -> 2.2345 pF func SI(input float64, unit string) string { value, prefix := ComputeSI(input) return Ftoa(value) + " " + prefix + unit } // SIWithDigits works like SI but limits the resulting string to the // given number of decimal places. // // e.g. SIWithDigits(1000000, 0, "B") -> 1 MB // e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF func SIWithDigits(input float64, decimals int, unit string) string { value, prefix := ComputeSI(input) return FtoaWithDigits(value, decimals) + " " + prefix + unit } var errInvalid = errors.New("invalid input") // ParseSI parses an SI string back into the number and unit. // // See also: SI, ComputeSI. // // e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil) func ParseSI(input string) (float64, string, error) { found := riParseRegex.FindStringSubmatch(input) if len(found) != 4 { return 0, "", errInvalid } mag := revSIPrefixTable[found[2]] unit := found[3] base, err := strconv.ParseFloat(found[1], 64) return base * mag, unit, err }
package humanize import ( "fmt" "math" "sort" "time" ) // Seconds-based time units const ( Day = 24 * time.Hour Week = 7 * Day Month = 30 * Day Year = 12 * Month LongTime = 37 * Year ) // Time formats a time into a relative string. // // Time(someT) -> "3 weeks ago" func Time(then time.Time) string { return RelTime(then, time.Now(), "ago", "from now") } // A RelTimeMagnitude struct contains a relative time point at which // the relative format of time will switch to a new format string. A // slice of these in ascending order by their "D" field is passed to // CustomRelTime to format durations. // // The Format field is a string that may contain a "%s" which will be // replaced with the appropriate signed label (e.g. "ago" or "from // now") and a "%d" that will be replaced by the quantity. // // The DivBy field is the amount of time the time difference must be // divided by in order to display correctly. // // e.g. if D is 2*time.Minute and you want to display "%d minutes %s" // DivBy should be time.Minute so whatever the duration is will be // expressed in minutes. type RelTimeMagnitude struct { D time.Duration Format string DivBy time.Duration } var defaultMagnitudes = []RelTimeMagnitude{ {time.Second, "now", time.Second}, {2 * time.Second, "1 second %s", 1}, {time.Minute, "%d seconds %s", time.Second}, {2 * time.Minute, "1 minute %s", 1}, {time.Hour, "%d minutes %s", time.Minute}, {2 * time.Hour, "1 hour %s", 1}, {Day, "%d hours %s", time.Hour}, {2 * Day, "1 day %s", 1}, {Week, "%d days %s", Day}, {2 * Week, "1 week %s", 1}, {Month, "%d weeks %s", Week}, {2 * Month, "1 month %s", 1}, {Year, "%d months %s", Month}, {18 * Month, "1 year %s", 1}, {2 * Year, "2 years %s", 1}, {LongTime, "%d years %s", Year}, {math.MaxInt64, "a long while %s", 1}, } // RelTime formats a time into a relative string. // // It takes two times and two labels. In addition to the generic time // delta string (e.g. 5 minutes), the labels are used applied so that // the label corresponding to the smaller time is applied. // // RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" func RelTime(a, b time.Time, albl, blbl string) string { return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) } // CustomRelTime formats a time into a relative string. // // It takes two times two labels and a table of relative time formats. // In addition to the generic time delta string (e.g. 5 minutes), the // labels are used applied so that the label corresponding to the // smaller time is applied. func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { lbl := albl diff := b.Sub(a) if a.After(b) { lbl = blbl diff = a.Sub(b) } n := sort.Search(len(magnitudes), func(i int) bool { return magnitudes[i].D > diff }) if n >= len(magnitudes) { n = len(magnitudes) - 1 } mag := magnitudes[n] args := []interface{}{} escaped := false for _, ch := range mag.Format { if escaped { switch ch { case 's': args = append(args, lbl) case 'd': args = append(args, diff/mag.DivBy) } escaped = false } else { escaped = ch == '%' } } return fmt.Sprintf(mag.Format, args...) }