Handle pre-release semantics by adjusting the max version of ranges (#28)

Rather than adding special-cases to each operation, we now just modify
the upper bound of version ranges when the special pre-release
semantics would come into play.

Closes dart-lang/pub#1885
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2e5e62..0efa0ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+# 1.4.0
+
+* Add a `Version.firstPreRelease` getter that returns the first possible
+  pre-release of a version.
+
+* Add a `Version.isFirstPreRelease` getter that returns whether a version is the
+  first possible pre-release.
+
+* `new VersionRange()` with an exclusive maximum now replaces the maximum with
+  its first pre-release version. This matches the existing semantics, where an
+  exclusive maximum would exclude pre-release versions of that maximum.
+
+  Explicitly representing this by changing the maximum version ensures that all
+  operations behave correctly with respect to the special pre-release semantics.
+  In particular, it fixes bugs where, for example,
+  `(>=1.0.0 <2.0.0-dev).union(>=2.0.0-dev <2.0.0)` and
+  `(>=1.0.0 <3.0.0).difference(^1.0.0)` wouldn't include `2.0.0-dev`.
+
+* Add an `alwaysIncludeMaxPreRelease` parameter to `new VersionRange()`, which
+  disables the replacement described above and allows users to create ranges
+  that do include the pre-release versions of an exclusive max version.
+
 # 1.3.7
 
 * Fix more bugs with `VersionRange.intersect()`, `VersionRange.difference()`,
diff --git a/lib/pub_semver.dart b/lib/pub_semver.dart
index bfb7d8b..4b6487c 100644
--- a/lib/pub_semver.dart
+++ b/lib/pub_semver.dart
@@ -4,5 +4,5 @@
 
 export 'src/version.dart';
 export 'src/version_constraint.dart';
-export 'src/version_range.dart';
+export 'src/version_range.dart' hide CompatibleWithVersionRange;
 export 'src/version_union.dart';
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 55025a0..60617f2 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -30,12 +30,6 @@
   if (range1.max == null) return range2.max != null;
   if (range2.max == null) return false;
 
-  // `<1.0.0-dev.1` allows higher versions than `<1.0.0`, such as `1.0.0-dev.0`.
-  if (disallowedByPreRelease(range2, range1.max)) return true;
-
-  // `<1.0.0` doesn't allow any versions higher than `<1.0.0-dev`.
-  if (disallowedByPreRelease(range1, range2.max)) return false;
-
   var comparison = range1.max.compareTo(range2.max);
   if (comparison == 1) return true;
   if (comparison == -1) return false;
@@ -47,11 +41,6 @@
 bool strictlyLower(VersionRange range1, VersionRange range2) {
   if (range1.max == null || range2.min == null) return false;
 
-  // `<1.0.0` doesn't allow any versions allowed by `>=1.0.0-dev.0`.
-  if (disallowedByPreRelease(range1, range2.min)) return true;
-
-  //if (disallowedByPreRelease(range2, range1.min)) return true;
-
   var comparison = range1.max.compareTo(range2.min);
   if (comparison == -1) return true;
   if (comparison == 1) return false;
@@ -63,40 +52,7 @@
 bool strictlyHigher(VersionRange range1, VersionRange range2) =>
     strictlyLower(range2, range1);
 
-// Returns whether [other] is disallowed by [range] because we disallow
-// pre-release versions that have the same major, minor, and patch version as
-// the max of a range, but only if neither the max nor the min is a pre-release
-// of that version.
-//
-// This ensures that `^1.2.3` doesn't include `2.0.0-pre`, while also allowing
-// both `>=2.0.0-pre.2 <2.0.0` and `>=1.2.3 <2.0.0-pre.7` to match
-// `2.0.0-pre.5`.
-//
-// It's worth noting that this is different than [NPM's semantics][]. NPM
-// disallows **all** pre-release versions unless their major, minor, and
-// patch numbers match those of a prerelease min or max. This ensures that
-// no prerelease versions will ever be selected if the user doesn't
-// explicitly allow them.
-//
-// [NPM's semantics]: https://www.npmjs.org/doc/misc/semver.html#prerelease-tags
-//
-// Instead, we ensure that release versions will always be preferred over
-// prerelease versions by ordering the release versions first in
-// [Version.prioritize]. This means that constraints like `any` or
-// `>1.2.3` can still match prerelease versions if they're the only things
-// available.
-bool disallowedByPreRelease(VersionRange range, Version other) {
-  var maxIsReleaseOfOther = !range.includeMax &&
-      !range.max.isPreRelease &&
-      other.isPreRelease &&
-      _equalsWithoutPreRelease(other, range.max);
-  var minIsPreReleaseOfOther = range.min != null &&
-      range.min.isPreRelease &&
-      _equalsWithoutPreRelease(other, range.min);
-  return maxIsReleaseOfOther && !minIsPreReleaseOfOther;
-}
-
-bool _equalsWithoutPreRelease(Version version1, Version version2) =>
+bool equalsWithoutPreRelease(Version version1, Version version2) =>
     version1.major == version2.major &&
     version1.minor == version2.minor &&
     version1.patch == version2.patch;
diff --git a/lib/src/version.dart b/lib/src/version.dart
index 6665873..19ae962 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -239,6 +239,12 @@
     return _incrementMajor();
   }
 
+  /// Returns the first possible pre-release of this version.
+  Version get firstPreRelease => new Version(major, minor, patch, pre: "0");
+
+  /// Returns whether this is the first possible pre-release of its version.
+  bool get isFirstPreRelease => preRelease.length == 1 && preRelease.first == 0;
+
   Version _incrementMajor() => new Version(major + 1, 0, 0);
   Version _incrementMinor() => new Version(major, minor + 1, 0);
   Version _incrementPatch() => new Version(major, minor, patch + 1);
@@ -262,7 +268,8 @@
             min: other.min,
             max: other.max,
             includeMin: true,
-            includeMax: other.includeMax);
+            includeMax: other.includeMax,
+            alwaysIncludeMaxPreRelease: true);
       }
 
       if (other.max == this) {
@@ -270,7 +277,8 @@
             min: other.min,
             max: other.max,
             includeMin: other.includeMin,
-            includeMax: true);
+            includeMax: true,
+            alwaysIncludeMaxPreRelease: true);
       }
     }
 
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
index 670a928..67093f7 100644
--- a/lib/src/version_constraint.dart
+++ b/lib/src/version_constraint.dart
@@ -84,7 +84,10 @@
         case '<=':
           return new VersionRange(max: version, includeMax: true);
         case '<':
-          return new VersionRange(max: version, includeMax: false);
+          return new VersionRange(
+              max: version,
+              includeMax: false,
+              alwaysIncludeMaxPreRelease: true);
         case '>=':
           return new VersionRange(min: version, includeMin: true);
         case '>':
@@ -175,7 +178,7 @@
   /// are greater than or equal to [version], but less than the next breaking
   /// version ([Version.nextBreaking]) of [version].
   factory VersionConstraint.compatibleWith(Version version) =>
-      new _CompatibleWithVersionRange(version);
+      new CompatibleWithVersionRange(version);
 
   /// Creates a new version constraint that is the intersection of
   /// [constraints].
@@ -278,14 +281,3 @@
   VersionConstraint difference(VersionConstraint other) => this;
   String toString() => '<empty>';
 }
-
-class _CompatibleWithVersionRange extends VersionRange {
-  _CompatibleWithVersionRange(Version version)
-      : super(
-            min: version,
-            includeMin: true,
-            max: version.nextBreaking,
-            includeMax: false);
-
-  String toString() => '^$min';
-}
diff --git a/lib/src/version_range.dart b/lib/src/version_range.dart
index ec15de4..99dc718 100644
--- a/lib/src/version_range.dart
+++ b/lib/src/version_range.dart
@@ -53,14 +53,36 @@
   ///
   /// If [includeMin] is `true`, then the minimum end of the range is inclusive.
   /// Likewise, passing [includeMax] as `true` makes the upper end inclusive.
-  VersionRange(
-      {this.min, this.max, this.includeMin: false, this.includeMax: false}) {
+  ///
+  /// If [alwaysIncludeMaxPreRelease] is `true`, this will always include
+  /// pre-release versions of an exclusive [max]. Otherwise, it will use the
+  /// default behavior for pre-release versions of [max].
+  factory VersionRange(
+      {Version min,
+      Version max,
+      bool includeMin: false,
+      bool includeMax: false,
+      bool alwaysIncludeMaxPreRelease: false}) {
     if (min != null && max != null && min > max) {
       throw new ArgumentError(
           'Minimum version ("$min") must be less than maximum ("$max").');
     }
+
+    if (!alwaysIncludeMaxPreRelease &&
+        !includeMax &&
+        max != null &&
+        !max.isPreRelease &&
+        (min == null ||
+            !min.isPreRelease ||
+            !equalsWithoutPreRelease(min, max))) {
+      max = max.firstPreRelease;
+    }
+
+    return new VersionRange._(min, max, includeMin, includeMax);
   }
 
+  VersionRange._(this.min, this.max, this.includeMin, this.includeMax);
+
   bool operator ==(other) {
     if (other is! VersionRange) return false;
 
@@ -90,7 +112,6 @@
     if (max != null) {
       if (other > max) return false;
       if (!includeMax && other == max) return false;
-      if (disallowedByPreRelease(this, other)) return false;
     }
 
     return true;
@@ -177,7 +198,8 @@
           min: intersectMin,
           max: intersectMax,
           includeMin: intersectIncludeMin,
-          includeMax: intersectIncludeMax);
+          includeMax: intersectIncludeMax,
+          alwaysIncludeMaxPreRelease: true);
     }
 
     throw new ArgumentError('Unknown VersionConstraint type $other.');
@@ -192,7 +214,8 @@
             min: this.min,
             max: this.max,
             includeMin: true,
-            includeMax: this.includeMax);
+            includeMax: this.includeMax,
+            alwaysIncludeMaxPreRelease: true);
       }
 
       if (other == max) {
@@ -200,7 +223,8 @@
             min: this.min,
             max: this.max,
             includeMin: this.includeMin,
-            includeMax: true);
+            includeMax: true,
+            alwaysIncludeMaxPreRelease: true);
       }
 
       return new VersionConstraint.unionOf([this, other]);
@@ -239,7 +263,8 @@
           min: unionMin,
           max: unionMax,
           includeMin: unionIncludeMin,
-          includeMax: unionIncludeMax);
+          includeMax: unionIncludeMax,
+          alwaysIncludeMaxPreRelease: true);
     }
 
     return new VersionConstraint.unionOf([this, other]);
@@ -254,20 +279,36 @@
       if (other == min) {
         if (!includeMin) return this;
         return new VersionRange(
-            min: min, max: max, includeMin: false, includeMax: includeMax);
+            min: min,
+            max: max,
+            includeMin: false,
+            includeMax: includeMax,
+            alwaysIncludeMaxPreRelease: true);
       }
 
       if (other == max) {
         if (!includeMax) return this;
         return new VersionRange(
-            min: min, max: max, includeMin: includeMin, includeMax: false);
+            min: min,
+            max: max,
+            includeMin: includeMin,
+            includeMax: false,
+            alwaysIncludeMaxPreRelease: true);
       }
 
       return new VersionUnion.fromRanges([
         new VersionRange(
-            min: min, max: other, includeMin: includeMin, includeMax: false),
+            min: min,
+            max: other,
+            includeMin: includeMin,
+            includeMax: false,
+            alwaysIncludeMaxPreRelease: true),
         new VersionRange(
-            min: other, max: max, includeMin: false, includeMax: includeMax)
+            min: other,
+            max: max,
+            includeMin: false,
+            includeMax: includeMax,
+            alwaysIncludeMaxPreRelease: true)
       ]);
     } else if (other is VersionRange) {
       if (!allowsAny(other)) return this;
@@ -284,7 +325,8 @@
             min: min,
             max: other.min,
             includeMin: includeMin,
-            includeMax: !other.includeMin);
+            includeMax: !other.includeMin,
+            alwaysIncludeMaxPreRelease: true);
       }
 
       VersionRange after;
@@ -299,7 +341,8 @@
             min: other.max,
             max: max,
             includeMin: !other.includeMax,
-            includeMax: includeMax);
+            includeMax: includeMax,
+            alwaysIncludeMaxPreRelease: true);
       }
 
       if (before == null && after == null) return VersionConstraint.empty;
@@ -379,11 +422,36 @@
 
     if (max != null) {
       if (min != null) buffer.write(' ');
-      buffer.write(includeMax ? '<=' : '<');
-      buffer.write(max);
+      if (includeMax) {
+        buffer.write('<=');
+        buffer.write(max);
+      } else {
+        buffer.write('<');
+        if (max.isFirstPreRelease) {
+          // Since `"<$max"` would parse the same as `"<$max-0"`, we just emit
+          // `<$max` to avoid confusing "-0" suffixes.
+          buffer.write("${max.major}.${max.minor}.${max.patch}");
+        } else {
+          buffer.write(max);
+
+          // If `">=$min <$max"` would parse as `">=$min <$max-0"`, add `-*` to
+          // indicate that actually does allow pre-release versions.
+          var minIsPreReleaseOfMax = min != null &&
+              min.isPreRelease &&
+              equalsWithoutPreRelease(min, max);
+          if (!max.isPreRelease && !minIsPreReleaseOfMax) buffer.write("-∞");
+        }
+      }
     }
 
     if (min == null && max == null) buffer.write('any');
     return buffer.toString();
   }
 }
+
+class CompatibleWithVersionRange extends VersionRange {
+  CompatibleWithVersionRange(Version version)
+      : super._(version, version.nextBreaking.firstPreRelease, true, false);
+
+  String toString() => '^$min';
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 1b738d7..a537788 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: pub_semver
-version: 1.3.7
+version: 1.4.0
 author: Dart Team <misc@dartlang.org>
 description: >
  Versions and version constraints implementing pub's versioning policy. This
diff --git a/test/utils.dart b/test/utils.dart
index 9b28c6a..8ecd8f5 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -22,6 +22,10 @@
 final v250 = new Version.parse('2.5.0');
 final v300 = new Version.parse('3.0.0');
 
+/// A range that allows pre-release versions of its max version.
+final includeMaxPreReleaseRange =
+    new VersionRange(max: v200, alwaysIncludeMaxPreRelease: true);
+
 /// A [Matcher] that tests if a [VersionConstraint] allows or does not allow a
 /// given list of [Version]s.
 class _VersionConstraintMatcher implements Matcher {
diff --git a/test/version_range_test.dart b/test/version_range_test.dart
index 63cf302..4bdc21c 100644
--- a/test/version_range_test.dart
+++ b/test/version_range_test.dart
@@ -14,7 +14,25 @@
       var range = new VersionRange(min: v123, max: v124);
       expect(range.isAny, isFalse);
       expect(range.min, equals(v123));
-      expect(range.max, equals(v124));
+      expect(range.max, equals(v124.firstPreRelease));
+    });
+
+    group("doesn't make the max a pre-release if", () {
+      test("it's already a pre-release", () {
+        expect(new VersionRange(max: new Version.parse("1.2.4-pre")).max,
+            equals(new Version.parse("1.2.4-pre")));
+      });
+
+      test("includeMax is true", () {
+        expect(new VersionRange(max: v124, includeMax: true).max, equals(v124));
+      });
+
+      test("min is a prerelease of max", () {
+        expect(
+            new VersionRange(min: new Version.parse("1.2.4-pre"), max: v124)
+                .max,
+            equals(v124));
+      });
     });
 
     test('allows omitting max', () {
@@ -158,6 +176,10 @@
       expect(range,
           allows(new Version.parse('0.0.0'), new Version.parse('999.99.9')));
     });
+
+    test('allows pre-releases of the max with includeMaxPreRelease', () {
+      expect(includeMaxPreReleaseRange, allows(new Version.parse('2.0.0-dev')));
+    });
   });
 
   group('allowsAll()', () {
@@ -247,6 +269,13 @@
             isFalse);
       });
 
+      test('of non-pre-release max are included with includeMaxPreRelease', () {
+        expect(
+            includeMaxPreReleaseRange
+                .allowsAll(new VersionConstraint.parse('<2.0.0-dev')),
+            isTrue);
+      });
+
       test(
           'of non-pre-release max are included if min is a pre-release of the '
           'same version', () {
@@ -388,6 +417,13 @@
             isFalse);
       });
 
+      test('of non-pre-release max are included with includeMaxPreRelease', () {
+        expect(
+            includeMaxPreReleaseRange
+                .allowsAny(new VersionConstraint.parse('>2.0.0-dev')),
+            isTrue);
+      });
+
       test(
           'of non-pre-release max are included if min is a pre-release of the '
           'same version', () {
@@ -419,15 +455,10 @@
 
   group('intersect()', () {
     test('two overlapping ranges', () {
-      var a = new VersionRange(min: v123, max: v250);
-      var b = new VersionRange(min: v200, max: v300);
-      var intersect = a.intersect(b);
-      expect(intersect, new isInstanceOf<VersionRange>());
-      var intersectRange = intersect as VersionRange;
-      expect(intersectRange.min, equals(v200));
-      expect(intersectRange.max, equals(v250));
-      expect(intersectRange.includeMin, isFalse);
-      expect(intersectRange.includeMax, isFalse);
+      expect(
+          new VersionRange(min: v123, max: v250)
+              .intersect(new VersionRange(min: v200, max: v300)),
+          equals(new VersionRange(min: v200, max: v250)));
     });
 
     test('a non-overlapping range allows no versions', () {
@@ -476,6 +507,48 @@
               .intersect(new VersionConstraint.parse("<2.0.0-dev")),
           equals(new VersionRange(max: v200)));
     });
+
+    group("with includeMaxPreRelease", () {
+      test('preserves includeMaxPreRelease if the max version is included', () {
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(new VersionConstraint.parse("<1.0.0")),
+            equals(new VersionConstraint.parse("<1.0.0")));
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(new VersionConstraint.parse("<2.0.0")),
+            equals(new VersionConstraint.parse("<2.0.0")));
+        expect(includeMaxPreReleaseRange.intersect(includeMaxPreReleaseRange),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(new VersionConstraint.parse("<3.0.0")),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(new VersionConstraint.parse(">1.1.4")),
+            equals(new VersionRange(
+                min: v114, max: v200, alwaysIncludeMaxPreRelease: true)));
+      });
+
+      test(
+          "and a range with a pre-release min, returns "
+          "an intersection", () {
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(new VersionConstraint.parse(">=2.0.0-dev")),
+            equals(new VersionConstraint.parse(">=2.0.0-dev <2.0.0")));
+      });
+
+      test(
+          "and a range with a pre-release max, returns "
+          "the narrower constraint", () {
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(new VersionConstraint.parse("<2.0.0-dev")),
+            equals(new VersionConstraint.parse("<2.0.0-dev")));
+      });
+    });
   });
 
   group('union()', () {
@@ -485,7 +558,10 @@
     });
 
     test("with a version on the edge of the range, expands the range", () {
-      expect(new VersionRange(min: v114, max: v124).union(v124),
+      expect(
+          new VersionRange(
+                  min: v114, max: v124, alwaysIncludeMaxPreRelease: true)
+              .union(v124),
           equals(new VersionRange(min: v114, max: v124, includeMax: true)));
       expect(new VersionRange(min: v114, max: v124).union(v114),
           equals(new VersionRange(min: v114, max: v124, includeMin: true)));
@@ -533,7 +609,8 @@
           .union(new VersionRange(min: v114, max: v200));
       expect(result, equals(new VersionRange(min: v003, max: v200)));
 
-      result = new VersionRange(min: v003, max: v114)
+      result = new VersionRange(
+              min: v003, max: v114, alwaysIncludeMaxPreRelease: true)
           .union(new VersionRange(min: v114, max: v200, includeMin: true));
       expect(result, equals(new VersionRange(min: v003, max: v200)));
 
@@ -541,8 +618,9 @@
           .union(new VersionRange(min: v003, max: v114, includeMax: true));
       expect(result, equals(new VersionRange(min: v003, max: v200)));
 
-      result = new VersionRange(min: v114, max: v200, includeMin: true)
-          .union(new VersionRange(min: v003, max: v114));
+      result = new VersionRange(min: v114, max: v200, includeMin: true).union(
+          new VersionRange(
+              min: v003, max: v114, alwaysIncludeMaxPreRelease: true));
       expect(result, equals(new VersionRange(min: v003, max: v200)));
     });
 
@@ -573,6 +651,39 @@
               .union(new VersionConstraint.parse("<2.0.0-dev")),
           equals(new VersionConstraint.parse("<2.0.0-dev")));
     });
+
+    group("with includeMaxPreRelease", () {
+      test('adds includeMaxPreRelease if the max version is included', () {
+        expect(
+            includeMaxPreReleaseRange
+                .union(new VersionConstraint.parse("<1.0.0")),
+            equals(includeMaxPreReleaseRange));
+        expect(includeMaxPreReleaseRange.union(includeMaxPreReleaseRange),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange
+                .union(new VersionConstraint.parse("<2.0.0")),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange
+                .union(new VersionConstraint.parse("<3.0.0")),
+            equals(new VersionConstraint.parse("<3.0.0")));
+      });
+
+      test("and a range with a pre-release min, returns any", () {
+        expect(
+            includeMaxPreReleaseRange
+                .union(new VersionConstraint.parse(">=2.0.0-dev")),
+            equals(VersionConstraint.any));
+      });
+
+      test("and a range with a pre-release max, returns the original", () {
+        expect(
+            includeMaxPreReleaseRange
+                .union(new VersionConstraint.parse("<2.0.0-dev")),
+            equals(includeMaxPreReleaseRange));
+      });
+    });
   });
 
   group('difference()', () {
@@ -592,7 +703,8 @@
       expect(
           new VersionRange(min: v003, max: v114).difference(v072),
           equals(new VersionConstraint.unionOf([
-            new VersionRange(min: v003, max: v072),
+            new VersionRange(
+                min: v003, max: v072, alwaysIncludeMaxPreRelease: true),
             new VersionRange(min: v072, max: v114)
           ])));
     });
@@ -601,7 +713,8 @@
       expect(
           new VersionRange(min: v003, max: v114, includeMax: true)
               .difference(v114),
-          equals(new VersionRange(min: v003, max: v114)));
+          equals(new VersionRange(
+              min: v003, max: v114, alwaysIncludeMaxPreRelease: true)));
     });
 
     test("with the min version makes the min exclusive", () {
@@ -630,11 +743,11 @@
       expect(
           new VersionRange(min: v080, max: v130)
               .difference(new VersionRange(min: v010, max: v114)),
-          equals(new VersionRange(min: v114, max: v130, includeMin: true)));
+          equals(new VersionConstraint.parse(">=1.1.4-0 <1.3.0")));
       expect(
           new VersionRange(min: v080, max: v130)
               .difference(new VersionRange(max: v114)),
-          equals(new VersionRange(min: v114, max: v130, includeMin: true)));
+          equals(new VersionConstraint.parse(">=1.1.4-0 <1.3.0")));
       expect(
           new VersionRange(min: v080, max: v130).difference(
               new VersionRange(min: v010, max: v114, includeMax: true)),
@@ -646,7 +759,7 @@
       expect(
           new VersionRange(min: v080, max: v130, includeMax: true)
               .difference(new VersionRange(min: v080, max: v130)),
-          equals(v130));
+          equals(new VersionConstraint.parse(">=1.3.0-0 <=1.3.0")));
     });
 
     test("with a range at the end cuts off the end of the range", () {
@@ -661,11 +774,13 @@
       expect(
           new VersionRange(min: v080, max: v130).difference(
               new VersionRange(min: v114, max: v140, includeMin: true)),
-          equals(new VersionRange(min: v080, max: v114)));
+          equals(new VersionRange(
+              min: v080, max: v114, alwaysIncludeMaxPreRelease: true)));
       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)));
+          equals(new VersionRange(
+              min: v080, max: v130, alwaysIncludeMaxPreRelease: true)));
       expect(
           new VersionRange(min: v080, max: v130, includeMin: true)
               .difference(new VersionRange(min: v080, max: v130)),
@@ -678,7 +793,7 @@
               .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)
+            new VersionConstraint.parse(">=1.1.4-0 <1.3.0")
           ])));
     });
 
@@ -715,8 +830,7 @@
             new VersionRange(min: v080, max: v123),
             new VersionRange(min: v130, max: v200)
           ])),
-          equals(new VersionRange(
-              min: v123, max: v130, includeMin: true, includeMax: true)));
+          equals(new VersionConstraint.parse(">=1.2.3-0 <=1.3.0")));
     });
 
     test("with a version union that intersects the middle, chops it up", () {
@@ -724,9 +838,12 @@
           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: v114, max: v123, alwaysIncludeMaxPreRelease: true),
+            new VersionRange(
+                min: v123, max: v124, alwaysIncludeMaxPreRelease: true),
+            new VersionRange(
+                min: v124, max: v130, alwaysIncludeMaxPreRelease: true),
             new VersionRange(min: v130, max: v140)
           ])));
     });
@@ -752,6 +869,84 @@
               .difference(new VersionConstraint.parse("<2.0.0-dev")),
           equals(VersionConstraint.empty));
     });
+
+    group("with includeMaxPreRelease", () {
+      group("for the minuend", () {
+        test("preserves includeMaxPreRelease if the max version is included",
+            () {
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(new VersionConstraint.parse("<1.0.0")),
+              equals(new VersionRange(
+                  min: new Version.parse("1.0.0-0"),
+                  max: v200,
+                  includeMin: true,
+                  alwaysIncludeMaxPreRelease: true)));
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(new VersionConstraint.parse("<2.0.0")),
+              equals(new VersionRange(
+                  min: v200.firstPreRelease,
+                  max: v200,
+                  includeMin: true,
+                  alwaysIncludeMaxPreRelease: true)));
+          expect(
+              includeMaxPreReleaseRange.difference(includeMaxPreReleaseRange),
+              equals(VersionConstraint.empty));
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(new VersionConstraint.parse("<3.0.0")),
+              equals(VersionConstraint.empty));
+        });
+
+        test("with a range with a pre-release min, adjusts the max", () {
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(new VersionConstraint.parse(">=2.0.0-dev")),
+              equals(new VersionConstraint.parse("<2.0.0-dev")));
+        });
+
+        test("with a range with a pre-release max, adjusts the min", () {
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(new VersionConstraint.parse("<2.0.0-dev")),
+              equals(new VersionConstraint.parse(">=2.0.0-dev <2.0.0")));
+        });
+      });
+
+      group("for the subtrahend", () {
+        group("doesn't create a pre-release minimum", () {
+          test("when cutting off the bottom", () {
+            expect(
+                new VersionConstraint.parse("<3.0.0")
+                    .difference(includeMaxPreReleaseRange),
+                equals(
+                    new VersionRange(min: v200, max: v300, includeMin: true)));
+          });
+
+          test("with splitting down the middle", () {
+            expect(
+                new VersionConstraint.parse("<4.0.0").difference(
+                    new VersionRange(
+                        min: v200,
+                        max: v300,
+                        includeMin: true,
+                        alwaysIncludeMaxPreRelease: true)),
+                equals(new VersionConstraint.unionOf([
+                  new VersionRange(max: v200, alwaysIncludeMaxPreRelease: true),
+                  new VersionConstraint.parse(">=3.0.0 <4.0.0")
+                ])));
+          });
+
+          test("can leave a single version", () {
+            expect(
+                new VersionConstraint.parse("<=2.0.0")
+                    .difference(includeMaxPreReleaseRange),
+                equals(v200));
+          });
+        });
+      });
+    });
   });
 
   test('isEmpty', () {
@@ -786,6 +981,11 @@
           new VersionRange(min: v003, max: v080, includeMax: true));
     });
 
+    test("includeMaxPreRelease comes after !includeMaxPreRelease", () {
+      _expectComparesSmaller(
+          new VersionRange(max: v200), includeMaxPreReleaseRange);
+    });
+
     test("no minimum comes before small minimum", () {
       _expectComparesSmaller(
           new VersionRange(max: v010), new VersionRange(min: v003, max: v010));
diff --git a/test/version_test.dart b/test/version_test.dart
index fc6dfef..448435a 100644
--- a/test/version_test.dart
+++ b/test/version_test.dart
@@ -190,8 +190,14 @@
     });
 
     test("with a range with the version on the edge, expands the range", () {
-      expect(v124.union(new VersionRange(min: v114, max: v124)),
+      expect(
+          v124.union(new VersionRange(
+              min: v114, max: v124, alwaysIncludeMaxPreRelease: true)),
           equals(new VersionRange(min: v114, max: v124, includeMax: true)));
+      expect(
+          v124.firstPreRelease.union(new VersionRange(min: v114, max: v124)),
+          equals(new VersionRange(
+              min: v114, max: v124.firstPreRelease, includeMax: true)));
       expect(v114.union(new VersionRange(min: v114, max: v124)),
           equals(new VersionRange(min: v114, max: v124, includeMin: true)));
     });
diff --git a/test/version_union_test.dart b/test/version_union_test.dart
index f948832..f629c83 100644
--- a/test/version_union_test.dart
+++ b/test/version_union_test.dart
@@ -101,25 +101,32 @@
           new VersionConstraint.unionOf([
             new VersionRange(min: v003, max: v072, includeMax: true),
             new VersionRange(min: v072, max: v080),
-            new VersionRange(min: v114, max: v124),
-            new VersionRange(min: v124, max: v130, includeMin: true)
+            new VersionRange(
+                min: v114, max: v124, alwaysIncludeMaxPreRelease: true),
+            new VersionRange(min: v124, max: v130, includeMin: true),
+            new VersionRange(
+                min: v130.firstPreRelease, max: v200, includeMin: true)
           ]),
           equals(new VersionConstraint.unionOf([
             new VersionRange(min: v003, max: v080),
-            new VersionRange(min: v114, max: v130)
+            new VersionRange(min: v114, max: v200)
           ])));
     });
 
     test("doesn't merge not-quite-adjacent ranges", () {
       expect(
           new VersionConstraint.unionOf([
+            new VersionRange(min: v114, max: v124),
+            new VersionRange(min: v124, max: v130, includeMin: true)
+          ]),
+          isNot(equals(new VersionRange(min: v114, max: v130))));
+
+      expect(
+          new VersionConstraint.unionOf([
             new VersionRange(min: v003, max: v072),
             new VersionRange(min: v072, max: v080)
           ]),
-          equals(new VersionConstraint.unionOf([
-            new VersionRange(min: v003, max: v072),
-            new VersionRange(min: v072, max: v080)
-          ])));
+          isNot(equals(new VersionRange(min: v003, max: v080))));
     });
 
     test("merges version numbers into ranges", () {
@@ -139,16 +146,30 @@
     test("merges adjacent version numbers into ranges", () {
       expect(
           new VersionConstraint.unionOf([
-            new VersionRange(min: v003, max: v072),
+            new VersionRange(
+                min: v003, max: v072, alwaysIncludeMaxPreRelease: true),
             v072,
             v114,
-            new VersionRange(min: v114, max: v124)
+            new VersionRange(min: v114, max: v124),
+            v124.firstPreRelease
           ]),
           equals(new VersionConstraint.unionOf([
             new VersionRange(min: v003, max: v072, includeMax: true),
-            new VersionRange(min: v114, max: v124, includeMin: true)
+            new VersionRange(
+                min: v114,
+                max: v124.firstPreRelease,
+                includeMin: true,
+                includeMax: true)
           ])));
     });
+
+    test("doesn't merge not-quite-adjacent version numbers into ranges", () {
+      expect(
+          new VersionConstraint.unionOf(
+              [new VersionRange(min: v003, max: v072), v072]),
+          isNot(equals(
+              new VersionRange(min: v003, max: v072, includeMax: true))));
+    });
   });
 
   test('isEmpty returns false', () {
@@ -424,7 +445,8 @@
             new VersionRange(min: v124)
           ])),
           equals(new VersionConstraint.unionOf([
-            new VersionRange(min: v072, max: v080, includeMin: true),
+            new VersionRange(
+                min: v072.firstPreRelease, max: v080, includeMin: true),
             new VersionRange(min: v123, max: v124, includeMax: true)
           ])));
     });
@@ -436,8 +458,10 @@
             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: v010, max: v072, alwaysIncludeMaxPreRelease: true),
+            new VersionRange(
+                min: v072, max: v080, alwaysIncludeMaxPreRelease: true),
             new VersionRange(min: v080, max: v114),
             new VersionRange(min: v130, max: v200)
           ])));
@@ -455,7 +479,8 @@
           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: v201.firstPreRelease, max: v234, includeMin: true),
             new VersionRange(min: v250, max: v300)
           ])));
     });