Limiting significant digits in formatted durations

Viewed 1152

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?

3 Answers

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       

%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)))
}

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

Related