This article will go through creating byte unit methods for both metric and binary prefixes. The two stringers will take a float64 value and round it with a metric unit.
Decimal(15000).String()
would return 15.0 kBBinary(15000).String()
would return 14.6 KiB
Unlike many other byte converters, this will also format the number using international digit separators. While numbers are universal, formatting is not.
For example, 15000.55 can be displayed differently depending on the country or language.
English: 15,000.55
Germany: 15.000,55
France: 15 000,55
Even if your program is primarily in English, it could be nice to allow users to localize numbers and byte units.
You can find the final result on GitHub or run it at Go Playground.
Localized separators
First, create a new global float64 named Decimal and a String method to convert any float64 values to a string.
package main
type Decimal float64
func (n Decimal) String() string {
return ""
}
Set up the main function with a placeholder number to print.
func main() {
s := Decimal(1000).String()
fmt.Println(s)
}
Running this is pointless as it will return a blank newline.
To make this string method useful, let’s apply language support using the Go text extended libraries. First, import the language package and create a t language.Tag parameter for String().
import (
"fmt"
"golang.org/x/text/language"
)
func (n Decimal) String(t language.Tag) string {
return ""
}
Add a new p message printer that formats our float64 value to the supplied language.
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
// round the n float to 2 precision points
return p.Sprintf("%.2f", n)
}
Finally, update the main function to use the t language Tag parameters, using English, German and French.
func main() {
const n = 1000.01
en := Decimal(n).String(language.English)
de := Decimal(n).String(language.German)
fr := Decimal(n).String(language.French)
fmt.Println(en)
fmt.Println(de)
fmt.Println(fr)
}
The result so far can be run in Go.
package main
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
type Decimal float64
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
// round the n float to 2 precision points
return p.Sprintf("%.2f", n)
}
func main() {
const n = 1000.01
en := Decimal(n).String(language.English)
de := Decimal(n).String(language.German)
fr := Decimal(n).String(language.French)
fmt.Println(en)
fmt.Println(de)
fmt.Println(fr)
}
$ go run .
1,000.01
1.000,01
1 000,01
Decimal units
Below the Decimal type, create a new kb const with a Decimal float64 type and a value of 1000.
type Decimal float64
const kb Decimal = 1000
Back to the String method, we will implement the kB unit. Create an x variable for switch case usage and a copy of the n Decimal float to the f variable. Finally, update the return to use the f variable.
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := n
switch {}
// round the n float to 2 precision points
return p.Sprintf("%.2f", f)
}
Update the switch to handle any x value greater or equal to the kb const and move the p message printer to the default case.
switch {
case x >= kb:
// do nothing
default:
// round the n float to 2 precision points
return p.Sprintf("%.2f", f)
}
In the switch case, divide the f Decimal value by the kb const and return the result. The Sprintf method also should return the kilobyte unit symbol.
f /= kb
is the same as f = f / kb
.
switch {
case x >= kb:
f /= kb
// round the n float to 1 precision point
return p.Sprintf("%.1f %s", f, "kB")
default:
// round the n float to 2 precision points
return p.Sprintf("%.2f", f)
}
We need to remove the two precision points from the default case as it returns the value as bytes, which display as integers, not precision floats.
default:
return p.Sprintf("%.0f", f)
The result so far should convert any byte value 1000 or greater into Kilobytes with one precision point. All other values get kept as bytes.
package main
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
type Decimal float64
const kb Decimal = 1000
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := n
switch {
case x >= kb:
f /= kb
// round the n float to 1 precision point
return p.Sprintf("%.1f %s", f, "kB")
default:
return p.Sprintf("%.0f", f)
}
}
func main() {
const n = 1000.01
en := Decimal(n).String(language.English)
de := Decimal(n).String(language.German)
fr := Decimal(n).String(language.French)
fmt.Println(en)
fmt.Println(de)
fmt.Println(fr)
fmt.Println(Decimal(123).String(language.Arabic))
}
$ go run .
1.0 kB
1,0 kB
1,0 kB
١٢٣
You can play around changing the n const value. But the switch logic has a problem. Whenever the n value is negative, the String method will display it as bytes.
const n = -1000.01
$ go run .
-1,000
To negate this, update the x var to save the absolute value of n.
import (
"fmt"
"math"
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := Decimal(math.Abs(float64(n)))
$ go run .
-1.0 kB
Let’s add support for Megabytes. Create a new mb Decimal const with a value of one million.
const (
kb Decimal = 1_000
mb Decimal = 1_000_000
)
Add a new case in the switch above the existing kb case to handle n values greater or equal to mb. All units that are equal or larger than a Megabyte will use all caps symbols and two precision points.
switch {
case x >= mb:
f /= mb
// round the n float to 2 precision points
return p.Sprintf("%.2f %s", f, "MB")
case x >= kb:
f /= kb
// round the n float to 1 precision point
return p.Sprintf("%.1f %s", f, "kB")
default:
return p.Sprintf("%.0f", f)
}
To test the new case, change the n value in main to a byte value larger than a million.
func main() {
const n = 9_876_543
$ go run .
9.88 MB
Now we will finish up the method to support units up to Exabytes (EB). In the Decimal const collection, add variables gb, tb, pb, and eb. Instead of using super long integer values, use the more manageable scientific notation.
const (
kb Decimal = 1e+3
mb Decimal = 1e+6
gb Decimal = 1e+9
tb Decimal = 1e+12
pb Decimal = 1e+15
eb Decimal = 1e+18
)
If these floats seem cryptic, consider the values as the digit 1 with x number of zeros.
kB = 1e+3
is a 1 combined with three zeros to make 1000. MB = 1e+6
is 1 combined with six zeros to make 1000000.
Create three constants to hold the various floating-point formats with none, one, and two precision points.
const (
precision0 = "%.0f"
precision1 = "%.1f\u00A0%s"
precision2 = "%.2f\u00A0%s"
)
%f
prints the float value.%s
prints the unit symbols (kB, MB, etc.).
\u00A0
is Unicode codepoint 160, no-break space to ensure the unit symbol is always with the float value.
The const precision0 is for byte values, so there is no need for a unit symbol.
In the switch, replace the p.Sprintf formats with the precision consts.
switch {
case x >= mb:
f /= mb
return p.Sprintf(precision2, f, "MB")
case x >= kb:
f /= kb
return p.Sprintf(precision1, f, "kB")
default:
return p.Sprintf(precision0, f)
}
Complete the switch with cases to support EB, PB, TB, and GB units in descending order.
switch {
case x >= eb:
f /= eb
return p.Sprintf(precision2, f, "EB")
case x >= pb:
f /= pb
return p.Sprintf(precision2, f, "PB")
case x >= tb:
f /= tb
return p.Sprintf(precision2, f, "TB")
case x >= gb:
f /= gb
return p.Sprintf(precision2, f, "GB")
case x >= mb:
f /= mb
return p.Sprintf(precision2, f, "MB")
case x >= kb:
f /= kb
return p.Sprintf(precision1, f, "kB")
default:
return p.Sprintf(precision0, f)
}
Try out some silly large n byte values and run the results.
func main() {
const n = 92233720368547758079223
en := language.English
de := language.German
fr := language.French
fmt.Println("English", Decimal(n).String(en))
fmt.Println("German", Decimal(n).String(de))
fmt.Println("French", Decimal(n).String(fr))
}
$ go run .
English 92,233.72 EB
German 92.233,72 EB
French 92 233,72 EB
Binary units
Creating the Binary method will be much quicker as it’s very similar to the Decimal method. The main difference is while the Decimal uses Base-10 metrics (KB = 1000), the Binary uses Base-2 metrics (KiB = 1024).
Create a new Binary type and constants for the binary bytes. These will rely on the left shift operator.
type (
Decimal float64
Binary float64
)
const (
kib Binary = 1 << 10
mib Binary = 1 << 20
gib Binary = 1 << 30
tib Binary = 1 << 40
pib Binary = 1 << 50
eib Binary = 1 << 60
)
If these are confusing, left-shifting implements powers of 2.
1 << 1
equates to 21 << 2
equates to 2 * 2 = 41 << 3
equates to 2 * 2 * 2 = 81 << 10
equates to 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 = 1,0241 << 20
equates to 1024 * 1024 = 1,048,5761 << 30
equates to 1024 * 1024 * 1024 = 1,073,741,824
Create a duplicate of the Decimal String method but replace the Decimal with Binary types.
func (n Binary) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := Binary(math.Abs(float64(n)))
switch {
case x >= eb:
f /= eb
return p.Sprintf(precision2, f, "EB")
case x >= pb:
f /= pb
return p.Sprintf(precision2, f, "PB")
case x >= tb:
f /= tb
return p.Sprintf(precision2, f, "TB")
case x >= gb:
f /= gb
return p.Sprintf(precision2, f, "GB")
case x >= mb:
f /= mb
return p.Sprintf(precision2, f, "MB")
case x >= kb:
f /= kb
return p.Sprintf(precision1, f, "kB")
default:
return p.Sprintf(precision0, f)
}
}
Finally, update the switch cases to use the binary constants and return binary units.
func (n Binary) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := Binary(math.Abs(float64(n)))
switch {
case x >= eib:
f /= eib
return p.Sprintf(precision2, f, "EiB")
case x >= pib:
f /= pib
return p.Sprintf(precision2, f, "PiB")
case x >= tib:
f /= tib
return p.Sprintf(precision2, f, "TiB")
case x >= gib:
f /= gib
return p.Sprintf(precision2, f, "GiB")
case x >= mib:
f /= mib
return p.Sprintf(precision2, f, "MiB")
case x >= kib:
f /= kib
return p.Sprintf(precision1, f, "KiB")
default:
return p.Sprintf(precision0, f)
}
}
Let’s wrap it up and test it out by adding some Binary methods in the main func.
package main
import (
"fmt"
"math"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
type (
Decimal float64
Binary float64
)
const (
kb Decimal = 1e+03
mb Decimal = 1e+06
gb Decimal = 1e+09
tb Decimal = 1e+12
pb Decimal = 1e+15
eb Decimal = 1e+18
)
const (
kib Binary = 1 << 10
mib Binary = 1 << 20
gib Binary = 1 << 30
tib Binary = 1 << 40
pib Binary = 1 << 50
eib Binary = 1 << 60
)
const (
precision0 = "%.0f"
precision1 = "%.1f\u00A0%s"
precision2 = "%.2f\u00A0%s"
)
func main() {
const n = 10000000000
en := language.English
de := language.German
fr := language.French
fmt.Println("English", Decimal(n).String(en))
fmt.Println("English", Binary(n).String(en))
fmt.Println("German", Decimal(n).String(de))
fmt.Println("German", Binary(n).String(de))
fmt.Println("French", Decimal(n).String(fr))
fmt.Println("French", Binary(n).String(fr))
}
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := Decimal(math.Abs(float64(n)))
switch {
case x >= eb:
f /= eb
return p.Sprintf(precision2, f, "EB")
case x >= pb:
f /= pb
return p.Sprintf(precision2, f, "PB")
case x >= tb:
f /= tb
return p.Sprintf(precision2, f, "TB")
case x >= gb:
f /= gb
return p.Sprintf(precision2, f, "GB")
case x >= mb:
f /= mb
return p.Sprintf(precision2, f, "MB")
case x >= kb:
f /= kb
return p.Sprintf(precision1, f, "kB")
default:
return p.Sprintf(precision0, f)
}
}
func (n Binary) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := Binary(math.Abs(float64(n)))
switch {
case x >= eib:
f /= eib
return p.Sprintf(precision2, f, "EiB")
case x >= pib:
f /= pib
return p.Sprintf(precision2, f, "PiB")
case x >= tib:
f /= tib
return p.Sprintf(precision2, f, "TiB")
case x >= gib:
f /= gib
return p.Sprintf(precision2, f, "GiB")
case x >= mib:
f /= mib
return p.Sprintf(precision2, f, "MiB")
case x >= kib:
f /= kib
return p.Sprintf(precision1, f, "KiB")
default:
return p.Sprintf(precision0, f)
}
}
$ go run .
English 10.00 GB
English 9.31 GiB
German 10,00 GB
German 9,31 GiB
French 10,00 GB
French 9,31 GiB
Congratulations, you now have working Binary and Decimal methods. I’ve also written a collection of unit tests for both. You can find the code on GitHub or run it at Go Playground.
