-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgetIntersection.js
140 lines (123 loc) · 4.45 KB
/
getIntersection.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
'use strict';
// I'm not using ts-check here because TS doesn't know that the Range constructor can take a Comparator
const { Comparator, Range, cmp } = require('semver');
const _ = require('lodash');
/**
* @param {Comparator} a
* @param {Comparator} b
* @returns {Range | null}
*/
const getComparatorIntersection = (a, b) => {
// Special cases:
// Either input is "any"
if (a.value === '') return new Range(b);
if (b.value === '') return new Range(a);
// Either input is "exact match"
if (a.operator === '' || a.operator === '=') return b.test(a.semver) ? new Range(a) : null;
if (b.operator === '' || b.operator === '=') return a.test(b.semver) ? new Range(b) : null;
// If they both go the same direction, pick the more restrictive one
if (a.operator[0] === b.operator[0]) {
if (b.operator[1] === '=')
return new Range(a.test(b.semver) ? b : a);
return new Range(b.test(a.semver) ? a : b);
}
// If they go opposite directions but have the same bound, e.g. "<= 1.0" and ">= 1.0", return just that bound.
if (
a.operator[1] === '=' &&
b.operator[1] === '=' &&
a.semver.version === b.semver.version
) return new Range(a.semver.version);
// They go different directions so they may define a range, e.g. "> 1.0" and "< 2.0".
if (cmp(a.semver, '<', b.semver) && a.operator[0] === '>' && b.operator[0] === '<') return new Range(`${a} ${b}`);
if (cmp(a.semver, '>', b.semver) && a.operator[0] === '<' && b.operator[0] === '>') return new Range(`${b} ${a}`);
// They don't intersect
return null;
};
/**
* If either parameter is null, returns the other. Never returns null for nonnull inputs.
* @param {Comparator | Range | null} a
* @param {Comparator | Range | null} b
* @returns {Range | null}
*/
const getUnion = (a, b) => {
if (!a) return b instanceof Comparator ? new Range(b) : b;
if (!b) return a instanceof Comparator ? new Range(a) : a;
// Determine the unique comparators and make a range out of those
const aComparators = a instanceof Range ? a.set : [[a]];
const bComparators = b instanceof Range ? b.set : [[b]];
const comparators = _.uniqWith(aComparators.concat(bComparators), _.isEqual);
return new Range(comparators.map(x => x.join(' ')).join('||'));
};
const any = new Range('*');
/**
* @param {readonly Comparator[]} set1
* @param {readonly Comparator[]} set2
* @returns {Range | null}
*/
const comparatorSetIntersect = (set1, set2) => {
const lowerBounds = [];
const upperBounds = [];
const others = [];
for (const comp of set1.concat(set2)) {
switch (comp.operator[0]) {
case '<':
upperBounds.push(comp);
break;
case '>':
lowerBounds.push(comp);
break;
default:
others.push(comp);
break;
}
}
/**
* @param {Range | null} a
* @param {Comparator} b
* @returns {Range | null}
*/
const reduceFn = (a, b) => {
if (a === null) return null;
return getComparatorIntersection(
new Comparator(a.toString()),
b
);
};
const lower = lowerBounds.reduce(reduceFn, any);
const upper = upperBounds.reduce(reduceFn, any);
const exactOrAny = others.reduce(reduceFn, any);
if (exactOrAny === null || lower === null || upper === null) {
// No match
return null;
}
if (exactOrAny.range !== '') {
// only one version can match
if (upper.test(exactOrAny.toString()) && lower.test(exactOrAny.toString())) return exactOrAny;
return null;
}
return getComparatorIntersection(
new Comparator(lower.toString()),
new Comparator(upper.toString())
);
};
/**
* @param {Range} a
* @param {Range} b
* @returns {Range | null}
*/
const getRangeIntersection = (a, b) => {
return a.set.map(aRange =>
b.set.map(bRange => comparatorSetIntersect(aRange, bRange)).reduce(getUnion, null)
).reduce(getUnion, null);
};
/**
* Returns the intersection of the given versions, or 'null' if there's no overlap.
* @param {Range | Comparator | string} a
* @param {Range | Comparator | string} b
* @returns {Range | null}
*/
module.exports = function getIntersection(a, b) {
if (a instanceof Comparator && b instanceof Comparator)
return getComparatorIntersection(a, b);
return getRangeIntersection(new Range(a), new Range(b));
};