The below is pretty a simple utility that can correctly emulate RANK
formula (including ascending and descending ranking). If you need to make it sort by another column of the range or use another comparator, it is easy to change and make it more versatile.
What you need to do is sort a copy of the target column values by some criteria, get the resulting index, and increment it by 1
since Array indices are 0
-based.
/**
* @summary ranks a range by first column
* @param {number[][]} range
* @param {(1|0)} [ascending]
* @return {number[][]}
* @customfunction
*/
const rankRange = (values, ascending = 0) => {
try {
const rows = values.map(v => v).sort((a, b) => {
const aParsed = parseFloat(a[0]);
const bParsed = parseFloat(b[0]);
return ascending ?
aParsed - bParsed :
bParsed - aParsed;
});
return values.map((row) => {
return [rows.findIndex(r => r[0] === row[0]) + 1];
});
} catch (error) {
console.warn(`failed to rank column: ${error}`);
return [[]];
}
};
Make sure that the V8 runtime is enabled in your project for it to work (or transpile it to ES5 syntax before using).
Performance note - usage of map(v => v)
to shallow copy values is preferential. If you expect the number of items to be high, use slice()
(thanks to TheMaster's comment, I decided to investigate what the performance hit is). Run the interactive snippet below to see the difference:
const rng = document.querySelector("input[type=range]");
const arr = [];
const bench = (func, lbl) => {
console.time(lbl);
func();
console.timeEnd(lbl);
}
rng.addEventListener("change", () => {
const {
value
} = rng;
const toAdd = new Array(+value).fill(42);
arr.length = 0;
arr.push( ...toAdd );
bench(() => arr.slice(), "slice");
bench(() => arr.map(v => v), "map");
});
.rng {
width: 100%
}
<input class="rng" type="range" min="1" max="1e5" step="1">