Skip to content

Commit

Permalink
Implement proper version sorting
Browse files Browse the repository at this point in the history
Got bored and wanted to explore Typescript.

Tested on https://typescriptlang.org/play:

    > console.log(sortVersions(['1.2.3', '1.2.3-d', '1.2.3.bcr.1', '1.2.3rc3', '1', 'k']))
    ["k", "1.2.3rc3", "1.2.3.bcr.1", "1.2.3", "1.2.3-d", "1"] 

Addresses #54.

Fixes #142.
  • Loading branch information
Wyverald authored Jul 24, 2024
1 parent 7d296a8 commit 7b4d324
Showing 1 changed file with 68 additions and 17 deletions.
85 changes: 68 additions & 17 deletions data/moduleStaticProps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { compareVersions, validate as validateVersion } from 'compare-versions'
import {
extractModuleInfo,
getModuleMetadata,
Expand Down Expand Up @@ -53,23 +52,75 @@ export const getStaticPropsModulePage = async (
}

/**
* Sort versions, by splitting them into sortable and unsortable ones.
* Parse and sort versions according to https://bazel.build/external/module#version_format.
*
* The sortable versions will form the start of the list and are sorted, while the unsortable ones will
* form the end of it.
*
* This is mostly a placeholder until we have proper version parsing and comparison
* (see discussion in https://github.com/bazel-contrib/bcr-ui/issues/54).
* This is a placeholder until we switch to a common "Bzlmod semver" library
* (see https://github.com/bazel-contrib/bcr-ui/issues/54#issuecomment-1521844089).
*/
const sortVersions = (versions: string[]): string[] => {
const sortableVersions = versions.filter((version) =>
validateVersion(version)
)
const unsortableVersions = versions.filter(
(version) => !validateVersion(version)
)
sortableVersions.sort(compareVersions)
sortableVersions.reverse()

return [...sortableVersions, ...unsortableVersions]
type Version = {
release: string[],
prerelease: string[],
original: string,
}

const parseVersion = (v: string): Version => {
const firstDash = v.indexOf('-')
if (firstDash === -1) {
return { release: v.split('.'), prerelease: [], original: v }
}
return {
release: v.slice(0, firstDash).split('.'),
prerelease: v.slice(firstDash + 1).split('.'),
original: v,
}
}

type Cmp<T> = (a: T, b: T) => number

function natural<T>(a: T, b: T): number {
return a < b ? -1 : a > b ? 1 : 0
}

function comparing<T, U>(f: (t: T) => U, innerCmp: Cmp<U> = natural): Cmp<T> {
return (a: T, b: T): number => innerCmp(f(a), f(b))
}

function lexicographically<T>(elementCmp: Cmp<T> = natural): Cmp<T[]> {
return (as: T[], bs: T[]): number => {
for (let i = 0; i < as.length && i < bs.length; i++) {
const result = elementCmp(as[i], bs[i])
if (result !== 0) return result
}
return as.length - bs.length
}
}

function composeCmps<T>(...cmps: Cmp<T>[]): Cmp<T> {
return (a: T, b: T): number => {
for (const cmp of cmps) {
const result = cmp(a, b)
if (result !== 0) return result
}
return 0
}
}

const compareIdentifiers: Cmp<string> = composeCmps(
comparing((id) => !/^\d+$/.test(id)), // pure numbers compare less than non-numbers
comparing((id) => /^\d+$/.test(id) ? parseInt(id) : 0),
natural,
)

const compareVersions: Cmp<Version> = composeCmps(
comparing((v) => v.release, lexicographically(compareIdentifiers)),
comparing((v) => v.prerelease.length === 0), // nonempty prerelease compares less than empty prerelease
comparing((v) => v.prerelease, lexicographically(compareIdentifiers)),
)

const sortVersions = (versions: string[]): string[] => {
const parsed = versions.map(parseVersion)
parsed.sort(compareVersions)
parsed.reverse()
return parsed.map((v) => v.original)
}

0 comments on commit 7b4d324

Please sign in to comment.