-
Notifications
You must be signed in to change notification settings - Fork 492
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
subset(): test if one range is a subset of another
This also removes `>=0.0.0` (or `>=0.0.0-0` in `includePrerelease` mode) from the comparators in a range set, because that is equivalent to a `*`.
- Loading branch information
Showing
8 changed files
with
250 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
const Range = require('../classes/range.js') | ||
const { ANY } = require('../classes/comparator.js') | ||
const satisfies = require('../functions/satisfies.js') | ||
const compare = require('../functions/compare.js') | ||
|
||
// Complex range `r1 || r2 || ...` is a subset of `R1 || R2 || ...` iff: | ||
// - Every simple range `r1, r2, ...` is a subset of some `R1, R2, ...` | ||
// | ||
// Simple range `c1 c2 ...` is a subset of simple range `C1 C2 ...` iff: | ||
// - If c is only the ANY comparator | ||
// - If C is only the ANY comparator, return true | ||
// - Else return false | ||
// - Let EQ be the set of = comparators in c | ||
// - If EQ is more than one, return true (null set) | ||
// - Let GT be the highest > or >= comparator in c | ||
// - Let LT be the lowest < or <= comparator in c | ||
// - If GT and LT, and GT.semver > LT.semver, return true (null set) | ||
// - If EQ | ||
// - If GT, and EQ does not satisfy GT, return true (null set) | ||
// - If LT, and EQ does not satisfy LT, return true (null set) | ||
// - If EQ satisfies every C, return true | ||
// - Else return false | ||
// - If GT | ||
// - If GT is lower than any > or >= comp in C, return false | ||
// - If GT is >=, and GT.semver does not satisfy every C, return false | ||
// - If LT | ||
// - If LT.semver is greater than that of any > comp in C, return false | ||
// - If LT is <=, and LT.semver does not satisfy every C, return false | ||
// - If any C is a = range, and GT or LT are set, return false | ||
// - Else return true | ||
|
||
const subset = (sub, dom, options) => { | ||
sub = new Range(sub, options) | ||
dom = new Range(dom, options) | ||
let sawNonNull = false | ||
|
||
OUTER: for (const simpleSub of sub.set) { | ||
for (const simpleDom of dom.set) { | ||
const isSub = simpleSubset(simpleSub, simpleDom, options) | ||
sawNonNull = sawNonNull || isSub !== null | ||
if (isSub) | ||
continue OUTER | ||
} | ||
// the null set is a subset of everything, but null simple ranges in | ||
// a complex range should be ignored. so if we saw a non-null range, | ||
// then we know this isn't a subset, but if EVERY simple range was null, | ||
// then it is a subset. | ||
if (sawNonNull) | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
const simpleSubset = (sub, dom, options) => { | ||
if (sub.length === 1 && sub[0].semver === ANY) | ||
return dom.length === 1 && dom[0].semver === ANY | ||
|
||
const eqSet = new Set() | ||
let gt, lt | ||
for (const c of sub) { | ||
if (c.operator === '>' || c.operator === '>=') | ||
gt = higherGT(gt, c, options) | ||
else if (c.operator === '<' || c.operator === '<=') | ||
lt = lowerLT(lt, c, options) | ||
else | ||
eqSet.add(c.semver) | ||
} | ||
|
||
if (eqSet.size > 1) | ||
return null | ||
|
||
let gtltComp | ||
if (gt && lt) { | ||
gtltComp = compare(gt.semver, lt.semver, options) | ||
if (gtltComp > 0) | ||
return null | ||
else if (gtltComp === 0 && (gt.operator !== '>=' || lt.operator !== '<=')) | ||
return null | ||
} | ||
|
||
// will iterate one or zero times | ||
for (const eq of eqSet) { | ||
if (gt && !satisfies(eq, String(gt), options)) | ||
return null | ||
|
||
if (lt && !satisfies(eq, String(lt), options)) | ||
return null | ||
|
||
for (const c of dom) { | ||
if (!satisfies(eq, String(c), options)) | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
let higher, lower | ||
let hasDomLT, hasDomGT | ||
for (const c of dom) { | ||
hasDomGT = hasDomGT || c.operator === '>' || c.operator === '>=' | ||
hasDomLT = hasDomLT || c.operator === '<' || c.operator === '<=' | ||
if (gt) { | ||
if (c.operator === '>' || c.operator === '>=') { | ||
higher = higherGT(gt, c, options) | ||
if (higher === c) | ||
return false | ||
} else if (gt.operator === '>=' && !satisfies(gt.semver, String(c), options)) | ||
return false | ||
} | ||
if (lt) { | ||
if (c.operator === '<' || c.operator === '<=') { | ||
lower = lowerLT(lt, c, options) | ||
if (lower === c) | ||
return false | ||
} else if (lt.operator === '<=' && !satisfies(lt.semver, String(c), options)) | ||
return false | ||
} | ||
if (!c.operator && (lt || gt) && gtltComp !== 0) | ||
return false | ||
} | ||
|
||
// if there was a < or >, and nothing in the dom, then must be false | ||
// UNLESS it was limited by another range in the other direction. | ||
// Eg, >1.0.0 <1.0.1 is still a subset of <2.0.0 | ||
if (gt && hasDomLT && !lt && gtltComp !== 0) | ||
return false | ||
|
||
if (lt && hasDomGT && !gt && gtltComp !== 0) | ||
return false | ||
|
||
return true | ||
} | ||
|
||
// >=1.2.3 is lower than >1.2.3 | ||
const higherGT = (a, b, options) => { | ||
if (!a) | ||
return b | ||
const comp = compare(a.semver, b.semver, options) | ||
return comp > 0 ? a | ||
: comp < 0 ? b | ||
: b.operator === '>' && a.operator === '>=' ? b | ||
: a | ||
} | ||
|
||
// <=1.2.3 is higher than <1.2.3 | ||
const lowerLT = (a, b, options) => { | ||
if (!a) | ||
return b | ||
const comp = compare(a.semver, b.semver, options) | ||
return comp < 0 ? a | ||
: comp > 0 ? b | ||
: b.operator === '<' && a.operator === '<=' ? b | ||
: a | ||
} | ||
|
||
module.exports = subset |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
const t = require('tap') | ||
const subset = require('../../ranges/subset.js') | ||
|
||
// sub, dom, expect, [options] | ||
const cases = [ | ||
['1.2.3', '1.2.3', true], | ||
['1.2.3 1.2.4', '1.2.3', true], | ||
['1.2.3 2.3.4 || 2.3.4', '3', false], | ||
['^1.2.3-pre.0', '1.x', false], | ||
['^1.2.3-pre.0', '1.x', true, { includePrerelease: true }], | ||
['>2 <1', '3', true], | ||
['1 || 2 || 3', '>=1.0.0', true], | ||
|
||
['*', '*', true], | ||
['', '*', true], | ||
['*', '', true], | ||
['', '', true], | ||
|
||
// >=0.0.0 is like * in non-prerelease mode | ||
// >=0.0.0-0 is like * in prerelease mode | ||
['*', '>=0.0.0-0', true, { includePrerelease: true }], | ||
['*', '>=0.0.0', true], | ||
['*', '>=0.0.0', false, { includePrerelease: true }], | ||
['*', '>=0.0.0-0', false], | ||
['^2 || ^3 || ^4', '>=1', true], | ||
['^2 || ^3 || ^4', '>1', true], | ||
['^2 || ^3 || ^4', '>=2', true], | ||
['^2 || ^3 || ^4', '>=3', false], | ||
['>=1', '^2 || ^3 || ^4', false], | ||
['>1', '^2 || ^3 || ^4', false], | ||
['>=2', '^2 || ^3 || ^4', false], | ||
['>=3', '^2 || ^3 || ^4', false], | ||
['^1', '^2 || ^3 || ^4', false], | ||
['^2', '^2 || ^3 || ^4', true], | ||
['^3', '^2 || ^3 || ^4', true], | ||
['^4', '^2 || ^3 || ^4', true], | ||
['1.x', '^2 || ^3 || ^4', false], | ||
['2.x', '^2 || ^3 || ^4', true], | ||
['3.x', '^2 || ^3 || ^4', true], | ||
['4.x', '^2 || ^3 || ^4', true], | ||
|
||
['>=1.0.0 <=1.0.0 || 2.0.0', '1.0.0 || 2.0.0', true], | ||
['<=1.0.0 >=1.0.0 || 2.0.0', '1.0.0 || 2.0.0', true], | ||
['>=1.0.0', '1.0.0', false], | ||
['>=1.0.0 <2.0.0', '<2.0.0', true], | ||
['>=1.0.0 <2.0.0', '>0.0.0', true], | ||
['>=1.0.0 <=1.0.0', '1.0.0', true], | ||
['>=1.0.0 <=1.0.0', '2.0.0', false], | ||
['<2.0.0', '>=1.0.0 <2.0.0', false], | ||
['>=1.0.0', '>=1.0.0 <2.0.0', false], | ||
['>=1.0.0 <2.0.0', '<2.0.0', true], | ||
['>=1.0.0 <2.0.0', '>=1.0.0', true], | ||
['>=1.0.0 <2.0.0', '>1.0.0', false], | ||
['>=1.0.0 <=2.0.0', '<2.0.0', false], | ||
['>=1.0.0', '<1.0.0', false], | ||
['<=1.0.0', '>1.0.0', false], | ||
['<=1.0.0 >1.0.0', '>1.0.0', true], | ||
['1.0.0 >1.0.0', '>1.0.0', true], | ||
['1.0.0 <1.0.0', '>1.0.0', true], | ||
['<1 <2 <3', '<4', true], | ||
['<3 <2 <1', '<4', true], | ||
['>1 >2 >3', '>0', true], | ||
['>3 >2 >1', '>0', true], | ||
['<=1 <=2 <=3', '<4', true], | ||
['<=3 <=2 <=1', '<4', true], | ||
['>=1 >=2 >=3', '>0', true], | ||
['>=3 >=2 >=1', '>0', true], | ||
] | ||
|
||
t.plan(cases.length) | ||
cases.forEach(([sub, dom, expect, options = {}]) => { | ||
const msg = `${sub || "''"} ⊂ ${dom || "''"} = ${expect}` + | ||
(options ? ' ' + Object.keys(options).join(',') : '') | ||
t.equal(subset(sub, dom, options), expect, msg) | ||
}) |