限制格式化持续时间中的有效数字

Limiting significant digits in formatted durations

我正在安排一些不可预测的时间 I/O。此代码

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

生产

123.456789ms

我喜欢自动选择和打印单位刻度(ms、μs、ns等),因为我事先不知道定时操作需要微秒、毫秒还是秒才能完成。

我不喜欢这种精确度 - 我宁愿只报告两位或三位有效数字。是否有 简单 方法来限制格式化指令 %v 或类似指令的精度?

%v 正在使用 Duration.String(),因此您要么必须编写自定义格式函数,例如:

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

或者你可以用格式化程序定义一个新类型,然后使用新类型打印:

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

前言: 我在 github.com/icza/gox, see timex.Round().

发布了这个实用程序

我认为没有简单的方法,因为当使用默认格式(例如%v)打印时,会调用Duration.String()来生成字符串表示。它 returns 是一个 string 值,因此小数位数等格式化选项不再适用。

控制结果小数位数的一种方法是在打印前截断或舍入持续时间,使用 Duration.Truncate() or Duration.Round()

当然,持续时间应截断或四舍五入的单位取决于持续时间的值,但逻辑并不难:

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
}

让我们用不同的持续时间来测试它:

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

输出将是(在 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       

如果您只关心有效的 3 位数字:

// 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