src/index.js
import is from 'is_js';
/**
* A Moodle version string. These are the version numbers used on the
* [Moodle download page](https://download.moodle.org/releases/latest/) and
* displayed in the footer of the Moodle admin area.
*
* Moodle version strings generally consist of the branch string separated from
* the release number by a period. The initial release for a given branch is
* given the release number zero, which may be omitted. Alpha and Beta releases
* have `dev` appended, and weekly releases have a plus symbol appended.
*
* For example, all the development releases of the Moodle 3.5 branch have the
* version string `3.5dev`, the initial release is `3.5`, the
* first update release is `3.5.1`, and all weekly releases between the
* release of `3.5.1` and `3.5.2` share `3.5.1+`.
*
* @typedef {string} VersionString
* @example '3.5dev'
* @example '3.5'
* @example '3.5+'
* @example '3.5.1'
* @example '3.5.1+'
*/
/**
* A moodle version number. These version numbers are used under-the-hood in
* the Moodle source code. They consist of the branch's branch date number
* followed by the release number, optionally followed by a period and an
* incremental change number.
*
* For example, the Moodle 3.3.6 release has the version number `2017051506.00`.
* The branch date number for Moodle 3.3 is `20170515` (a {@link DateNumber}),
* which is then followed by the two-digit form of the release number, `06`, and
* an incremental change number of `00`.
*
* @typedef {string|number} VersionNumber
* @example '2017051506.00'
* @see DateNumber
*/
/**
* A Moodle branch string. These are the human-friendly major-version numbers
* used throught the official Moodle documentation. They take the form of two
* sets of digits separated by a period, e.g. `3.5` or `3.10`.
*
* @typedef {string} BranchString
* @example '3.5'
*/
/**
* A Moodle branch number. These are used under-the-hood to represent Moodel
* branches within the Moodle code. They take the form of a (usuall) two-digit
* integer number - the human-friendly branch without the period e.g. Moodle
* 3.5 has a branch number of `35`. The exception being Moodle 3.10 which has a
* branch number of 310.
*
* @typedef {number|string} BranchNumber
* @see BranchString
* @example 35
*/
/**
* A Moodle release string. These are the release strings Moodle's documentation
* describes as *human friendly*. They are used in the following contexts:
*
* 1. The admin section of web interface
* 2. The CLI command `admin/cli/cfg.php --name=release`
* 3. The variable `$release` in `version.php`
*
* In some contexts they're pre-fixed with the word *Moodle*, in others they're
* not.
*
* Examples:
*
* * `'3.3.6 (Build: 20180517)'` - the offical Moodle 3.3.6 release.
* * `'Moodle 3.5+ (Build: 20180614)'` - a weekly Moodle 3.5.0 release.
*
* @typedef {string} ReleaseString
* @see VersionString
* @example '3.3.6 (Build: 20180517)'
*/
/**
* An integer representing an offical Moodle release within a branch. The
* initial release of any branch has the release number 0, subsequent offical
* update releases then count up from there.
*
* @typedef {number|string} ReleaseNumber
*/
/**
* Moodle releases are categoriesed into one of three types:
*
* 1. Development releases, both betas and alphas.
* 2. Official stable releases.
* 3. Weekly updates to official stable releases.
*
* These three values are represented as `'development'`, `'official'`, and
* `'weekly'`.
*
* @typedef {string} ReleaseType
*/
/**
* A release suffix is used to indicate the release type in some Moodle version
* strings. Possible values are:
*
* * `'dev'` for development releases.
* * `''` (an empty string) for official stable releases.
* * `+` for weekly updates to official stable releases.
*
* @typedef {string} ReleaseSuffix
*/
/**
* Each released moodle build, both the weeklys and the offical updates are
* assigned a build number which takes the form of a {@link DateNumber}.
*
* @typedef {DateNumber} BuildNumber
*/
/**
* An 8-digit date representation used within a number of Moodle version
* identifiers. The first four digits represent the year, the next two the
* month and the last two the day of the month.
*
* For example, Christmas 2018 has the date number `20181225`.
*
* @typedef {string|number} DateNumber
* @example '20181225'
*/
/**
* A mapping form branch numbers to branching date numbers.
*
* @type {Map<BranchNumber, DateNumber>}
* @protected
*/
const BNUM_BDNUM_MAP = {
'22': 20111205,
'23': 20120625,
'24': 20121203,
'25': 20130514,
'26': 20131118,
'27': 20140512,
'28': 20141110,
'29': 20150511,
'30': 20151116,
'31': 20160523,
'32': 20161205,
'33': 20170515,
'34': 20171113,
'35': 20180517,
'36': 20181203,
'37': 20190520,
'38': 20191118,
'39': 20200615,
'310': 20201109,
'311': 20210517,
'400': 20220419,
'401': 20221128,
'402': 20230424
};
/**
* A mapping form branching date numbers to branch numbers.
*
* @type {Map<DateNumber, BranchNumber>}
* @protected
*/
const BDNUM_BNUM_MAP = {};
for(const bn of Object.keys(BNUM_BDNUM_MAP)){
BDNUM_BNUM_MAP[BNUM_BDNUM_MAP[bn]] = parseInt(bn);
}
/**
* A list of LTS (Long-Term Support) branch numbers.
*
* @type {Array<number>}
* @protected
*/
const LTS_BNUMS = [27, 31, 35];
/**
* A lookup table to test if a given branch number is a long-term support (LTS)
* branch. This lookup table is generated from {@link LTS_BNUMS}.
*
* @type {Object}
* @protected
* @see LTS_BNUMS
*/
const BNUM_LTS_LOOKUP = {};
for(const bnum of LTS_BNUMS){
BNUM_LTS_LOOKUP[bnum] = true;
}
/**
* Convert a value to a string for use in string representations of the
* version. `undefined` is returned as `'??'` and all other values are
* converted to a string with JavaScript's `String()` function.
*
* @param {*} val
* @return {string}
* @private
*/
function TO_STR(val){
return is.undefined(val) ? '??' : String(val);
}
/**
* A class for parsing and representing
* [version information](https://docs.moodle.org/35/en/Moodle_version) for the
* [Moodle VLE](http://moodle.org/).
*
* The class can parse both the human-friendly Moodle release strings like
* `3.3.6 (Build: 20180517)`, and the underlying raw version numbers like
* `2017051506`.
*
* Note that this class only reliably understands version information from
* Moodle 2.2 up. The reason for this limitation is that this is the first
* Moodle version which uses the now standard versioning conventions.
*
* @see https://docs.moodle.org/35/en/Moodle_version
* @see https://docs.moodle.org/dev/Releases
*/
class MoodleVersion {
/**
* By default version objects contain no information.
*
* If a string is passed, the object is initialised using
* {@link MoodleVersion.fromString}, and if an object is passed then
* {@link MoodleVersion.fromObject} is used instead.
*
* @param {string|Object} versionInfo
* @throws TypeError
* @throws RangeError
*/
constructor(versionInfo) {
let newObj;
if(is.not.undefined(versionInfo)){
if(is.string(versionInfo)){
newObj = MoodleVersion.fromString(versionInfo);
}else if(is.object(versionInfo) && is.not.array(versionInfo) && is.not.function(versionInfo) && is.not.error(versionInfo)){
newObj = MoodleVersion.fromObject(versionInfo);
}else{
throw new TypeError('the MoodleVersion constructor only accepts strings and objects');
}
}
/**
* @type {BranchNumber|undefined}
*/
this._branchNumber = undefined;
if(newObj) this._branchNumber = newObj._branchNumber;
/**
* @type {DateNumber|undefined}
*/
this._branchingDateNumber = undefined;
if(newObj) this._branchingDateNumber = newObj._branchingDateNumber;
/**
* @type {ReleaseNumber|undefined}
*/
this._releaseNumber = undefined;
if(newObj) this._releaseNumber = newObj._releaseNumber;
/**
* @type {ReleaseType|undefined}
*/
this._releaseType = undefined;
if(newObj) this._releaseType = newObj._releaseType;
/**
* @type {BuildNumber|undefined}
*/
this._buildNumber = undefined;
if(newObj) this._buildNumber = newObj._buildNumber;
}
/**
* Test if a given value is a date number, i.e. an 8-digit number of the
* form `YYYYMMDD`.
*
* @param {*} val - the value to test.
* @param {boolean} [strictTypeCheck=false] - whether or not to enable
* strict type checking. With strict type cheking enabled, string
* representation of otherwise valid values will return `false`.
* @return {boolean}
*/
static isDateNumber(val, strictTypeCheck){
if(is.not.number(val)){
if(strictTypeCheck) return false;
if(is.not.string(val)) return false;
}
return String(val).match(/^[12]\d{3}[01]\d[0123]\d$/) ? true : false;
}
/**
* Test if a given value is a branch string, e.g. `'3.5'` or `'3.10'`.
*
* @param {*} val - the value to test.
* @return {boolean}
*/
static isBranch(val){
return is.string(val) && val.match(/^[1-9][.]\d{1,2}$/) ? true : false;
}
/**
* Test if a given value is a branch number, e.g. `35`, `'35'`, `310` or
* `'310'.
*
* @param {*} val - the value to test.
* @param {boolean} [strictTypeCheck=false] - whether or not to enable
* strict type checking. With strict type cheking enabled, string
* representation of otherwise valid values will return `false`.
* @return {boolean}
*/
static isBranchNumber(val, strictTypeCheck = false){
if(is.not.number(val)){
if(strictTypeCheck) return false;
if(is.not.string(val)) return false;
}
return String(val).match(/^[1-9]\d{1,2}$/) ? true : false;
}
/**
* Test if a given value is a release number.
*
* Note that if strict type checking is not enabled, the empty string is
* considered a valid release number, being synonymous with zero in
* Moodle release strings.
*
* @param {*} val - the value to test.
* @param {boolean} [strictTypeCheck=false] - whether or not to enable
* strict type checking. With strict type cheking enabled, string
* representation of otherwise valid values will return `false`.
* @return {boolean}
*/
static isReleaseNumber(val, strictTypeCheck = false){
if(is.not.number(val)){
if(strictTypeCheck) return false;
if(is.not.string(val)) return false;
}
return val === '' || String(val).match(/^\d+$/) ? true : false;
}
/**
* Test if a given value is a valid release type.
*
* @param {*} val - the value to test.
* @return {boolean}
* @see ReleaseType
*/
static isReleaseType(val){
return val === 'development' || val === 'official' || val === 'weekly' ? true : false;
}
/**
* Test if a given value is a valid release suffix.
*
* @param {*} val - the value to test.
* @return {boolean}
* @see ReleaseSuffix
*/
static isReleaseSuffix(val){
return val === 'dev' || val === '' || val === '+' ? true : false;
}
/**
* Convert a branch number into a branch string, i.e. `35` to `'3.5'` and
* `310` to `'3.10'`.
*
* @param {BranchNumber} bn
* @return {BranchString|undefined} If the passed value can't be converted
* to a branch `undefined` is returned.
*/
static branchFromBranchNumber(bn){
if(is.undefined(bn)) return undefined;
if(!MoodleVersion.isBranchNumber(bn, false)) return undefined;
const bnMatch = String(bn).match(/^(\d)(\d+)$/);
if(!bnMatch) return undefined;
const major = bnMatch[1];
let minor = bnMatch[2];
minor = minor.replace(/^0*([1-9]*\d)$/, '$1'); // strip leading zeros from minor number
return `${major}.${minor}`;
}
/**
* Convert a branching date number to a branch, e.g. `20180517` to
* `'3.5'`.
*
* @param {DateNumber} bdn
* @return {BranchString|undefined}
*/
static branchFromBranchingDateNumber(bdn){
if(is.undefined(bdn)) return undefined;
if(!MoodleVersion.isDateNumber(bdn, false)) return undefined;
let bn = BDNUM_BNUM_MAP[bdn];
if(is.undefined(bn)) return undefined;
return MoodleVersion.branchFromBranchNumber(bn);
}
/**
* Convert a branch string into a branch number, i.e. `'3.5'` to `35`.
*
* @param {BranchString} b
* @return {number|undefined} If the passed value can't be converted
* to a branch number `undefined` is returned.
*/
static branchNumberFromBranch(b){
if(is.undefined(b)) return undefined;
if(!MoodleVersion.isBranch(b)) return undefined;
const parts = b.split(/[.]/);
const major = parts[0];
let minor = parts[1];
if(parseInt(major) >= 4 && minor.length == 1) minor = `0${minor}`;
return parseInt(`${major}${minor}`);
}
/**
* Convert a branching date number to a branch number, e.g. `20180517` to
* `35`.
*
* @param {DateNumber} bdn
* @return {number|undefined}
*/
static branchNumberFromBranchingDateNumber(bdn){
if(is.undefined(bdn)) return undefined;
if(!MoodleVersion.isDateNumber(bdn, false)) return undefined;
return BDNUM_BNUM_MAP[bdn] ? BDNUM_BNUM_MAP[bdn] : undefined;
}
/**
* Convert a branch to a branching date number, e.g. `'3.5'` to `20180517`.
*
* @param {BranchString} b
* @return {number|undefined}
*/
static branchingDateNumberFromBranch(b){
if(is.undefined(b)) return undefined;
const bn = MoodleVersion.branchNumberFromBranch(b);
if(is.undefined(bn)) return undefined;
return BNUM_BDNUM_MAP[bn] ? BNUM_BDNUM_MAP[bn] : undefined;
}
/**
* Convert a branch number to a branching date number, e.g. `35` to `20180517`.
*
* @param {BranchNumber} bn
* @return {number|undefined}
*/
static branchingDateNumberFromBranchNumber(bn){
if(is.undefined(bn)) return undefined;
return BNUM_BDNUM_MAP[bn] ? BNUM_BDNUM_MAP[bn] : undefined;
}
/**
* Convert a date number to a date object. The date object will represent
* midnight UTC on the given date.
*
* @param {DateNumber} dn
* @return {Date}
* @throws {TypeError}
*/
static dateFromDateNumber(dn){
if(!MoodleVersion.isDateNumber(dn)) throw new TypeError('date number must of the form YYYYMMDD');
const parts = String(dn).match(/^(\d{4})(\d{2})(\d{2})$/);
return new Date(`${parts[1]}-${parts[2]}-${parts[3]}T00:00:00.000Z`);
}
/**
* Convert a date object to a date number. The date will be interpreted as
* UTC.
*
* @param {Date} d
* @return {DateNumber}
* @throws {TypeError}
*/
static dateNumberFromDate(d){
if(is.not.date(d)) throw new TypeError('date object required');
let m = d.getUTCMonth() + 1;
let mm = `${m < 10 ? '0' : ''}${m}`;
let day = d.getUTCDate();
let dd = `${day < 10 ? '0' : ''}${day}`;
return parseInt(`${d.getUTCFullYear()}${mm}${dd}`);
}
/**
* Convert a release type to a release suffix, e.g. `'weekly'` to `'+'`.
*
* @param {ReleaseType} rt
* @return {ReleaseSuffix|undefined}
*/
static releaseSuffixFromReleaseType(rt){
if(is.not.string(rt)) return undefined;
switch(rt.toLowerCase()){
case 'development':
return 'dev';
case 'official':
return '';
case 'weekly':
return '+';
}
return undefined;
}
/**
* Convert a relase suffix to a release type, e.g. `'+'` to `'weekly'`.
*
* @param {ReleaseSuffix} rs
* @return {ReleaseType|undefined}
*/
static releaseTypeFromReleaseSuffix(rs){
if(is.not.string(rs)) return undefined;
switch(rs.toLowerCase()){
case 'dev':
return 'development';
case '':
return 'official';
case '+':
return 'weekly';
}
return undefined;
}
/**
* Convert a release type to a number. Useful for version comparisons.
*
* All invalid values convert to `0`, `'development'` to `1`, `'official'`
* to `2`, and `'weekly'` to 3.
*
* @param {*} rt
* @return {number}
*/
static numberFromReleaseType(rt){
if(!MoodleVersion.isReleaseType(rt)) return 0;
switch(rt){
case 'weekly': return 3;
case 'official': return 2;
default: return 1;
}
}
/**
* Compare two values to see if they represent the same version, a
* greater version, or a lesser version.
*
* When ranking versions, the branch is given the highest weight, then the
* release number, then the release type,
* and finally the build number. When comparing release types,
* `'development'` is considered earlier `'official'`, which is considered
* earlier than `'weekly'`.
*
* @param {*} val1
* @param {*} val2
* @return {number} Unless both values are moodle vesion objects, `NaN` is
* returned. If `val1` represents an earlier version than `val2` `-1` is
* returned, if `val1` and `val2` represent the same version `0` is
* returned, and if `val1` represents a later version than `val2` `1` is
* returned.
*/
static compare(val1, val2){
// unless we get two Moodle versions, return NaN
if(!((val1 instanceof MoodleVersion) && (val2 instanceof MoodleVersion))) return NaN;
// try find a difference in branch
const b1 = is.number(val1.branchNumber) ? val1.branchNumber : 0;
const b2 = is.number(val2.branchNumber) ? val2.branchNumber : 0;
if(b1 < b2) return -1;
if(b1 > b2) return 1;
// if there was no difference in branch, try find a difference in release number
const r1 = is.number(val1.releaseNumber) ? val1.releaseNumber : 0;
const r2 = is.number(val2.releaseNumber) ? val2.releaseNumber : 0;
if(r1 < r2) return -1;
if(r1 > r2) return 1;
// if we've still not found a difference, check the release type
const t1 = MoodleVersion.numberFromReleaseType(val1.releaseType);
const t2 = MoodleVersion.numberFromReleaseType(val2.releaseType);
if(t1 < t2) return -1;
if(t1 > t2) return 1;
// finally, try split the difference with the build number
const bn1 = is.number(val1.buildNumber) ? val1.buildNumber : 0;
const bn2 = is.number(val2.buildNumber) ? val2.buildNumber : 0;
if(bn1 < bn2) return -1;
if(bn1 > bn2) return 1;
// if we still haven't split the difference, they must be equal
return 0;
}
/**
* A factory method for producing a Moodle Version object given all its
* properties.
*
* If only one of the branch and branching date are passed, and if a known
* mapping exists, the other is auto-completed.
*
* This function can be used to create version objects which contain
* unknown mappings between Moodle branches and branching dates.
*
* @param {Object} obj - an object defining zero or more of the following
* keys:
*
* * `branch` (e.g. `'3.5'`) or `branchNumber` (e.g. `35`) - if both are
* specified `branchNumber` takes precedence.
* * `branchingDate` or `branchingDateNumber` - if both are specified
* `branchingDateNumber` takes precedence.
* * `releaseNumber`
* * `releaseType` (e.g. `'weekly'`) or `releaseSuffix` (e.g. `'+'`) - if
* both are specified, `releaseSuffix` takes precedence
* * `buildNumber`
* @throws {TypeError} A type error is thrown if an object is not passed,
* or, if any of the keys within that object map to an invalid value.
*/
static fromObject(obj){
if(is.not.object(obj)) throw new TypeError('object required');
const ans = new MoodleVersion();
// set the branch if passed
if(is.propertyDefined(obj, 'branch') || is.propertyDefined(obj, 'branchNumber')){
if(is.not.undefined(obj.branchNumber)){
if(!MoodleVersion.isBranchNumber(obj.branchNumber)) throw new TypeError('invalid branch number');
ans._branchNumber = parseInt(obj.branchNumber);
}else if(is.not.undefined(obj.branch)){
if(!MoodleVersion.isBranch(obj.branch)) throw new TypeError('invalid branch');
ans._branchNumber = MoodleVersion.branchNumberFromBranch(obj.branch);
}
}
// set the branching date if passed
if(is.propertyDefined(obj, 'branchingDate') || is.propertyDefined(obj, 'branchingDateNumber')){
if(is.not.undefined(obj.branchingDateNumber)){
if(!MoodleVersion.isDateNumber(obj.branchingDateNumber)) throw new TypeError('invalid branching date number');
ans._branchingDateNumber = parseInt(obj.branchingDateNumber);
}else if(is.not.undefined(obj.branchingDate)){
if(is.not.date(obj.branchingDate)) throw new TypeError('invalid branching date');
ans._branchingDateNumber = MoodleVersion.dateNumberFromDate(obj.branchingDate);
}
}
// set the release number if passed
if(is.not.undefined(obj.releaseNumber)){
if(!MoodleVersion.isReleaseNumber(obj.releaseNumber)) throw new TypeError('invalid release number');
ans._releaseNumber = parseInt(obj.releaseNumber);
}
// set the release type if passed
if(is.propertyDefined(obj, 'releaseType') || is.propertyDefined(obj, 'releaseSuffix')){
if(is.not.undefined(obj.releaseSuffix)){
if(!MoodleVersion.isReleaseSuffix(obj.releaseSuffix)) throw new TypeError('invalid release suffix');
ans._releaseType = MoodleVersion.releaseTypeFromReleaseSuffix(obj.releaseSuffix);
}else if(is.not.undefined(obj.releaseType)){
if(!MoodleVersion.isReleaseType(obj.releaseType)) throw new TypeError('invalid release type');
ans._releaseType = obj.releaseType.toLowerCase();
}
}
// set the build number if passed
if(is.not.undefined(obj.buildNumber)){
if(!MoodleVersion.isDateNumber(obj.buildNumber)) throw new TypeError('invalid build number');
ans._buildNumber = parseInt(obj.buildNumber);
}
// if there's a branch but no branching date, try auto-complete it
if(is.number(ans._branchNumber) && is.undefined(ans._branchingDateNumber) && is.number(BNUM_BDNUM_MAP[ans._branchNumber])){
ans._branchingDateNumber = BNUM_BDNUM_MAP[ans._branchNumber];
}
// if there's a branching date but no branch, try auto-complete it
if(is.number(ans._branchingDateNumber) && is.undefined(ans._branchNumber) && is.number(BDNUM_BNUM_MAP[ans._branchingDateNumber])){
ans._branchNumber = BDNUM_BNUM_MAP[ans._branchingDateNumber];
}
return ans;
}
/**
* A regular expression for matching human-friendly Moodle release strings.
* This RE is case-insensitive and will allow for the optional pre-fixing of
* the word *Moodle* with or whithout a separating space.
*
* @type {RegExp}
* @see {@link ReleaseString}
*/
static get releaseRE(){
return /(?:Moodle[ ]?)?(\d[.]\d{1,2})(?:[.](\d+))?(dev|[+])?[ ]?[(]Build[:][ ]?(\d{8})[)]/i;
}
/**
* A regular expression for matching short version strings like `'3.5+'` (as
* used on the Moodle download page). This RE is case-insensitive and will
* allow for the optional pre-fixing of the word *Moodle* with or whithout
* a separating space.
*
* @type {RegExp}
* @see {@link VersionString}
*/
static get versionRE(){
return /(?:Moodle[ ]?)?(\b\d[.]\d{1,2})(?:[.](\d+))?(dev|[+])?/i;
}
/**
* A regular expression for matching under-the-hood version numbers like
* `'2017051506'` or `'2017051506.00'`.
*
* @type {RegExp}
* @see {@link VersionNumber}
*/
static get versionNumberRE(){
return /(\d{8})(\d{2})(?:[.](\d{2}))?/i;
}
/**
* Build a version object from a version string. The vesion string can be
* in one of the following formats:
*
* * A human-friendly full release string ({@link ReleaseString}), e.g.
* `'Moodle 3.5+ (Build: 20180614)'` (will be accepted with or without
* the `'Moodle'` prefix).
* * A human-friendly short version string ({@link VersionString}), e.g.
* `'Moodle 3.3.6+'` (will be accepted with or without the `'Moodle'`
* prefix).
* * An under-the-hood version number ({@link VersionNumber}), e.g.
* * `'2017051506'` or `'2017051506.00'`.
* * A string as returned by calling `.toString()` on an instance of this
* class.
*
* @param {string} verStr - the version string to parse.
* @return {MoodleVersion}
* @throws {TypeError}
* @throws {RangeError}
* @see {@link ReleaseString}
* @see {@link VersionString}
* @see {@link VersionNumber}
* @see {@link MoodleVersion#toString}
*/
static fromString(verStr){
if(is.not.string(verStr)) throw new TypeError('version string required');
const ans = new MoodleVersion();
// first try match against a full human-friendly release string
let matched = MoodleVersion.releaseRE.exec(verStr);
if(matched){
ans.branch = matched[1];
ans.releaseNumber = matched[2] ? matched[2] : 0;
ans.releaseSuffix = matched[3] ? matched[3] : '';
ans.buildNumber = matched[4];
return ans;
}
// then try match against an under-the-hood Moodle version number
matched = MoodleVersion.versionNumberRE.exec(verStr);
if(matched){
ans.branchingDateNumber = matched[1];
ans.releaseNumber = matched[2] ? parseInt(matched[2]) : 0;
return ans;
}
// next try match a string as produced by .toString()
matched = (/((?:[0-9]|[?]{2})[.](?:[0-9]|[?]{2}))[.]((?:[0-9]+|[?]{2}))(dev|[+])?[ ][(]type[:][ ](development|official|weekly|[?]{2})[,][ ]branching[ ]date[:][ ](\d{8}|[?]{2})[ ][&][ ]build[:][ ](\d{8}|[?]{2})[)]/i).exec(verStr);
if(matched){
const ansObj = {};
if(matched[1] != '??.??') ansObj.branch = matched[1];
if(matched[2] != '??') ansObj.releaseNumber = matched[2];
if(matched[4] != '??') ansObj.releaseType = matched[4];
if(matched[5] != '??') ansObj.branchingDateNumber = matched[5];
if(matched[6] != '??') ansObj.buildNumber = matched[6];
return MoodleVersion.fromObject(ansObj);
}
// finally try match against a short human-friendly version string
matched = MoodleVersion.versionRE.exec(verStr);
if(matched){
ans.branch = matched[1];
ans.releaseNumber = matched[2] ? matched[2] : 0;
ans.releaseSuffix = matched[3] ? matched[3] : '';
return ans;
}
// if no match was found, throw a range error
throw new RangeError(`failed to extract Moodle version from string: ${verStr}`);
}
// TO DO - update constructor to accept strings and objects
/**
* The version's branch number, if known. This is the two-digit number
* used internally within the Moodle code to identify a branch, or major
* release.
*
* For example, all Moodle 3.5.* releases will have the branch number `35`.
*
* @type {number|undefined}
*/
get branchNumber(){
return this._branchNumber;
}
/**
* The branch number must be a two-digit integer between 10 and 99.
*
* Setting the branch number will also update the branching date to match.
*
* To create an object with an un-known combination of branch and branching
* date, use the {@link MoodleVersion.fromObject} factory method.
*
* @type {BranchNumber|undefined}
* @throws {TypeError}
* @throws {RangeError} A range error is thrown if the branch does not have
* a known mapping to a branching date.
*/
set branchNumber(bn){
// short-circuit requests to set undefined
if(is.undefined(bn)){
this._branchNumber = undefined;
this._branchingDateNumber = undefined;
return;
}
// check the validity of the branch number
if(!MoodleVersion.isBranchNumber(bn, false)){
throw new TypeError('Branch Numbers must be integers between 10 and 99');
}
//test if we have a mapping to a branching date
let bdn = MoodleVersion.branchingDateNumberFromBranchNumber(bn);
if(is.undefined(bdn)){
throw new RangeError(`the branch number ${bn} does not have a known mapping to a branching date`);
}
// set the branch number and branching date
this._branchNumber = parseInt(bn);
this._branchingDateNumber = bdn;
}
/**
* The major version part of the version number, officially known as the
* *branch*.
*
* For example, the branch for each of the 3.4, 3.4+, 3.4.1, and 3.4.1+
* releases is `'3.4'`.
*
* @type {BranchString|undefined}
*/
get branch(){
if(is.undefined(this._branchNumber)) return undefined;
return MoodleVersion.branchFromBranchNumber(this._branchNumber);
}
/**
* The branch (AKA major version) must be a string consisting of two
* digits separated by a period, e.g. `'3.5'`.
*
* Setting the branch will also update the branching date to match.
*
* To create an object with an un-known combination of branch and branching
* date, use the {@link MoodleVersion.fromObject} factory method.
*
* @type {BranchString}
* @throws {TypeError}
* @throws {RangeError} A range error is thrown if the branch does not have
* a known mapping to a branching date.
*/
set branch(b){
// short-circuit requests to set undefined
if(is.undefined(b)){
this._branchNumber = undefined;
this._branchingDateNumber = undefined;
return;
}
// try convert the branch to a branch number
let bn = MoodleVersion.branchNumberFromBranch(b);
if(is.not.number(bn)){
throw new TypeError(`branches must be strings consisting of a digit, a period, and another digit. Got: ${b}`);
}
// test if we have a mapping to a branching date
let bdn = MoodleVersion.branchingDateNumberFromBranchNumber(bn);
if(is.undefined(bdn)){
throw new RangeError(`the branch ${b} does not have a known mapping to a branching date`);
}
// store the branch number & branching date
this._branchNumber = bn;
this._branchingDateNumber = bdn;
}
/**
* The branching date as a date object.
*
* @type {Date|undefined}
*/
get branchingDate(){
if(is.undefined(this._branchingDateNumber)) return undefined;
return MoodleVersion.dateFromDateNumber(this._branchingDateNumber);
}
/**
* Setting the branching date will update the branch to match.
*
* To create an object with an un-known combination of branch and branching
* date, use the {@link MoodleVersion.fromObject} factory method.
*
* @type{Date|undefined}
* @throws {TypeError}
* @throws {RangeError} A range error is thrown if the branching date does
* not have a known mapping to a branch.
*/
set branchingDate(bd){
// deal with un-setting
if(is.undefined(bd)){
this._branchNumber = undefined;
this._branchingDateNumber = undefined;
return;
}
// make sure we got valid data
if(is.not.date(bd)) throw new TypeError('the branching date must be a Date object');
// convert the date to a date number
let bdn = MoodleVersion.dateNumberFromDate(bd);
// test if there's a known mapping to a branch
let bn = MoodleVersion.branchNumberFromBranchingDateNumber(bdn);
if(is.undefined(bn)){
throw new RangeError(`the branching date ${bd.toISOString()} does not have a known mapping to a Moodle branch`);
}
// set the new values
this._branchNumber = bn;
this._branchingDateNumber = bdn;
}
/**
* The branching date as a {@link DateNumber}.
*
* @type {DateNumber|undefined}
*/
get branchingDateNumber(){
return this._branchingDateNumber;
}
/**
* Setting the branching date will update the branch to match.
*
* To create an object with an un-known combination of branch and branching
* date, use the {@link MoodleVersion.fromObject} factory method.
*
* @type{DateNumber|undefined}
* @throws {TypeError}
* @throws {RangeError} A range error is thrown if the branching date does
* not have a known mapping to a branch.
*/
set branchingDateNumber(bdn){
// deal with un-setting
if(is.undefined(bdn)){
this._branchNumber = undefined;
this._branchingDateNumber = undefined;
return;
}
// make sure we got valid data
if(!MoodleVersion.isDateNumber(bdn)) throw new TypeError('the branching date number must be of the form YYYYMMDD');
bdn = parseInt(bdn); // force the date number to a number
// test if there's a known mapping to a branch
let bn = MoodleVersion.branchNumberFromBranchingDateNumber(bdn);
if(is.undefined(bn)){
throw new RangeError(`the branching date number ${bdn} does not have a known mapping to a Moodle branch`);
}
// set the new values
this._branchNumber = bn;
this._branchingDateNumber = bdn;
}
/**
* The known mappings between Moodle braches and branching date numbers.
*
* @type{Map<Branch, DateNumber>}
*/
get branchingDateNumbersByBranch(){
const ans = {};
for(const bn of Object.keys(BNUM_BDNUM_MAP)){
ans[MoodleVersion.branchFromBranchNumber(bn)] = BNUM_BDNUM_MAP[bn];
}
return ans;
}
/**
* The known mappings between Moodle brache numberss and branching date
* numbers.
*
* @type{Map<BranchNumber, DateNumber>}
*/
get branchingDateNumbersByBranchNumber(){
const ans = {};
for(const bn of Object.keys(BNUM_BDNUM_MAP)){
ans[bn] = BNUM_BDNUM_MAP[bn];
}
return ans;
}
/**
* The known mappings between branching date numbers and Moodle branches.
*
* @type{Map<DateNumber, Branch>}
*/
get branchesByBranchingDateNumber(){
const ans = {};
for(const bdn of Object.keys(BDNUM_BNUM_MAP)){
ans[bdn] = MoodleVersion.branchFromBranchNumber(BDNUM_BNUM_MAP[bdn]);
}
return ans;
}
/**
* The known mappings between branching date numbers and Moodle branch
* numbers.
*
* @type{Map<DateNumber, BranchNumber>}
*/
get brancheNumbersByBranchingDateNumber(){
const ans = {};
for(const bdn of Object.keys(BDNUM_BNUM_MAP)){
ans[bdn] = BDNUM_BNUM_MAP[bdn];
}
return ans;
}
/**
* The release number part of the version number.
*
* @type {ReleaseNumber|undefined}
*/
get releaseNumber(){
return this._releaseNumber;
}
/**
* The release number must be an integer greater than or equal to zero.
*
* @type {ReleaseNumber|undefined}
* @throws {TypeError}
*/
set releaseNumber(rn){
// short-circuit requests to set undefined
if(is.undefined(rn)){
this._releaseNumber = undefined;
return;
}
// check the validity of the release number
if(!MoodleVersion.isReleaseNumber(rn, false)){
throw new TypeError('Release Numbers must be integers greater than or equal to zero');
}
// set the branch number (coercing the empty string to 0)
this._releaseNumber = rn === '' ? 0 : parseInt(rn);
}
/**
* The release's type, e.g. `'development'`.
*
* @type {ReleaseType|undefined}
*/
get releaseType(){
return this._releaseType;
}
/**
* The release type must be one of `'development'`, `'official'`, or
* `'weekly'`. The value will get automatically cast to lower case before
* validation is applied.
*
* @type {ReleaseType|undefined}
* @throws {TypeError}
*/
set releaseType(rt){
const errMsg = "release type must be one of 'development', 'official', or 'weekly'";
if(is.not.string(rt)) throw new TypeError(errMsg);
rt = rt.toLowerCase();
if(!MoodleVersion.isReleaseType(rt)) throw new TypeError(errMsg);
this._releaseType = rt;
}
/**
* The release suffix for the release's type, e.g. `'+'` for weekly
* updates to the official releases.
*
* @type {ReleaseSuffix|undefined}
*/
get releaseSuffix(){
return MoodleVersion.releaseSuffixFromReleaseType(this._releaseType);
}
/**
* The release suffix must be one of `'dev'`, an empty string, or `'+'`. The
* value will get automatically cast to lower case before validation is
* applied.
*
* @type {ReleaseSuffix|undefined}
* @throws {TypeError}
*/
set releaseSuffix(rs){
const errMsg = "release suffix must be one of 'dev', '', or '+'";
if(is.not.string(rs)) throw new TypeError(errMsg);
rs = rs.toLowerCase();
if(!MoodleVersion.isReleaseSuffix(rs)) throw new TypeError(errMsg);
this._releaseType = MoodleVersion.releaseTypeFromReleaseSuffix(rs);
}
/**
* The build number.
*
* @type {BuildNumber|undefined}
*/
get buildNumber(){
return this._buildNumber;
}
/**
* Build numbers must be valid date numbers, i.e. of the form `YYYYMMDD`.
*
* @type {ReleaseSuffix|undefined}
* @throws {TypeError}
*/
set buildNumber(bn){
// short-circuit setting to undefined
if(is.undefined(bn)){
this._buildNumber = undefined;
return;
}
// make sure we got valid data
if(!MoodleVersion.isDateNumber(bn)) throw new TypeError('build number must be of the form YYYYMMDD');
// store the build number
this._buildNumber = parseInt(bn);
}
/**
* The short human-friendly form of the version number.
*
* In keeping with how Moodle presents version strings, release numbers of
* zero are omitted. If the release type is unknown no suffix is appended.
* If the branch is unknown it is represented as `'??.??'`, and if the
* release number is unknown it's represented as `'.??'`.
*
* @type {VersionString}
*/
get version(){
let ans = is.undefined(this.branch) ? '??.??' : this.branch;
if(this.releaseNumber !== 0) ans += `.${TO_STR(this.releaseNumber)}`;
if(is.string(this.releaseSuffix)) ans += this.releaseSuffix;
return ans;
}
/**
* The under-the-hood form of the version number.
*
* If the branch is unknown its replaced with eight question marks, and if
* the release number is unknown it's replaced with two.
*
* @type {VersionNumber}
*/
get versionNumber(){
let ans = is.undefined(this.branchingDateNumber) ? '????????' : TO_STR(this.branchingDateNumber);
ans += `${this.releaseNumber < 10 ? '0' : ''}${TO_STR(this.releaseNumber)}`;
return ans;
}
/**
* The long human-friendly form of the version information.
*
* In keeping with how Moodle presents version strings, release numbers of
* zero are omitted. If the release type is unknown no suffix is appended.
* If the branch is unknown it is represented as `'??.??'`, if the
* release number is unknown it's represented as `'.??'`, and if the build
* number is unknown it's represented as `'????????'`.
*
* @type {ReleaseString}
*/
get release(){
return `${this.version} (Build: ${is.undefined(this.buildNumber) ? '????????' : this.buildNumber})`;
}
/**
* Create a new Moodle version object representing the same version
* information.
*
* @return {MoodleVersion}
*/
clone(){
return MoodleVersion.fromObject({
branchNumber: this._branchNumber,
branchingDateNumber: this._branchingDateNumber,
releaseNumber: this._releaseNumber,
releaseType: this._releaseType,
buildNumber: this._buildNumber
});
}
/**
* Return a string representation of the version. The output will be of the
* form `B.B.R[S] (type: T, branching date: BD & build: BN)`, e.g.
* `3.3.6 (type: official, branching date: 20170515 & build: 20180517)`.
* Undefined components will be rendered as `??`.
*
* @return {string}
*/
toString(){
let ans = `${is.undefined(this.branch) ? '??.??' : this.branch }.${TO_STR(this.releaseNumber)}`;
if(is.string(this.releaseSuffix)) ans += this.releaseSuffix;
ans += ` (type: ${TO_STR(this.releaseType)}, branching date: ${TO_STR(this.branchingDateNumber)} & build: ${TO_STR(this.buildNumber)})`;
return ans;
}
/**
* The version as a plain object indexed by zero or more of:
*
* * `version`
* * `versionNumber`
* * `release`
* * `branch`
* * `branchNumber`
* * `branchingDateNumber`
* * `branchingDate`
* * `releaseNumber`
* * `releaseType`
* * `releaseSuffix`
* * `buildNumber`
*
* @return {Object}
*/
toObject(){
return {
version: this.version,
versionNumber: this.versionNumber,
release: this.release,
branch: this.branch,
branchNumber: this.branchNumber,
branchingDateNumber: this.branchingDateNumber,
branchingDate: this.branchingDate,
releaseNumber: this.releaseNumber,
releaseType: this.releaseType,
releaseSuffix: this.releaseSuffix,
buildNumber: this.buildNumber
};
}
/**
* An object containing a SemVer
* ([Semantic Versioning](https://semver.org)) representation of the
* version information.
*
* @return {{major: number, minor: number, patch: number}}
*/
toSemVerObject(){
const ans = {
major: 0,
minor: 0,
patch: 0
}
if(this.branch){
const branchParts = this.branch.split('.');
ans.major = parseInt(branchParts[0]);
ans.minor = parseInt(branchParts[1]);
}
if(this.releaseNumber){
ans.patch = this.releaseNumber;
}
return ans;
}
/**
* An array containing a SemVer
* ([Semantic Versioning](https://semver.org)) representation of the
* version information. The first element will be the major version number,
* the second the minor, and the third the patch.
*
* @return {Array<number>} An array of three integers.
*/
toSemVerArray(){
const semVer = this.toSemVerObject()
return [semVer.major, semVer.minor, semVer.patch];
}
/**
* Test if a given value is a Moodle Version object representing the same
* version.
*
* @param {*} val
* @return {boolean}
*/
equals(val){
return MoodleVersion.compare(this, val) === 0 ? true : false;
}
/**
* Compare this version to another.
*
* @param {MoodleVersion} mv
* @return {number} `-1` returned if passed version is lesser, `0` if the
* passed version is the same, and `1` if the passed version is greater. If
* the passed value is not a Moodle version object, `NaN` will be returned.
*/
compareTo(mv){
return MoodleVersion.compare(mv, this);
}
/**
* Determine whether this versions is on the same branch as a given version.
*
* @param {MoodleVersion} mv
* @return {boolean|undefined} If the two versions share a branch then
* `true` is returned, if the branch numbers differ, `false` is returned.
* If the value passed is not a Moodle version object, or, the branch
* is undefined in both versions, `undefined` is returned.
*/
sameBranch(mv){
if(!(mv instanceof MoodleVersion)) return undefined;
if(is.all.undefined(this.branch, mv.branch)) return undefined;
return this.branch === mv.branch;
}
/**
* Determine whether this version is less than a given version.
*
* @param {MoodleVersion} mv
* @return {boolean|undefined} If the version is definitely lesser then
* `true` is returned, and if the version is equal or definitely greater
* then `false` is returned. If the value is not a Moodle version object
* then `undefined` is returned.
*/
lessThan(mv){
const cmp = MoodleVersion.compare(this, mv);
if(is.nan(cmp)) return undefined;
return cmp === -1 ? true : false;
}
/**
* Determine whether this version is greater than a given version.
*
* @param {MoodleVersion} mv
* @return {boolean|undefined} If the version is definitely greater then
* `true` is returned, and if the version is equal or definitely less than
* then `false` is returned. If the value is not a Moodle version object
* then `undefined` is returned.
*/
greaterThan(mv){
const cmp = MoodleVersion.compare(this, mv);
if(is.nan(cmp)) return undefined;
return cmp === 1 ? true : false;
}
/**
* Determine whether this version is greater than or equal to the given version.
*
* @param {MoodleVersion} mv
* @return {boolean|undefined} If the version is definitely greater or
* equal then `true` is returned, and if the version is definitely less
* than then `false` is returned. If the value is not a Moodle version
* object then `undefined` is returned.
*/
atLeast(mv){
const cmp = MoodleVersion.compare(this, mv);
if(is.nan(cmp)) return undefined;
return cmp >= 0 ? true : false;
}
/**
* Is this a stable release? I.e. is the release type `official` or
* `weekly`?
*
* @return {boolean|undefined} Both official and weekly releases are
* considered stable, while development releases are not. If the release
* type is not defined, `undefined` is returned.
*/
isStable(){
if(is.undefined(this.releaseType)) return undefined;
return this.releaseType === 'official' || this.releaseType === 'weekly' ? true : false;
}
/**
* Is this version on a branch the library knows about?
*
* @return {boolean}
*/
isKnownBranch(){
return is.not.undefined(BNUM_BDNUM_MAP[this.branchNumber]);
}
/**
* Determine whether or not this is version is on a long-term support
* branch. If the branch is not defined or unknown, `undefined` is returned.
*
* @return {boolean|undefined}
*/
isLTS(){
if(is.undefined(this.branchNumber)) return undefined;
if(!this.isKnownBranch()) return undefined;
return is.not.undefined(BNUM_LTS_LOOKUP[this.branchNumber]);
}
}
export default MoodleVersion;