Add VersionConstraint.difference().

See dart-lang/pub#912

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//2045803002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf95427..db3d320 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
   implement `new VersionConstraint.unionOf()` and `VersionConstraint.union()`.
   Now it's public so you can use it too.
 
+* Added `VersionConstraint.difference()`. This returns a constraint matching all
+  versions matched by one constraint but not another.
+
 * Make `VersionRange` implement `Comparable<VersionRange>`. Ranges are ordered
   first by lower bound, then by upper bound.
 
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 6493dda..efe105a 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -13,17 +13,46 @@
       (!range1.includeMax && range2.includeMin);
 }
 
-/// A [Comparator] that compares the maximum versions of [range1] and [range2].
-int compareMax(VersionRange range1, VersionRange range2) {
-  if (range1.max == null) {
-    if (range2.max == null) return 0;
-    return 1;
-  } else if (range2.max == null) {
-    return -1;
-  }
+/// Returns whether [range1] allows lower versions than [range2].
+bool allowsLower(VersionRange range1, VersionRange range2) {
+  if (range1.min == null) return range2.min != null;
+  if (range2.min == null) return false;
 
-  var result = range1.max.compareTo(range2.max);
-  if (result != 0) return result;
-  if (range1.includeMax != range2.includeMax) return range1.includeMax ? 1 : -1;
-  return 0;
+  var comparison = range1.min.compareTo(range2.min);
+  if (comparison == -1) return true;
+  if (comparison == 1) return false;
+  return range1.includeMin && !range2.includeMin;
+}
+
+/// Returns whether [range1] allows higher versions than [range2].
+bool allowsHigher(VersionRange range1, VersionRange range2) {
+  if (range1.max == null) return range2.max != null;
+  if (range2.max == null) return false;
+
+  var comparison = range1.max.compareTo(range2.max);
+  if (comparison == 1) return true;
+  if (comparison == -1) return false;
+  return range1.includeMax && !range2.includeMax;
+}
+
+/// Returns whether [range1] allows only versions lower than those allowed by
+/// [range2].
+bool strictlyLower(VersionRange range1, VersionRange range2) {
+  if (range1.max == null || range2.min == null) return false;
+
+  var comparison = range1.max.compareTo(range2.min);
+  if (comparison == -1) return true;
+  if (comparison == 1) return false;
+  return !range1.includeMax || !range2.includeMin;
+}
+
+/// Returns whether [range1] allows only versions higher than those allowed by
+/// [range2].
+bool strictlyHigher(VersionRange range1, VersionRange range2) {
+  if (range1.min == null || range2.max == null) return false;
+
+  var comparison = range1.min.compareTo(range2.max);
+  if (comparison == 1) return true;
+  if (comparison == -1) return false;
+  return !range1.includeMin || !range2.includeMax;
 }
diff --git a/lib/src/version.dart b/lib/src/version.dart
index f2662b9..2d43a1a 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -267,6 +267,9 @@
     return new VersionConstraint.unionOf([this, other]);
   }
 
+  VersionConstraint difference(VersionConstraint other) =>
+      other.allows(this) ? VersionConstraint.empty : this;
+
   int compareTo(VersionRange other) {
     if (other is Version) {
       if (major != other.major) return major.compareTo(other.major);
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
index 68827a4..edd8abd 100644
--- a/lib/src/version_constraint.dart
+++ b/lib/src/version_constraint.dart
@@ -229,13 +229,17 @@
   /// allows.
   bool allowsAny(VersionConstraint other);
 
-  /// Creates a new [VersionConstraint] that only allows [Version]s allowed by
-  /// both this and [other].
+  /// Returns a [VersionConstraint] that only allows [Version]s allowed by both
+  /// this and [other].
   VersionConstraint intersect(VersionConstraint other);
 
-  /// Creates a new [VersionConstraint] that allows [Versions]s allowed by
-  /// either this or [other].
+  /// Returns a [VersionConstraint] that allows [Versions]s allowed by either
+  /// this or [other].
   VersionConstraint union(VersionConstraint other);
+
+  /// Returns a [VersionConstraint] that allows [Version]s allowed by this but
+  /// not [other].
+  VersionConstraint difference(VersionConstraint other);
 }
 
 class _EmptyVersion implements VersionConstraint {
@@ -248,6 +252,7 @@
   bool allowsAny(VersionConstraint other) => false;
   VersionConstraint intersect(VersionConstraint other) => this;
   VersionConstraint union(VersionConstraint other) => other;
+  VersionConstraint difference(VersionConstraint other) => this;
   String toString() => '<empty>';
 }
 
diff --git a/lib/src/version_range.dart b/lib/src/version_range.dart
index a8d6069..ffa50ab 100644
--- a/lib/src/version_range.dart
+++ b/lib/src/version_range.dart
@@ -160,37 +160,7 @@
     }
 
     if (other is VersionRange) {
-      // If neither range has a minimum, they'll overlap at some point.
-      //
-      //     ...     this     ]
-      //     ...     other         ]
-      if (min == null && other.min == null) return true;
-
-      // If this range has a lower minimum than the other range, it overlaps as
-      // long as its maximum is higher than or the same as the other range's
-      // minimum.
-      //
-      //     [  this  ]            [  this  ]
-      //        [  other  ]                 [  other  ]
-      if (min == null || (other.min != null && min < other.min)) {
-        if (max == null) return true;
-        if (max > other.min) return true;
-        if (max < other.min) return false;
-        assert(max == other.min);
-        return includeMax && other.includeMin;
-      }
-
-      // If this range has a higher minimum than the other range, it overlaps as
-      // long as its minimum is lower than or the same as the other range's
-      // maximum.
-      //
-      //         [  this  ]               [  this  ]
-      //     [  other  ]        [  other  ]
-      if (other.max == null) return true;
-      if (min < other.max) return true;
-      if (min > other.max) return false;
-      assert(min == other.max);
-      return includeMin && other.includeMax;
+      return !strictlyLower(other, this) && !strictlyHigher(other, this);
     }
 
     throw new ArgumentError('Unknown VersionConstraint type $other.');
@@ -320,9 +290,100 @@
     return new VersionConstraint.unionOf([this, other]);
   }
 
+  VersionConstraint difference(VersionConstraint other) {
+    if (other.isEmpty) return this;
+
+    if (other is Version) {
+      if (!allows(other)) return this;
+
+      if (other == min) {
+        if (!includeMin) return this;
+        return new VersionRange(
+            min: min, max: max,
+            includeMin: false, includeMax: includeMax);
+      }
+
+      if (other == max) {
+        if (!includeMax) return this;
+        return new VersionRange(
+            min: min, max: max,
+            includeMin: includeMin, includeMax: false);
+      }
+
+      return new VersionUnion.fromRanges([
+        new VersionRange(
+            min: min, max: other,
+            includeMin: includeMin, includeMax: false),
+        new VersionRange(
+            min: other, max: max,
+            includeMin: false, includeMax: includeMax)
+      ]);
+    } else if (other is VersionRange) {
+      if (!allowsAny(other)) return this;
+
+      VersionConstraint before;
+      if (!allowsLower(this, other)) {
+        before = VersionConstraint.empty;
+      } else if (min == other.min) {
+        assert(includeMin && !other.includeMin);
+        assert(min != null);
+        before = min;
+      } else {
+        before = new VersionRange(
+            min: min, max: other.min,
+            includeMin: includeMin, includeMax: !other.includeMin);
+      }
+
+      VersionConstraint after;
+      if (!allowsHigher(this, other)) {
+        after = VersionConstraint.empty;
+      } else if (max == other.max) {
+        assert(includeMax && !other.includeMax);
+        assert(max != null);
+        after = max;
+      } else {
+        after = new VersionRange(
+            min: other.max, max: max,
+            includeMin: !other.includeMax, includeMax: includeMax);
+      }
+
+      if (before == VersionConstraint.empty) return after;
+      if (after == VersionConstraint.empty) return before;
+      return new VersionUnion.fromRanges([before, after]);
+    } else if (other is VersionUnion) {
+      var ranges = <VersionRange>[];
+      var current = this;
+
+      for (var range in other.ranges) {
+        // Skip any ranges that are strictly lower than [current].
+        if (strictlyLower(range, current)) continue;
+
+        // If we reach a range strictly higher than [current], no more ranges
+        // will be relevant so we can bail early.
+        if (strictlyHigher(range, current)) break;
+
+        var difference = current.difference(range);
+        if (difference is VersionUnion) {
+          // If [range] split [current] in half, we only need to continue
+          // checking future ranges against the latter half.
+          assert(difference.ranges.length == 2);
+          ranges.add(difference.ranges.first);
+          current = difference.ranges.last;
+        } else {
+          current = difference as VersionRange;
+        }
+      }
+
+      if (ranges.isEmpty) return current;
+      return new VersionUnion.fromRanges(ranges..add(current));
+    }
+
+    throw new ArgumentError('Unknown VersionConstraint type $other.');
+  }
+
   int compareTo(VersionRange other) {
     if (min == null) {
-      if (other.min == null) return compareMax(this, other);
+      if (other.min == null) return _compareMax(other);
       return -1;
     } else if (other.min == null) {
       return 1;
@@ -332,7 +393,22 @@
     if (result != 0) return result;
     if (includeMin != other.includeMin) return includeMin ? -1 : 1;
 
-    return compareMax(this, other);
+    return _compareMax(other);
+  }
+
+  /// Compares the maximum values of [this] and [other].
+  int _compareMax(VersionRange other) {
+    if (max == null) {
+      if (other.max == null) return 0;
+      return 1;
+    } else if (other.max == null) {
+      return -1;
+    }
+
+    var result = max.compareTo(other.max);
+    if (result != 0) return result;
+    if (includeMax != other.includeMax) return includeMax ? 1 : -1;
+    return 0;
   }
 
   String toString() {
diff --git a/lib/src/version_union.dart b/lib/src/version_union.dart
index d0e1e2b..a26cb52 100644
--- a/lib/src/version_union.dart
+++ b/lib/src/version_union.dart
@@ -76,7 +76,7 @@
 
       // Move the constraint with the lower max value forward. This ensures that
       // we keep both lists in sync as much as possible.
-      if (compareMax(ourRanges.current, theirRanges.current) < 0) {
+      if (allowsHigher(theirRanges.current, ourRanges.current)) {
         ourRanges.moveNext();
       } else {
         theirRanges.moveNext();
@@ -104,7 +104,7 @@
       // Move the constraint with the lower max value forward. This ensures that
       // we keep both lists in sync as much as possible, and that large ranges
       // have a chance to match multiple small ranges that they contain.
-      if (compareMax(ourRanges.current, theirRanges.current) < 0) {
+      if (allowsHigher(theirRanges.current, ourRanges.current)) {
         ourRanges.moveNext();
       } else {
         theirRanges.moveNext();
@@ -117,6 +117,80 @@
     return new VersionUnion.fromRanges(newRanges);
   }
 
+  VersionConstraint difference(VersionConstraint other) {
+    var ourRanges = ranges.iterator;
+    var theirRanges = _rangesFor(other).iterator;
+
+    var newRanges = <VersionRange>[];
+    ourRanges.moveNext();
+    theirRanges.moveNext();
+    var current = ourRanges.current;
+
+    theirNextRange() {
+      if (theirRanges.moveNext()) return true;
+
+      // If there are no more of their ranges, none of the rest of our ranges
+      // need to be subtracted so we can add them as-is.
+      newRanges.add(current);
+      while (ourRanges.moveNext()) {
+        newRanges.add(ourRanges.current);
+      }
+      return false;
+    }
+
+    ourNextRange({bool includeCurrent: true}) {
+      if (includeCurrent) newRanges.add(current);
+      if (!ourRanges.moveNext()) return false;
+      current = ourRanges.current;
+      return true;
+    }
+
+    while (true) {
+      // If the current ranges are disjoint, move the lowest one forward.
+      if (strictlyLower(theirRanges.current, current)) {
+        if (!theirNextRange()) break;
+        continue;
+      }
+
+      if (strictlyHigher(theirRanges.current, current)) {
+        if (!ourNextRange()) break;
+        continue;
+      }
+
+      // If we're here, we know [theirRanges.current] overlaps [current].
+      var difference = current.difference(theirRanges.current);
+      if (difference is VersionUnion) {
+        // If their range split [current] in half, we only need to continue
+        // checking future ranges against the latter half.
+        assert(difference.ranges.length == 2);
+        newRanges.add(difference.ranges.first);
+        current = difference.ranges.last;
+
+        // Since their range split [current], it definitely doesn't allow higher
+        // versions, so we should move their ranges forward.
+        if (!theirNextRange()) break;
+      } else if (difference.isEmpty) {
+        if (!ourNextRange(includeCurrent: false)) break;
+      } else {
+        current = difference as VersionRange;
+
+        // Move the constraint with the lower max value forward. This ensures
+        // that we keep both lists in sync as much as possible, and that large
+        // ranges have a chance to subtract or be subtracted by multiple small
+        // ranges that they contain.
+        if (allowsHigher(current, theirRanges.current)) {
+          if (!theirNextRange()) break;
+        } else {
+          if (!ourNextRange()) break;
+        }
+      }
+    }
+
+    if (newRanges.isEmpty) return VersionConstraint.empty;
+    if (newRanges.length == 1) return newRanges.single;
+    return new VersionUnion.fromRanges(newRanges);
+  }
+
   /// Returns [constraint] as a list of ranges.
   ///
   /// This is used to normalize ranges of various types.
diff --git a/pubspec.yaml b/pubspec.yaml
index 53ef4d3..531ace6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: pub_semver
-version: 1.3.0-dev
+version: 1.3.0
 author: Dart Team <misc@dartlang.org>
 description: >
  Versions and version constraints implementing pub's versioning policy. This
diff --git a/test/version_range_test.dart b/test/version_range_test.dart
index eaa473f..a2dbf98 100644
--- a/test/version_range_test.dart
+++ b/test/version_range_test.dart
@@ -340,8 +340,8 @@
     });
 
     test('adjacent ranges allow no versions if exclusive', () {
-      var a = new VersionRange(min: v114, max: v124, includeMax: false);
-      var b = new VersionRange(min: v124, max: v200, includeMin: true);
+      var a = new VersionRange(min: v114, max: v124);
+      var b = new VersionRange(min: v124, max: v200);
       expect(a.intersect(b).isEmpty, isTrue);
     });
 
@@ -441,6 +441,164 @@
     });
   });
 
+  group('difference()', () {
+    test("with an empty range returns the original range", () {
+      expect(
+          new VersionRange(min: v003, max: v114)
+              .difference(VersionConstraint.empty),
+          equals(new VersionRange(min: v003, max: v114)));
+    });
+
+    test("with a version outside the range returns the original range", () {
+      expect(
+          new VersionRange(min: v003, max: v114).difference(v200),
+          equals(new VersionRange(min: v003, max: v114)));
+    });
+
+    test("with a version in the range splits the range", () {
+      expect(
+          new VersionRange(min: v003, max: v114).difference(v072),
+          equals(new VersionConstraint.unionOf([
+            new VersionRange(min: v003, max: v072),
+            new VersionRange(min: v072, max: v114)
+          ])));
+    });
+
+    test("with the max version makes the max exclusive", () {
+      expect(
+          new VersionRange(min: v003, max: v114, includeMax: true)
+              .difference(v114),
+          equals(new VersionRange(min: v003, max: v114)));
+    });
+
+    test("with the min version makes the min exclusive", () {
+      expect(
+          new VersionRange(min: v003, max: v114, includeMin: true)
+              .difference(v003),
+          equals(new VersionRange(min: v003, max: v114)));
+    });
+
+    test("with a disjoint range returns the original", () {
+      expect(
+          new VersionRange(min: v003, max: v114)
+              .difference(new VersionRange(min: v123, max: v140)),
+          equals(new VersionRange(min: v003, max: v114)));
+    });
+
+    test("with an adjacent range returns the original", () {
+      expect(
+          new VersionRange(min: v003, max: v114, includeMax: true)
+              .difference(new VersionRange(min: v114, max: v140)),
+          equals(new VersionRange(min: v003, max: v114, includeMax: true)));
+    });
+
+    test("with a range at the beginning cuts off the beginning of the range",
+        () {
+      expect(
+          new VersionRange(min: v080, max: v130)
+              .difference(new VersionRange(min: v010, max: v114)),
+          equals(new VersionRange(min: v114, max: v130, includeMin: true)));
+      expect(
+          new VersionRange(min: v080, max: v130)
+              .difference(new VersionRange(max: v114)),
+          equals(new VersionRange(min: v114, max: v130, includeMin: true)));
+      expect(
+          new VersionRange(min: v080, max: v130).difference(
+              new VersionRange(min: v010, max: v114, includeMax: true)),
+          equals(new VersionRange(min: v114, max: v130)));
+      expect(
+          new VersionRange(min: v080, max: v130, includeMin: true).difference(
+              new VersionRange(min: v010, max: v080, includeMax: true)),
+          equals(new VersionRange(min: v080, max: v130)));
+      expect(
+          new VersionRange(min: v080, max: v130, includeMax: true)
+              .difference(new VersionRange(min: v080, max: v130)),
+          equals(v130));
+    });
+
+    test("with a range at the end cuts off the end of the range",
+        () {
+      expect(
+          new VersionRange(min: v080, max: v130)
+              .difference(new VersionRange(min: v114, max: v140)),
+          equals(new VersionRange(min: v080, max: v114, includeMax: true)));
+      expect(
+          new VersionRange(min: v080, max: v130)
+              .difference(new VersionRange(min: v114)),
+          equals(new VersionRange(min: v080, max: v114, includeMax: true)));
+      expect(
+          new VersionRange(min: v080, max: v130).difference(
+              new VersionRange(min: v114, max: v140, includeMin: true)),
+          equals(new VersionRange(min: v080, max: v114)));
+      expect(
+          new VersionRange(min: v080, max: v130, includeMax: true).difference(
+              new VersionRange(min: v130, max: v140, includeMin: true)),
+          equals(new VersionRange(min: v080, max: v130)));
+      expect(
+          new VersionRange(min: v080, max: v130, includeMin: true)
+              .difference(new VersionRange(min: v080, max: v130)),
+          equals(v080));
+    });
+
+    test("with a range in the middle cuts the range in half", () {
+      expect(
+          new VersionRange(min: v003, max: v130)
+              .difference(new VersionRange(min: v072, max: v114)),
+          equals(new VersionConstraint.unionOf([
+            new VersionRange(min: v003, max: v072, includeMax: true),
+            new VersionRange(min: v114, max: v130, includeMin: true)
+          ])));
+    });
+
+    test("with a totally covering range returns empty", () {
+      expect(
+          new VersionRange(min: v114, max: v200)
+              .difference(new VersionRange(min: v072, max: v300)),
+          isEmpty);
+      expect(
+          new VersionRange(min: v003, max: v114)
+              .difference(new VersionRange(min: v003, max: v114)),
+          isEmpty);
+      expect(
+          new VersionRange(
+                  min: v003, max: v114, includeMin: true, includeMax: true)
+              .difference(new VersionRange(
+                  min: v003, max: v114, includeMin: true, includeMax: true)),
+          isEmpty);
+    });
+
+    test("with a version union that doesn't cover the range, returns the "
+        "original", () {
+      expect(
+          new VersionRange(min: v114, max: v140)
+              .difference(new VersionConstraint.unionOf([v010, v200])),
+          equals(new VersionRange(min: v114, max: v140)));
+    });
+
+    test("with a version union that intersects the ends, chops them off", () {
+      expect(
+          new VersionRange(min: v114, max: v140)
+              .difference(new VersionConstraint.unionOf([
+                new VersionRange(min: v080, max: v123),
+                new VersionRange(min: v130, max: v200)
+              ])),
+          equals(new VersionRange(
+              min: v123, max: v130, includeMin: true, includeMax: true)));
+    });
+
+    test("with a version union that intersects the middle, chops it up", () {
+      expect(
+          new VersionRange(min: v114, max: v140)
+              .difference(new VersionConstraint.unionOf([v123, v124, v130])),
+          equals(new VersionConstraint.unionOf([
+            new VersionRange(min: v114, max: v123),
+            new VersionRange(min: v123, max: v124),
+            new VersionRange(min: v124, max: v130),
+            new VersionRange(min: v130, max: v140)
+          ])));
+    });
+  });
+
   test('isEmpty', () {
     expect(new VersionRange().isEmpty, isFalse);
     expect(new VersionRange(min: v123, max: v124).isEmpty, isFalse);
diff --git a/test/version_test.dart b/test/version_test.dart
index ddc6c94..b278d4c 100644
--- a/test/version_test.dart
+++ b/test/version_test.dart
@@ -202,6 +202,27 @@
     });
   });
 
+  group('difference()', () {
+    test("with the same version returns an empty constraint", () {
+      expect(v123.difference(v123), isEmpty);
+    });
+
+    test("with a different version returns the original version", () {
+      expect(v123.difference(v080), equals(v123));
+    });
+
+    test("returns an empty constraint with a range that contains the version",
+        () {
+      expect(v123.difference(new VersionRange(min: v114, max: v124)), isEmpty);
+    });
+
+    test("returns the version constraint with a range that doesn't contain it",
+        () {
+      expect(v123.difference(new VersionRange(min: v140, max: v300)),
+          equals(v123));
+    });
+  });
+
   test('isEmpty', () {
     expect(v123.isEmpty, isFalse);
   });
diff --git a/test/version_union_test.dart b/test/version_union_test.dart
index f3518a0..06a427d 100644
--- a/test/version_union_test.dart
+++ b/test/version_union_test.dart
@@ -349,4 +349,62 @@
       });
     });
   });
+
+  group("difference()", () {
+    test("ignores ranges that don't intersect", () {
+      expect(new VersionConstraint.unionOf([
+        new VersionRange(min: v072, max: v080),
+        new VersionRange(min: v123, max: v130)
+      ]).difference(new VersionConstraint.unionOf([
+        new VersionRange(min: v003, max: v010),
+        new VersionRange(min: v080, max: v123),
+        new VersionRange(min: v140)
+      ])), equals(new VersionConstraint.unionOf([
+        new VersionRange(min: v072, max: v080),
+        new VersionRange(min: v123, max: v130)
+      ])));
+    });
+
+    test("removes overlapping portions", () {
+      expect(new VersionConstraint.unionOf([
+        new VersionRange(min: v010, max: v080),
+        new VersionRange(min: v123, max: v130)
+      ]).difference(new VersionConstraint.unionOf([
+        new VersionRange(min: v003, max: v072),
+        new VersionRange(min: v124)
+      ])), equals(new VersionConstraint.unionOf([
+        new VersionRange(min: v072, max: v080, includeMin: true),
+        new VersionRange(min: v123, max: v124, includeMax: true)
+      ])));
+    });
+
+    test("removes multiple portions from the same range", () {
+      expect(new VersionConstraint.unionOf([
+        new VersionRange(min: v010, max: v114),
+        new VersionRange(min: v130, max: v200)
+      ]).difference(new VersionConstraint.unionOf([v072, v080])),
+          equals(new VersionConstraint.unionOf([
+        new VersionRange(min: v010, max: v072),
+        new VersionRange(min: v072, max: v080),
+        new VersionRange(min: v080, max: v114),
+        new VersionRange(min: v130, max: v200)
+      ])));
+    });
+
+    test("removes the same range from multiple ranges", () {
+      expect(new VersionConstraint.unionOf([
+        new VersionRange(min: v010, max: v072),
+        new VersionRange(min: v080, max: v123),
+        new VersionRange(min: v124, max: v130),
+        new VersionRange(min: v200, max: v234),
+        new VersionRange(min: v250, max: v300)
+      ]).difference(new VersionRange(min: v114, max: v201)),
+          equals(new VersionConstraint.unionOf([
+        new VersionRange(min: v010, max: v072),
+        new VersionRange(min: v080, max: v114, includeMax: true),
+        new VersionRange(min: v201, max: v234, includeMin: true),
+        new VersionRange(min: v250, max: v300)
+      ])));
+    });
+  });
 }
\ No newline at end of file