Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for range and comparators comparison #187

Closed
wants to merge 5 commits into from

Conversation

rtfpessoa
Copy link
Contributor

  • Check if two ranges intersect
  • Check if one comparator satisfies a range
  • Check if two comparators intersect

Fixes #178

* Check if two ranges intersect
* Check if one comparator satisfies a range
* Check if two comparators intersect
@isaacs isaacs added the review label Apr 1, 2017
semver.js Outdated
((compA.operator === '<=' || compA.operator === '<') && (compB.operator === '>=' || compB.operator === '>')));
}

exports.comparatorSatisfiesRange = comparatorSatisfiesRange;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function names are a little overly verbose/long for my tastes. but looks like good functionality

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly; if this was a prototype method on Comparator it could just be satisfiesRange?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I think I was following the toComparators logic but I think I like it more this way too.

@rtfpessoa
Copy link
Contributor Author

@shellscape I am completely open for alternatives. Let me know if you have any suggestions.

semver.js Outdated
@@ -793,6 +793,56 @@ function toComparators(range, loose) {
});
}

exports.comparatorsIntersect = comparatorsIntersect;
function comparatorsIntersect(compA, compB, loose, platform) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this better as a standalone function, or as a prototype method on Comparator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

semver.js Outdated
exports.comparatorsIntersect = comparatorsIntersect;
function comparatorsIntersect(compA, compB, loose, platform) {
compA = new Comparator(compA, loose);
compB = new Comparator(compB, loose);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reassigning function arguments deopts in v8; could you use new variables for these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

semver.js Outdated
((compA.operator === '<=' || compA.operator === '<') && (compB.operator === '>=' || compB.operator === '>')));
}

exports.comparatorSatisfiesRange = comparatorSatisfiesRange;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly; if this was a prototype method on Comparator it could just be satisfiesRange?

semver.js Outdated
}

exports.rangesIntersect = rangesIntersect;
function rangesIntersect(rangeA, rangeB, loose, platform) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if this was a prototype method on Range it could just be intersects?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

test/index.js Outdated
['<1.0.0 >2.0.0', '>0.0.0', true],
['<1.0.0 >2.0.0', '>1.4.0 <1.6.0', false],
['<1.0.0 >2.0.0', '>1.4.0 <1.6.0 || 2.0.0', false],
['<1.0.0 >=2.0.0', '2.1.0', false],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test case for a single version returning true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

test/index.js Outdated
var range2 = v[1];
var expect = v[2];
var actual = semver.rangesIntersect(range1, range2);
t.equal(actual, expect);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also assert that if you flip the range order that it provides the same value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

['<=1.3.0', '<=1.3.0', true],
['>1.3.0', '<=1.3.0', false],
['>=1.3.0', '<1.3.0', false],
// Opposite matching directions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could instead be done in the iterator function, for every test, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not understanding exactly what you mean here. Can you extend a bit?

Also, should I amend my previous commit or should I create new commits to fix the requested changes?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, line 752 asks if comparator1 intersects comparator2; could you also add an assertion after that asking if comparator2 intersects comparator1?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think amending and force pushing is always fine; unless one of this repo's collabs says different.

semver.js Outdated
@@ -704,6 +704,42 @@ Comparator.prototype.test = function(version) {
return cmp(version, this.operator, this.semver, this.loose);
};

Comparator.prototype.intersects = function(compOrStr, loose, platform) {
var rangeTmp;
var comp = new Comparator(compOrStr, loose);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be bad if this only took a Comparator? That would make the API more explicit and delegate more decisions to the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok with this change, but I actually do not think the user makes more decisions. Since we allow the method to receive either a comparator or a string they can do all the same decisions if they want.

But the API is definitely more explicit if we force a Comparator.

Usually I am a bit more defensive and that is why I usually try to handle all the weird possibilities. But if this was Typescript it would make much more sense to receive only the comparator.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think typescript has anything to do with it; it's nicer to have monomorphic APIs with or without types. By "decisions" I mean, the comparator and the range having the same loose setting, for example - forcing the user to make that choice means they can never be ignorant of the default.

semver.js Outdated

Comparator.prototype.satisfiesRange = function(rangeOrStr, loose, platform) {
var comp = this;
var range = new Range(rangeOrStr, loose, platform);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here

semver.js Outdated
@@ -783,6 +819,16 @@ Range.prototype.parseRange = function(range) {
return set;
};

Range.prototype.intersects = function(rangeOrStr, loose, platform) {
var range = new Range(rangeOrStr, loose, platform);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here

['<=1.3.0', '<=1.3.0', true],
['>1.3.0', '<=1.3.0', false],
['>=1.3.0', '<1.3.0', false],
// Opposite matching directions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, line 752 asks if comparator1 intersects comparator2; could you also add an assertion after that asking if comparator2 intersects comparator1?

['<=1.3.0', '<=1.3.0', true],
['>1.3.0', '<=1.3.0', false],
['>=1.3.0', '<1.3.0', false],
// Opposite matching directions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think amending and force pushing is always fine; unless one of this repo's collabs says different.

@rtfpessoa
Copy link
Contributor Author

@ljharb I think I made all the changes you suggested in this last commit. Take a look when you have some time.

Copy link

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks great!

I would strongly prefer much more defensive throwing to check the types of arguments at runtime, but il leave insisting on that to the collaborators.

Hopefully this gets merged and released soon!

semver.js Outdated
return satisfies(comp.semver, rangeTmp, loose, platform);
}
// Same direction increasing
return ((this.operator === '>=' || this.operator === '>') && (comp.operator === '>=' || comp.operator === '>')) ||
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like making this one statement makes it a bit more confusing to read; what do you think about using if/else here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide an example?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - maybe just separate variables would help. Here's a subset:

var sameDirectionIncreasing = (this.operator === '>=' || this.operator === '>') && (comp.operator === '>=' || comp.operator === '>');
var sameDirectionDecreasing = (this.operator === '<=' || this.operator === '<') && (comp.operator === '<=' || comp.operator === '<');
var sameSemver = this.semver.raw === comp.semver.raw;
var differentDirectionsInclusive = (this.operator === '>=' || this.operator === '<=') && (comp.operator === '>=' || comp.operator === '<=');
return sameDirectionIncreasing || sameDirectionDecreasing || (sameSemver && differentDirectionsInclusive);

something like that? the long chain of boolean operations makes it hard for me to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a nice improvement.

@rtfpessoa
Copy link
Contributor Author

When you say "I would strongly prefer much more defensive throwing to check the types of arguments at runtime, but il leave insisting on that to the collaborators", like what check if instanceof?

@ljharb
Copy link

ljharb commented Apr 2, 2017

@rtfpessoa exactly like that - i'd do if (!(range instanceof Range)) { throw new TypeError('a Range is required'); }, if (typeof loose !== 'undefined' || typeof loose !== 'boolean') { throw new TypeError('loose must be a boolean if present'); } and things like that.

In other words, if I'm passing the wrong kind of argument, I want to be alerted to that fact as soon and explicitly as possible.

@rtfpessoa
Copy link
Contributor Author

Just did both changes except for the loose and platform check since I never use them and for that reason it would make more sense to be checked in Range or Comparator.

@rtfpessoa rtfpessoa force-pushed the master branch 2 times, most recently from 5da850d to 7a525c5 Compare April 2, 2017 22:20
@rtfpessoa
Copy link
Contributor Author

Also, I added a couple more tests for the parameter checks and fixed the format issues.

@coveralls
Copy link

coveralls commented Apr 2, 2017

Coverage Status

Coverage increased (+0.2%) to 94.872% when pulling b5504e7 on rtfpessoa:master into 8fff305 on npm:master.

Copy link

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Much cleaner imo :-)

@iarna
Copy link

iarna commented Apr 9, 2017

This seems super useful! It just needs documentation to land.

@ljharb
Copy link

ljharb commented Apr 9, 2017

Awesome! If @rtfpessoa can't find time to write it soon, I'll try to find time to write some :-)

@rtfpessoa
Copy link
Contributor Author

rtfpessoa commented Apr 9, 2017

@iarna can you extend a bit on what do you mean by documentation?
I could not find any docs explaining a proper contribution, maybe I looked in the wrong places. Can you point me in the right direction?

Edit: Do you mean README docs? If that is it, I completely missed it, will do it now.

@coveralls
Copy link

coveralls commented Apr 9, 2017

Coverage Status

Coverage increased (+0.2%) to 94.872% when pulling 57c77f3 on rtfpessoa:master into 8fff305 on npm:master.

@@ -2,3 +2,4 @@
.*.swp
coverage/
.nyc_output/
.idea
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally this belongs in a global git config, rather than polluting specific git repos with IDE-specific knowledge (nbd tho)

@iarna
Copy link

iarna commented Apr 12, 2017

@rtfpessoa Yes, that's exactly what I was looking for!

@rtfpessoa
Copy link
Contributor Author

@iarna, not sure if you noticed but I already did it. Le me know if you need any other changes for the merge.

@rtfpessoa
Copy link
Contributor Author

any updates about this @isaacs @iarna ?

@franher
Copy link

franher commented Jun 1, 2017

looking forward for the landing of this PR 🙏

@agebhardt
Copy link

Really excited to be able to use this feature! Are there any updates on this PR?

Copy link
Contributor

@isaacs isaacs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it works, and Range intersection is definitely something that people ask for often. Thanks for taking the lead on this, and sorry for the delays in reviewing.

Some changes requested, but I think we can get this landed relatively quickly. Thanks!

semver.js Outdated
return satisfies(comp.semver, rangeTmp, loose, platform);
}

var sameDirectionIncreasing = (this.operator === '>=' || this.operator === '>') && (comp.operator === '>=' || comp.operator === '>');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These really long lines are very hard to read.

Can you please break this up into separate stanzas? There should be no lines longer than 80 chars under any circumstances. 70 is an even better limit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For more context on this, I've got somewhat bad vision, and usually keep my font size turned way up. It's a pretty common accessibility issue, though.

image

semver.js Outdated
oppositeDirectionsLessThan || oppositeDirectionsGreaterThan;
};

Comparator.prototype.satisfiesRange = function(range, loose, platform) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the "platform" variable being passed around here? It doesn't seem like it's ever used for anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ported this from the Ruby version and somehow did not notice platform did not exist here.

README.md Outdated
@@ -329,6 +329,10 @@ strings that they parse.
(`major`, `premajor`, `minor`, `preminor`, `patch`, `prepatch`, or `prerelease`),
or null if the versions are the same.

### Comparators
* `intersects(comparator)`: Return true if the comparators intersect
* `satisfiesRange(range)`: Return true if the comparator intersects with any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming of this is a bit weird. You wouldn't normally say that a comparator "satisfies" a range. A range is satisfied by a single thing which has a specific version, not by a comparator, which is like a mini-range. Also, a comparator like <=1.4.5 would return true for this method, given the Range >=1.3.0, even though many versions allowed by that comparator are not satisfying answers for the Range. We can only talk about intersections and unions with ranges (and their building blocks, Comparators) because they are sets, not discrete atomic items.

Having a comparator.intersects(comparator) method seems like a building block towards having range intersection. People generally don't interact with comparators directly. Having (effectively) comparator.intersects(range) is a bit odd, and using the language of "satisfying" makes it odder.

It seems like what you'd really want in most cases is range.intersects(range). It's probably better to leave aside things that don't get us closer to that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably addressed by calling it comparator.intersectsRange()? Or even cleaner, make Comparator.intersects take either a Range or Comparator as an argument.

@rtfpessoa
Copy link
Contributor Author

@isaacs will try to do this soon.

@rtfpessoa
Copy link
Contributor Author

@isaacs let me know if this was what you asked. I removed the confusing method and exposed only the two intersections between the same type.

@coveralls
Copy link

coveralls commented Jul 15, 2017

Coverage Status

Coverage increased (+0.2%) to 94.835% when pulling 4fa71d9 on rtfpessoa:master into 8fff305 on npm:master.

@ljharb
Copy link

ljharb commented Jul 23, 2017

@isaacs @iarna @zkat any update on this one? I've been waiting on its release to fix an eslint bug for about 9 months now :-/

var sameDirectionDecreasing =
(this.operator === '<=' || this.operator === '<') &&
(comp.operator === '<=' || comp.operator === '<');
var sameSemVer = this.semver.raw === comp.semver.raw;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you're after isn't semver.raw here, but semver.version. Consider this:

> r2 = new s.Range('>=v1.2.3')
Range {
  loose: undefined,
  raw: '>=v1.2.3',
  set: [ [ [Object] ] ],
  range: '>=1.2.3' }
> r = new s.Range('<=1.2.3')
Range {
  loose: undefined,
  raw: '<=1.2.3',
  set: [ [ [Object] ] ],
  range: '<=1.2.3' }
> r.intersects(r2)
false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that fixes it. I'll include it in the merge, no worries.

@isaacs
Copy link
Contributor

isaacs commented Jul 24, 2017

Landed on master and published as v5.4.0. Also added a top-level semver.intersects(r1, r2) command, and a bunch of tests to improve coverage.

@isaacs isaacs closed this Jul 24, 2017
@ljharb
Copy link

ljharb commented Jul 24, 2017

Yay, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants