1

JS compare function

 1 month ago
source link: https://www.wavebeem.com/blog/2024/04/02/js-compare-function/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The problem

The JS Array sort method is notorious for taking verbose comparison functions.

By adding a short compare function and using the || operator with unary negation (-), we can clearly express ordering without resorting to external libraries like Lodash.

The compare function

I went against my usual coding style (always use braces) in order to make this short function more memorable. I also spared y’all the horror of nested ?: conditional operators :)

function compare(a, b) {
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}

Note: This function won’t behave well with objects and arrays. A stricter function would throw an error or use a more complex algorithm to compare data structures.

What now?

Two key facts allow this function to shine:

  1. JS defines a || b as evaluating to either a or b, not just true or false

  2. Since -compare(a, b) is equivalent to compare(b, a), you can prefix each descending_ comparison with a special character to make it more obvious than flipping the arguments

Seeing as 0 || x evaluates to x, and compare(a, b) === 0 means a === b, the || operator lets us chain these “failing” comparisons elegantly to define complex ordering:

const users = [
  { name: "flynn", age: 48 },
  { name: "bridget", age: 36 },
  { name: "flynn", age: 40 },
  { name: "bridget", age: 34 },
];

// In SQL this would be `ORDER BY name DESC, age ASC`
[...users].sort((a, b) => -compare(a.name, b.name) || compare(a.age, b.age));
// =>
[
  { name: "flynn", age: 40 },
  { name: "flynn", age: 48 },
  { name: "bridget", age: 34 },
  { name: "bridget", age: 36 },
];

I think it’s pretty clear that compare is a massive improvement over writing a comparison function from scratch:

[...users].sort((a, b) => {
  // DESCENDING by name
  if (b.name < a.name) return -1;
  if (b.name > a.name) return 1;
  // ASCENDING by age
  if (a.age < b.age) return -1;
  if (a.age > b.age) return 1;
  return 0;
});
// =>
[
  { name: "flynn", age: 40 },
  { name: "flynn", age: 48 },
  { name: "bridget", age: 34 },
  { name: "bridget", age: 36 },
];

A real world example

And for a realistic example from my website Pokémon Type Calculator:

array.sort((a, b) => {
  return (
    // Put the largest cash bounty at the top
    -compare(languageBounty[a], languageBounty[b]) ||
    // Then sort by percentage completion
    compare(languageCompletions[a] || 0, languageCompletions[b] || 0) ||
    // Then sort by language name
    compare(a, b)
  );
});

It’s not quite as readable as Lodash’s orderBy, but I like how the ascending/descending information is colocated with the sort properties.

_.orderBy(
  array,
  [
    // Put the largest cash bounty at the top
    (item) => languageBounty[item],
    // Then sort by percentage completion
    (item) => languageCompletions[item] || 0,
    // Then sort by language name
    (item) => item,
  ],
  ["desc", "asc", "asc"]
);

Other thoughts

This pattern can be also applied to functions that return undefined or null in the failure case by chaining the ?? operator:

function parseInteger(value) {
  // Convert numbers to strings before parsing
  if (typeof value === "number") {
    return parseInteger(String(value));
  }
  // Ignore non-string values
  if (typeof value !== "string") {
    return undefined;
  }
  value = value.trim();
  // `Number("") === 0`, but we want `undefined`
  if (value === "") {
    return undefined;
  }
  const n = Number(value);
  // Check for NaN, Infinity, and -Infinity
  if (!Number.isFinite(n)) {
    return undefined;
  }
  // Remove the part after the decimal point
  return Math.trunc(n);
}

parseInteger(NaN) ??
  parseInteger(Infinity) ??
  parseInteger("   ") ??
  parseInteger("x2") ??
  parseInteger(" 42.1");
// => 42

You can even return entire objects, taking the result from the first call that didn’t fail. Debugging can be tougher compared with using exceptions or failure objects, though.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK