diff --git a/widgets/linechart/value_formatter.go b/widgets/linechart/value_formatter.go index 4b2fe2e..35e71d5 100644 --- a/widgets/linechart/value_formatter.go +++ b/widgets/linechart/value_formatter.go @@ -20,6 +20,7 @@ package linechart import ( "fmt" "math" + "strings" "time" ) @@ -42,33 +43,34 @@ func durationSingleUnitPrettyFormat(d time.Duration, decimals int) string { return fmt.Sprintf(dFmt, d.Nanoseconds()) // Microseconds. case d.Seconds()*1000*1000 < 1000: - dFmt := prefix + decimalFormat(decimals, "µs") + dFmt := prefix + suffixDecimalFormat(decimals, "µs") return fmt.Sprintf(dFmt, d.Seconds()*1000*1000) // Milliseconds. case d.Seconds()*1000 < 1000: - dFmt := prefix + decimalFormat(decimals, "ms") + dFmt := prefix + suffixDecimalFormat(decimals, "ms") return fmt.Sprintf(dFmt, d.Seconds()*1000) // Seconds. case d.Seconds() < 60: - dFmt := prefix + decimalFormat(decimals, "s") + dFmt := prefix + suffixDecimalFormat(decimals, "s") return fmt.Sprintf(dFmt, d.Seconds()) // Minutes. case d.Minutes() < 60: - dFmt := prefix + decimalFormat(decimals, "m") + dFmt := prefix + suffixDecimalFormat(decimals, "m") return fmt.Sprintf(dFmt, d.Minutes()) // Hours. case d.Hours() < 24: - dFmt := prefix + decimalFormat(decimals, "h") + dFmt := prefix + suffixDecimalFormat(decimals, "h") return fmt.Sprintf(dFmt, d.Hours()) // Days. default: - dFmt := prefix + decimalFormat(decimals, "d") + dFmt := prefix + suffixDecimalFormat(decimals, "d") return fmt.Sprintf(dFmt, d.Hours()/24) } } -func decimalFormat(decimals int, unit string) string { - return fmt.Sprintf("%%.%df%s", decimals, unit) +func suffixDecimalFormat(decimals int, suffix string) string { + suffix = strings.ReplaceAll(suffix, "%", "%%") // safe `%` character for fmt. + return fmt.Sprintf("%%.%df%s", decimals, suffix) } // ValueFormatterSingleUnitDuration is a factory to create a custom duration @@ -103,3 +105,42 @@ func ValueFormatterSingleUnitSeconds(seconds float64) string { f := ValueFormatterSingleUnitDuration(time.Second, 0) return f(seconds) } + +// ValueFormatterRound is a formatter that will receive a float64 +// value and will round to the nearest value without decimals. +func ValueFormatterRound(value float64) string { + f := ValueFormatterRoundWithSuffix("") + return f(value) +} + +// ValueFormatterRoundWithSuffix is a factory that returns a formatter +// that will receive a float64 value and will round to the nearest value +// without decimals adding a suffix to the final value string representation. +func ValueFormatterRoundWithSuffix(suffix string) ValueFormatter { + return valueFormatterSuffixWithTransformer(0, suffix, math.Round) +} + +// ValueFormatterSuffix is a factory that returns a formatter +// that will receive a float64 value and return a string representation with +// the desired number of decimal truncated and a suffix. +func ValueFormatterSuffix(decimals int, suffix string) ValueFormatter { + return valueFormatterSuffixWithTransformer(decimals, suffix, nil) +} + +// valueFormatterSuffixWithTransformer is a factory that returns a formatter +// that will apply a tranform function to the received value before +// returning the decimal with suffix representation. +func valueFormatterSuffixWithTransformer(decimals int, suffix string, transformFunc func(float64) float64) ValueFormatter { + dFmt := suffixDecimalFormat(decimals, suffix) + return func(value float64) string { + if math.IsNaN(value) { + return "" + } + + if transformFunc != nil { + value = transformFunc(value) + } + + return fmt.Sprintf(dFmt, value) + } +} diff --git a/widgets/linechart/value_formatter_test.go b/widgets/linechart/value_formatter_test.go index cbdee8f..2e8af32 100644 --- a/widgets/linechart/value_formatter_test.go +++ b/widgets/linechart/value_formatter_test.go @@ -143,6 +143,90 @@ func TestFormatters(t *testing.T) { formatter: ValueFormatterSingleUnitDuration(time.Nanosecond, 8), want: "", }, + { + desc: "Round formatter handles NaN values", + value: math.NaN(), + formatter: ValueFormatterRound, + want: "", + }, + { + desc: "Round formatter handles 0 values", + value: 0, + formatter: ValueFormatterRound, + want: "0", + }, + { + desc: "Round formatter handles > x.5 values", + value: 96.7, + formatter: ValueFormatterRound, + want: "97", + }, + { + desc: "Round formatter handles < x.5 values", + value: 1621.2, + formatter: ValueFormatterRound, + want: "1621", + }, + { + desc: "Round formatter handles x.5 values", + value: 6.5, + formatter: ValueFormatterRound, + want: "7", + }, + { + desc: "Round formatter handles minus > x.5 values", + value: -96.7, + formatter: ValueFormatterRound, + want: "-97", + }, + { + desc: "Round formatter handles minus < x.5 values", + value: -1621.2, + formatter: ValueFormatterRound, + want: "-1621", + }, + { + desc: "Round formatter handles minus x.5 values", + value: -6.5, + formatter: ValueFormatterRound, + want: "-7", + }, + { + desc: "Round formatter handles values with suffix", + value: 96.7, + formatter: ValueFormatterRoundWithSuffix("km"), + want: "97km", + }, + { + desc: "Suffix formatter handles values with decimals", + value: 11234567890.71234567890, + formatter: ValueFormatterSuffix(4, " reqps"), + want: "11234567890.7123 reqps", + }, + { + desc: "Suffix formatter handles NaN values", + value: math.NaN(), + formatter: ValueFormatterSuffix(2, "test"), + want: "", + }, + { + desc: "Suffix formatter handles 0 values", + value: 0, + formatter: ValueFormatterSuffix(2, "test"), + want: "0.00test", + }, + { + desc: "Suffix formatters handles correctly percent suffix", + value: 96.78, + formatter: ValueFormatterSuffix(2, "%"), + want: "96.78%", + }, + { + desc: "Round formatter handles values with percent suffix", + value: 96.7, + formatter: ValueFormatterRoundWithSuffix("%"), + want: "97%", + }, } for _, tc := range tests {