diff --git a/README.md b/README.md index a7f243c2..5e03abcc 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ const semverCompareLoose = require('semver/functions/compare-loose') const semverCompareBuild = require('semver/functions/compare-build') const semverSort = require('semver/functions/sort') const semverRsort = require('semver/functions/rsort') +const semverTruncate = require('semver/functions/truncate') // low-level comparators between versions const semverGt = require('semver/functions/gt') @@ -456,6 +457,12 @@ strings that they parse. or comparators intersect. * `parse(v)`: Attempt to parse a string as a semantic version, returning either a `SemVer` object or `null`. +* `truncate(v, releaseType)`: Return the version with components _lower_ + than `releaseType` dropped off, e.g.: + * `major` removes build & prerelease info and sets minor & patch to 0. + * `minor` removes build & prerelease info, and sets patch to 0 + * `patch` removes build & prerelease info + * All prerelease types remove build info only ### Comparison @@ -657,6 +664,7 @@ The following modules are available: * `require('semver/functions/rsort')` * `require('semver/functions/satisfies')` * `require('semver/functions/sort')` +* `require('semver/functions/truncate')` * `require('semver/functions/valid')` * `require('semver/ranges/gtr')` * `require('semver/ranges/intersects')` diff --git a/functions/truncate.js b/functions/truncate.js new file mode 100644 index 00000000..8314e4e9 --- /dev/null +++ b/functions/truncate.js @@ -0,0 +1,48 @@ +'use strict' + +const parse = require('./parse') +const constants = require('../internal/constants') +const SemVer = require('../classes/semver') + +const truncate = (version, truncation, options) => { + if (!constants.RELEASE_TYPES.includes(truncation)) { + return null + } + + const clonedVersion = cloneInputVersion(version, options) + return clonedVersion && doTruncation(clonedVersion, truncation) +} + +const cloneInputVersion = (version, options) => { + const versionStringToParse = ( + version instanceof SemVer ? version.version : version + ) + + return parse(versionStringToParse, options) +} + +const doTruncation = (version, truncation) => { + if (isPrerelease(truncation)) { + return version.version + } + + version.prerelease = [] + + switch (truncation) { + case 'major': + version.minor = 0 + version.patch = 0 + break + case 'minor': + version.patch = 0 + break + } + + return version.format() +} + +const isPrerelease = (type) => { + return type.startsWith('pre') +} + +module.exports = truncate diff --git a/index.js b/index.js index 285662ac..bc1f608c 100644 --- a/index.js +++ b/index.js @@ -28,6 +28,7 @@ const gte = require('./functions/gte') const lte = require('./functions/lte') const cmp = require('./functions/cmp') const coerce = require('./functions/coerce') +const truncate = require('./functions/truncate') const Comparator = require('./classes/comparator') const Range = require('./classes/range') const satisfies = require('./functions/satisfies') @@ -66,6 +67,7 @@ module.exports = { lte, cmp, coerce, + truncate, Comparator, Range, satisfies, diff --git a/test/fixtures/truncations.js b/test/fixtures/truncations.js new file mode 100644 index 00000000..a4ef84cc --- /dev/null +++ b/test/fixtures/truncations.js @@ -0,0 +1,39 @@ +'use strict' + +// [version, releaseType, result] +// truncate(version, type) -> result +module.exports = [ + ['1.2.3-foo', 'patch', '1.2.3'], + ['1.2.3', 'patch', '1.2.3'], + ['1.2.3', 'minor', '1.2.0'], + ['1.2.3', 'major', '1.0.0'], + + // invalid inputs + ['1.2.3', 'fake', null], + ['fake', 'major', null], + + // additional pre-release, build, and pre+build inputs + ['4.5.6-rc2', 'prerelease', '4.5.6-rc2'], + ['4.5.6-rc2', 'prepatch', '4.5.6-rc2'], + ['4.5.6-rc2', 'preminor', '4.5.6-rc2'], + ['4.5.6-rc2', 'premajor', '4.5.6-rc2'], + ['4.5.6-rc2', 'patch', '4.5.6'], + ['4.5.6-rc2', 'minor', '4.5.0'], + ['4.5.6-rc2', 'major', '4.0.0'], + + ['4.5.6+dadb0d', 'prerelease', '4.5.6'], + ['4.5.6+dadb0d', 'prepatch', '4.5.6'], + ['4.5.6+dadb0d', 'preminor', '4.5.6'], + ['4.5.6+dadb0d', 'premajor', '4.5.6'], + ['4.5.6+dadb0d', 'patch', '4.5.6'], + ['4.5.6+dadb0d', 'minor', '4.5.0'], + ['4.5.6+dadb0d', 'major', '4.0.0'], + + ['4.5.6-rc2+dadb0d', 'prerelease', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'prepatch', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'preminor', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'premajor', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'patch', '4.5.6'], + ['4.5.6-rc2+dadb0d', 'minor', '4.5.0'], + ['4.5.6-rc2+dadb0d', 'major', '4.0.0'], +] diff --git a/test/functions/truncate.js b/test/functions/truncate.js new file mode 100644 index 00000000..c1f7aa1b --- /dev/null +++ b/test/functions/truncate.js @@ -0,0 +1,45 @@ +'use strict' + +const { test } = require('tap') +const truncate = require('../../functions/truncate') +const parse = require('../../functions/parse') +const truncations = require('../fixtures/truncations.js') +const validVersions = require('../fixtures/valid-versions.js') + +// Freezing SemVer object inputs to truncate ensures that the truncate function +// does not mutate them +const parseAndFreezeSemVerObject = (version) => { + const parsed = parse(version) + Object.freeze(parsed) + return parsed +} + +test('truncate fixture versions test', (t) => { + truncations.forEach(([pre, truncation, expected]) => { + const actual = truncate(pre, truncation) + const cmd = `truncate(${pre}, ${truncation})` + t.equal(actual, expected, `${cmd} === ${expected}`) + + const parsed = parseAndFreezeSemVerObject(pre) + const semverTruncated = truncate(parsed, truncation) + t.equal(semverTruncated, expected, `${cmd} works on Semver object inputs`) + }) + + t.end() +}) + +test('truncate pre* only removes build info', (t) => { + ['prerelease', 'prepatch', 'preminor', 'premajor'].forEach(what => { + validVersions.forEach((v) => { + const versionToTruncate = v[0] + const parsed = parseAndFreezeSemVerObject(versionToTruncate) + const expected = parsed.version + const actual = truncate(versionToTruncate, what) + + const cmd = `truncate(${versionToTruncate}, ${what})` + t.equal(actual, expected, `${cmd} === ${expected}`) + t.same(parse(actual).build, [], `${cmd} build info should be removed`) + }) + }) + t.end() +})