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; }
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); } } }