diff --git a/utils/types.ts b/utils/types.ts index f3924494..efc203e8 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -15,3 +15,11 @@ export interface CountryInfo { fiscal_year_end: string; locale: string; } + + +export interface VersionParts { + major: number; + minor: number; + patch: number; + beta?: number; +}; \ No newline at end of file diff --git a/utils/version.ts b/utils/version.ts new file mode 100644 index 00000000..007c0820 --- /dev/null +++ b/utils/version.ts @@ -0,0 +1,93 @@ +import { VersionParts } from './types'; + +export class Version { + /** + * comparators for version strings of the form + * x.x.x[-beta.x] + */ + + static gte(a: string, b: string) { + return compare(a, b, (c) => c < 0); + } + + static lte(a: string, b: string) { + return compare(a, b, (c) => c > 0); + } + + static eq(a: string, b: string) { + return compare(a, b, (c) => c !== 0); + } + + static gt(a: string, b: string) { + return Version.gte(a, b) && !Version.eq(a, b); + } + + static lt(a: string, b: string) { + return Version.lte(a, b) && !Version.eq(a, b); + } +} + +const seq = ['major', 'minor', 'patch', 'beta'] as (keyof VersionParts)[]; + +function compare(a: string, b: string, isInvalid: (x: number) => boolean) { + const partsA = parseVersionString(a); + const partsB = parseVersionString(b); + + for (const p of seq) { + const c = compareSingle(partsA, partsB, p); + if (isInvalid(c)) { + return false; + } + } + + return true; +} + +function parseVersionString(a: string): VersionParts { + const parts = a.split('-'); + const nonbeta = parts[0].split('.').map((n) => parseFloat(n)); + + const versionParts: VersionParts = { + major: nonbeta[0], + minor: nonbeta[1], + patch: nonbeta[2], + }; + + const beta = parseFloat(parts[1]?.split('.')?.[1]); + if (!Number.isNaN(beta)) { + versionParts.beta = beta; + } + + if (Number.isNaN(beta) && parts[1]?.includes('beta')) { + versionParts.beta = 0; + } + + return versionParts; +} + +function compareSingle( + partsA: VersionParts, + partsB: VersionParts, + key: keyof VersionParts +): number { + if (key !== 'beta') { + return partsA[key] - partsB[key]; + } + + if (typeof partsA.beta === 'number' && typeof partsB.beta === 'number') { + return partsA.beta - partsB.beta; + } + + // A is not in beta + if (partsA.beta === undefined && typeof partsB.beta === 'number') { + return 1; + } + + // B is not in beta + if (typeof partsA.beta === 'number' && partsB.beta === undefined) { + return -1; + } + + // Both A and B are not in Beta + return 0; +}