depyth/pep440version.ts

417 lines
8.3 KiB
TypeScript
Raw Normal View History

import { compiler } from "melodyc";
/*
VERSION_PATTERN = r"""
v?
(?:
(?:(?P<epoch>[0-9]+)!)? # epoch
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_\.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
[-_\.]?
(?P<pre_n>[0-9]+)?
)?
(?P<post> # post release
(?:-(?P<post_n1>[0-9]+))
|
(?:
[-_\.]?
(?P<post_l>post|rev|r)
[-_\.]?
(?P<post_n2>[0-9]+)?
)
)?
(?P<dev> # dev release
[-_\.]?
(?P<dev_l>dev)
[-_\.]?
(?P<dev_n>[0-9]+)?
)?
)
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
"""
*/
const VERSION_PATTERN = `
<start>;
option of "v";
match {
option of match {
capture epoch {
some of <digit>;
}
"!";
}
capture release {
some of <digit>;
any of match {
".";
some of <digit>;
}
}
option of capture pre {
option of either {
".";
"_";
"-";
}
capture pre_l {
either {
"a";
"b";
"c";
"rc";
"alpha";
"beta";
"pre";
"preview";
}
}
option of either {
".";
"_";
"-";
}
capture pre_n {
some of <digit>;
}
}
option of capture post {
either {
match {
"-";
capture post_n {
some of <digit>;
}
}
match {
option of either {
".";
"_";
"-";
}
capture post_l {
either {
"post";
"rev";
"r";
}
}
option of either {
".";
"_";
"-";
}
option of capture post_n {
some of <digit>;
}
}
}
}
option of capture dev {
option of either {
".";
"_";
"-";
}
capture dev_l {
"dev";
}
option of either {
".";
"_";
"-";
}
option of capture dev_n {
some of <digit>;
}
}
option of match {
"+";
capture local {
some of either {
a to z;
0 to 9;
}
any of match {
option of either {
".";
"_";
"-";
}
some of either {
a to z;
0 to 9;
}
}
}
}
}
// get rid of random bullshit at the end
capture random_bullshit {
any of <char>;
}
<end>;
`;
/*
*/
const output = compiler(VERSION_PATTERN);
console.log(output);
const VERSION_REGEX = new RegExp(output);
export default class Pep440Version {
epoch?: number;
release: {
major: number;
minor: number;
patch: number;
};
preRelease?: {
name: string;
version?: number;
};
postRelease?: {
name?: string;
version?: number;
};
dev?: {
name: string;
version?: number;
};
random_bullshit_at_the_end?: string;
/**
*
* @param {string} versionString
*/
constructor(versionString: string) {
console.log(`Constructing version from ${versionString}`);
const match = versionString.match(VERSION_REGEX);
if( match === null || match.groups === undefined ) {
throw new Error("Couldn't parse version: " + versionString)
}
const g = match.groups;
if( g['release'] === undefined ) {
throw new Error("Couldn't parse version (because of the absence of a release): " + versionString)
}
const [ major, minor, patch ] = g['release'].split(".").map( e => Number(e) );
this.release = {
major: major ?? 0,
minor: minor ?? 0,
patch: patch ?? 0,
}
this.epoch = Number(g['epoch']);
if( g['pre'] ) {
this.preRelease = {
name: g['pre_l'],
version: g['pre_n'] ? Number(g['pre_n']) : undefined,
}
}
if( g['post'] ) {
this.postRelease = {
name: g['post_l'],
version: g['post_n'] ? Number(g['post_n']) : undefined,
}
}
if( g['dev'] ) {
this.dev = {
name: g['dev_l'],
version: g['dev_n'] ? Number(g['dev_n']) : undefined,
}
}
this.random_bullshit_at_the_end = g['random_bullshit'];
}
/**
* @returns the version (as as tring) in a format that can be correctly compared
* even if it looks goofy
*/
toComparableString() {
// comparing strings is annoying
// for example: "1.2" > "1.12", even though 12 > 2
//
// To fix that issue, we're going to have fixed-length numbers,
// so that comparing 1.2 and 1.12 would mean comparing
// "000012" > "000002", this one being correct because 1 > 0.
// pad to 8 because who in their right mind would use 8 digits for the major version
// (noting that some people use 4 because they use years)
const PAD_LENGTH = 8;
const epoch = (this.epoch?.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release_major = (this.release.major.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release_minor = (this.release.minor.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release_patch = (this.release.patch.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release = `${release_major}.${release_minor}.${release_patch}`;
const prename = (this.preRelease?.name || '').padStart(PAD_LENGTH, '0');
const postname = (this.postRelease?.name || '').padStart(PAD_LENGTH, '0');
const devname = (this.dev?.name || '').padStart(PAD_LENGTH, '0');
const prever = (this.preRelease?.version?.toString() || '0').padStart(PAD_LENGTH, '0');
const postver = (this.postRelease?.version?.toString() || '0').padStart(PAD_LENGTH, '0');
const devver = (this.dev?.version?.toString() || '0').padStart(PAD_LENGTH, '0');
return `${epoch}!${release}.${prename}${prever}.${postname}${postver}.${devname}${devver}`;
}
isAbove(other: Pep440Version) {
return this.toComparableString() > other.toComparableString();
}
isBelow(other: Pep440Version) {
return this.toComparableString() < other.toComparableString();
}
isEqual(other: Pep440Version) {
return this.toComparableString() === other.toComparableString();
}
compare(other: Pep440Version) {
if( this.isAbove(other) ) {
return -1;
}
else if( this.isBelow(other) ) {
return 1;
}
else {
return 0;
}
}
/**
* @returns the version as a Pep440-compliant, human-readable string (e.g. "2.1.3")
*/
releaseString() {
let str = "";
if( this.epoch ) {
str += `${this.epoch}!`;
}
str += this.release.major ?? 0;
str += '.';
str += this.release.minor ?? 0;
str += '.';
str += this.release.patch ?? 0;
if(this.preRelease) {
str += '.';
if( this.preRelease.name ) {
str += this.preRelease.name;
}
if( this.preRelease.version ) {
str += this.preRelease.version;
}
}
if(this.postRelease) {
str += '.';
if( this.postRelease.name ) {
str += this.postRelease.name;
}
if( this.postRelease.version ) {
str += this.postRelease.version;
}
}
if(this.dev) {
str += '.';
if( this.dev.name ) {
str += this.dev.name;
}
if( this.dev.version ) {
str += this.dev.version;
}
}
if( this.random_bullshit_at_the_end ) {
str += this.random_bullshit_at_the_end;
}
return str;
}
//static fromString(...versionStrings: string[]) {
// return versionStrings.map( v => new Pep440Version(v) );
//}
}
type OPERATOR_FUNCTION = (source: Pep440Version, target: Pep440Version) => boolean
type OPERATORS_FN_type = {
[key: string]: OPERATOR_FUNCTION;
};
const OPERATORS_FUNCTIONS: OPERATORS_FN_type = {
">=": (source, target) => target.isAbove(source) || target.isEqual(source),
">": (source, target) => target.isAbove(source),
"==": (source, target) => target.isEqual(source),
"": (source, target) => true,
};
export class Pep440VersionRule {
version: Pep440Version;
operator: OPERATOR_FUNCTION;
constructor(version: Pep440Version, operator: string = "") {
this.version = version;
this.operator = OPERATORS_FUNCTIONS[operator];
}
/**
*/
getSortedMatchingVersions(versions: Pep440Version[]) {
const sorted = versions.sort( (a, b) => a.compare(b) );
let filtered;
if( typeof this.operator === "function" ) {
filtered = sorted.filter( v => this.operator( this.version, v ) );
}
else {
filtered = sorted;
}
return filtered;
}
static getRuleMatchingAnyVersion() {
return new Pep440VersionRule(new Pep440Version("0.0.0"))
}
}
//console.log( new Pep440Version("4!1.2.3dev2").toComparableString() )