Make VersionRange implement Comparable.

This changes the ordering of ranges in a VersionUnion. It used to order
solely by the upper bound; it now orders by lower bound, then upper
bound. In addition to being a more intuitive ordering, this is easier to
programatically work with, because iterating through the ranges reaches
each one in the order of the versions it contains.

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//2035983002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc169fb..cf95427 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.
 
+* Make `VersionRange` implement `Comparable<VersionRange>`. Ranges are ordered
+  first by lower bound, then by upper bound.
+
 # 1.2.4
 
 * Fix all remaining strong mode warnings.
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 74d015a..6493dda 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -15,10 +15,15 @@
 
 /// A [Comparator] that compares the maximum versions of [range1] and [range2].
 int compareMax(VersionRange range1, VersionRange range2) {
-  if (range1.max < range2.max) return -1;
-  if (range1.max > range2.max) return 1;
+  if (range1.max == null) {
+    if (range2.max == null) return 0;
+    return 1;
+  } else if (range2.max == null) {
+    return -1;
+  }
 
-  if (!range1.includeMax && range2.includeMax) return -1;
-  if (range1.includeMax && !range2.includeMax) return 1;
+  var result = range1.max.compareTo(range2.max);
+  if (result != 0) return result;
+  if (range1.includeMax != range2.includeMax) return range1.includeMax ? 1 : -1;
   return 0;
 }
diff --git a/lib/src/version.dart b/lib/src/version.dart
index 9872289..f2662b9 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -14,7 +14,7 @@
 final _equality = const IterableEquality();
 
 /// A parsed semantic version number.
-class Version implements Comparable<Version>, VersionConstraint, VersionRange {
+class Version implements VersionConstraint, VersionRange {
   /// No released version: i.e. "0.0.0".
   static Version get none => new Version(0, 0, 0);
 
@@ -267,22 +267,26 @@
     return new VersionConstraint.unionOf([this, other]);
   }
 
-  int compareTo(Version other) {
-    if (major != other.major) return major.compareTo(other.major);
-    if (minor != other.minor) return minor.compareTo(other.minor);
-    if (patch != other.patch) return patch.compareTo(other.patch);
+  int compareTo(VersionRange other) {
+    if (other is Version) {
+      if (major != other.major) return major.compareTo(other.major);
+      if (minor != other.minor) return minor.compareTo(other.minor);
+      if (patch != other.patch) return patch.compareTo(other.patch);
 
-    // Pre-releases always come before no pre-release string.
-    if (!isPreRelease && other.isPreRelease) return 1;
-    if (!other.isPreRelease && isPreRelease) return -1;
+      // Pre-releases always come before no pre-release string.
+      if (!isPreRelease && other.isPreRelease) return 1;
+      if (!other.isPreRelease && isPreRelease) return -1;
 
-    var comparison = _compareLists(preRelease, other.preRelease);
-    if (comparison != 0) return comparison;
+      var comparison = _compareLists(preRelease, other.preRelease);
+      if (comparison != 0) return comparison;
 
-    // Builds always come after no build string.
-    if (build.isEmpty && other.build.isNotEmpty) return -1;
-    if (other.build.isEmpty && build.isNotEmpty) return 1;
-    return _compareLists(build, other.build);
+      // Builds always come after no build string.
+      if (build.isEmpty && other.build.isNotEmpty) return -1;
+      if (other.build.isEmpty && build.isNotEmpty) return 1;
+      return _compareLists(build, other.build);
+    } else {
+      return -other.compareTo(this);
+    }
   }
 
   String toString() => _text;
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
index 9f55da0..68827a4 100644
--- a/lib/src/version_constraint.dart
+++ b/lib/src/version_constraint.dart
@@ -194,7 +194,7 @@
       throw new ArgumentError('Unknown VersionConstraint type $constraint.');
     }
 
-    flattened.sort(compareMax);
+    flattened.sort();
 
     var merged = <VersionRange>[];
     for (var constraint in flattened) {
diff --git a/lib/src/version_range.dart b/lib/src/version_range.dart
index 145151f..a8d6069 100644
--- a/lib/src/version_range.dart
+++ b/lib/src/version_range.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'utils.dart';
 import 'version.dart';
 import 'version_constraint.dart';
 import 'version_union.dart';
@@ -11,7 +12,11 @@
 /// If there is a minimum, then this only allows versions that are at that
 /// minimum or greater. If there is a maximum, then only versions less than
 /// that are allowed. In other words, this allows `>= min, < max`.
-class VersionRange implements VersionConstraint {
+///
+/// Version ranges are ordered first by their lower bounds, then by their upper
+/// bounds. For example, `>=1.0.0 <2.0.0` is before `>=1.5.0 <2.0.0` is before
+/// `>=1.5.0 <3.0.0`.
+class VersionRange implements Comparable<VersionRange>, VersionConstraint {
   /// The minimum end of the range.
   ///
   /// If [includeMin] is `true`, this will be the minimum allowed version.
@@ -315,6 +320,21 @@
     return new VersionConstraint.unionOf([this, other]);
   }
 
+  int compareTo(VersionRange other) {
+    if (min == null) {
+      if (other.min == null) return compareMax(this, other);
+      return -1;
+    } else if (other.min == null) {
+      return 1;
+    }
+
+    var result = min.compareTo(other.min);
+    if (result != 0) return result;
+    if (includeMin != other.includeMin) return includeMin ? -1 : 1;
+
+    return compareMax(this, other);
+  }
+
   String toString() {
     var buffer = new StringBuffer();
 
diff --git a/lib/src/version_union.dart b/lib/src/version_union.dart
index dbdadcf..d0e1e2b 100644
--- a/lib/src/version_union.dart
+++ b/lib/src/version_union.dart
@@ -19,7 +19,7 @@
   ///
   /// This list has two invariants:
   ///
-  /// * Its contents are sorted from lowest to highest matched versions.
+  /// * Its contents are sorted using the standard ordering of [VersionRange]s.
   /// * Its contents are disjoint and non-adjacent. In other words, for any two
   ///   constraints next to each other in the list, there's some version between
   ///   those constraints that they don't match.
@@ -74,8 +74,8 @@
         return true;
       }
 
-      // Move the constraint with the higher max value forward. This ensures
-      // that we keep both lists in sync as much as possible.
+      // 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) {
         ourRanges.moveNext();
       } else {
@@ -101,9 +101,9 @@
 
       if (!intersection.isEmpty) newRanges.add(intersection);
 
-      // Move the constraint with the higher 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.
+      // 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) {
         ourRanges.moveNext();
       } else {
diff --git a/test/version_range_test.dart b/test/version_range_test.dart
index 9eaa980..eaa473f 100644
--- a/test/version_range_test.dart
+++ b/test/version_range_test.dart
@@ -445,4 +445,61 @@
     expect(new VersionRange().isEmpty, isFalse);
     expect(new VersionRange(min: v123, max: v124).isEmpty, isFalse);
   });
+
+  group('compareTo()', () {
+    test("orders by minimum first", () {
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v080),
+          new VersionRange(min: v010, max: v072));
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v080),
+          new VersionRange(min: v010, max: v080));
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v080),
+          new VersionRange(min: v010, max: v114));
+    });
+
+    test("orders by maximum second", () {
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v010),
+          new VersionRange(min: v003, max: v072));
+    });
+
+    test("includeMin comes before !includeMin", () {
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v080, includeMin: true),
+          new VersionRange(min: v003, max: v080, includeMin: false));
+    });
+
+    test("includeMax comes after !includeMax", () {
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v080, includeMax: false),
+          new VersionRange(min: v003, max: v080, includeMax: true));
+    });
+
+    test("no minimum comes before small minimum", () {
+      _expectComparesSmaller(
+          new VersionRange(max: v010),
+          new VersionRange(min: v003, max: v010));
+      _expectComparesSmaller(
+          new VersionRange(max: v010, includeMin: true),
+          new VersionRange(min: v003, max: v010));
+    });
+
+    test("no maximium comes after large maximum", () {
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v300),
+          new VersionRange(min: v003));
+      _expectComparesSmaller(
+          new VersionRange(min: v003, max: v300),
+          new VersionRange(min: v003, includeMax: true));
+    });
+  });
+}
+
+void _expectComparesSmaller(VersionRange smaller, VersionRange larger) {
+  expect(smaller.compareTo(larger), lessThan(0),
+      reason: "expected $smaller to sort below $larger");
+  expect(larger.compareTo(smaller), greaterThan(0),
+      reason: "expected $larger to sort above $smaller");
 }