Harvest from pub. Mostly a straight copy, except:

- Reorganized into smaller libraries.
- Added tests for prioritize() and antiprioritize().
- Filled in a few doc comments.

R=nweiz@google.com

Review URL: https://chromiumcodereview.appspot.com//607663002
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7178642
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+packages
+pubspec.lock
+
+# IntelliJ project.
+.idea
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..bf0e1f9
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 1.0.0
+
+* Initial release.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1cb1527
--- /dev/null
+++ b/README.md
@@ -0,0 +1,77 @@
+Handles version numbers and version constraints in the same way that [pub][]
+does. The semantics here very closely follow the [Semantic Versioning][semver]
+spec. It differs from semver in a few corner cases:
+
+ *  **Version ordering does take build suffixes into account.** This is unlike
+    semver 2.0.0 but like earlier versions of semver. Version `1.2.3+1` is
+    considered a lower number than `1.2.3+2`.
+
+    Since a package may have published multiple versions that differ only by
+    build suffix, pub still has to pick one of them *somehow*. Semver leaves
+    that issue unresolved, so we just say that build numbers are sorted like
+    pre-release suffixes.
+
+ *  **Pre-release versions are excluded from most max ranges.** Let's say a
+    user is depending on "foo" with constraint `>=1.0.0 <2.0.0` and that "foo"
+    has published these versions:
+
+     *  `1.0.0`
+     *  `1.1.0`
+     *  `1.2.0`
+     *  `2.0.0-alpha`
+     *  `2.0.0-beta`
+     *  `2.0.0`
+     *  `2.1.0`
+
+    Versions `2.0.0` and `2.1.0` are excluded by the constraint since neither
+    matches `<2.0.0`. However, since semver specifies that pre-release versions
+    are lower than the non-prerelease version (i.e. `2.0.0-beta < 2.0.0`, then
+    the `<2.0.0` constraint does technically allow those.
+
+    But that's almost never what the user wants. If their package doesn't work
+    with foo `2.0.0`, it's certainly not likely to work with experimental,
+    unstable versions of `2.0.0`'s API, which is what pre-release versions
+    represent.
+
+    To handle that, `<` version ranges to not allow pre-release versions of the
+    maximum unless the max is itself a pre-release. In other words, a `<2.0.0`
+    constraint will prohibit not just `2.0.0` but any pre-release of `2.0.0`.
+    However, `<2.0.0-beta` will exclude `2.0.0-beta` but allow `2.0.0-alpha`.
+
+ *  **Pre-release versions are avoided when possible.** The above case
+    handles pre-release versions at the top of the range, but what about in
+    the middle? What if "foo" has these versions:
+
+     *  `1.0.0`
+     *  `1.2.0-alpha`
+     *  `1.2.0`
+     *  `1.3.0-experimental`
+
+    When a number of versions are valid, pub chooses the best one where "best"
+    usually means "highest numbered". That follows the user's intuition that,
+    all else being equal, they want the latest and greatest. Here, that would
+    mean `1.3.0-experimental`. However, most users don't want to use unstable
+    versions of their dependencies.
+
+    We want pre-releases to be explicitly opt-in so that package consumers
+    don't get unpleasant surprises and so that package maintainers are free to
+    put out pre-releases and get feedback without dragging all of their users
+    onto the bleeding edge.
+
+    To accommodate that, when pub is choosing a version, it uses *priority*
+    order which is different from strict comparison ordering. Any stable
+    version is considered higher priority than any unstable version. The above
+    versions, in priority order, are:
+
+     *  `1.2.0-alpha`
+     *  `1.3.0-experimental`
+     *  `1.0.0`
+     *  `1.2.0`
+
+    This ensures that users only end up with an unstable version when there are
+    no alternatives. Usually this means they've picked a constraint that
+    specifically selects that unstable version -- they've deliberately opted
+    into it.
+
+[pub]: http://pub.dartlang.org/
+[semver]: http://semver.org/
diff --git a/lib/pub_semver.dart b/lib/pub_semver.dart
new file mode 100644
index 0000000..436e226
--- /dev/null
+++ b/lib/pub_semver.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver;
+
+export 'src/version.dart';
+export 'src/version_constraint.dart';
+export 'src/version_range.dart';
diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart
new file mode 100644
index 0000000..4a22618
--- /dev/null
+++ b/lib/src/patterns.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.src.patterns;
+
+/// Regex that matches a version number at the beginning of a string.
+final START_VERSION = new RegExp(
+    r'^'                                        // Start at beginning.
+    r'(\d+).(\d+).(\d+)'                        // Version number.
+    r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'    // Pre-release.
+    r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'); // Build.
+
+/// Like [START_VERSION] but matches the entire string.
+final COMPLETE_VERSION = new RegExp(START_VERSION.pattern + r'$');
+
+/// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of
+/// a string.
+final START_COMPARISON = new RegExp(r"^[<>]=?");
diff --git a/lib/src/version.dart b/lib/src/version.dart
new file mode 100644
index 0000000..866d89c
--- /dev/null
+++ b/lib/src/version.dart
@@ -0,0 +1,295 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.src.version;
+
+import 'dart:math';
+
+import 'package:collection/equality.dart';
+
+import 'patterns.dart';
+import 'version_constraint.dart';
+import 'version_range.dart';
+
+/// The equality operator to use for comparing version components.
+final _equality = const IterableEquality();
+
+/// A parsed semantic version number.
+class Version implements Comparable<Version>, VersionConstraint {
+  /// No released version: i.e. "0.0.0".
+  static Version get none => new Version(0, 0, 0);
+
+  /// Compares [a] and [b] to see which takes priority over the other.
+  ///
+  /// Returns `1` if [a] takes priority over [b] and `-1` if vice versa. If
+  /// [a] and [b] are equivalent, returns `0`.
+  ///
+  /// Unlike [compareTo], which *orders* versions, this determines which
+  /// version a user is likely to prefer. In particular, it prioritizes
+  /// pre-release versions lower than stable versions, regardless of their
+  /// version numbers. Pub uses this when determining which version to prefer
+  /// when a number of versions are allowed. In that case, it will always
+  /// choose a stable version when possible.
+  ///
+  /// When used to sort a list, orders in ascending priority so that the
+  /// highest priority version is *last* in the result.
+  static int prioritize(Version a, Version b) {
+    // Sort all prerelease versions after all normal versions. This way
+    // the solver will prefer stable packages over unstable ones.
+    if (a.isPreRelease && !b.isPreRelease) return -1;
+    if (!a.isPreRelease && b.isPreRelease) return 1;
+
+    return a.compareTo(b);
+  }
+
+  /// Like [proiritize], but lower version numbers are considered greater than
+  /// higher version numbers.
+  ///
+  /// This still considers prerelease versions to be lower than non-prerelease
+  /// versions. Pub uses this when downgrading -- it chooses the lowest version
+  /// but still excludes pre-release versions when possible.
+  static int antiprioritize(Version a, Version b) {
+    if (a.isPreRelease && !b.isPreRelease) return -1;
+    if (!a.isPreRelease && b.isPreRelease) return 1;
+
+    return b.compareTo(a);
+  }
+
+  /// The major version number: "1" in "1.2.3".
+  final int major;
+
+  /// The minor version number: "2" in "1.2.3".
+  final int minor;
+
+  /// The patch version number: "3" in "1.2.3".
+  final int patch;
+
+  /// The pre-release identifier: "foo" in "1.2.3-foo".
+  ///
+  /// This is split into a list of components, each of which may be either a
+  /// string or a non-negative integer. It may also be empty, indicating that
+  /// this version has no pre-release identifier.
+  final List preRelease;
+
+  /// The build identifier: "foo" in "1.2.3+foo".
+  ///
+  /// This is split into a list of components, each of which may be either a
+  /// string or a non-negative integer. It may also be empty, indicating that
+  /// this version has no build identifier.
+  final List build;
+
+  /// The original string representation of the version number.
+  ///
+  /// This preserves textual artifacts like leading zeros that may be left out
+  /// of the parsed version.
+  final String _text;
+
+  Version._(this.major, this.minor, this.patch, String preRelease, String build,
+            this._text)
+      : preRelease = preRelease == null ? [] : _splitParts(preRelease),
+        build = build == null ? [] : _splitParts(build) {
+    if (major < 0) throw new ArgumentError(
+        'Major version must be non-negative.');
+    if (minor < 0) throw new ArgumentError(
+        'Minor version must be non-negative.');
+    if (patch < 0) throw new ArgumentError(
+        'Patch version must be non-negative.');
+  }
+
+  /// Creates a new [Version] object.
+  factory Version(int major, int minor, int patch, {String pre, String build}) {
+    var text = "$major.$minor.$patch";
+    if (pre != null) text += "-$pre";
+    if (build != null) text += "+$build";
+
+    return new Version._(major, minor, patch, pre, build, text);
+  }
+
+  /// Creates a new [Version] by parsing [text].
+  factory Version.parse(String text) {
+    final match = COMPLETE_VERSION.firstMatch(text);
+    if (match == null) {
+      throw new FormatException('Could not parse "$text".');
+    }
+
+    try {
+      int major = int.parse(match[1]);
+      int minor = int.parse(match[2]);
+      int patch = int.parse(match[3]);
+
+      String preRelease = match[5];
+      String build = match[8];
+
+      return new Version._(major, minor, patch, preRelease, build, text);
+    } on FormatException catch (ex) {
+      throw new FormatException('Could not parse "$text".');
+    }
+  }
+
+  /// Returns the primary version out of a list of candidates.
+  ///
+  /// This is the highest-numbered stable (non-prerelease) version. If there
+  /// are no stable versions, it's just the highest-numbered version.
+  static Version primary(List<Version> versions) {
+    var primary;
+    for (var version in versions) {
+      if (primary == null || (!version.isPreRelease && primary.isPreRelease) ||
+          (version.isPreRelease == primary.isPreRelease && version > primary)) {
+        primary = version;
+      }
+    }
+    return primary;
+  }
+
+  /// Splits a string of dot-delimited identifiers into their component parts.
+  ///
+  /// Identifiers that are numeric are converted to numbers.
+  static List _splitParts(String text) {
+    return text.split('.').map((part) {
+      try {
+        return int.parse(part);
+      } on FormatException catch (ex) {
+        // Not a number.
+        return part;
+      }
+    }).toList();
+  }
+
+  bool operator ==(other) {
+    if (other is! Version) return false;
+    return major == other.major && minor == other.minor &&
+        patch == other.patch &&
+        _equality.equals(preRelease, other.preRelease) &&
+        _equality.equals(build, other.build);
+  }
+
+  int get hashCode => major ^ minor ^ patch ^ _equality.hash(preRelease) ^
+      _equality.hash(build);
+
+  bool operator <(Version other) => compareTo(other) < 0;
+  bool operator >(Version other) => compareTo(other) > 0;
+  bool operator <=(Version other) => compareTo(other) <= 0;
+  bool operator >=(Version other) => compareTo(other) >= 0;
+
+  bool get isAny => false;
+  bool get isEmpty => false;
+
+  /// Whether or not this is a pre-release version.
+  bool get isPreRelease => preRelease.isNotEmpty;
+
+  /// Gets the next major version number that follows this one.
+  ///
+  /// If this version is a pre-release of a major version release (i.e. the
+  /// minor and patch versions are zero), then it just strips the pre-release
+  /// suffix. Otherwise, it increments the major version and resets the minor
+  /// and patch.
+  Version get nextMajor {
+    if (isPreRelease && minor == 0 && patch == 0) {
+      return new Version(major, minor, patch);
+    }
+
+    return new Version(major + 1, 0, 0);
+  }
+
+  /// Gets the next minor version number that follows this one.
+  ///
+  /// If this version is a pre-release of a minor version release (i.e. the
+  /// patch version is zero), then it just strips the pre-release suffix.
+  /// Otherwise, it increments the minor version and resets the patch.
+  Version get nextMinor {
+    if (isPreRelease && patch == 0) {
+      return new Version(major, minor, patch);
+    }
+
+    return new Version(major, minor + 1, 0);
+  }
+
+  /// Gets the next patch version number that follows this one.
+  ///
+  /// If this version is a pre-release, then it just strips the pre-release
+  /// suffix. Otherwise, it increments the patch version.
+  Version get nextPatch {
+    if (isPreRelease) {
+      return new Version(major, minor, patch);
+    }
+
+    return new Version(major, minor, patch + 1);
+  }
+
+  /// Tests if [other] matches this version exactly.
+  bool allows(Version other) => this == other;
+
+  VersionConstraint intersect(VersionConstraint other) {
+    if (other.isEmpty) return other;
+
+    // Intersect a version and a range.
+    if (other is VersionRange) return other.intersect(this);
+
+    // Intersecting two versions only works if they are the same.
+    if (other is Version) {
+      return this == other ? this : VersionConstraint.empty;
+    }
+
+    throw new ArgumentError(
+        'Unknown VersionConstraint type $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);
+
+    // 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;
+
+    // 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);
+  }
+
+  String toString() => _text;
+
+  /// Compares a dot-separated component of two versions.
+  ///
+  /// This is used for the pre-release and build version parts. This follows
+  /// Rule 12 of the Semantic Versioning spec (v2.0.0-rc.1).
+  int _compareLists(List a, List b) {
+    for (var i = 0; i < max(a.length, b.length); i++) {
+      var aPart = (i < a.length) ? a[i] : null;
+      var bPart = (i < b.length) ? b[i] : null;
+
+      if (aPart == bPart) continue;
+
+      // Missing parts come before present ones.
+      if (aPart == null) return -1;
+      if (bPart == null) return 1;
+
+      if (aPart is num) {
+        if (bPart is num) {
+          // Compare two numbers.
+          return aPart.compareTo(bPart);
+        } else {
+          // Numbers come before strings.
+          return -1;
+        }
+      } else {
+        if (bPart is num) {
+          // Strings come after numbers.
+          return 1;
+        } else {
+          // Compare two strings.
+          return aPart.compareTo(bPart);
+        }
+      }
+    }
+
+    // The lists are entirely equal.
+    return 0;
+  }
+}
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
new file mode 100644
index 0000000..499892e
--- /dev/null
+++ b/lib/src/version_constraint.dart
@@ -0,0 +1,151 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.src.version_constraint;
+
+import 'patterns.dart';
+import 'version.dart';
+import 'version_range.dart';
+
+/// A [VersionConstraint] is a predicate that can determine whether a given
+/// version is valid or not.
+///
+/// For example, a ">= 2.0.0" constraint allows any version that is "2.0.0" or
+/// greater. Version objects themselves implement this to match a specific
+/// version.
+abstract class VersionConstraint {
+  /// A [VersionConstraint] that allows all versions.
+  static VersionConstraint any = new VersionRange();
+
+  /// A [VersionConstraint] that allows no versions -- the empty set.
+  static VersionConstraint empty = const _EmptyVersion();
+
+  /// Parses a version constraint.
+  ///
+  /// This string is either "any" or a series of version parts. Each part can
+  /// be one of:
+  ///
+  ///   * A version string like `1.2.3`. In other words, anything that can be
+  ///     parsed by [Version.parse()].
+  ///   * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version
+  ///     string.
+  ///
+  /// Whitespace is ignored.
+  ///
+  /// Examples:
+  ///
+  ///     any
+  ///     1.2.3-alpha
+  ///     <=5.1.4
+  ///     >2.0.4 <= 2.4.6
+  factory VersionConstraint.parse(String text) {
+    // Handle the "any" constraint.
+    if (text.trim() == "any") return new VersionRange();
+
+    var originalText = text;
+    var constraints = [];
+
+    skipWhitespace() {
+      text = text.trim();
+    }
+
+    // Try to parse and consume a version number.
+    matchVersion() {
+      var version = START_VERSION.firstMatch(text);
+      if (version == null) return null;
+
+      text = text.substring(version.end);
+      return new Version.parse(version[0]);
+    }
+
+    // Try to parse and consume a comparison operator followed by a version.
+    matchComparison() {
+      var comparison = START_COMPARISON.firstMatch(text);
+      if (comparison == null) return null;
+
+      var op = comparison[0];
+      text = text.substring(comparison.end);
+      skipWhitespace();
+
+      var version = matchVersion();
+      if (version == null) {
+        throw new FormatException('Expected version number after "$op" in '
+            '"$originalText", got "$text".');
+      }
+
+      switch (op) {
+        case '<=': return new VersionRange(max: version, includeMax: true);
+        case '<': return new VersionRange(max: version, includeMax: false);
+        case '>=': return new VersionRange(min: version, includeMin: true);
+        case '>': return new VersionRange(min: version, includeMin: false);
+      }
+      throw "Unreachable.";
+    }
+
+    while (true) {
+      skipWhitespace();
+      if (text.isEmpty) break;
+
+      var version = matchVersion();
+      if (version != null) {
+        constraints.add(version);
+        continue;
+      }
+
+      var comparison = matchComparison();
+      if (comparison != null) {
+        constraints.add(comparison);
+        continue;
+      }
+
+      // If we got here, we couldn't parse the remaining string.
+      throw new FormatException('Could not parse version "$originalText". '
+          'Unknown text at "$text".');
+    }
+
+    if (constraints.isEmpty) {
+      throw new FormatException('Cannot parse an empty string.');
+    }
+
+    return new VersionConstraint.intersection(constraints);
+  }
+
+  /// Creates a new version constraint that is the intersection of
+  /// [constraints].
+  ///
+  /// It only allows versions that all of those constraints allow. If
+  /// constraints is empty, then it returns a VersionConstraint that allows
+  /// all versions.
+  factory VersionConstraint.intersection(
+      Iterable<VersionConstraint> constraints) {
+    var constraint = new VersionRange();
+    for (var other in constraints) {
+      constraint = constraint.intersect(other);
+    }
+    return constraint;
+  }
+
+  /// Returns `true` if this constraint allows no versions.
+  bool get isEmpty;
+
+  /// Returns `true` if this constraint allows all versions.
+  bool get isAny;
+
+  /// Returns `true` if this constraint allows [version].
+  bool allows(Version version);
+
+  /// Creates a new [VersionConstraint] that only allows [Version]s allowed by
+  /// both this and [other].
+  VersionConstraint intersect(VersionConstraint other);
+}
+
+class _EmptyVersion implements VersionConstraint {
+  const _EmptyVersion();
+
+  bool get isEmpty => true;
+  bool get isAny => false;
+  bool allows(Version other) => false;
+  VersionConstraint intersect(VersionConstraint other) => this;
+  String toString() => '<empty>';
+}
diff --git a/lib/src/version_range.dart b/lib/src/version_range.dart
new file mode 100644
index 0000000..7020077
--- /dev/null
+++ b/lib/src/version_range.dart
@@ -0,0 +1,179 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.src.version_range;
+
+import 'version.dart';
+import 'version_constraint.dart';
+
+/// Constrains versions to a fall within a given range.
+///
+/// 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 {
+  /// The minimum end of the range.
+  ///
+  /// If [includeMin] is `true`, this will be the minimum allowed version.
+  /// Otherwise, it will be the highest version below the range that is not
+  /// allowed.
+  ///
+  /// This may be `null` in which case the range has no minimum end and allows
+  /// any version less than the maximum.
+  final Version min;
+
+  /// The maximum end of the range.
+  ///
+  /// If [includeMax] is `true`, this will be the maximum allowed version.
+  /// Otherwise, it will be the lowest version above the range that is not
+  /// allowed.
+  ///
+  /// This may be `null` in which case the range has no maximum end and allows
+  /// any version greater than the minimum.
+  final Version max;
+
+  /// If `true` then [min] is allowed by the range.
+  final bool includeMin;
+
+  /// If `true`, then [max] is allowed by the range.
+  final bool includeMax;
+
+  /// Creates a new version range from [min] to [max], either inclusive or
+  /// exclusive.
+  ///
+  /// If it is an error if [min] is greater than [max].
+  ///
+  /// Either [max] or [min] may be omitted to not clamp the range at that end.
+  /// If both are omitted, the range allows all versions.
+  ///
+  /// 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 (min != null && max != null && min > max) {
+      throw new ArgumentError(
+          'Minimum version ("$min") must be less than maximum ("$max").');
+    }
+  }
+
+  bool operator ==(other) {
+    if (other is! VersionRange) return false;
+
+    return min == other.min &&
+           max == other.max &&
+           includeMin == other.includeMin &&
+           includeMax == other.includeMax;
+  }
+
+  bool get isEmpty => false;
+
+  bool get isAny => min == null && max == null;
+
+  /// Tests if [other] falls within this version range.
+  bool allows(Version other) {
+    if (min != null) {
+      if (other < min) return false;
+      if (!includeMin && other == min) return false;
+    }
+
+    if (max != null) {
+      if (other > max) return false;
+      if (!includeMax && other == max) return false;
+
+      // If the max isn't itself a pre-release, don't allow any pre-release
+      // versions of the max.
+      //
+      // See: https://www.npmjs.org/doc/misc/semver.html
+      if (!includeMax &&
+          !max.isPreRelease && other.isPreRelease &&
+          other.major == max.major && other.minor == max.minor &&
+          other.patch == max.patch) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  VersionConstraint intersect(VersionConstraint other) {
+    if (other.isEmpty) return other;
+
+    // A range and a Version just yields the version if it's in the range.
+    if (other is Version) {
+      return allows(other) ? other : VersionConstraint.empty;
+    }
+
+    if (other is VersionRange) {
+      // Intersect the two ranges.
+      var intersectMin = min;
+      var intersectIncludeMin = includeMin;
+      var intersectMax = max;
+      var intersectIncludeMax = includeMax;
+
+      if (other.min == null) {
+        // Do nothing.
+      } else if (intersectMin == null || intersectMin < other.min) {
+        intersectMin = other.min;
+        intersectIncludeMin = other.includeMin;
+      } else if (intersectMin == other.min && !other.includeMin) {
+        // The edges are the same, but one is exclusive, make it exclusive.
+        intersectIncludeMin = false;
+      }
+
+      if (other.max == null) {
+        // Do nothing.
+      } else if (intersectMax == null || intersectMax > other.max) {
+        intersectMax = other.max;
+        intersectIncludeMax = other.includeMax;
+      } else if (intersectMax == other.max && !other.includeMax) {
+        // The edges are the same, but one is exclusive, make it exclusive.
+        intersectIncludeMax = false;
+      }
+
+      if (intersectMin == null && intersectMax == null) {
+        // Open range.
+        return new VersionRange();
+      }
+
+      // If the range is just a single version.
+      if (intersectMin == intersectMax) {
+        // If both ends are inclusive, allow that version.
+        if (intersectIncludeMin && intersectIncludeMax) return intersectMin;
+
+        // Otherwise, no versions.
+        return VersionConstraint.empty;
+      }
+
+      if (intersectMin != null && intersectMax != null &&
+          intersectMin > intersectMax) {
+        // Non-overlapping ranges, so empty.
+        return VersionConstraint.empty;
+      }
+
+      // If we got here, there is an actual range.
+      return new VersionRange(min: intersectMin, max: intersectMax,
+          includeMin: intersectIncludeMin, includeMax: intersectIncludeMax);
+    }
+
+    throw new ArgumentError('Unknown VersionConstraint type $other.');
+  }
+
+  String toString() {
+    var buffer = new StringBuffer();
+
+    if (min != null) {
+      buffer.write(includeMin ? '>=' : '>');
+      buffer.write(min);
+    }
+
+    if (max != null) {
+      if (min != null) buffer.write(' ');
+      buffer.write(includeMax ? '<=' : '<');
+      buffer.write(max);
+    }
+
+    if (min == null && max == null) buffer.write('any');
+    return buffer.toString();
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..63b09e1
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,13 @@
+name: pub_semver
+version: 1.0.0
+author: Dart Team <misc@dartlang.org>
+description: >
+ Versions and version constraints implementing pub's versioning policy. This
+ is very similar to vanilla semver, with a few corner cases.
+homepage: http://github.com/dart-lang/pub_semver
+dependencies:
+  collection: ">=0.9.0 <2.0.0"
+dev_dependencies:
+  unittest: ">=0.9.0 <0.12.0"
+environment:
+  sdk: ">=1.0.0 <2.0.0"
diff --git a/test/test_all.dart b/test/test_all.dart
new file mode 100644
index 0000000..533dde8
--- /dev/null
+++ b/test/test_all.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.test.test_all;
+
+import 'package:unittest/unittest.dart';
+
+import 'version_constraint_test.dart' as version_constraint_test;
+import 'version_range_test.dart' as version_range_test;
+import 'version_test.dart' as version_test;
+
+main() {
+  group('Version', version_test.main);
+  group('VersionConstraint', version_constraint_test.main);
+  group('VersionRange', version_range_test.main);
+}
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..da89683
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.test.utils;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+/// Some stock example versions to use in tests.
+final v114 = new Version.parse('1.1.4');
+final v123 = new Version.parse('1.2.3');
+final v124 = new Version.parse('1.2.4');
+final v130 = new Version.parse('1.3.0');
+final v140 = new Version.parse('1.4.0');
+final v200 = new Version.parse('2.0.0');
+final v201 = new Version.parse('2.0.1');
+final v234 = new Version.parse('2.3.4');
+final v250 = new Version.parse('2.5.0');
+final v300 = new Version.parse('3.0.0');
+
+/// A [Matcher] that tests if a [VersionConstraint] allows or does not allow a
+/// given list of [Version]s.
+class _VersionConstraintMatcher implements Matcher {
+  final List<Version> _expected;
+  final bool _allow;
+
+  _VersionConstraintMatcher(this._expected, this._allow);
+
+  bool matches(item, Map matchState) => (item is VersionConstraint) &&
+      _expected.every((version) => item.allows(version) == _allow);
+
+  Description describe(Description description) {
+    description.addAll(' ${_allow ? "allows" : "does not allow"} versions ',
+        ', ', '', _expected);
+    return description;
+  }
+
+  Description describeMismatch(item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    if (item is! VersionConstraint) {
+      mismatchDescription.add('was not a VersionConstraint');
+      return mismatchDescription;
+    }
+
+    var first = true;
+    for (var version in _expected) {
+      if (item.allows(version) != _allow) {
+        if (first) {
+          if (_allow) {
+            mismatchDescription.addDescriptionOf(item).add(' did not allow ');
+          } else {
+            mismatchDescription.addDescriptionOf(item).add(' allowed ');
+          }
+        } else {
+          mismatchDescription.add(' and ');
+        }
+        first = false;
+
+        mismatchDescription.add(version.toString());
+      }
+    }
+
+    return mismatchDescription;
+  }
+}
+
+/// Gets a [Matcher] that validates that a [VersionConstraint] allows all
+/// given versions.
+Matcher allows(Version v1, [Version v2, Version v3, Version v4,
+    Version v5, Version v6, Version v7, Version v8]) {
+  var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
+  return new _VersionConstraintMatcher(versions, true);
+}
+
+/// Gets a [Matcher] that validates that a [VersionConstraint] allows none of
+/// the given versions.
+Matcher doesNotAllow(Version v1, [Version v2, Version v3, Version v4,
+    Version v5, Version v6, Version v7, Version v8]) {
+  var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
+  return new _VersionConstraintMatcher(versions, false);
+}
+
+List<Version> _makeVersionList(Version v1, [Version v2, Version v3, Version v4,
+    Version v5, Version v6, Version v7, Version v8]) {
+  var versions = [v1];
+  if (v2 != null) versions.add(v2);
+  if (v3 != null) versions.add(v3);
+  if (v4 != null) versions.add(v4);
+  if (v5 != null) versions.add(v5);
+  if (v6 != null) versions.add(v6);
+  if (v7 != null) versions.add(v7);
+  if (v8 != null) versions.add(v8);
+  return versions;
+}
\ No newline at end of file
diff --git a/test/version_constraint_test.dart b/test/version_constraint_test.dart
new file mode 100644
index 0000000..df8c099
--- /dev/null
+++ b/test/version_constraint_test.dart
@@ -0,0 +1,143 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.test.version_constraint_test;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import 'utils.dart';
+
+main() {
+  test('any', () {
+    expect(VersionConstraint.any.isAny, isTrue);
+    expect(VersionConstraint.any, allows(
+        new Version.parse('0.0.0-blah'),
+        new Version.parse('1.2.3'),
+        new Version.parse('12345.678.90')));
+  });
+
+  test('empty', () {
+    expect(VersionConstraint.empty.isEmpty, isTrue);
+    expect(VersionConstraint.empty.isAny, isFalse);
+    expect(VersionConstraint.empty, doesNotAllow(
+        new Version.parse('0.0.0-blah'),
+        new Version.parse('1.2.3'),
+        new Version.parse('12345.678.90')));
+  });
+
+  group('parse()', () {
+    test('parses an exact version', () {
+      var constraint = new VersionConstraint.parse('1.2.3-alpha');
+
+      expect(constraint is Version, isTrue);
+      expect(constraint, equals(new Version(1, 2, 3, pre: 'alpha')));
+    });
+
+    test('parses "any"', () {
+      var constraint = new VersionConstraint.parse('any');
+
+      expect(constraint is VersionConstraint, isTrue);
+      expect(constraint, allows(
+          new Version.parse('0.0.0'),
+          new Version.parse('1.2.3'),
+          new Version.parse('12345.678.90')));
+    });
+
+    test('parses a ">" minimum version', () {
+      var constraint = new VersionConstraint.parse('>1.2.3');
+
+      expect(constraint, allows(
+          new Version.parse('1.2.3+foo'),
+          new Version.parse('1.2.4')));
+      expect(constraint, doesNotAllow(
+          new Version.parse('1.2.1'),
+          new Version.parse('1.2.3-build'),
+          new Version.parse('1.2.3')));
+    });
+
+    test('parses a ">=" minimum version', () {
+      var constraint = new VersionConstraint.parse('>=1.2.3');
+
+      expect(constraint, allows(
+          new Version.parse('1.2.3'),
+          new Version.parse('1.2.3+foo'),
+          new Version.parse('1.2.4')));
+      expect(constraint, doesNotAllow(
+          new Version.parse('1.2.1'),
+          new Version.parse('1.2.3-build')));
+    });
+
+    test('parses a "<" maximum version', () {
+      var constraint = new VersionConstraint.parse('<1.2.3');
+
+      expect(constraint, allows(
+          new Version.parse('1.2.1'),
+          new Version.parse('1.2.2+foo')));
+      expect(constraint, doesNotAllow(
+          new Version.parse('1.2.3'),
+          new Version.parse('1.2.3+foo'),
+          new Version.parse('1.2.4')));
+    });
+
+    test('parses a "<=" maximum version', () {
+      var constraint = new VersionConstraint.parse('<=1.2.3');
+
+      expect(constraint, allows(
+          new Version.parse('1.2.1'),
+          new Version.parse('1.2.3-build'),
+          new Version.parse('1.2.3')));
+      expect(constraint, doesNotAllow(
+          new Version.parse('1.2.3+foo'),
+          new Version.parse('1.2.4')));
+    });
+
+    test('parses a series of space-separated constraints', () {
+      var constraint = new VersionConstraint.parse('>1.0.0 >=1.2.3 <1.3.0');
+
+      expect(constraint, allows(
+          new Version.parse('1.2.3'),
+          new Version.parse('1.2.5')));
+      expect(constraint, doesNotAllow(
+          new Version.parse('1.2.3-pre'),
+          new Version.parse('1.3.0'),
+          new Version.parse('3.4.5')));
+    });
+
+    test('ignores whitespace around operators', () {
+      var constraint = new VersionConstraint.parse(' >1.0.0>=1.2.3 < 1.3.0');
+
+      expect(constraint, allows(
+          new Version.parse('1.2.3'),
+          new Version.parse('1.2.5')));
+      expect(constraint, doesNotAllow(
+          new Version.parse('1.2.3-pre'),
+          new Version.parse('1.3.0'),
+          new Version.parse('3.4.5')));
+    });
+
+    test('does not allow "any" to be mixed with other constraints', () {
+      expect(() => new VersionConstraint.parse('any 1.0.0'),
+          throwsFormatException);
+    });
+
+    test('throws FormatException on a bad string', () {
+      var bad = [
+         "", "   ",               // Empty string.
+         "foo",                   // Bad text.
+         ">foo",                  // Bad text after operator.
+         "1.0.0 foo", "1.0.0foo", // Bad text after version.
+         "anything",              // Bad text after "any".
+         "<>1.0.0",               // Multiple operators.
+         "1.0.0<"                 // Trailing operator.
+      ];
+
+      for (var text in bad) {
+        expect(() => new VersionConstraint.parse(text),
+            throwsFormatException);
+      }
+    });
+  });
+}
diff --git a/test/version_range_test.dart b/test/version_range_test.dart
new file mode 100644
index 0000000..388917f
--- /dev/null
+++ b/test/version_range_test.dart
@@ -0,0 +1,202 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.test.version_range_test;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import 'utils.dart';
+
+main() {
+  group('constructor', () {
+    test('takes a min and max', () {
+      var range = new VersionRange(min: v123, max: v124);
+      expect(range.isAny, isFalse);
+      expect(range.min, equals(v123));
+      expect(range.max, equals(v124));
+    });
+
+    test('allows omitting max', () {
+      var range = new VersionRange(min: v123);
+      expect(range.isAny, isFalse);
+      expect(range.min, equals(v123));
+      expect(range.max, isNull);
+    });
+
+    test('allows omitting min and max', () {
+      var range = new VersionRange();
+      expect(range.isAny, isTrue);
+      expect(range.min, isNull);
+      expect(range.max, isNull);
+    });
+
+    test('takes includeMin', () {
+      var range = new VersionRange(min: v123, includeMin: true);
+      expect(range.includeMin, isTrue);
+    });
+
+    test('includeMin defaults to false if omitted', () {
+      var range = new VersionRange(min: v123);
+      expect(range.includeMin, isFalse);
+    });
+
+    test('takes includeMax', () {
+      var range = new VersionRange(max: v123, includeMax: true);
+      expect(range.includeMax, isTrue);
+    });
+
+    test('includeMax defaults to false if omitted', () {
+      var range = new VersionRange(max: v123);
+      expect(range.includeMax, isFalse);
+    });
+
+    test('throws if min > max', () {
+      expect(() => new VersionRange(min: v124, max: v123), throwsArgumentError);
+    });
+  });
+
+  group('allows()', () {
+    test('version must be greater than min', () {
+      var range = new VersionRange(min: v123);
+
+      expect(range, allows(
+          new Version.parse('1.3.3'),
+          new Version.parse('2.3.3')));
+      expect(range, doesNotAllow(
+          new Version.parse('1.2.2'),
+          new Version.parse('1.2.3')));
+    });
+
+    test('version must be min or greater if includeMin', () {
+      var range = new VersionRange(min: v123, includeMin: true);
+
+      expect(range, allows(
+          new Version.parse('1.2.3'),
+          new Version.parse('1.3.3'),
+          new Version.parse('2.3.3')));
+      expect(range, doesNotAllow(new Version.parse('1.2.2')));
+    });
+
+    test('pre-release versions of inclusive min are excluded', () {
+      var range = new VersionRange(min: v123, includeMin: true);
+
+      expect(range, allows(new Version.parse('1.2.4-dev')));
+      expect(range, doesNotAllow(new Version.parse('1.2.3-dev')));
+    });
+
+    test('version must be less than max', () {
+      var range = new VersionRange(max: v234);
+
+      expect(range, allows(new Version.parse('2.3.3')));
+      expect(range, doesNotAllow(
+          new Version.parse('2.3.4'),
+          new Version.parse('2.4.3')));
+    });
+
+    test('pre-release versions of non-pre-release max are excluded', () {
+      var range = new VersionRange(max: v234);
+
+      expect(range, allows(new Version.parse('2.3.3')));
+      expect(range, doesNotAllow(
+          new Version.parse('2.3.4-dev'),
+          new Version.parse('2.3.4')));
+    });
+
+    test('pre-release versions of pre-release max are included', () {
+      var range = new VersionRange(max: new Version.parse('2.3.4-dev.2'));
+
+      expect(range, allows(
+          new Version.parse('2.3.4-dev.1')));
+      expect(range, doesNotAllow(
+          new Version.parse('2.3.4-dev.2'),
+          new Version.parse('2.3.4-dev.3')));
+    });
+
+    test('version must be max or less if includeMax', () {
+      var range = new VersionRange(min: v123, max: v234, includeMax: true);
+
+      expect(range, allows(
+          new Version.parse('2.3.3'),
+          new Version.parse('2.3.4'),
+          // Pre-releases of the max are allowed.
+          new Version.parse('2.3.4-dev')));
+      expect(range, doesNotAllow(new Version.parse('2.4.3')));
+    });
+
+    test('has no min if one was not set', () {
+      var range = new VersionRange(max: v123);
+
+      expect(range, allows(new Version.parse('0.0.0')));
+      expect(range, doesNotAllow(new Version.parse('1.2.3')));
+    });
+
+    test('has no max if one was not set', () {
+      var range = new VersionRange(min: v123);
+
+      expect(range, allows(
+          new Version.parse('1.3.3'),
+          new Version.parse('999.3.3')));
+      expect(range, doesNotAllow(new Version.parse('1.2.3')));
+    });
+
+    test('allows any version if there is no min or max', () {
+      var range = new VersionRange();
+
+      expect(range, allows(
+          new Version.parse('0.0.0'),
+          new Version.parse('999.99.9')));
+    });
+  });
+
+  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.min, equals(v200));
+      expect(intersect.max, equals(v250));
+      expect(intersect.includeMin, isFalse);
+      expect(intersect.includeMax, isFalse);
+    });
+
+    test('a non-overlapping range allows no versions', () {
+      var a = new VersionRange(min: v114, max: v124);
+      var b = new VersionRange(min: v200, max: v250);
+      expect(a.intersect(b).isEmpty, isTrue);
+    });
+
+    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);
+      expect(a.intersect(b).isEmpty, isTrue);
+    });
+
+    test('adjacent ranges allow version if inclusive', () {
+      var a = new VersionRange(min: v114, max: v124, includeMax: true);
+      var b = new VersionRange(min: v124, max: v200, includeMin: true);
+      expect(a.intersect(b), equals(v124));
+    });
+
+    test('with an open range', () {
+      var open = new VersionRange();
+      var a = new VersionRange(min: v114, max: v124);
+      expect(open.intersect(open), equals(open));
+      expect(a.intersect(open), equals(a));
+    });
+
+    test('returns the version if the range allows it', () {
+      expect(new VersionRange(min: v114, max: v124).intersect(v123),
+          equals(v123));
+      expect(new VersionRange(min: v123, max: v124).intersect(v114).isEmpty,
+          isTrue);
+    });
+  });
+
+  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
new file mode 100644
index 0000000..b271ecc
--- /dev/null
+++ b/test/version_test.dart
@@ -0,0 +1,248 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// 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.
+
+library pub_semver.test.version_test;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import 'utils.dart';
+
+main() {
+  test('none', () {
+    expect(Version.none.toString(), equals('0.0.0'));
+  });
+
+  test('prioritize()', () {
+    // A correctly sorted list of versions in order of increasing priority.
+    var versions = [
+      '1.0.0-alpha',
+      '2.0.0-alpha',
+      '1.0.0',
+      '1.0.0+build',
+      '1.0.1',
+      '1.1.0',
+      '2.0.0'
+    ];
+
+    // Ensure that every pair of versions is prioritized in the order that it
+    // appears in the list.
+    for (var i = 0; i < versions.length; i++) {
+      for (var j = 0; j < versions.length; j++) {
+        var a = new Version.parse(versions[i]);
+        var b = new Version.parse(versions[j]);
+        expect(Version.prioritize(a, b), equals(i.compareTo(j)));
+      }
+    }
+  });
+
+  test('antiprioritize()', () {
+    // A correctly sorted list of versions in order of increasing antipriority.
+    var versions = [
+      '2.0.0-alpha',
+      '1.0.0-alpha',
+      '2.0.0',
+      '1.1.0',
+      '1.0.1',
+      '1.0.0+build',
+      '1.0.0'
+    ];
+
+    // Ensure that every pair of versions is prioritized in the order that it
+    // appears in the list.
+    for (var i = 0; i < versions.length; i++) {
+      for (var j = 0; j < versions.length; j++) {
+        var a = new Version.parse(versions[i]);
+        var b = new Version.parse(versions[j]);
+        expect(Version.antiprioritize(a, b), equals(i.compareTo(j)));
+      }
+    }
+  });
+
+  group('constructor', () {
+    test('throws on negative numbers', () {
+      expect(() => new Version(-1, 1, 1), throwsArgumentError);
+      expect(() => new Version(1, -1, 1), throwsArgumentError);
+      expect(() => new Version(1, 1, -1), throwsArgumentError);
+    });
+  });
+
+  group('comparison', () {
+    // A correctly sorted list of versions.
+    var versions = [
+      '1.0.0-alpha',
+      '1.0.0-alpha.1',
+      '1.0.0-beta.2',
+      '1.0.0-beta.11',
+      '1.0.0-rc.1',
+      '1.0.0-rc.1+build.1',
+      '1.0.0',
+      '1.0.0+0.3.7',
+      '1.3.7+build',
+      '1.3.7+build.2.b8f12d7',
+      '1.3.7+build.11.e0f985a',
+      '2.0.0',
+      '2.1.0',
+      '2.2.0',
+      '2.11.0',
+      '2.11.1'
+    ];
+
+    test('compareTo()', () {
+      // Ensure that every pair of versions compares in the order that it
+      // appears in the list.
+      for (var i = 0; i < versions.length; i++) {
+        for (var j = 0; j < versions.length; j++) {
+          var a = new Version.parse(versions[i]);
+          var b = new Version.parse(versions[j]);
+          expect(a.compareTo(b), equals(i.compareTo(j)));
+        }
+      }
+    });
+
+    test('operators', () {
+      for (var i = 0; i < versions.length; i++) {
+        for (var j = 0; j < versions.length; j++) {
+          var a = new Version.parse(versions[i]);
+          var b = new Version.parse(versions[j]);
+          expect(a < b, equals(i < j));
+          expect(a > b, equals(i > j));
+          expect(a <= b, equals(i <= j));
+          expect(a >= b, equals(i >= j));
+          expect(a == b, equals(i == j));
+          expect(a != b, equals(i != j));
+        }
+      }
+    });
+
+    test('equality', () {
+      expect(new Version.parse('01.2.3'), equals(new Version.parse('1.2.3')));
+      expect(new Version.parse('1.02.3'), equals(new Version.parse('1.2.3')));
+      expect(new Version.parse('1.2.03'), equals(new Version.parse('1.2.3')));
+      expect(new Version.parse('1.2.3-01'),
+          equals(new Version.parse('1.2.3-1')));
+      expect(new Version.parse('1.2.3+01'),
+          equals(new Version.parse('1.2.3+1')));
+    });
+  });
+
+  test('allows()', () {
+    expect(v123, allows(v123));
+    expect(v123, doesNotAllow(
+        new Version.parse('2.2.3'),
+        new Version.parse('1.3.3'),
+        new Version.parse('1.2.4'),
+        new Version.parse('1.2.3-dev'),
+        new Version.parse('1.2.3+build')));
+  });
+
+  test('intersect()', () {
+    // Intersecting the same version returns the version.
+    expect(v123.intersect(v123), equals(v123));
+
+    // Intersecting a different version allows no versions.
+    expect(v123.intersect(v114).isEmpty, isTrue);
+
+    // Intersecting a range returns the version if the range allows it.
+    expect(v123.intersect(new VersionRange(min: v114, max: v124)),
+        equals(v123));
+
+    // Intersecting a range allows no versions if the range doesn't allow it.
+    expect(v114.intersect(new VersionRange(min: v123, max: v124)).isEmpty,
+        isTrue);
+  });
+
+  test('isEmpty', () {
+    expect(v123.isEmpty, isFalse);
+  });
+
+  test('nextMajor', () {
+    expect(v123.nextMajor, equals(v200));
+    expect(v114.nextMajor, equals(v200));
+    expect(v200.nextMajor, equals(v300));
+
+    // Ignores pre-release if not on a major version.
+    expect(new Version.parse('1.2.3-dev').nextMajor, equals(v200));
+
+    // Just removes it if on a major version.
+    expect(new Version.parse('2.0.0-dev').nextMajor, equals(v200));
+
+    // Strips build suffix.
+    expect(new Version.parse('1.2.3+patch').nextMajor, equals(v200));
+  });
+
+  test('nextMinor', () {
+    expect(v123.nextMinor, equals(v130));
+    expect(v130.nextMinor, equals(v140));
+
+    // Ignores pre-release if not on a minor version.
+    expect(new Version.parse('1.2.3-dev').nextMinor, equals(v130));
+
+    // Just removes it if on a minor version.
+    expect(new Version.parse('1.3.0-dev').nextMinor, equals(v130));
+
+    // Strips build suffix.
+    expect(new Version.parse('1.2.3+patch').nextMinor, equals(v130));
+  });
+
+  test('nextPatch', () {
+    expect(v123.nextPatch, equals(v124));
+    expect(v200.nextPatch, equals(v201));
+
+    // Just removes pre-release version if present.
+    expect(new Version.parse('1.2.4-dev').nextPatch, equals(v124));
+
+    // Strips build suffix.
+    expect(new Version.parse('1.2.3+patch').nextPatch, equals(v124));
+  });
+
+  test('parse()', () {
+    expect(new Version.parse('0.0.0'), equals(new Version(0, 0, 0)));
+    expect(new Version.parse('12.34.56'), equals(new Version(12, 34, 56)));
+
+    expect(new Version.parse('1.2.3-alpha.1'),
+        equals(new Version(1, 2, 3, pre: 'alpha.1')));
+    expect(new Version.parse('1.2.3-x.7.z-92'),
+        equals(new Version(1, 2, 3, pre: 'x.7.z-92')));
+
+    expect(new Version.parse('1.2.3+build.1'),
+        equals(new Version(1, 2, 3, build: 'build.1')));
+    expect(new Version.parse('1.2.3+x.7.z-92'),
+        equals(new Version(1, 2, 3, build: 'x.7.z-92')));
+
+    expect(new Version.parse('1.0.0-rc-1+build-1'),
+        equals(new Version(1, 0, 0, pre: 'rc-1', build: 'build-1')));
+
+    expect(() => new Version.parse('1.0'), throwsFormatException);
+    expect(() => new Version.parse('1.2.3.4'), throwsFormatException);
+    expect(() => new Version.parse('1234'), throwsFormatException);
+    expect(() => new Version.parse('-2.3.4'), throwsFormatException);
+    expect(() => new Version.parse('1.3-pre'), throwsFormatException);
+    expect(() => new Version.parse('1.3+build'), throwsFormatException);
+    expect(() => new Version.parse('1.3+bu?!3ild'), throwsFormatException);
+  });
+
+  group('toString()', () {
+    test('returns the version string', () {
+      expect(new Version(0, 0, 0).toString(), equals('0.0.0'));
+      expect(new Version(12, 34, 56).toString(), equals('12.34.56'));
+
+      expect(new Version(1, 2, 3, pre: 'alpha.1').toString(),
+          equals('1.2.3-alpha.1'));
+      expect(new Version(1, 2, 3, pre: 'x.7.z-92').toString(),
+          equals('1.2.3-x.7.z-92'));
+
+      expect(new Version(1, 2, 3, build: 'build.1').toString(),
+          equals('1.2.3+build.1'));
+      expect(new Version(1, 2, 3, pre: 'pre', build: 'bui').toString(),
+          equals('1.2.3-pre+bui'));
+    });
+
+    test('preserves leading zeroes', () {
+      expect(new Version.parse('001.02.0003-01.dev+pre.002').toString(),
+          equals('001.02.0003-01.dev+pre.002'));
+    });
+  });
+}