Among the mod-tools there is a few that are intended to just help get a bird's eye perspective on what happens on the site. These tools generally consist of tables. Tables over tables.
Stuff that would make an executive business analyst happy.
One of these tools reports some quite useful numbers as both absolute numbers and percentages. These percentages are pretty useful, but each of them is in a separate column.
The table columns basically follow this schema:
- User
- Metric A
- Metric B
- ...
- Metric F
- Metric A%
- Metric B%
- ...
- Metric F%
- Metric G
- Metric H
- ...
Not every metric has a corresponding percentage report. None of the percentage reports comes before its corresponding metric. Unfortunately there's a rather large number of these metrics, which results in the table exceeding the horizontal space available on my screen most of the time.
That's not useful, and neither is the separation between the Metrics and their percentage report. So I wrote the following user-script to collapse the percentage reports into the same column as the absolute numbers.
// ==UserScript==
// @name Advanced Review Stats TableCollapser
// @namespace http://github.com/Vogel612
// @version 1.0
// @description Collapse review-stats columns
// @updateURL https://raw.githubusercontent.com/Vogel612/mini-se-userscripts/master/advanced-review-stats-collapser.user.js
// @downloadURL https://raw.githubusercontent.com/Vogel612/mini-se-userscripts/master/advanced-review-stats-collapser.user.js
// @author Vogel612
// @match *://*.stackexchange.com/admin/review/breakdown*
// @match *://*.stackoverflow.com/admin/review/breakdown*
// @match *://*.superuser.com/admin/review/breakdown*
// @match *://*.serverfault.com/admin/review/breakdown*
// @match *://*.askubuntu.com/admin/review/breakdown*
// @match *://*.stackapps.com/admin/review/breakdown*
// @match *://*.mathoverflow.net/admin/review/breakdown*
// @grant none
// ==/UserScript==
(function() {
'use strict';
let statsTable = document.querySelectorAll("#content .mainbar-full table")[0];
let headers = Array.from(statsTable.getElementsByTagName("th"));
let percentageHeaders = filterHeaders(headers);
let collapsings = buildCollapseSpecs(headers, percentageHeaders);
collapseTable(statsTable, collapsings);
function filterHeaders(headers) {
let percentageHeaders = [];
for (let head of headers) {
if (head.innerHTML.endsWith("%")) {
percentageHeaders.push(head);
}
}
return percentageHeaders;
}
function buildCollapseSpecs(headers, percentageHeaders) {
let collapsings = [];
for (let percentage of percentageHeaders) {
let h = percentage.innerHTML;
// drop trailing %
let header = h.substr(0, h.length - 1);
let from = headers.indexOf(percentage);
// find matching non-percentage header
var to = -1;
for (let tableHead of headers) {
if (tableHead.innerHTML === header) {
to = headers.indexOf(tableHead);
break;
}
}
// we can't collapse upwards
if (to !== -1 && to <= from) {
collapsings.push({from: from, to: to});
}
}
return collapsings;
}
function collapseTable(table, collapsings) {
// sort collapsings by from descending to allow us to drop the column we're done with
collapsings.sort((a,b) => { return b.from - a.from });
// collapse the columns
for (let collapse of collapsings) {
for (let row of table.getElementsByTagName("tr")) {
collapseRow(row, collapse);
}
}
}
function collapseRow(row, collapseSpec) {
let from = row.children[collapseSpec.from];
let percentageValue = from.innerHTML.trim();
if (from.tagName === "TH") {
row.children[collapseSpec.to].innerHTML += " (%)";
} else {
row.children[collapseSpec.to].innerHTML += " (" + percentageValue + ")";
}
row.removeChild(from);
}
})();
How could this script be more extensible and maintainable?
1 Answer 1
Algorithm
The algorithm to build the collapse-specs is inefficient:
- It makes a pass over all headers to find the percentage headers
- Then for each percentage header:
- It finds the index using
Array.indexOf
, which is \$O(n)\$ - It loops over the headers to find the one corresponding to the percentage header by name, and again finds the index of that header using
Array.indexOf
- It finds the index using
That is, the multiple Array.indexOf
calls to find the index of header columns is inefficient, when the indexes could be recorded in one preparatory pass.
A simpler and more efficient alternative is possible:
- Build a map of
{ name: {from: ..., to: ...} }
, wherename
is the trailing%
stripped from the column header being visited. - For each header:
- If it's not a percentage header, update in the map the
to
value for the header's name - If it's a percentage header, update in the map the
from
value for the header's sanitized name
- If it's not a percentage header, update in the map the
- Extract from the map the values that have both
from
andto
values -> this should be equivalent to thecollapsings
in the posted code
Functional programming
Functional writing style can make the code more compact,
and potentially more natural and easier to read.
For example you could replace filterHeaders
with this one-liner:
return headers.filter(head => head.innerHTML.endsWith("%"))
Similarly, the sort can be written simpler too:
collapsings.sort((a, b) => b.from - a.from);
Other parts of the script can be written simpler too, using functional features, but I suggest to change the algorithm first before optimizing the existing code.
Explore related questions
See similar questions with these tags.