Counting digit occurrences in a range

I was asked if I knew an easy way to count how many times digits occur within a range of numbers. For example in the range 10–15, how many times does each digit occur? The numbers 0, 2, 3, 4 & 5 each occur once, and 1 occurs 6 times, that is twice in the number 11, and one in every other number in the range, because the tens all start with a 1. It seemed like an odd request, but it’s for number tags on utility poles, like this one:

There might be several rows of yellow tag numbers on a given utility pole

By Onore Baka Sama – Own work, Public Domain, source

Electricians use these tags to identify the poles and, at least in Hungary, they’re made up of individual plates for each digit, that are screwed on to a larger plate on the pole. If you estimate too few when ordering the digits you’ll run out obviously, and if you order too many you’ll be stuck with a pile of unused digits.

It seemed like a problem that should have a simple mathematical solution to it. Unfortunately, the answers I found on Math Overflow and Stack Overflow (like this one) went over my head in how far they went with optimising it. Considering the range of numbers this needs to work with IRL is relatively small in practice (e.g. 230–340), I decided to stick with the simple, straightforward approach: Iterate through each number in the range and tally each digit individually.

The code is up on github at sionleroux/count-digits but the main part is this bit of Javscript:

function countDigitOccurances(from, to) {
    const counts = {};
    for (let num = from; num <= to; num++) {
        num.toString().split("").forEach((digit) => {
            // Increment and if missing then start from zero
            counts[digit] = (counts[digit] ?? 0) +1
        })
    }
    return counts
}

The first version (3fb6131) had an initialisation step to set all digits to 0, but I didn’t like it and it also meant you’d have to filter the 0-values from the list afterwards if you didn’t want them displayed. If you don’t do this initialisation step you can’t increment the elements because by default they aren’t necessarily numbers, just missing values, that is nothing, or undefined. Adding +1 to undefined will give you NaN because undefined is indeed… Not a Number. I decided it’s more elegant to lazily set them to 0 when they’re first encountered, using the nullish coalescing operator ??.

Anyway, for this to be easily usable by others I made a simple web form for it, so it can be shared easily and used on your phone. Basic, minimalist design using unmodified sscaffold. Interactivity using Alpine.js, lightweight and gives a kind of “if jQuery was cool again” vibe. The table content rows’ markup is defined with an HTML <template> tag, really cool new (2017) HTML feature that lets you write elements that won’t be rendered to the page, but are available in the DOM for use later with Javascript. With Alpine you make a loop, by adding the x-for attribute to the template element, mentioning the data you want it to iterate over, like this:

<table x-cloak x-show="Object.keys(counts).length > 0" x-transition>
    <thead><tr><th>Digit</th><th>Occurances</th></tr></thead>
    <tbody>
        <template x-for="[digit, occurances] in Object.entries(counts)">
            <tr>
                <td x-text="digit"></td>
                <td x-text="occurances"></td>
            </tr>
        </template>
    </tbody>
</table>

The values’ text is inserted using x-text and to only show the table of results when there is at least one item to show, we use x-show with the length of the counts list as the condition. The x-transition attribute is used for polish, to give the table a fade-in transition when it appears the first time, and x-cloak is used to hide the table header in the first render, to avoid it flickering out of sight in the moment when Alpine loads and hides it.

Here’s a link if you want to share it with anyone: sionleroux.com/count-digits.html and here’s an embedded version to try out live: