public async Task<SemanticVersion> ParseAsync(string version) {
            version = version?.Trim();
            if (String.IsNullOrEmpty(version))
                return null;
            
            var cacheValue = await _localCache.GetAsync<SemanticVersion>(version).AnyContext();
            if (cacheValue.HasValue)
                return cacheValue.Value;
            
            int spaceIndex = version.IndexOf(" ", StringComparison.OrdinalIgnoreCase);
            if (spaceIndex > 0)
                version = version.Substring(0, spaceIndex).Trim();

            int wildCardIndex = version.IndexOf("*", StringComparison.OrdinalIgnoreCase);
            if (wildCardIndex > 0)
                version = version.Replace(".*", String.Empty).Replace("*", String.Empty);

            SemanticVersion semanticVersion = null;
            if (version.Length >= 5 && SemanticVersion.TryParse(version, out semanticVersion)) {
                await _localCache.SetAsync(version, semanticVersion).AnyContext();
                return semanticVersion;
            }
            
            Version v;
            int major;
            if (version.Length >= 3 && Version.TryParse(version, out v))
                semanticVersion = new SemanticVersion(v.Major > 0 ? v.Major : 0, v.Minor > 0 ? v.Minor : 0, v.Build > 0 ? v.Build : 0, v.Revision >= 0 ? new[] { v.Revision.ToString() } : Enumerable.Empty<string>());
            else if (Int32.TryParse(version, out major))
                semanticVersion = new SemanticVersion(major, 0);
            else
                _logger.Info("Unable to parse version: {version}", version);

            await _localCache.SetAsync(version, semanticVersion).AnyContext();
            return semanticVersion;
        }
Ejemplo n.º 2
0
 public static void MarkFixed(this Stack stack, SemanticVersion version = null)
 {
     stack.IsRegressed = false;
     stack.DateFixed = DateTime.UtcNow;
     stack.FixedInVersion = version != null ? version.ToString() : null;
 }
            /// <summary>
            /// <para>
            /// Exposes the formatter for <see cref="SemanticVersion"/>
            /// instances.
            /// </para>
            /// </summary>
            /// <param name="semver">
            /// The <see cref="SemanticVersion"/> to be formatted.
            /// </param>
            /// <param name="format">
            /// The format string specifying how the <see cref="SemanticVersion"/>
            /// should be formatted.
            /// </param>
            /// <returns></returns>
            public static string Format(SemanticVersion semver, string format)
            {
                // Null should be treated as equivalent to the default format.
                if (format == null)
                    format = SVF.Default;

                // Attempt to retrieve the formatter from our collection of
                // formatters and, if we can retrieve it, call it and return
                // what it produces.
                FmtRoutine fmt;
                if (Fmtrs.TryGetValue(format, out fmt))
                    return fmt(semver);

                // If we couldn't retrieve a formatter, throw an exception.
                throw new FormatException(
                    $@"Unrecognised format specifier ""{format}"".");
            }
            private static string Default(SemanticVersion semver)
            {
                var sb = new StringBuilder();

                // The three numeric version components are always 
                // present, so we can add them to the builder without
                // any checks.
                sb.Append($"{semver.Major}.{semver.Minor}.{semver.Patch}");

                // Pre-release identifiers always come before metadata,
                // but we need to make sure there are identifiers to add
                // first.
                if (semver.Identifiers.Any())
                {
                    // Identifiers are separated from the three-part 
                    // version by a hyphen character.
                    sb.Append('-');

                    // Identifiers are separated from each other by
                    // periods.
                    //
                    // We concatenate them in this way to avoid an
                    // extra step at the end. If we concatenated using
                    // the format string [$"{id}."], we'd need to get
                    // rid of an extra period at the end.
                    semver.Identifiers.Skip(1).Aggregate(
                        seed: sb.Append(semver.Identifiers.First()),
                        func: (bdr, id) => bdr.Append($".{id}"));
                }

                // Like with the pre-release identifiers, we want to make sure
                // there is metadata to add before we attempt to add it.
                if (semver.Metadata.Any())
                {
                    // Metadata is separated from the three-part version/pre-
                    // -release identifiers by a plus character.
                    sb.Append('+');

                    // Identifiers are separated from each other by
                    // periods.
                    //
                    // We concatenate them in this way to avoid an
                    // extra step at the end. If we concatenated using
                    // the format string [$"{id}."], we'd need to get
                    // rid of an extra period at the end.
                    semver.Metadata.Skip(1).Aggregate(
                        seed: sb.Append(semver.Metadata.First()),
                        func: (bdr, md) => bdr.Append($".{md}"));
                }

                return sb.ToString();
            }
            private static string Concise(SemanticVersion semver)
            {
                var sb = new StringBuilder();
                
                // Major-Minor is always included.
                sb.Append($"{semver.Major}.{semver.Minor}");

                // The patch version must be greater than zero
                // to be included.
                if (semver.Patch > 0)
                    sb.Append($".{semver.Patch}");

                // If there are any identifiers, include them in
                // the version.
                if (semver.Identifiers.Any())
                {
                    // Identifiers are separated from the maj/min
                    // by a hyphen.
                    sb.Append("-");

                    // Identifiers are separated from each other by
                    // periods.
                    //
                    // We concatenate them in this way to avoid an
                    // extra step at the end. If we concatenated using
                    // the format string [$"{id}."], we'd need to get
                    // rid of an extra period at the end.
                    semver.Identifiers.Skip(1).Aggregate(
                        seed: sb.Append(semver.Identifiers.First()),
                        func: (bdr, id) => bdr.Append($".{id}"));
                }

                return sb.ToString();
            }
        public void Equivalence()
        {
            // [EquivalentTo] ignores any information that isn't relevant to
            // determining the compatibility of two versions. Basically, it
            // ignores any metadata.

            var sv0 = new SemanticVersion(1, 7, 0);
            var sv1 = new SemanticVersion(1, 7, 0, new[] { "beta", "3" });
            var sv2 = new SemanticVersion(1, 7, 0, Empty<string>(),
                                                   new[] { "d116bf47" });
            var sv3 = new SemanticVersion(1, 7, 0, new[] { "beta", "3" },
                                                   new[] { "d116bf47" });

            Assert.IsTrue(sv0.EquivalentTo(sv2), "Equivalence check failed (0).");
            Assert.IsTrue(sv1.EquivalentTo(sv3), "Equivalence check failed (1).");

            Assert.IsFalse(sv0.EquivalentTo(sv1), "Equivalence incorrect (0).");
            Assert.IsFalse(sv1.EquivalentTo(sv2), "Equivalence incorrect (1).");
            Assert.IsFalse(sv2.EquivalentTo(sv3), "Equivalence incorrect (2).");

            // Now we want to make sure that equivalence is working in both
            // directs (e.g. A = B and B = A).
            Assert.IsTrue(sv0.EquivalentTo(sv2) && sv2.EquivalentTo(sv0),
                          "Equivalence not commutative (0).");
            Assert.IsTrue(sv1.EquivalentTo(sv3) && sv3.EquivalentTo(sv1),
                          "Equivalence not commutative (1).");

            // And we might as well check the same for inequivalence.
            Assert.IsTrue(!sv0.EquivalentTo(sv1) && !sv1.EquivalentTo(sv0),
                          "Inequivalence not commutative (0).");
            Assert.IsTrue(!sv1.EquivalentTo(sv2) && !sv2.EquivalentTo(sv1),
                          "Inequivalence not commutative (1).");
            Assert.IsTrue(!sv2.EquivalentTo(sv3) && !sv3.EquivalentTo(sv2),
                          "Inequivalence not commutative (2).");
        }
        public void Compatibility()
        {
            // Backwards-compatibility determination is not commutative,
            // so a few of these tests will just be reversed parameters.

            #region "Always False" checks (numbers 0 to 10)
            {
                // First thing we're going to do is a basic test of the "always
                // false" conditions listed in the remarks for [CompatibleWith].
                // These are:
                //
                //      - Major version of zero on either version
                //      - Different Major versions
                //      - Null [SemanticVersion] instance
                //
                // We can't really test here that these are *always* true, but
                // that doesn't mean we shouldn't try testing them anyway.
                var af_sv0 = new SemanticVersion(0, 5, 0);
                var af_sv1 = new SemanticVersion(0, 6, 0);
                var af_sv2 = new SemanticVersion(1, 2, 0);
                var af_sv3 = new SemanticVersion(2, 2, 0);
                var af_sv4 = (SemanticVersion)null;

                // Zero major versions
                Assert.IsFalse(af_sv0.CompatibleWith(af_sv1),
                               "Incorrect compatibility (0).");
                Assert.IsFalse(af_sv1.CompatibleWith(af_sv0),
                               "Incorrect compatibility (1).");
                Assert.IsFalse(af_sv1.CompatibleWith(af_sv2),
                               "Incorrect compatibility (2).");
                Assert.IsFalse(af_sv2.CompatibleWith(af_sv1),
                               "Incorrect compatibility (3).");
                // Zero major versions, equivalent versions
                Assert.IsTrue(af_sv1.CompatibleWith(af_sv1),
                               "Incorrect compatibility (4).");

                // Different major versions
                Assert.IsFalse(af_sv2.CompatibleWith(af_sv3),
                               "Incorrect compatibility (5).");
                Assert.IsFalse(af_sv3.CompatibleWith(af_sv2),
                               "Incorrect compatibility (6).");

                // Null version
                Assert.IsFalse(af_sv0.CompatibleWith(af_sv4),
                               "Incorrect compatibility (7).");
                Assert.IsFalse(af_sv1.CompatibleWith(af_sv4),
                               "Incorrect compatibility (8).");
                Assert.IsFalse(af_sv2.CompatibleWith(af_sv4),
                               "Incorrect compatibility (9).");
                Assert.IsFalse(af_sv3.CompatibleWith(af_sv4),
                               "Incorrect compatibility (10).");
            }
            #endregion
            #region Pre-release Versions (numbers 11 to 22)
            {
                // Pre-release versions are a bit more difficult with
                // their compatibility. See [CompatibleWith] remarks
                // and code comments for more information.

                // No pre-release identifiers for the "control" comparison.
                //
                // This should be compatible with [pr_sv3] and [pr_sv4] as
                // they are both pre-release versions of versions later than
                // this one, so they should (for standards compliance) implement
                // the APIs this depends on.
                var pr_sv0 = new SemanticVersion(1, 0, 0);

                // These two can't be compatible because they are equal in all
                // but pre-release identifiers.
                var pr_sv1 = new SemanticVersion(1, 0, 0, new[] { "rc", "1" });
                var pr_sv2 = new SemanticVersion(1, 0, 0, new[] { "rc", "2" });

                Assert.IsFalse(pr_sv0.CompatibleWith(pr_sv1),
                               "Incorrect compatibility (11).");
                Assert.IsFalse(pr_sv1.CompatibleWith(pr_sv0),
                               "Incorrect compatibility (12).");

                Assert.IsFalse(pr_sv0.CompatibleWith(pr_sv2),
                               "Incorrect compatibility (13).");
                Assert.IsFalse(pr_sv2.CompatibleWith(pr_sv0),
                               "Incorrect compatibility (14).");

                Assert.IsFalse(pr_sv1.CompatibleWith(pr_sv2),
                               "Incorrect compatibility (15).");
                Assert.IsFalse(pr_sv2.CompatibleWith(pr_sv1),
                               "Incorrect compatibility (16).");

                // These two can't be compatible because they are both pre-release
                // versions.
                var pr_sv3 = new SemanticVersion(1, 1, 0, new[] { "rc" });
                var pr_sv4 = new SemanticVersion(1, 2, 0, new[] { "rc" });

                Assert.IsFalse(pr_sv3.CompatibleWith(pr_sv4),
                               "Incorrect compatibility (17).");
                Assert.IsFalse(pr_sv4.CompatibleWith(pr_sv3),
                               "Incorrect compatibility (18).");

                Assert.IsTrue(pr_sv0.CompatibleWith(pr_sv3),
                               "Incorrect compatibility (19).");
                Assert.IsFalse(pr_sv3.CompatibleWith(pr_sv0),
                               "Incorrect compatibility (20).");

                Assert.IsTrue(pr_sv0.CompatibleWith(pr_sv4),
                               "Incorrect compatibility (21).");
                Assert.IsFalse(pr_sv4.CompatibleWith(pr_sv0),
                               "Incorrect compatibility (22).");

            }
            #endregion
            #region Regular checks (numbers 23 to 30)
            {
                var sv0 = new SemanticVersion(1, 0, 0);
                var sv1 = new SemanticVersion(1, 1, 0);

                Assert.IsTrue(sv0.CompatibleWith(sv1),
                              "Incorrect compatibility (23).");
                Assert.IsFalse(sv1.CompatibleWith(sv0),
                               "Incorrect compatibility (24).");

                var sv2 = new SemanticVersion(2, 0, 0);
                var sv3 = new SemanticVersion(2, 2, 0);

                Assert.IsTrue(sv2.CompatibleWith(sv3),
                              "Incorrect compatibility (25).");
                Assert.IsFalse(sv3.CompatibleWith(sv2),
                               "Incorrect compatibility (26).");

                Assert.IsFalse(sv0.CompatibleWith(sv2),
                               "Incorrect compatibility (27).");
                Assert.IsFalse(sv1.CompatibleWith(sv2),
                               "Incorrect compatibility (28).");

                Assert.IsFalse(sv0.CompatibleWith(sv3),
                               "Incorrect compatibility (29).");
                Assert.IsFalse(sv1.CompatibleWith(sv3),
                               "Incorrect compatibility (30).");
            }
            #endregion
        }
        public void Equality()
        {
            var sv0 = new SemanticVersion(1, 5, 0);
            var sv1 = new SemanticVersion(1, 5, 0, Empty<string>());
            var sv2 = new SemanticVersion(1, 5, 0, Empty<string>(),
                                                   Empty<string>());
            var sv3 = new SemanticVersion(1, 6, 0);
            var sv4 = new SemanticVersion(1, 6, 0, new[] { "rc", "1" });
            var sv5 = new SemanticVersion(1, 6, 0, Empty<string>(),
                                                   new[] { "d116bf47" });
            
            Assert.IsTrue(sv0 == sv1, "Equality check failed (0).");
            Assert.IsTrue(sv0 == sv2, "Equality check failed (1).");
            Assert.IsTrue(sv1 == sv2, "Equality check failed (2).");
            Assert.IsFalse(sv0 == sv3, "Equality check failed (3).");

            // Now we test the equality operators.
            //
            // Our [SemanticVersion] class is immutable, so it makes sense
            // to overload these to check value equality instead of reference
            // equality.
            Assert.IsTrue(sv0 == sv1, "Operator check failed (0).");
            Assert.IsTrue(sv0 == sv2, "Operator check failed (1).");
            Assert.IsTrue(sv1 == sv2, "Operator check failed (2).");
            Assert.IsTrue(sv0 != sv3, "Operator check failed (3).");

            // Now the tests mentioned in the guidelines for overloading
            // [Equals].
            Assert.IsTrue(sv0.Equals(sv1), "Guideline test failed (0).");
            Assert.IsTrue(sv1.Equals(sv0), "Guideline test failed (1).");
            Assert.IsTrue(sv1.Equals(sv0) && sv1.Equals(sv2) && sv0.Equals(sv2),
                          "Guideline test failed (2).");
            Assert.IsFalse(sv0.Equals(null), "Guideline test failed (3).");

            // The guideline tests again, but for our operators.
            Assert.IsTrue(sv0 == sv1, "Guideline test failed (4).");
            Assert.IsTrue(sv1 == sv0, "Guideline test failed (5).");
            Assert.IsTrue(sv1 == sv0 && sv1 == sv2 && sv0 == sv2,
                          "Guideline test failed (6).");
            Assert.IsFalse(sv0 == null, "Guideline test failed (7).");

            // Two null [SemanticVersion]s should be equal, so we're going to
            // test for that, too.
            SemanticVersion nsv0 = null, nsv1 = null;
            Assert.IsTrue(nsv0 == nsv1, "Null equality check failed (0).");

            // [Equals] and the equality operator must take into account metadata
            // and pre-release identifiers.
            Assert.AreNotEqual(sv3, sv4, "Inequality check failed (0).");
            Assert.AreNotEqual(sv3, sv5, "Inequality check failed (1).");
            Assert.AreNotEqual(sv4, sv5, "Inequality check failed (2).");

            Assert.IsFalse(sv3 == sv4, "Inequality check failed (3).");
            Assert.IsFalse(sv3 == sv5, "Inequality check failed (4).");
            Assert.IsFalse(sv4 == sv5, "Inequality check failed (5).");
        }
        public void Comparison()
        {
            Assert.IsTrue(typeof(IComparable<SemanticVersion>)
                            .IsAssignableFrom(typeof(SemanticVersion)),
                          "SemanticVersion is not IComparable<SemanticVersion>.");

            #region Definitions
            var cmp = new SemanticVersion[]
            {
                null,

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    new[] { "alpha" },
                                    metadata:       Empty<string>()),

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    new[] { "alpha", "1" },
                                    metadata:       Empty<string>()),

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    new[] { "alpha", "beta" },
                                    metadata:       Empty<string>()),

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    new[] { "beta" },
                                    metadata:       Empty<string>()),

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    new[] { "beta", "2" },
                                    metadata:       Empty<string>()),

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    new[] { "beta", "11" },
                                    metadata:       Empty<string>()),

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    new[] { "rc", "1" },
                                    metadata:       Empty<string>()),

                new SemanticVersion(major:          1,
                                    minor:          0,
                                    patch:          0,
                                    identifiers:    Empty<string>(),
                                    metadata:       Empty<string>()),
            };
            #endregion

            #region CompareTo
            // Test the [CompareTo] method.
            Assert.AreEqual(Ordering.Greater,
                            cmp[1].CompareTo<SemanticVersion>(cmp[0]),
                            "Comparison failed (0).");
            Assert.AreEqual(Ordering.Lesser,
                            cmp[1].CompareTo<SemanticVersion>(cmp[2]),
                            "Comparison failed (1).");
            Assert.AreEqual(Ordering.Lesser,
                            cmp[2].CompareTo<SemanticVersion>(cmp[3]),
                            "Comparison failed (2).");
            Assert.AreEqual(Ordering.Lesser,
                            cmp[3].CompareTo<SemanticVersion>(cmp[4]),
                            "Comparison failed (3).");
            Assert.AreEqual(Ordering.Lesser,
                            cmp[4].CompareTo<SemanticVersion>(cmp[5]),
                            "Comparison failed (4).");
            Assert.AreEqual(Ordering.Lesser,
                            cmp[5].CompareTo<SemanticVersion>(cmp[6]),
                            "Comparison failed (5).");
            Assert.AreEqual(Ordering.Lesser,
                            cmp[6].CompareTo<SemanticVersion>(cmp[7]),
                            "Comparison failed (6).");
            Assert.AreEqual(Ordering.Lesser,
                            cmp[7].CompareTo<SemanticVersion>(cmp[8]),
                            "Comparison failed (7).");

            // These tests are mentioned by the MSDN docs, so we're going to
            // use them here just to make sure everything is working fine.
            Assert.AreEqual(cmp[1].CompareTo(cmp[1]), 0,
                            "Comparison failed (9).");
            Assert.AreEqual(cmp[1].CompareTo(cmp[2]),
                            -cmp[2].CompareTo(cmp[1]),
                            "Comparison failed (10).");
            #endregion
            #region Sorting
            // To be extra sure, stick them in a collection, sort it, and
            // check the order they come out of the collection in. We jumble
            // up the items by ordering them using a newly-generated GUID.
            var sl = new List<SemanticVersion>(cmp.OrderBy(ks => Guid.NewGuid()));
            sl.Sort();

            // [cmp] is already in the correct lowest-to-highest order.
            Assert.IsTrue(sl.SequenceEqual(cmp), "Comparison failed (8).");
            #endregion
            #region Operators > and <
            // Now we have to do practically the same tests again, but this time
            // testing the comparison operators rather than the [CompareTo]
            // method.
            Assert.IsTrue(cmp[1] > cmp[0], "Operator comparison failed (0).");
            Assert.IsTrue(cmp[2] > cmp[1], "Operator comparison failed (1).");
            Assert.IsTrue(cmp[3] > cmp[2], "Operator comparison failed (2).");
            Assert.IsTrue(cmp[4] > cmp[3], "Operator comparison failed (3).");
            Assert.IsTrue(cmp[5] > cmp[4], "Operator comparison failed (4).");
            Assert.IsTrue(cmp[6] > cmp[5], "Operator comparison failed (5).");
            Assert.IsTrue(cmp[7] > cmp[6], "Operator comparison failed (6).");
            Assert.IsTrue(cmp[8] > cmp[7], "Operator comparison failed (7).");

            // Same tests, but with the other operator.
            Assert.IsTrue(cmp[0] < cmp[1], "Operator comparison failed (8).");
            Assert.IsTrue(cmp[1] < cmp[2], "Operator comparison failed (9).");
            Assert.IsTrue(cmp[2] < cmp[3], "Operator comparison failed (10).");
            Assert.IsTrue(cmp[3] < cmp[4], "Operator comparison failed (11).");
            Assert.IsTrue(cmp[4] < cmp[5], "Operator comparison failed (12).");
            Assert.IsTrue(cmp[5] < cmp[6], "Operator comparison failed (13).");
            Assert.IsTrue(cmp[6] < cmp[7], "Operator comparison failed (14).");
            Assert.IsTrue(cmp[7] < cmp[8], "Operator comparison failed (15).");

            // These are the ones mentioned by the MSDN docs.
            Assert.IsFalse(cmp[1] > cmp[1], "Operator comparison failed (16).");
            Assert.IsFalse(cmp[1] < cmp[1], "Operator comparison failed (17).");
            Assert.IsTrue((cmp[1] > cmp[2]) == !(cmp[2] > cmp[1]),
                          "Operator comparison failed (18).");
            Assert.IsTrue((cmp[1] < cmp[2]) == !(cmp[2] < cmp[1]),
                          "Operator comparison failed (19).");
            #endregion
            #region Operators >= and <=
            // We're also testing the [>=] and [<=] operators.
            Assert.IsTrue(cmp[8] >= cmp[8], "Operator comparison failed (20).");
            Assert.IsTrue(cmp[8] >= cmp[7], "Operator comparison failed (21).");
            Assert.IsTrue(cmp[8] >= cmp[6], "Operator comparison failed (22).");
            Assert.IsTrue(cmp[8] >= cmp[5], "Operator comparison failed (23).");
            Assert.IsTrue(cmp[8] >= cmp[4], "Operator comparison failed (24).");
            Assert.IsTrue(cmp[8] >= cmp[3], "Operator comparison failed (25).");
            Assert.IsTrue(cmp[8] >= cmp[2], "Operator comparison failed (26).");
            Assert.IsTrue(cmp[8] >= cmp[1], "Operator comparison failed (27).");
            Assert.IsTrue(cmp[8] >= cmp[0], "Operator comparison failed (28).");

            Assert.IsTrue(cmp[0] <= cmp[0], "Operator comparison failed (29).");
            Assert.IsTrue(cmp[0] <= cmp[1], "Operator comparison failed (30).");
            Assert.IsTrue(cmp[0] <= cmp[2], "Operator comparison failed (31).");
            Assert.IsTrue(cmp[0] <= cmp[3], "Operator comparison failed (32).");
            Assert.IsTrue(cmp[0] <= cmp[4], "Operator comparison failed (33).");
            Assert.IsTrue(cmp[0] <= cmp[5], "Operator comparison failed (34).");
            Assert.IsTrue(cmp[0] <= cmp[6], "Operator comparison failed (35).");
            Assert.IsTrue(cmp[0] <= cmp[7], "Operator comparison failed (36).");
            Assert.IsTrue(cmp[0] <= cmp[8], "Operator comparison failed (37).");

            Assert.IsFalse(cmp[1] <= cmp[0], "Operator comparison failed (38).");
            Assert.IsFalse(cmp[0] >= cmp[1], "Operator comparison failed (39).");
            #endregion
        }
        public void ConstructingValid()
        {
            // These tests are fairly simple, we just test each constructor
            // available with something we know is valid and test the values
            // of the [SemanticVersion]'s properties afterwards.

            #region (int, int) 0-4
            {
                var sv = new SemanticVersion(1, 6);

                Assert.AreEqual(1, sv.Major, "Incorrect initialisation (0).");
                Assert.AreEqual(6, sv.Minor, "Incorrect initialisation (1).");
                Assert.AreEqual(0, sv.Patch, "Incorrect initialisation (2).");

                Assert.IsTrue(sv.Identifiers.SequenceEqual(Empty<string>()),
                              "Incorrect initialisation (3).");
                Assert.IsTrue(sv.Metadata.SequenceEqual(Empty<string>()),
                              "Incorrect initialisation (4).");
            }
            #endregion
            #region (int, int, int) 5-9
            {
                var sv = new SemanticVersion(1, 2, 8);
                
                Assert.AreEqual(1, sv.Major, "Incorrect initialisation (5).");
                Assert.AreEqual(2, sv.Minor, "Incorrect initialisation (6).");
                Assert.AreEqual(8, sv.Patch, "Incorrect initialisation (7).");

                Assert.IsTrue(sv.Identifiers.SequenceEqual(Empty<string>()),
                              "Incorrect initialisation (8).");
                Assert.IsTrue(sv.Metadata.SequenceEqual(Empty<string>()),
                              "Incorrect initialisation (9).");
            }
            #endregion
            #region (int, int, int, IEnumerable<string> 10-14
            {
                var sv = new SemanticVersion(2, 5, 6, new[] { "rc" });
                
                Assert.AreEqual(2, sv.Major, "Incorrect initialisation (10).");
                Assert.AreEqual(5, sv.Minor, "Incorrect initialisation (11).");
                Assert.AreEqual(6, sv.Patch, "Incorrect initialisation (12).");

                Assert.IsTrue(sv.Identifiers.SequenceEqual(new[] { "rc" }),
                              "Incorrect initialisation (13).");
                Assert.IsTrue(sv.Metadata.SequenceEqual(Empty<string>()),
                              "Incorrect initialisation (14).");
            }
            #endregion
            #region (int, int, int, IEnumerable<string>, IEnumerable<string>)
            {
                var sv = new SemanticVersion(5, 1, 2, new[] { "rc" }, 
                                                      new[] { "2015" });
                
                Assert.AreEqual(5, sv.Major, "Incorrect initialisation (15).");
                Assert.AreEqual(1, sv.Minor, "Incorrect initialisation (16).");
                Assert.AreEqual(2, sv.Patch, "Incorrect initialisation (17).");

                Assert.IsTrue(sv.Identifiers.SequenceEqual(new[] { "rc" }),
                              "Incorrect initialisation (18).");
                Assert.IsTrue(sv.Metadata.SequenceEqual(new[] { "2015" }),
                              "Incorrect initialisation (19).");
            }
            #endregion
        }        
        public void Stringifying()
        {
            var sv0 = new SemanticVersion(1, 0, 0);
            var sv1 = new SemanticVersion(1, 1, 1);
            var sv2 = new SemanticVersion(1, 2, 0, new[] { "rc", "1" });
            var sv3 = new SemanticVersion(1, 2, 1, new[] { "rc", "1" });
            var sv4 = new SemanticVersion(1, 3, 0, new[] { "rc", "1" },
                                                   new[] { "20150925", "1" });
            var sv5 = new SemanticVersion(1, 3, 1, new[] { "rc", "1" },
                                                   new[] { "20150925", "1" });

            #region ToString(void)
            // First, we're going to test the "standard" [ToString]
            // method accepting no arguments. This should produce
            // strings formatted as given in the Semantic Versioning
            // spec.
            Assert.AreEqual("1.0.0", sv0.ToString(), 
                            "ToString() failure (0).");
            Assert.AreEqual("1.1.1", sv1.ToString(), 
                            "ToString() failure (1).");
            Assert.AreEqual("1.2.0-rc.1", sv2.ToString(), 
                            "ToString() failure (2).");
            Assert.AreEqual("1.2.1-rc.1", sv3.ToString(), 
                            "ToString() failure (3).");
            Assert.AreEqual("1.3.0-rc.1+20150925.1", sv4.ToString(), 
                            "ToString() failure (4).");
            Assert.AreEqual("1.3.1-rc.1+20150925.1", sv5.ToString(), 
                            "ToString() failure (5).");
            #endregion
            #region ToString(string, IFormatProvider) + ToString(string)
            // Next, we're going to test the result of each format specifier
            // given to the [IFormattable] implementation of [ToString].
            //
            // To do this, we need to cast our [SemanticVersion] instances
            // to [IFormattable] instances, because this [ToString] overload
            // is implemented with an explicit interface implementation.
            var if0 = (IFormattable)sv0;
            var if1 = (IFormattable)sv1;
            var if2 = (IFormattable)sv2;
            var if3 = (IFormattable)sv3;
            var if4 = (IFormattable)sv4;
            var if5 = (IFormattable)sv5;

            #region Format Specifier: "G"
            // First up is the "G" specifier, which should produce the same
            // result as normal [ToString].
            Assert.AreEqual(sv0.ToString(), if0.ToString("G", null), 
                            "Format specifier 'G' failure (0).");
            Assert.AreEqual(sv1.ToString(), if1.ToString("G", null),
                            "Format specifier 'G' failure (1).");
            Assert.AreEqual(sv2.ToString(), if2.ToString("G", null),
                            "Format specifier 'G' failure (2).");
            Assert.AreEqual(sv3.ToString(), if3.ToString("G", null),
                            "Format specifier 'G' failure (3).");
            Assert.AreEqual(sv4.ToString(), if4.ToString("G", null),
                            "Format specifier 'G' failure (4).");
            Assert.AreEqual(sv5.ToString(), if5.ToString("G", null),
                            "Format specifier 'G' failure (5).");
            // We're also going to test the overload that doesn't take an
            // [IFormatProvider] to make sure it produces the same result.
            Assert.AreEqual(sv0.ToString("G"), if0.ToString("G", null), 
                            "Format specifier 'G' failure (6).");
            Assert.AreEqual(sv1.ToString("G"), if1.ToString("G", null),
                            "Format specifier 'G' failure (7).");
            Assert.AreEqual(sv2.ToString("G"), if2.ToString("G", null),
                            "Format specifier 'G' failure (8).");
            Assert.AreEqual(sv3.ToString("G"), if3.ToString("G", null),
                            "Format specifier 'G' failure (9).");
            Assert.AreEqual(sv4.ToString("G"), if4.ToString("G", null),
                            "Format specifier 'G' failure (10).");
            Assert.AreEqual(sv5.ToString("G"), if5.ToString("G", null),
                            "Format specifier 'G' failure (11).");
            #endregion
            #region Format Specifier: "g"
            // The "g" specifier is like "G", but the output is prefixed
            // with a "v".
            Assert.AreEqual("v" + sv0.ToString(), if0.ToString("g", null),
                            "Format specifier 'g' failure (0).");
            Assert.AreEqual("v" + sv1.ToString(), if1.ToString("g", null),
                            "Format specifier 'g' failure (1).");
            Assert.AreEqual("v" + sv2.ToString(), if2.ToString("g", null),
                            "Format specifier 'g' failure (2).");
            Assert.AreEqual("v" + sv3.ToString(), if3.ToString("g", null),
                            "Format specifier 'g' failure (3).");
            Assert.AreEqual("v" + sv4.ToString(), if4.ToString("g", null),
                            "Format specifier 'g' failure (4).");
            Assert.AreEqual("v" + sv5.ToString(), if5.ToString("g", null),
                            "Format specifier 'g' failure (5).");
            
            Assert.AreEqual(sv0.ToString("g"), if0.ToString("g", null),
                            "Format specifier 'g' failure (6).");
            Assert.AreEqual(sv1.ToString("g"), if1.ToString("g", null),
                            "Format specifier 'g' failure (7).");
            Assert.AreEqual(sv2.ToString("g"), if2.ToString("g", null),
                            "Format specifier 'g' failure (8).");
            Assert.AreEqual(sv3.ToString("g"), if3.ToString("g", null),
                            "Format specifier 'g' failure (9).");
            Assert.AreEqual(sv4.ToString("g"), if4.ToString("g", null),
                            "Format specifier 'g' failure (10).");
            Assert.AreEqual(sv5.ToString("g"), if5.ToString("g", null),
                            "Format specifier 'g' failure (11).");
            #endregion
            #region Format Specifier: null
            // The null specifier must produce the same result as "G".
            Assert.AreEqual(sv0.ToString(), if0.ToString(null, null),
                            "Format specifier null failure (0).");
            Assert.AreEqual(sv1.ToString(), if1.ToString(null, null),
                            "Format specifier null failure (1).");
            Assert.AreEqual(sv2.ToString(), if2.ToString(null, null),
                            "Format specifier null failure (2).");
            Assert.AreEqual(sv3.ToString(), if3.ToString(null, null),
                            "Format specifier null failure (3).");
            Assert.AreEqual(sv4.ToString(), if4.ToString(null, null),
                            "Format specifier null failure (4).");
            Assert.AreEqual(sv5.ToString(), if5.ToString(null, null),
                            "Format specifier null failure (5).");

            Assert.AreEqual(sv0.ToString(null), if0.ToString(null, null),
                            "Format specifier null failure (6).");
            Assert.AreEqual(sv1.ToString(null), if1.ToString(null, null),
                            "Format specifier null failure (7).");
            Assert.AreEqual(sv2.ToString(null), if2.ToString(null, null),
                            "Format specifier null failure (8).");
            Assert.AreEqual(sv3.ToString(null), if3.ToString(null, null),
                            "Format specifier null failure (9).");
            Assert.AreEqual(sv4.ToString(null), if4.ToString(null, null),
                            "Format specifier null failure (10).");
            Assert.AreEqual(sv5.ToString(null), if5.ToString(null, null),
                            "Format specifier null failure (11).");
            #endregion
            #region Format Specifier: "C"
            // The "C" specifier gives us the concise format, where some
            // information may be omitted.
            Assert.AreEqual("1.0", if0.ToString("C", null),
                            "Format specifier 'C' failure (0).");
            Assert.AreEqual("1.1.1", if1.ToString("C", null),
                            "Format specifier 'C' failure (1).");
            Assert.AreEqual("1.2-rc.1", if2.ToString("C", null),
                            "Format specifier 'C' failure (2).");
            Assert.AreEqual("1.2.1-rc.1", if3.ToString("C", null),
                            "Format specifier 'C' failure (3).");
            Assert.AreEqual("1.3-rc.1", if4.ToString("C", null),
                            "Format specifier 'C' failure (4).");
            Assert.AreEqual("1.3.1-rc.1", if5.ToString("C", null),
                            "Format specifier 'C' failure (5).");

            Assert.AreEqual(sv0.ToString("C"), if0.ToString("C", null),
                            "Format specifier 'C' failure (6).");
            Assert.AreEqual(sv1.ToString("C"), if1.ToString("C", null),
                            "Format specifier 'C' failure (7).");
            Assert.AreEqual(sv2.ToString("C"), if2.ToString("C", null),
                            "Format specifier 'C' failure (8).");
            Assert.AreEqual(sv3.ToString("C"), if3.ToString("C", null),
                            "Format specifier 'C' failure (9).");
            Assert.AreEqual(sv4.ToString("C"), if4.ToString("C", null),
                            "Format specifier 'C' failure (10).");
            Assert.AreEqual(sv5.ToString("C"), if5.ToString("C", null),
                            "Format specifier 'C' failure (11).");
            #endregion
            #region Format Specifier: "c"
            // The "c" specifier gives us the same output as "C", but
            // prefixed with the letter "v".
            Assert.AreEqual("v1.0", if0.ToString("c", null),
                            "Format specifier 'c' failure (0).");
            Assert.AreEqual("v1.1.1", if1.ToString("c", null),
                            "Format specifier 'c' failure (1).");
            Assert.AreEqual("v1.2-rc.1", if2.ToString("c", null),
                            "Format specifier 'c' failure (2).");
            Assert.AreEqual("v1.2.1-rc.1", if3.ToString("c", null),
                            "Format specifier 'c' failure (3).");
            Assert.AreEqual("v1.3-rc.1", if4.ToString("c", null),
                            "Format specifier 'c' failure (4).");
            Assert.AreEqual("v1.3.1-rc.1", if5.ToString("c", null),
                            "Format specifier 'c' failure (5).");

            Assert.AreEqual(sv0.ToString("c"), if0.ToString("c", null),
                            "Format specifier 'c' failure (6).");
            Assert.AreEqual(sv1.ToString("c"), if1.ToString("c", null),
                            "Format specifier 'c' failure (7).");
            Assert.AreEqual(sv2.ToString("c"), if2.ToString("c", null),
                            "Format specifier 'c' failure (8).");
            Assert.AreEqual(sv3.ToString("c"), if3.ToString("c", null),
                            "Format specifier 'c' failure (9).");
            Assert.AreEqual(sv4.ToString("c"), if4.ToString("c", null),
                            "Format specifier 'c' failure (10).");
            Assert.AreEqual(sv5.ToString("c"), if5.ToString("c", null),
                            "Format specifier 'c' failure (11).");
            #endregion

            #endregion

            // New tests will need to be added here for any new format specifiers.
        }
            /// <summary>
            /// <para>
            /// Exposes the formatter for <see cref="SemanticVersion"/>
            /// instances.
            /// </para>
            /// </summary>
            /// <param name="semver">
            /// The <see cref="SemanticVersion"/> to be formatted.
            /// </param>
            /// <param name="format">
            /// The format string specifying how the <see cref="SemanticVersion"/>
            /// should be formatted.
            /// </param>
            /// <returns></returns>
            public static string Format(SemanticVersion semver, string format)
            {
                var sb = new StringBuilder();


                // We'll probably be passed the general format specifier most of
                // the time, so we can return that format without getting into
                // parsing the format string.
                if (String.IsNullOrEmpty(format) || format == SemanticVersionFormat.Default)
                {
                    // The basics are always present
                    sb.AppendFormat("{0}.{1}.{2}", semver.Major, semver.Minor, semver.Patch);

                    // If there are pre-release identifiers, they're next
                    if (semver.Identifiers.Count > 0)
                    {
                        sb.AppendFormat("-{0}", semver.Identifiers[0]);

                        for (int i = 1; i < semver.Identifiers.Count; i++)
                        {
                            sb.AppendFormat(".{0}", semver.Identifiers[i]);
                        }
                    }

                    // And the same with metadata items
                    if (semver.Metadata.Count > 0)
                    {
                        sb.AppendFormat("+{0}", semver.Metadata[0]);

                        for (int i = 1; i < semver.Metadata.Count; i++)
                        {
                            sb.AppendFormat(".{0}", semver.Metadata[i]);
                        }
                    }

                    return(sb.ToString());
                }


                IEnumerator <char> iter = null;
                char?current            = null;
                int  pos = -1;

                // If it isn't the general format specifier, we need to interpret
                // the pattern and build the string as we go.
                RecurseOver(format);


                return(sb.ToString());


                // Recursively parses the custom format pattern, adding to the
                // contents of the [StringBuilder] as it proceeds.
                void RecurseOver(string str)
                {
                    var storedIter    = iter;
                    var storedCurrent = current;
                    var storedPos     = pos;

                    iter = str.GetEnumerator();
                    iter.MoveNext();

                    current = iter.Current;

                    while (current.HasValue)
                    {
                        Evaluate();
                    }

                    iter    = storedIter;
                    current = storedCurrent;
                    pos     = storedPos;
                }

                // Evaluates the current input character
                void Evaluate()
                {
                    // 'M' is for the major version component
                    if (Take('M'))
                    {
                        Major();
                    }

                    // 'm' is for the minor version component
                    else if (Take('m'))
                    {
                        Minor();
                    }

                    // 'p' is for the patch version component, however...
                    else if (Take('p'))
                    {
                        // If we have 'pp', we only include the patch component
                        // in the formatted string if it's non-zero.
                        if (Take('p'))
                        {
                            if (semver.Patch != 0)
                            {
                                sb.Append('.');
                                Patch();
                            }
                        }
                        // But, if we have just 'p', we always include it.
                        else
                        {
                            Patch();
                        }
                    }

                    // 'R' is for standalone identifiers, but 'RR' means we
                    // have to prefix them with the hyphen separator.
                    else if (Take('R'))
                    {
                        // If we have identifiers, they'll be included.
                        if (semver.Identifiers.Count > 0)
                        {
                            // If the specifier is 'RR', we have to include the
                            // hyphen separator before the identifiers.
                            if (Take('R'))
                            {
                                sb.Append('-');
                            }

                            Identifiers();
                        }
                        // If we don't have identifiers, we consume a following 'R'
                        // if it's present and proceed without including anything.
                        else
                        {
                            Take('R');
                        }
                    }

                    // A character 'r' can either be the first part of an indexed
                    // pre-release identifier specifier 'r#', or the first part of
                    // the reserved specifier 'rr'.
                    else if (Take('r'))
                    {
                        if (Take('r'))
                        {
                            Error(@"Format specifier ""rr"" is reserved and must not be used.");
                        }

                        else
                        {
                            var intBdr = new StringBuilder();

                            // A single 'r' followed by numbers is an index into
                            // the pre-release identifiers, which we have to turn
                            // into a number and use to find the correct identifier.
                            if (TakeNumerals(intBdr))
                            {
                                // If we can parse it, then we include the identifier
                                // at the specified index.
                                if (Int32.TryParse(intBdr.ToString(), out var idx))
                                {
                                    if (idx >= semver.Identifiers.Count)
                                    {
                                        Error("Pre-release identifier index out of bounds.");
                                    }

                                    sb.Append(semver.Identifiers[idx]);
                                }
                                else
                                {
                                    Error("Could not convert index to Int32.");
                                }
                            }
                            // If the single 'r' isn't followed by numbers, then we
                            // store it and whatever came after it.
                            else
                            {
                                sb.Append('r');
                                Store();
                            }
                        }
                    }

                    // The 'D' and 'DD' specifiers are for standalone and prefixed
                    // metadata item groups, like with 'R' and 'RR'.
                    else if (Take('D'))
                    {
                        // As with 'R' and 'RR', we include nothing if there are
                        // no metadata items.
                        if (semver.Metadata.Count > 0)
                        {
                            // Including the separator for 'DD'.
                            if (Take('D'))
                            {
                                sb.Append('+');
                            }

                            Metadata();
                        }
                        // And consuming the second 'D' without doing anything
                        // if we don't have any metadata items.
                        else
                        {
                            Take('D');
                        }
                    }

                    // As with 'r', 'd' can be followed by a number or another 'd',
                    // and it's handled in exactly the same way.
                    else if (Take('d'))
                    {
                        if (Take('d'))
                        {
                            Error(@"Format specifier ""dd"" is reserved and must not be used.");
                        }

                        else
                        {
                            var intBdr = new StringBuilder();

                            if (TakeNumerals(intBdr))
                            {
                                if (Int32.TryParse(intBdr.ToString(), out var idx))
                                {
                                    if (idx >= semver.Metadata.Count)
                                    {
                                        Error("Metadata item index index out of bounds.");
                                    }

                                    sb.Append(semver.Metadata[idx]);
                                }
                                else
                                {
                                    Error("Could not convert index to Int32.");
                                }
                            }
                            else
                            {
                                sb.Append('d');
                                Store();
                            }
                        }
                    }

                    // The 'G' specifier expands to the general format
                    else if (Take('G'))
                    {
                        RecurseOver(FMT_GENERAL);
                    }

                    // While 'g' is the same, but prefixed.
                    else if (Take('g'))
                    {
                        RecurseOver(FMT_GENERAL_PREFIX);
                    }

                    // The 'C' specifier is for the concise format, where patch
                    // and metadata components can be excluded.
                    else if (Take('C'))
                    {
                        RecurseOver(FMT_CONCISE);
                    }

                    // And, as above, 'c' is the prefixed form of 'C'.
                    else if (Take('c'))
                    {
                        RecurseOver(FMT_CONCISE_PREFIX);
                    }

                    // To aid in formatting, we allow our caller to specify that
                    // portions of the custom format pattern should be included
                    // verbatim by surrounding them by double braces.
                    else if (Take('{'))
                    {
                        // If we get two braces, then we need to continue until
                        // we find the two closing braces.
                        if (Take('{'))
                        {
                            TakeUntilBraces();


                            void TakeUntilBraces()
                            {
                                // If we reach the end of the string without a closing
                                // brace pair, that's an error.
                                if (!current.HasValue)
                                {
                                    Error("Verbatim block not terminated.");
                                }

                                // If we find a single closing brace, we need
                                // to check for a second.
                                if (Take('}'))
                                {
                                    // If we don't get the second brace, we need
                                    // to store this character and try again.
                                    if (!Take('}'))
                                    {
                                        Store();
                                        TakeUntilBraces();
                                    }

                                    // Otherwise, store and recurse.
                                }

                                // If it's something else, then we'll store it verbatim
                                // and continue looking.
                                else
                                {
                                    Store();
                                    TakeUntilBraces();
                                }
                            }
                        }
                        // Otherwise, we store a single brace and move on.
                        else
                        {
                            sb.Append('{');
                        }
                    }

                    // If we don't have any particular logic for this character,
                    // include it in the final string as-is.
                    else
                    {
                        Store();
                    }
                }

                // Consumes an input character if it equals the argument, returning
                // true when a character has been consumed.
                bool Take(char c)
                {
                    if (current == c)
                    {
                        Consume();

                        return(true);
                    }
                    else
                    {
                        return(false);
                    }
                }

                // Unconditionally consumes an input character and stores it in
                // the [StringBuilder].
                void Store()
                {
                    sb.Append(current.Value);
                    Consume();
                }

                // Unconditionally consumes an input character.
                void Consume()
                {
                    current = iter.MoveNext() ? iter.Current : default(char?);
                    pos++;
                }

                // Produces an error with the specified message.
                void Error(string message)
                {
                    throw new FormatException(
                              $"Invalid format string (at position {pos}): {message}"
                              );
                }

                // Consumes input characters while they are numeric, storing them
                // in the provided [StringBuilder]. Returns true if any characters
                // were consumed.
                bool TakeNumerals(StringBuilder builder)
                {
                    // If we reach the end of the input, our success depends on
                    // whether we consumed any input.
                    if (!current.HasValue)
                    {
                        return(builder.Length > 0);
                    }

                    // If this isn't the end, we continue while we're consuming
                    // numeric characters only.
                    else if ('0' <= current.Value && current.Value <= '9')
                    {
                        builder.Append(current.Value);
                        Consume();

                        TakeNumerals(builder);

                        return(true);
                    }

                    // And, as soon as we reach a non-numeric, we end.
                    else
                    {
                        return(false);
                    }
                }

                void Major() => sb.Append(semver.Major);
                void Minor() => sb.Append(semver.Minor);
                void Patch() => sb.Append(semver.Patch);

                void Identifiers()
                {
                    // The custom format pattern parser should ensure we never
                    // come here when it would be invalid.

                    sb.Append(semver.Identifiers[0]);

                    foreach (var id in semver.Identifiers.Skip(1))
                    {
                        sb.AppendFormat(".{0}", id);
                    }
                }

                void Metadata()
                {
                    sb.Append(semver.Metadata[0]);

                    foreach (var md in semver.Metadata.Skip(1))
                    {
                        sb.AppendFormat(".{0}", md);
                    }
                }
            }