Yesterday I started writing up "How to see your download count on NPM", but ended up going on a tangent and writing my own npm download count library, in which I produce neat little ASCII tables like this:

Package Name                               Downloads

how-npm-am-i:                                      0
osx-wifi-volume-remote:                           34
hexdump.js:                                       41
jwk-to-ssh:                                       46
batchasync:                                       98
ssh-to-jwk:                                      111
sclient:                                         154
telebit:                                         325
unibabel:                                      1,826
authenticator-cli:                             8,796
authenticator:                                 9,265
human-readable-ids:                           10,957
s2-geometry:                                  17,667
letsencrypt:                                  19,926
knuth-shuffle:                                21,579
sequence:                                     50,900
cert-info:                                    66,161
acme:                                         70,879
keypairs:                                     80,491
foreachasync:                              1,183,985
walk:                                      1,260,904
btoa:                                      1,808,722
atob:                                     36,066,814

(generated by npx how-npm-am-i coolaj86 --verbose)

What's cool about it is is that it has variable-width columns that conform to the data.

TL;DR Hints

The full script is down at the bottom. This time I'm just putting the hints here.

(don't want to ruin all the fun, y'know?)

Really, all you need are these:

  • forEach to pre-loop through the rows of your table
  • Math.max to keep track of the longest columns of all rows
  • sort - to put it in ascending or descinding order
  • padStart (rightPad) to pad left-aligned columns
  • padEnd (leftPad) to pad right-aligned colums
  • toLocaleString to put the commas in numbers

Math.max()

Math.max() accepts any number of number arguments and returns the one closest to Infinity... or NaN (be careful about that).

Math.max(18, 5, 24);
// 24

Math.max(18, Infinity, 24);
// Infinity

Math.max(18, 'hotdog', 24);
// NaN

You can get the length of any string easy-peezy (you know this):

var nameLength = 'js-git'.length;

forEach(), map(), reduce()

So if you have an array of strings, you can loop over to get the length of the longest:

var maxLen = ['my', 'name', 'is'].reduce(function(max, str) {
    return Math.max(max, str.length);
}, 0);
// 4

I used reduce to be fancy, but the trusty-rusty forEach will do just as well:

var words = ['my', 'name', 'is'];
var maxLen = 0;
words.forEach(function(str) {
    maxLen = Math.max(maxLen, str.length);
});
// 4

padEnd(), padStart(), toLocaleString()

Then we could print each with a padding (+1 to leave a space between columns):

// a second loop
words.forEach(function(str) {
    console.log(str.padEnd(maxLen + 1, ' '));
});
// "my   "
// "name "
// "is   "

Likewise, we can calculate number length and print them in similar fasion:

var maxNumLen = 0;
var nums = [42, 65535, 17, 1100];
nums.forEach(function(n) {
    // (65535).toLocaleString() // -> 65,535
    maxNumLen = Math.max(maxNumLen, n.toLocaleString().length);
});
nums.map(function(n) {
    return n.toLocaleString().padStart(maxNumLen + 1, ' ');
});
[ "     42"
, " 65,535"
, "     17"
, "  1,100"
]

In the case that you have a table header or footer (such as "Package Name" and "Downloads"), don't forget to put their lengths in the mix too.

sort()

Oh! And one last thing, don't forget to sort your table. To put it in ascending order you can just subtract one download count from the other (or reverse it for descending order):

nums.sort(function(a, b) {
    return a - b;
});
// 17, 42, 1100, 65535

Putting it all together

If we put that all together in one big happy sloppy function we get something like this:

(and at the very least, this example does work copy/paste work - so feel free to try it out)

// raw rows of data
var pkgs = [
    { name: 'foobarbaz', count: 37 },
    { name: 'hadroncollider', count: 2700000 },
    { name: 'nomnomnoms', count: 8 }
];

// meta data for our table
var maxP = 'Package Name'.length;
var maxN = 'Downloads'.length;

// manipulations and transformations
var rows = pkgs
    .sort(function(a, b) {
        maxP = Math.max(maxP, a.name.length, b.name.length);
        maxN = Math.max(maxN, a.count.toLocaleString().length, b.count.toLocaleString().length);
        return a.count - b.count;
    })
    .map(function(pkg) {
        return {
            col1: pkg.name.padEnd(maxP + 1, ' '),
            col2: pkg.count.toLocaleString().padStart(maxN + 1, ' ')
        };
    });

// Print the table, finally
console.log('');
console.log('Package Name'.padEnd(maxP + 1, ' '), 'Downloads'.padStart(maxN + 1, ' '));
rows.forEach(function(row) {
    console.log(row.col1, row.col2);
});

// And a summary too - because why not
console.log(
    'Total:',
    pkgs.reduce(function(sum, pkg) {
        return sum + pkg.count;
    }, 0).toLocaleString()
);

Nix the comments and that's 30 lines, as promised.

I hope that helps, enjoy!


By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )