@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
112 lines (90 loc) • 3.89 kB
text/typescript
import { isString } from '../utils/lang';
const NUMERIC_IDENTIFIER_REGEX = /^[0-9]+$/;
const METADATA_DELIMITER = '+';
const PRERELEASE_DELIMITER = '-';
const VALUE_DELIMITER = '.';
/**
* Compares two strings. If both strings are numeric identifiers, they are compared numerically. Otherwise, they are compared lexicographically.
* This could be implemented using `a.localeCompare(b, undefined, { numeric: true })` but locale options are not broadly supported.
*/
function compareStrings(a: string, b: string): number {
if (NUMERIC_IDENTIFIER_REGEX.test(a) && NUMERIC_IDENTIFIER_REGEX.test(b)) {
const result = a.length - b.length;
if (result !== 0) {
return result;
}
}
return a < b ? -1 : a > b ? 1 : 0;
}
// Sanitizes a numeric identifier by removing leading zeros
function sanitizeNumericIdentifier(value: string): string {
return value.replace(/^0+(?=\d)/, '');
}
function throwError(version: string) {
throw new Error('Unable to convert to Semver, incorrect format: ' + version);
}
export class Semver {
private readonly _major: string;
private readonly _minor: string;
private readonly _patch: string;
private readonly _preRelease: string[];
private readonly _isStable: boolean;
// Version string for 'equal' and 'in list' comparisons
public readonly version: string;
public constructor(version: string) {
if (!isString(version)) throwError(version);
// Separate metadata if exists
let index = version.indexOf(METADATA_DELIMITER);
let [vWithoutMetadata, metadata] = index === -1 ? [version] : [version.slice(0, index), version.slice(index + 1)];
if (metadata === '') throwError(version);
// Set pre-release versions if exists
index = vWithoutMetadata.indexOf(PRERELEASE_DELIMITER);
if (index === -1) {
this._isStable = true;
this._preRelease = [];
} else {
this._isStable = false;
this._preRelease = vWithoutMetadata.slice(index + 1).split(VALUE_DELIMITER).map((value) => {
if (!value) throwError(version);
return NUMERIC_IDENTIFIER_REGEX.test(value) ?
sanitizeNumericIdentifier(value) :
value;
});
vWithoutMetadata = vWithoutMetadata.slice(0, index);
}
// Set major, minor, and patch versions
const vParts = vWithoutMetadata.split(VALUE_DELIMITER).map((value) => {
if (!value || !NUMERIC_IDENTIFIER_REGEX.test(value)) throwError(version);
return sanitizeNumericIdentifier(value);
});
if (vParts.length !== 3) throwError(version);
this._major = vParts[0];
this._minor = vParts[1];
this._patch = vParts[2];
// Set version string
this.version = vParts.join(VALUE_DELIMITER);
if (this._preRelease.length) this.version += PRERELEASE_DELIMITER + this._preRelease.join(VALUE_DELIMITER);
if (metadata) this.version += METADATA_DELIMITER + metadata;
}
/**
* Precedence comparision between 2 Semver objects.
*
* @returns `0` if `this === toCompare`, `-1` if `this < toCompare`, and `1` if `this > toCompare`
*/
public compare(toCompare: Semver): number {
if (this.version === toCompare.version) return 0;
let result = compareStrings(this._major, toCompare._major);
if (result !== 0) return result;
result = compareStrings(this._minor, toCompare._minor);
if (result !== 0) return result;
result = compareStrings(this._patch, toCompare._patch);
if (result !== 0) return result;
if (!this._isStable && toCompare._isStable) return -1;
if (this._isStable && !toCompare._isStable) return 1;
for (let i = 0, length = Math.min(this._preRelease.length, toCompare._preRelease.length); i < length; i++) {
const result = compareStrings(this._preRelease[i], toCompare._preRelease[i]);
if (result !== 0) return result;
}
return this._preRelease.length - toCompare._preRelease.length;
}
}