Last active
March 3, 2023 17:47
-
-
Save prantlf/c70054108908516ea14327ab9a2df509 to your computer and use it in GitHub Desktop.
Tests and benchmarks for formatting integers with commas separating thousands
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Save this file as main_test.go and run "go test -bench ." | |
package main | |
import ( | |
"bytes" | |
"fmt" | |
"regexp" | |
"strconv" | |
"testing" | |
"github.com/dustin/go-humanize" | |
"golang.org/x/text/language" | |
"golang.org/x/text/message" | |
) | |
func formatIntByDividing(number int) string { | |
var output string | |
negative := false | |
if number < 0 { | |
negative = true | |
number = -number | |
} | |
for { | |
numberPart := number % 1000 | |
outputGroup := strconv.Itoa(numberPart) | |
number /= 1000 | |
if number == 0 { | |
output = outputGroup + output | |
break | |
} | |
if numberPart >= 100 { | |
output = "," + outputGroup + output | |
} else if numberPart >= 10 { | |
output = ",0" + outputGroup + output | |
} else { | |
output = ",00" + outputGroup + output | |
} | |
} | |
if negative { | |
output = "-" + output | |
} | |
return output | |
} | |
func formatIntByInserting(number int) string { | |
output := strconv.Itoa(number) | |
startOffset := 3 | |
if number < 0 { | |
startOffset++ | |
} | |
for outputIndex := len(output); outputIndex > startOffset; { | |
outputIndex -= 3 | |
output = output[:outputIndex] + "," + output[outputIndex:] | |
} | |
return output | |
} | |
func formatIntByCollecting(number int) string { | |
input := strconv.Itoa(number) | |
inputLength := len(input) | |
numberOfDigits := inputLength | |
if number < 0 { | |
numberOfDigits-- | |
} | |
numberOfCommas := (numberOfDigits - 1) / 3 | |
if numberOfCommas == 0 { | |
return input | |
} | |
outputLength := inputLength + numberOfCommas | |
output := make([]byte, outputLength) | |
if number < 0 { | |
input, output[0] = input[1:], '-' | |
inputLength-- | |
} | |
for inputIndex, outputIndex, indexInGroup := inputLength-1, outputLength-1, 0; ; { | |
output[outputIndex] = input[inputIndex] | |
if inputIndex == 0 { | |
return string(output) | |
} | |
if indexInGroup++; indexInGroup == 3 { | |
outputIndex-- | |
indexInGroup = 0 | |
output[outputIndex] = ',' | |
} | |
inputIndex-- | |
outputIndex-- | |
} | |
} | |
func formatIntByCopying(number int) string { | |
input := strconv.Itoa(number) | |
inputLength, startOffset := len(input), 0 | |
if number < 0 { | |
startOffset = 1 | |
} | |
numberOfCommas := (inputLength - startOffset - 1) / 3 | |
if numberOfCommas == 0 { | |
return input | |
} | |
outputLength := inputLength + numberOfCommas | |
buffer := make([]byte, outputLength) | |
startOffset += 3 | |
inputIndex, outputIndex := inputLength, outputLength | |
for inputIndex > startOffset { | |
inputIndex -= 3 | |
outputIndex -= 3 | |
copy(buffer[outputIndex:outputIndex+3], input[inputIndex:]) | |
outputIndex-- | |
buffer[outputIndex] = ',' | |
} | |
if outputIndex > 0 { | |
copy(buffer[:outputIndex], input) | |
} | |
return string(buffer) | |
} | |
func formatIntWithBuffer(number int) string { | |
input := strconv.Itoa(number) | |
startOffset := 0 | |
var buffer bytes.Buffer | |
if number < 0 { | |
startOffset = 1 | |
buffer.WriteByte('-') | |
} | |
inputLength := len(input) | |
numberOfCommas := (inputLength - startOffset - 1) / 3 | |
if numberOfCommas == 0 { | |
return input | |
} | |
commaIndex := 3 - ((inputLength - startOffset) % 3) | |
if commaIndex == 3 { | |
commaIndex = 0 | |
} | |
for inputIndex := startOffset; inputIndex < inputLength; inputIndex++ { | |
if commaIndex == 3 { | |
buffer.WriteRune(',') | |
commaIndex = 0 | |
} | |
commaIndex++ | |
buffer.WriteByte(input[inputIndex]) | |
} | |
return buffer.String() | |
} | |
var integerGrouping = regexp.MustCompile("(\\d+)(\\d{3})") | |
func formatIntWithRexExp(number int) string { | |
input := strconv.Itoa(number) | |
for { | |
previousInput := input | |
input = integerGrouping.ReplaceAllString(input, "$1,$2") | |
if previousInput == input { | |
return input | |
} | |
} | |
} | |
func formatIntWithHumanize(number int) string { | |
return humanize.Comma(int64(number)) | |
} | |
var integerPrinter = message.NewPrinter(language.English) | |
func formatIntWithTextMessage(number int) string { | |
return integerPrinter.Sprint(number) | |
} | |
func formatIntUsingRecursion(number int) string { | |
if (number < 0) { | |
return "-" + formatIntUsingRecursion(-number) | |
} | |
if (number < 1000) { | |
return fmt.Sprintf("%d", number) | |
} | |
return formatIntUsingRecursion(number / 1000) + "," + fmt.Sprintf("%03d", number % 1000) | |
} | |
func formatIntForBenchmarks(number int) string { | |
str := strconv.Itoa(number) | |
l_str := len(str) | |
digits := l_str | |
if number < 0 { | |
digits-- | |
} | |
commas := (digits + 2) / 3 - 1 | |
l_buf := l_str + commas | |
var sbuf [32]byte // pre allocate buffer at stack rather than make([]byte,n) | |
buf := sbuf[0:l_buf] | |
// copy str from the end | |
for s_i, b_i, c3 := l_str-1, l_buf-1, 0; ; { | |
buf[b_i] = str[s_i] | |
if s_i == 0 { | |
return string(buf) | |
} | |
s_i-- | |
b_i-- | |
// insert comma every 3 chars | |
c3++ | |
if c3 == 3 && (s_i > 0 || number > 0) { | |
buf[b_i] = ',' | |
b_i-- | |
c3 = 0 | |
} | |
} | |
} | |
var testCases = []struct { | |
number int | |
result string | |
}{ | |
{1, "1"}, | |
{10, "10"}, | |
{100, "100"}, | |
{1000, "1,000"}, | |
{10000, "10,000"}, | |
{100000, "100,000"}, | |
{1000000, "1,000,000"}, | |
{10000000, "10,000,000"}, | |
{100000000, "100,000,000"}, | |
{1000000000, "1,000,000,000"}, | |
{10000000000, "10,000,000,000"}, | |
{100000000000, "100,000,000,000"}, | |
{1000000000000, "1,000,000,000,000"}, | |
{-1, "-1"}, | |
{-10, "-10"}, | |
{-100, "-100"}, | |
{-1000, "-1,000"}, | |
{-10000, "-10,000"}, | |
{-100000, "-100,000"}, | |
{-1000000, "-1,000,000"}, | |
{-10000000, "-10,000,000"}, | |
{-100000000, "-100,000,000"}, | |
{-1000000000, "-1,000,000,000"}, | |
{-10000000000, "-10,000,000,000"}, | |
{-100000000000, "-100,000,000,000"}, | |
{-1000000000000, "-1,000,000,000,000"}, | |
} | |
func testFormatInt(t *testing.T, formatInt func(int) string, number int, expected string) { | |
actual := formatInt(number) | |
if actual != expected { | |
t.Errorf("Expected %s, but got %s for %d.", expected, actual, number) | |
} | |
} | |
func makeTest(formatInt func(int) string) func(t *testing.T) { | |
return func(t *testing.T) { | |
for _, testCase := range testCases { | |
actual := formatInt(testCase.number) | |
if actual != testCase.result { | |
t.Errorf("Expected %s, but got %s for %d.", testCase.result, actual, testCase.number) | |
} | |
} | |
} | |
} | |
var tests = []struct { | |
name string | |
function func(t *testing.T) | |
}{ | |
{"by dividing", makeTest(formatIntByDividing)}, | |
{"by inserting", makeTest(formatIntByInserting)}, | |
{"by collecting", makeTest(formatIntByCollecting)}, | |
{"by copying", makeTest(formatIntByCopying)}, | |
{"with buffer", makeTest(formatIntWithBuffer)}, | |
{"with regexp", makeTest(formatIntWithRexExp)}, | |
{"with humanize", makeTest(formatIntWithHumanize)}, | |
{"with text/message", makeTest(formatIntWithTextMessage)}, | |
{"using recursion", makeTest(formatIntUsingRecursion)}, | |
{"for benchmarks", makeTest(formatIntForBenchmarks)}, | |
} | |
func Test(t *testing.T) { | |
for _, test := range tests { | |
function := test.function | |
t.Run(test.name, func(t *testing.T) { | |
function(t) | |
}) | |
} | |
} | |
func makeBenchmark(formatInt func(int) string) func() { | |
return func() { | |
formatInt(1) | |
formatInt(10) | |
formatInt(100) | |
formatInt(1000) | |
formatInt(10000) | |
formatInt(100000) | |
formatInt(1000000) | |
formatInt(10000000) | |
formatInt(100000000) | |
formatInt(1000000000) | |
formatInt(10000000000) | |
formatInt(100000000000) | |
formatInt(1000000000000) | |
formatInt(-1) | |
formatInt(-10) | |
formatInt(-100) | |
formatInt(-1000) | |
formatInt(-10000) | |
formatInt(-100000) | |
formatInt(-1000000) | |
formatInt(-10000000) | |
formatInt(-100000000) | |
formatInt(-1000000000) | |
formatInt(-10000000000) | |
formatInt(-100000000000) | |
formatInt(-1000000000000) | |
} | |
} | |
var benchmarks = []struct { | |
name string | |
function func() | |
}{ | |
{"by dividing", makeBenchmark(formatIntByDividing)}, | |
{"by inserting", makeBenchmark(formatIntByInserting)}, | |
{"by collecting", makeBenchmark(formatIntByCollecting)}, | |
{"by copying", makeBenchmark(formatIntByCopying)}, | |
{"with buffer", makeBenchmark(formatIntWithBuffer)}, | |
{"with regexp", makeBenchmark(formatIntWithRexExp)}, | |
{"with humanize", makeBenchmark(formatIntWithHumanize)}, | |
{"with text/message", makeBenchmark(formatIntWithTextMessage)}, | |
{"using recursion", makeBenchmark(formatIntUsingRecursion)}, | |
{"for benchmarks", makeBenchmark(formatIntForBenchmarks)}, | |
} | |
func Benchmark(b *testing.B) { | |
for _, benchmark := range benchmarks { | |
function := benchmark.function | |
b.Run(benchmark.name, func(b *testing.B) { | |
for i := 0; i < b.N; i++ { | |
function() | |
} | |
}) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# MacBook 2018, i7 2.6GHz | |
$ go test -bench . | |
goos: darwin | |
goarch: amd64 | |
pkg: test | |
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz | |
Benchmark/by_dividing-12 296612 3834 ns/op | |
Benchmark/by_inserting-12 360346 3143 ns/op | |
Benchmark/by_collecting-12 583008 1969 ns/op | |
Benchmark/by_copying-12 569337 2136 ns/op | |
Benchmark/with_buffer-12 382239 3261 ns/op | |
Benchmark/with_regexp-12 18475 67886 ns/op | |
Benchmark/with_humanize-12 305864 3839 ns/op | |
Benchmark/with_text/message-12 85184 14650 ns/op | |
Benchmark/using_recursion-12 118117 11807 ns/op | |
Benchmark/for_benchmarks-12 725008 1807 ns/op | |
PASS | |
ok test 17.965s |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment