DEV Community

姜炜奇
姜炜奇

Posted on Fully Autonomous

Sort patch versions as numbers, but keep release history explicit

Disclosure: I maintain AzerothSurvivors.app, an independent fan reference for Aethus's game. This article was prepared with AI assistance. The code is a standalone example, not extracted game code or a claim about the site's implementation.

A patch-history page has at least two different orders: the numeric order of release identifiers and the order in which announcements were published. Treating those as the same field creates subtle mistakes.

Consider 0.9.0 and 0.10.0. A plain string sort can put the first label after the second because it compares the characters, not the numeric components. Turning the whole label into a decimal number is not a solution either: the third component is not a decimal fraction.

Here is a deliberately narrow approach for a reference that accepts exactly three numeric components, optionally prefixed with v.

Define the accepted format before sorting

The parser below rejects prerelease suffixes, missing components, whitespace and ambiguous leading zeroes. That is an explicit boundary, not a complete Semantic Versioning implementation. A project that publishes -beta labels needs an appropriate versioning library or a larger specification; silently removing the suffix would lose information.

export function parseRelease(value) {
  if (typeof value !== 'string') throw new TypeError('Version must be a string');
  const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(value);
  if (!match) throw new TypeError(`Unsupported release: ${value}`);
  const parts = match.slice(1).map(Number);
  if (!parts.every(Number.isSafeInteger)) throw new RangeError('Unsafe version component');
  return parts;
}

export function compareReleases(a, b) {
  const left = parseRelease(a);
  const right = parseRelease(b);
  for (let i = 0; i < left.length; i += 1) {
    if (left[i] !== right[i]) return left[i] < right[i] ? -1 : 1;
  }
  return 0;
}

export function newestFirst(records) {
  const seen = new Set();
  for (const row of records) {
    const canonical = parseRelease(row.version).join('.');
    if (seen.has(canonical)) throw new Error(`Duplicate release: ${canonical}`);
    seen.add(canonical);
  }
  return [...records].sort((a, b) => compareReleases(b.version, a.version));
}

Enter fullscreen mode Exit fullscreen mode

Save this as release-order.mjs. Sorting creates a copy rather than changing the caller's array. The duplicate check treats v0.114.2 and 0.114.2 as the same numeric release. It prevents a copied announcement from quietly appearing twice under different display labels.

The safe-integer check also makes the accepted input range explicit. These are ordinary version components, not arbitrary-length numeric identifiers. A failed check should send the record back for review, not substitute zero or guess the closest release.

Test the awkward cases, not only the happy path

Save this as release-order.test.mjs, then run node release-order.test.mjs. The example has 16 assertions, including seven malformed inputs and a check that the original order is preserved.

import assert from 'node:assert/strict';
import { parseRelease, compareReleases, newestFirst } from './release-order.mjs';
assert.deepEqual(parseRelease('v0.114.2'), [0, 114, 2]);
assert.equal(compareReleases('0.9.0', '0.10.0'), -1);
assert.equal(compareReleases('0.114.2', '0.114.1'), 1);
assert.equal(compareReleases('v0.114.2', '0.114.2'), 0);
assert.equal(compareReleases('1.0.0', '0.999.999'), 1);
for (const bad of ['', '0.114', '0.114.2-beta', ' 0.114.2', '01.2.3', '0.-1.0', 114]) {
  assert.throws(() => parseRelease(bad), TypeError);
}
assert.throws(() => parseRelease('9007199254740992.0.0'), RangeError);
assert.throws(() => newestFirst([{version:'0.114.2'}, {version:'v0.114.2'}]));
const input = [{version:'0.9.0'}, {version:'0.114.1'}, {version:'0.114.2'}];
assert.deepEqual(newestFirst(input).map(x => x.version), ['0.114.2', '0.114.1', '0.9.0']);
assert.equal(input[0].version, '0.9.0');
console.log('16 release-order assertions passed');

Enter fullscreen mode Exit fullscreen mode

A newer identifier does not erase an older explanation

The Azeroth Survivors update history is a concrete example of why ordering and context belong together. Its dated records distinguish successive co-op changes instead of merging them into one timeless paragraph. Readers can follow the source posts and see which release changed an earlier behavior.

A robust record therefore needs more than version. Keep the source publication time, the time you checked it, and the source reference separately. A website rebuild is not proof that a game release happened that day. Likewise, a delayed announcement can describe an earlier release.

For a ?latest releases? view, numeric descending order can be useful under the format contract above. For an incident timeline, the actual event time may be the right primary key. Do not choose one simply because it happens to produce the desired first row.

Keep editorial advice out of the comparator

The sorter should not decide whether an older recommendation remains valid. That requires checking what changed. An equipment rule, a lobby limit and a guest synchronization fix affect different sections of a guide; a version comparison alone cannot identify those dependencies.

A practical review step is to record the affected page and reason alongside an update. Keep the old entry intact, add the new one, and revise only the advice that the evidence actually changes. This is less dramatic than relabelling every page ?latest,? but it leaves a trail another editor can audit.

The fan reference is not affiliated with Aethus or Blizzard Entertainment and does not redistribute game files.

Top comments (0)