Integer math
I very much enjoy making tabular data easy to read on the terminal. Often, this takes me to strange and wonderful places.
tabular data and scanning and pretty
When writing tabular data to the terminal, ‘pretty’ is often a shorthand for ‘easy to read’, ‘easy to scan’, and ‘aligned’. Nail the last one and you get most of the other two already.
So if you have a bunch of numbers (I’m talking int
s and the like, here), you
want them to line up in a way the user expects, and that is
usually
I have no idea how this works in languages where
text doesn’t flow left-to-right, beyond that they exist, and that it’s
complicated and contextual, so I’m assuming left-to-right text and look forward
to learning about other things some day
with numbers aligned to
the right.
Year Units Net
2021 7 7
2022 14 -23
2023 654386 5
If you’re using Go, and using its standard library’s text/tabwriter
, and all
you’re printing is numbers, and you don’t mind if your column headers are also
aligned to the right, it has a handy AlignRight
formatting option that aligns
everything to the right and then you’re done:
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight)
w.Write(data)
w.Flush()
gives you
Year Units Net
2021 7 7
2022 14 -23
2023 654386 5
which is quite good.
how long is that number in the window?
However, sometimes you have other things than numbers mixed in, like a column of
ASCII text
Non-ASCII text will be covered in a future
article.
. Or you are required to align the column headers to the
left. In this case, you need to align the numbers to the right using something
like fmt.Sprintf("%5d", num)
, where often you’ll have a natural upper bound
for the width of the numbers as presented on your terminal (e.g. for years
that’ll be 4, or 6 if you’re into history or futurology). But sometimes that
’natural upper bound’ is something very large and unusual like 1<<64
. So how
much space do you really need for your numbers? You’ve got to walk all the numbers in
the column, find the extremes, and see how big they are.
How do you see how big they are? Well, you could just check the length of
its fmt.Sprint
. Easy!
func lenFmt[V constraints.Unsigned](n V) V {
return V(len(fmt.Sprint(n)))
}
However, if you’re suspicious of fmt
vis-à-vis its performance and/or memory
usage (and you probably should be) you might want to tweak it a little, without
changing the ‘algorithm’,
func lenStrconv[V constraints.Unsigned](n V) V {
return V(len(strconv.FormatUint(uint64(n), 10)))
}
Now, the length of the decimal representation of an integer u
is just
⌊log₁₀u⌋+1
(with a special case for 0). So you could do just that,
func lenMath[V constraints.Unsigned](n V) V {
return V(math.Floor(math.Log10(float64(n)))) + 1
}
or … you could use integer math to do ⌊log₁₀u⌋
. Which is what I’ve done in
intmath.Len
.
$ go test -short -bench Len -benchmem
goos: linux
goarch: amd64
pkg: chipaca.com/intmath
cpu: 12th Gen Intel(R) Core(TM) i5-12600H
BenchmarkLenFmt-12 21367644 49.89 ns/op 16 B/op 1 allocs/op
BenchmarkLenStrconv-12 76015239 15.01 ns/op 7 B/op 0 allocs/op
BenchmarkLenMath-12 179116516 6.703 ns/op 0 B/op 0 allocs/op
BenchmarkLenInt-12 1000000000 0.3558 ns/op 0 B/op 0 allocs/op
PASS
ok chipaca.com/intmath 5.826s
chipaca.com/intmath
is very much still a work in progress, but the idea is for
it to complement math/bits
so that together they can do most of the boring
stuff from math
, but using algorithms that work faster for integers.