diff --git a/progressbar.go b/progressbar.go index 8c908bb..51eb32f 100644 --- a/progressbar.go +++ b/progressbar.go @@ -153,6 +153,9 @@ type config struct { // specifies how many rows of details to show,default value is 0 and no details will be shown maxDetailRow int + // separator to format numbers (e.g. "1,048,576" or "1_048_576") + numberSeparator string + stdBuffer bytes.Buffer } @@ -445,6 +448,23 @@ func OptionSetStartingBytes(num int64) Option { } } +// OptionSetNumberSeparator sets the separator used when formatting numbers (e.g. "," or "_"). +func OptionSetNumberSeparator(separator string) Option { + return func(p *ProgressBar) { + p.config.numberSeparator = separator + } +} + +// OptionSetSeparator is an alias for OptionSetNumberSeparator. +func OptionSetSeparator(separator string) Option { + return OptionSetNumberSeparator(separator) +} + +// OptionSeparator is an alias for OptionSetNumberSeparator. +func OptionSeparator(separator string) Option { + return OptionSetNumberSeparator(separator) +} + // NewOptions constructs a new instance of ProgressBar, with any options you specify func NewOptions(max int, options ...Option) *ProgressBar { return NewOptions64(int64(max), options...) @@ -1269,18 +1289,18 @@ func renderProgressBar(c config, s *state) (int, error) { sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix)) } } else if c.showTotalBytes { - sb.WriteString(fmt.Sprintf("%.0f/%d", s.currentBytes, c.max)) + sb.WriteString(fmt.Sprintf("%s/%s", formatNumber(s.currentBytes, c.numberSeparator), formatNumber(float64(c.max), c.numberSeparator))) } else { - sb.WriteString(fmt.Sprintf("%.0f", s.currentBytes)) + sb.WriteString(formatNumber(s.currentBytes, c.numberSeparator)) } } else { if c.showBytes { currentHumanize, currentSuffix := humanizeBytes(s.currentBytes, c.useIECUnits) sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix)) } else if c.showTotalBytes { - sb.WriteString(fmt.Sprintf("%.0f/%s", s.currentBytes, "-")) + sb.WriteString(fmt.Sprintf("%s/%s", formatNumber(s.currentBytes, c.numberSeparator), "-")) } else { - sb.WriteString(fmt.Sprintf("%.0f", s.currentBytes)) + sb.WriteString(formatNumber(s.currentBytes, c.numberSeparator)) } } } @@ -1304,11 +1324,11 @@ func renderProgressBar(c config, s *state) (int, error) { sb.WriteString(", ") } if averageRate > 1 { - sb.WriteString(fmt.Sprintf("%0.0f %s/s", averageRate, c.iterationString)) + sb.WriteString(fmt.Sprintf("%s %s/s", formatNumber(averageRate, c.numberSeparator), c.iterationString)) } else if averageRate*60 > 1 { - sb.WriteString(fmt.Sprintf("%0.0f %s/min", 60*averageRate, c.iterationString)) + sb.WriteString(fmt.Sprintf("%s %s/min", formatNumber(60*averageRate, c.numberSeparator), c.iterationString)) } else { - sb.WriteString(fmt.Sprintf("%0.0f %s/hr", 3600*averageRate, c.iterationString)) + sb.WriteString(fmt.Sprintf("%s %s/hr", formatNumber(3600*averageRate, c.numberSeparator), c.iterationString)) } } if sb.Len() > 0 { @@ -1653,3 +1673,49 @@ func Bprintf(pb *ProgressBar, format string, a ...interface{}) (int, error) { return fmt.Fprintf(&pb.config.stdBuffer, format, a...) } } + +func formatNumber(val float64, sep string) string { + s := fmt.Sprintf("%.0f", val) + return insertSeparator(s, sep) +} + +func insertSeparator(s string, sep string) string { + if sep == "" || len(s) <= 3 { + return s + } + + isNeg := false + if s[0] == '-' { + isNeg = true + s = s[1:] + } + + if len(s) <= 3 { + if isNeg { + return "-" + s + } + return s + } + + var b strings.Builder + if isNeg { + b.WriteByte('-') + } + + rem := len(s) % 3 + if rem > 0 { + b.WriteString(s[:rem]) + if rem < len(s) { + b.WriteString(sep) + } + } + + for i := rem; i < len(s); i += 3 { + b.WriteString(s[i : i+3]) + if i+3 < len(s) { + b.WriteString(sep) + } + } + + return b.String() +} diff --git a/progressbar_test.go b/progressbar_test.go index 8238e6d..789a01c 100644 --- a/progressbar_test.go +++ b/progressbar_test.go @@ -1393,3 +1393,76 @@ func TestRateAveragingWindow(t *testing.T) { t.Errorf("windowed: expected more than %d retained samples, got %d", maxLegacyRateSamples, got) } } + +func TestFormatNumber(t *testing.T) { + tests := []struct { + val float64 + sep string + expected string + }{ + {val: 0, sep: "_", expected: "0"}, + {val: 9, sep: "_", expected: "9"}, + {val: 99, sep: "_", expected: "99"}, + {val: 999, sep: "_", expected: "999"}, + {val: 1000, sep: "_", expected: "1_000"}, + {val: 10000, sep: ",", expected: "10,000"}, + {val: 100000, sep: ",", expected: "100,000"}, + {val: 1048576, sep: "_", expected: "1_048_576"}, + {val: 1048576, sep: ",", expected: "1,048,576"}, + {val: 1048576, sep: " ", expected: "1 048 576"}, + {val: 1048576, sep: "", expected: "1048576"}, + {val: 1000000000, sep: ",", expected: "1,000,000,000"}, + {val: -1234, sep: ",", expected: "-1,234"}, + {val: -1048576, sep: "_", expected: "-1_048_576"}, + } + + for _, tt := range tests { + actual := formatNumber(tt.val, tt.sep) + assert.Equal(t, tt.expected, actual, "val=%v sep=%q", tt.val, tt.sep) + } +} + +func TestOptionSetNumberSeparator(t *testing.T) { + buf := strings.Builder{} + bar := NewOptions(1048576, + OptionSetWidth(10), + OptionShowCount(), + OptionSetPredictTime(false), + OptionSetNumberSeparator("_"), + OptionSetWriter(&buf), + ) + _ = bar.Add(1024) + assert.Contains(t, buf.String(), "1_024/1_048_576") + + // test OptionSetSeparator alias + buf.Reset() + bar = NewOptions(1000000, + OptionSetWidth(10), + OptionShowCount(), + OptionSetPredictTime(false), + OptionSetSeparator(","), + OptionSetWriter(&buf), + ) + _ = bar.Add(5000) + assert.Contains(t, buf.String(), "5,000/1,000,000") + + // test OptionSeparator alias + buf.Reset() + bar = NewOptions(1000000, + OptionSetWidth(10), + OptionShowCount(), + OptionSetPredictTime(false), + OptionSeparator(" "), + OptionSetWriter(&buf), + ) + _ = bar.Add(5000) + assert.Contains(t, buf.String(), "5 000/1 000 000") +} + +func ExampleOptionSetNumberSeparator() { + bar := NewOptions(1048576, OptionSetWidth(10), OptionShowCount(), OptionSetPredictTime(false), OptionSetNumberSeparator("_")) + bar.Add(1024) + // Output: + // 0% | | (1_024/1_048_576) +} +