feat: add measurement value formatter (#2773)

This commit is contained in:
Srikanth Chekuri 2023-06-07 12:10:05 +05:30 committed by GitHub
parent 826cbe0803
commit 745626f516
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 666 additions and 1 deletions

View File

@ -140,4 +140,5 @@ test:
go test ./pkg/query-service/app/metrics/... go test ./pkg/query-service/app/metrics/...
go test ./pkg/query-service/cache/... go test ./pkg/query-service/cache/...
go test ./pkg/query-service/app/... go test ./pkg/query-service/app/...
go test ./pkg/query-service/converter/... go test ./pkg/query-service/converter/...
go test ./pkg/query-service/formatter/...

2
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.5.1 github.com/ClickHouse/clickhouse-go/v2 v2.5.1
github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb
github.com/coreos/go-oidc/v3 v3.4.0 github.com/coreos/go-oidc/v3 v3.4.0
github.com/dustin/go-humanize v1.0.0
github.com/go-kit/log v0.2.1 github.com/go-kit/log v0.2.1
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/go-redis/redismock/v8 v8.11.5 github.com/go-redis/redismock/v8 v8.11.5
@ -30,6 +31,7 @@ require (
github.com/rs/cors v1.8.2 github.com/rs/cors v1.8.2
github.com/russellhaering/gosaml2 v0.8.0 github.com/russellhaering/gosaml2 v0.8.0
github.com/russellhaering/goxmldsig v1.2.0 github.com/russellhaering/goxmldsig v1.2.0
github.com/samber/lo v1.38.1
github.com/sethvargo/go-password v0.2.0 github.com/sethvargo/go-password v0.2.0
github.com/smartystreets/goconvey v1.6.4 github.com/smartystreets/goconvey v1.6.4
github.com/soheilhy/cmux v0.1.5 github.com/soheilhy/cmux v0.1.5

3
go.sum
View File

@ -163,6 +163,7 @@ github.com/docker/docker v20.10.22+incompatible h1:6jX4yB+NtcbldT90k7vBSaWJDB3i+
github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y= github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y=
github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q= github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
@ -605,6 +606,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.10 h1:wsfMs0iv+MJiViM37qh5VEKISi3/ZUq2nNKNdqmumAs= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.10 h1:wsfMs0iv+MJiViM37qh5VEKISi3/ZUq2nNKNdqmumAs=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=

View File

@ -0,0 +1,45 @@
package formatter
import "fmt"
type boolFormatter struct{}
func NewBoolFormatter() Formatter {
return &boolFormatter{}
}
func toBool(value float64) string {
if value == 0 {
return "false"
}
return "true"
}
func toBoolYesNo(value float64) string {
if value == 0 {
return "no"
}
return "yes"
}
func toBoolOnOff(value float64) string {
if value == 0 {
return "off"
}
return "on"
}
func (f *boolFormatter) Format(value float64, unit string) string {
switch unit {
case "bool":
return toBool(value)
case "bool_yes_no":
return toBoolYesNo(value)
case "bool_on_off":
return toBoolOnOff(value)
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@ -0,0 +1,50 @@
package formatter
import (
"fmt"
"github.com/dustin/go-humanize"
"go.signoz.io/signoz/pkg/query-service/converter"
)
type dataFormatter struct {
}
func NewDataFormatter() Formatter {
return &dataFormatter{}
}
func (f *dataFormatter) Format(value float64, unit string) string {
switch unit {
case "bytes":
return humanize.IBytes(uint64(value))
case "decbytes":
return humanize.Bytes(uint64(value))
case "bits":
return humanize.IBytes(uint64(value * converter.Bit))
case "decbits":
return humanize.Bytes(uint64(value * converter.Bit))
case "kbytes":
return humanize.IBytes(uint64(value * converter.Kibibit))
case "deckbytes":
return humanize.IBytes(uint64(value * converter.Kilobit))
case "mbytes":
return humanize.IBytes(uint64(value * converter.Mebibit))
case "decmbytes":
return humanize.Bytes(uint64(value * converter.Megabit))
case "gbytes":
return humanize.IBytes(uint64(value * converter.Gibibit))
case "decgbytes":
return humanize.Bytes(uint64(value * converter.Gigabit))
case "tbytes":
return humanize.IBytes(uint64(value * converter.Tebibit))
case "dectbytes":
return humanize.Bytes(uint64(value * converter.Terabit))
case "pbytes":
return humanize.IBytes(uint64(value * converter.Pebibit))
case "decpbytes":
return humanize.Bytes(uint64(value * converter.Petabit))
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@ -0,0 +1,70 @@
package formatter
import (
"fmt"
"github.com/dustin/go-humanize"
"go.signoz.io/signoz/pkg/query-service/converter"
)
type dataRateFormatter struct {
}
func NewDataRateFormatter() Formatter {
return &dataRateFormatter{}
}
func (f *dataRateFormatter) Format(value float64, unit string) string {
switch unit {
case "binBps":
return humanize.IBytes(uint64(value)) + "/s"
case "Bps":
return humanize.Bytes(uint64(value)) + "/s"
case "binbps":
return humanize.IBytes(uint64(value*converter.BitPerSecond)) + "/s"
case "bps":
return humanize.Bytes(uint64(value*converter.BitPerSecond)) + "/s"
case "KiBs":
return humanize.IBytes(uint64(value*converter.KibibitPerSecond)) + "/s"
case "Kibits":
return humanize.IBytes(uint64(value*converter.KibibytePerSecond)) + "/s"
case "KBs":
return humanize.IBytes(uint64(value*converter.KilobitPerSecond)) + "/s"
case "Kbits":
return humanize.IBytes(uint64(value*converter.KilobytePerSecond)) + "/s"
case "MiBs":
return humanize.IBytes(uint64(value*converter.MebibitPerSecond)) + "/s"
case "Mibits":
return humanize.IBytes(uint64(value*converter.MebibytePerSecond)) + "/s"
case "MBs":
return humanize.IBytes(uint64(value*converter.MegabitPerSecond)) + "/s"
case "Mbits":
return humanize.IBytes(uint64(value*converter.MegabytePerSecond)) + "/s"
case "GiBs":
return humanize.IBytes(uint64(value*converter.GibibitPerSecond)) + "/s"
case "Gibits":
return humanize.IBytes(uint64(value*converter.GibibytePerSecond)) + "/s"
case "GBs":
return humanize.IBytes(uint64(value*converter.GigabitPerSecond)) + "/s"
case "Gbits":
return humanize.IBytes(uint64(value*converter.GigabytePerSecond)) + "/s"
case "TiBs":
return humanize.IBytes(uint64(value*converter.TebibitPerSecond)) + "/s"
case "Tibits":
return humanize.IBytes(uint64(value*converter.TebibytePerSecond)) + "/s"
case "TBs":
return humanize.IBytes(uint64(value*converter.TerabitPerSecond)) + "/s"
case "Tbits":
return humanize.IBytes(uint64(value*converter.TerabytePerSecond)) + "/s"
case "PiBs":
return humanize.IBytes(uint64(value*converter.PebibitPerSecond)) + "/s"
case "Pibits":
return humanize.IBytes(uint64(value*converter.PebibytePerSecond)) + "/s"
case "PBs":
return humanize.IBytes(uint64(value*converter.PetabitPerSecond)) + "/s"
case "Pbits":
return humanize.IBytes(uint64(value*converter.PetabytePerSecond)) + "/s"
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@ -0,0 +1,23 @@
package formatter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestData(t *testing.T) {
dataFormatter := NewDataFormatter()
assert.Equal(t, "1 B", dataFormatter.Format(1, "bytes"))
assert.Equal(t, "1.0 KiB", dataFormatter.Format(1024, "bytes"))
assert.Equal(t, "2.3 GiB", dataFormatter.Format(2.3*1024, "mbytes"))
assert.Equal(t, "1.0 MiB", dataFormatter.Format(1024*1024, "bytes"))
assert.Equal(t, "69 TiB", dataFormatter.Format(69*1024*1024, "mbytes"))
assert.Equal(t, "102 KiB", dataFormatter.Format(102*1024, "bytes"))
assert.Equal(t, "240 MiB", dataFormatter.Format(240*1024, "kbytes"))
assert.Equal(t, "1.0 GiB", dataFormatter.Format(1024*1024, "kbytes"))
assert.Equal(t, "23 GiB", dataFormatter.Format(23*1024*1024, "kbytes"))
assert.Equal(t, "32 TiB", dataFormatter.Format(32*1024*1024*1024, "kbytes"))
assert.Equal(t, "24 MiB", dataFormatter.Format(24, "mbytes"))
}

View File

@ -0,0 +1,34 @@
package formatter
type Formatter interface {
Format(value float64, unit string) string
}
var (
DurationFormatter = NewDurationFormatter()
BoolFormatter = NewBoolFormatter()
PercentFormatter = NewPercentFormatter()
NoneFormatter = NewNoneFormatter()
DataFormatter = NewDataFormatter()
DataRateFormatter = NewDataRateFormatter()
ThroughputFormatter = NewThroughputFormatter()
)
func FromUnit(u string) Formatter {
switch u {
case "ns", "us", "ms", "s", "m", "h", "d":
return DurationFormatter
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "mbytes", "decMbytes", "gbytes", "decGbytes", "tbytes", "decTbytes", "pbytes", "decPbytes":
return DataFormatter
case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits":
return DataRateFormatter
case "percent", "percentunit":
return PercentFormatter
case "bool", "bool_yes_no", "bool_true_false", "bool_1_0":
return BoolFormatter
case "cps", "ops", "reqps", "rps", "wps", "iops", "cpm", "opm", "rpm", "wpm":
return ThroughputFormatter
default:
return NoneFormatter
}
}

View File

@ -0,0 +1,13 @@
package formatter
import "fmt"
type noneFormatter struct{}
func NewNoneFormatter() Formatter {
return &noneFormatter{}
}
func (f *noneFormatter) Format(value float64, unit string) string {
return fmt.Sprintf("%v", value)
}

View File

@ -0,0 +1,28 @@
package formatter
import "fmt"
type percentFormatter struct{}
func NewPercentFormatter() Formatter {
return &percentFormatter{}
}
func toPercent(value float64, decimals DecimalCount) string {
return toFixed(value, decimals) + "%"
}
func toPercentUnit(value float64, decimals DecimalCount) string {
return toFixed(value*100, decimals) + "%"
}
func (f *percentFormatter) Format(value float64, unit string) string {
switch unit {
case "percent":
return toPercent(value, nil)
case "percentunit":
return toPercentUnit(value, nil)
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@ -0,0 +1,137 @@
package formatter
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/samber/lo"
)
type IntervalsInSecondsType map[Interval]int
type Interval string
const (
Year Interval = "year"
Month Interval = "month"
Week Interval = "week"
Day Interval = "day"
Hour Interval = "hour"
Minute Interval = "minute"
Second Interval = "second"
Millisecond Interval = "millisecond"
)
var Units = []Interval{
Year,
Month,
Week,
Day,
Hour,
Minute,
Second,
Millisecond,
}
var IntervalsInSeconds = IntervalsInSecondsType{
Year: 31536000,
Month: 2592000,
Week: 604800,
Day: 86400,
Hour: 3600,
Minute: 60,
Second: 1,
Millisecond: 1,
}
type DecimalCount *int
func toFixed(value float64, decimals DecimalCount) string {
if value == 0 {
return strconv.FormatFloat(value, 'f', getDecimalsForValue(value), 64)
}
if math.IsInf(value, 0) {
return strconv.FormatFloat(value, 'f', -1, 64)
}
if decimals == nil {
count := getDecimalsForValue(value)
decimals = &count
}
factor := math.Pow(10, math.Max(0, float64(*decimals)))
formatted := strconv.FormatFloat(math.Round(value*factor)/factor, 'f', -1, 64)
if formatted == "NaN" {
return ""
}
if formatted == "-0" {
formatted = "0"
}
if value == 0 || strings.Contains(formatted, "e") {
return formatted
}
decimalPos := strings.Index(formatted, ".")
precision := 0
if decimalPos != -1 {
precision = len(formatted) - decimalPos - 1
}
if precision < *decimals {
return formatted + strings.Repeat("0", *decimals-precision)
}
return formatted
}
func toFixedScaled(value float64, decimals DecimalCount, scaleFormat string) string {
return toFixed(value, decimals) + scaleFormat
}
func getDecimalsForValue(value float64) int {
absValue := math.Abs(value)
log10 := math.Floor(math.Log10(absValue))
dec := int(-log10 + 1)
magn := math.Pow10(-dec)
norm := absValue / magn
if norm > 2.25 {
dec++
}
if math.Mod(value, 1) == 0 {
dec = 0
}
return int(math.Max(float64(dec), 0))
}
type ValueFormatter func(value float64, decimals *int) string
func logb(base, x float64) float64 {
return math.Log10(x) / math.Log10(base)
}
func scaledUnits(factor float64, extArray []string, offset int) ValueFormatter {
return func(value float64, decimals *int) string {
if value == 0 || math.IsNaN(value) || math.IsInf(value, 0) {
return fmt.Sprintf("%f", value)
}
siIndex := int(math.Floor(logb(factor, math.Abs(value))))
if value < 0 {
siIndex = -siIndex
}
siIndex = lo.Clamp(siIndex+offset, 0, len(extArray)-1)
suffix := extArray[siIndex]
return toFixed(value/math.Pow(factor, float64(siIndex-offset)), decimals) + suffix
}
}

View File

@ -0,0 +1,44 @@
package formatter
import "fmt"
type throughputFormatter struct {
}
func NewThroughputFormatter() Formatter {
return &throughputFormatter{}
}
func simpleCountUnit(value float64, decimals *int, symbol string) string {
units := []string{"", "K", "M", "B", "T"}
scaler := scaledUnits(1000, units, 0)
return scaler(value, decimals) + " " + symbol
}
func (f *throughputFormatter) Format(value float64, unit string) string {
switch unit {
case "cps":
return simpleCountUnit(value, nil, "c/s")
case "ops":
return simpleCountUnit(value, nil, "op/s")
case "reqps":
return simpleCountUnit(value, nil, "req/s")
case "rps":
return simpleCountUnit(value, nil, "r/s")
case "wps":
return simpleCountUnit(value, nil, "w/s")
case "iops":
return simpleCountUnit(value, nil, "iops")
case "cpm":
return simpleCountUnit(value, nil, "c/m")
case "opm":
return simpleCountUnit(value, nil, "op/m")
case "rpm":
return simpleCountUnit(value, nil, "r/m")
case "wpm":
return simpleCountUnit(value, nil, "w/m")
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@ -0,0 +1,15 @@
package formatter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestThroughput(t *testing.T) {
throughputFormatter := NewThroughputFormatter()
assert.Equal(t, "10 req/s", throughputFormatter.Format(10, "reqps"))
assert.Equal(t, "1K req/s", throughputFormatter.Format(1000, "reqps"))
assert.Equal(t, "1M req/s", throughputFormatter.Format(1000000, "reqps"))
}

View File

@ -0,0 +1,171 @@
package formatter
import (
"fmt"
"math"
)
type durationFormatter struct {
}
func NewDurationFormatter() Formatter {
return &durationFormatter{}
}
func (f *durationFormatter) Format(value float64, unit string) string {
switch unit {
case "ns":
return toNanoSeconds(value)
case "µs":
return toMicroSeconds(value)
case "ms":
return toMilliSeconds(value)
case "s":
return toSeconds(value)
case "m":
return toMinutes(value)
case "h":
return toHours(value)
case "d":
return toDays(value)
case "w":
return toWeeks(value)
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}
// toNanoSeconds returns a easy to read string representation of the given value in nanoseconds
func toNanoSeconds(value float64) string {
absValue := math.Abs(value)
if absValue < 1000 {
return toFixed(value, nil) + " ns"
} else if absValue < 1000000 { // 2000 ns is better represented as 2 µs
return toFixedScaled(value/1000, nil, " µs")
} else if absValue < 1000000000 { // 2000000 ns is better represented as 2 ms
return toFixedScaled(value/1000000, nil, " ms")
} else if absValue < 60000000000 {
return toFixedScaled(value/1000000000, nil, " s")
} else if absValue < 3600000000000 {
return toFixedScaled(value/60000000000, nil, " min")
} else if absValue < 86400000000000 {
return toFixedScaled(value/3600000000000, nil, " hour")
} else {
return toFixedScaled(value/86400000000000, nil, " day")
}
}
// toMicroSeconds returns a easy to read string representation of the given value in microseconds
func toMicroSeconds(value float64) string {
absValue := math.Abs(value)
if absValue < 1000 {
return toFixed(value, nil) + " µs"
} else if absValue < 1000000 { // 2000 µs is better represented as 2 ms
return toFixedScaled(value/1000, nil, " ms")
} else {
return toFixedScaled(value/1000000, nil, " s")
}
}
// toMilliSeconds returns a easy to read string representation of the given value in milliseconds
func toMilliSeconds(value float64) string {
absValue := math.Abs(value)
if absValue < 1000 {
return toFixed(value, nil) + " ms"
} else if absValue < 60000 {
return toFixedScaled(value/1000, nil, " s")
} else if absValue < 3600000 {
return toFixedScaled(value/60000, nil, " min")
} else if absValue < 86400000 { // 172800000 ms is better represented as 2 day
return toFixedScaled(value/3600000, nil, " hour")
} else if absValue < 31536000000 {
return toFixedScaled(value/86400000, nil, " day")
}
return toFixedScaled(value/31536000000, nil, " year")
}
// toSeconds returns a easy to read string representation of the given value in seconds
func toSeconds(value float64) string {
absValue := math.Abs(value)
if absValue < 0.000001 {
return toFixedScaled(value*1e9, nil, " ns")
} else if absValue < 0.001 {
return toFixedScaled(value*1e6, nil, " µs")
} else if absValue < 1 {
return toFixedScaled(value*1e3, nil, " ms")
} else if absValue < 60 {
return toFixed(value, nil) + " s"
} else if absValue < 3600 {
return toFixedScaled(value/60, nil, " min")
} else if absValue < 86400 { // 56000 s is better represented as 15.56 hour
return toFixedScaled(value/3600, nil, " hour")
} else if absValue < 604800 {
return toFixedScaled(value/86400, nil, " day")
} else if absValue < 31536000 {
return toFixedScaled(value/604800, nil, " week")
}
return toFixedScaled(value/3.15569e7, nil, " year")
}
// toMinutes returns a easy to read string representation of the given value in minutes
func toMinutes(value float64) string {
absValue := math.Abs(value)
if absValue < 60 {
return toFixed(value, nil) + " min"
} else if absValue < 1440 {
return toFixedScaled(value/60, nil, " hour")
} else if absValue < 10080 {
return toFixedScaled(value/1440, nil, " day")
} else if absValue < 604800 {
return toFixedScaled(value/10080, nil, " week")
} else {
return toFixedScaled(value/5.25948e5, nil, " year")
}
}
// toHours returns a easy to read string representation of the given value in hours
func toHours(value float64) string {
absValue := math.Abs(value)
if absValue < 24 {
return toFixed(value, nil) + " hour"
} else if absValue < 168 {
return toFixedScaled(value/24, nil, " day")
} else if absValue < 8760 {
return toFixedScaled(value/168, nil, " week")
} else {
return toFixedScaled(value/8760, nil, " year")
}
}
// toDays returns a easy to read string representation of the given value in days
func toDays(value float64) string {
absValue := math.Abs(value)
if absValue < 7 {
return toFixed(value, nil) + " day"
} else if absValue < 365 {
return toFixedScaled(value/7, nil, " week")
} else {
return toFixedScaled(value/365, nil, " year")
}
}
// toWeeks returns a easy to read string representation of the given value in weeks
func toWeeks(value float64) string {
absValue := math.Abs(value)
if absValue < 52 {
return toFixed(value, nil) + " week"
} else {
return toFixedScaled(value/52, nil, " year")
}
}

View File

@ -0,0 +1,29 @@
package formatter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDuration(t *testing.T) {
durationFormatter := NewDurationFormatter()
assert.Equal(t, "1 s", durationFormatter.Format(1, "s"))
assert.Equal(t, "5 µs", durationFormatter.Format(5000, "ns"))
assert.Equal(t, "1 min", durationFormatter.Format(60, "s"))
assert.Equal(t, "18.2 min", durationFormatter.Format(1092000000000, "ns"))
assert.Equal(t, "20 min", durationFormatter.Format(1200, "s"))
assert.Equal(t, "3.40 µs", durationFormatter.Format(3400, "ns"))
assert.Equal(t, "1 µs", durationFormatter.Format(1000, "ns"))
assert.Equal(t, "1 s", durationFormatter.Format(1000, "ms"))
assert.Equal(t, "2 day", durationFormatter.Format(172800, "s"))
assert.Equal(t, "4 week", durationFormatter.Format(2419200, "s"))
assert.Equal(t, "1.01 year", durationFormatter.Format(31736420, "s"))
assert.Equal(t, "38.5 year", durationFormatter.Format(2000, "w"))
assert.Equal(t, "1 ns", durationFormatter.Format(1, "ns"))
assert.Equal(t, "69 ms", durationFormatter.Format(69000000, "ns"))
assert.Equal(t, "1.82 min", durationFormatter.Format(109200000000, "ns"))
assert.Equal(t, "1.27 day", durationFormatter.Format(109800000000000, "ns"))
assert.Equal(t, "2 day", durationFormatter.Format(172800000, "ms"))
}