Limiting significant digits in formatted durations
Asked Answered
C

4

6

I am timing some unpredictable I/O. This code

started := time.Now()
time.Sleep(123456789 * time.Nanosecond) // unpredictable process    
fmt.Printf("%v", time.Since(started))

Produces

123.456789ms

I like the automatic selection and printing of unit scale (ms, μs, ns etc) because I don't know in advance whether the timed operation takes microseconds, milliseconds or seconds to complete.

I don't like the precision - I'd prefer to report only two or three significant digits. Is there a simple way to limit the precision in the formatting directive %v or similar?

Chalone answered 16/10, 2019 at 13:43 Comment(1)
The precision for milliseconds is hardcoded to 6 decimals and time.Duration doesn't implement fmt.Formatter, so you'll have to write your own formatting function. You can start by copying time.Duration.String and then modify it to suit your needs.Damiandamiani
T
6

Foreword: I released this utility in github.com/icza/gox, see timex.Round().


I don't think there's a simple way because when printed using the default format (e.g. %v), Duration.String() is called to produce the string representation. It returns a string value, so formatting options like number of fraction digits are not applicable anymore.

One way to control the resulting fraction digits is to truncate or round the duration before printing it, using Duration.Truncate() or Duration.Round().

Of course the unit to which the duration should be truncated or rounded to depends on the duration's value, but the logic is not that hard:

var divs = []time.Duration{
    time.Duration(1), time.Duration(10), time.Duration(100), time.Duration(1000)}

func round(d time.Duration, digits int) time.Duration {
    switch {
    case d > time.Second:
        d = d.Round(time.Second / divs[digits])
    case d > time.Millisecond:
        d = d.Round(time.Millisecond / divs[digits])
    case d > time.Microsecond:
        d = d.Round(time.Microsecond / divs[digits])
    }
    return d
}

Let's test it with different durations:

ds := []time.Duration{
    time.Hour + time.Second + 123*time.Millisecond, // 1h0m1.123s
    time.Hour + time.Second + time.Microsecond,     // 1h0m1.000001s
    123456789 * time.Nanosecond,                    // 123.456789ms
    123456 * time.Nanosecond,                       // 123.456µs
    123 * time.Nanosecond,                          // 123ns
}

for _, d := range ds {
    fmt.Printf("%-15v", d)
    for digits := 0; digits <= 3; digits++ {
        fmt.Printf("%-15v", round(d, digits))

    }
    fmt.Println()
}

Output will be (try it on the Go Playground):

duration       0 digits       1 digit        2 digits       3 digits
-----------------------------------------------------------------------
1h0m1.123s     1h0m1s         1h0m1.1s       1h0m1.12s      1h0m1.123s     
1h0m1.000001s  1h0m1s         1h0m1s         1h0m1s         1h0m1s         
123.456789ms   123ms          123.5ms        123.46ms       123.457ms      
123.456µs      123µs          123.5µs        123.46µs       123.456µs      
123ns          123ns          123ns          123ns          123ns       
Tenedos answered 16/10, 2019 at 14:20 Comment(0)
N
3

%v is using Duration.String(), so you either have to write a custom format function, something like:

func fmtTime(in time.Duration, prec int) string {
  s := in.String()
  ix := strings.IndexRune(s, '.')
  if ix == -1 {
    return s
  }
  unit:=len(s)
  for i,x:=range s[:ix+1] {
     if !unicode.IsDigit(x) {
       unit=i+ix+1
       break
     }
  }
  if prec == 0 {
     return s[:ix]+s[unit:]
  }
  if prec>len(s)-ix-(len(s)-unit)-1 {
     prec=len(s)-ix-(len(s)-unit)-1
  }
  return s[:ix+prec+1]+s[unit:]
}

func main() {
   ...
   fmt.Printf("%v\n", fmtTime(time.Since(started), 3))
}

Or you can define a new type with a formatter, and use the new type to print:

type FmtDuration time.Duration

func (d FmtDuration) Format(f fmt.State, c rune) {
   prec,_ := f.Precision()
   f.Write([]byte(fmtTime(time.Duration(d), prec)))
}

func main() {
   fmt.Printf("%.2v", FmtDuration(time.Since(started)))
}
Noisy answered 16/10, 2019 at 14:18 Comment(2)
Thanks. So far as I can tell, these examples lop off the units - so I cant tell if the process took 123 seconds or 123 millseconds. Obviously fmtTime() can be amended to retain units (at least in simple cases) but I thought it worth mentioning.Chalone
@Chalone you're right. I fixed the code to add the unit.Noisy
S
3

If you only care about the significant 3 digits:

// FormatDuration formats a duration with a precision of 3 digits
// if it is less than 100s.
func FormatDuration(d time.Duration) string {
    scale := 100 * time.Second
    // look for the max scale that is smaller than d
    for scale > d {
        scale = scale / 10
    }
    return d.Round(scale / 100).String()
}

func Test_FormatDuration(t *testing.T) {
    for i := 0; i < 15; i++ {
        dur := time.Duration(3.455555 * math.Pow(10, float64(i)))
        t.Logf("%2d  %12v  %6s", i, dur,  FormatDuration(dur))
    }
}
//      original     formatted
//  0           3ns     3ns
//  1          34ns    34ns
//  2         345ns   345ns
//  3       3.455µs  3.46µs
//  4      34.555µs  34.6µs
//  5     345.555µs   346µs
//  6    3.455555ms  3.46ms
//  7    34.55555ms  34.6ms
//  8    345.5555ms   346ms
//  9     3.455555s   3.46s
// 10     34.55555s   34.6s
// 11    5m45.5555s   5m46s
// 12    57m35.555s  57m36s
// 13   9h35m55.55s  9h35m56s
// 14   95h59m15.5s  95h59m16s

Separates answered 21/8, 2021 at 4:13 Comment(0)
M
2

time.Duration has a Round() method. So the following can be used to round durations and print the result:

fmt.Printf("%v\n", time.Since(t.lastSeen).Round(time.Second))
Monosepalous answered 7/12, 2023 at 17:53 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.