Skip to content

Commit

Permalink
Merge pull request #72 from OctopusDeploy/tl/add-unsortable-version-type
Browse files Browse the repository at this point in the history
Add LexicographicSortedVersion
  • Loading branch information
tleed5 authored Jan 15, 2024
2 parents 4fcaa4f + 48638fd commit 08377a1
Show file tree
Hide file tree
Showing 6 changed files with 360 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using NUnit.Framework;
using Octopus.Versioning.Lexicographic;

namespace Octopus.Versioning.Tests.LexicographicSortedVersion;

public class LexicographicSortedVersionCompareTests
{
static readonly LexicographicSortedVersionParser LexicographicSortedVersionParser = new();

[Test]
[TestCase("release", "release", 0)]
[TestCase("release", "qrelease", 1)]
[TestCase("release", "srelease", -1)]
[TestCase("123", "123", 0)]
[TestCase("123", "100", 1)]
[TestCase("123", "321", -1)]
[TestCase("123Release", "123Release", 0)]
[TestCase("123Release", "100Release", 1)]
[TestCase("123Release", "321Release", -1)]
[TestCase("release-1", "release-1", 0)]
[TestCase("release-1", "release-0", 1)]
[TestCase("release-1", "release-2", -1)]
[TestCase("release.1", "release.1", 0)]
[TestCase("release.1", "release.0", 1)]
[TestCase("release.1", "release.2", -1)]
[TestCase("release_1", "release_1", 0)]
[TestCase("release_1", "release_0", 1)]
[TestCase("release_1", "release_2", -1)]
[TestCase("release-1", "release_1", 0)]
[TestCase("release.1", "release_1", 0)]
[TestCase("release-1", "release_0", 1)]
[TestCase("release.1", "release_2", -1)]
[TestCase("release+123", "release+321", 0)]
[TestCase("release-1+123", "release-1+321", 0)]
[TestCase("release-1+123", "release-0+321", 1)]
[TestCase("release-1+123", "release-2+321", -1)]
public void TestComparisons(string version1, string version2, int result)
{
var parsedVersion1 = LexicographicSortedVersionParser.Parse(version1);
var parsedVersion2 = LexicographicSortedVersionParser.Parse(version2);
Assert.AreEqual(result, parsedVersion1.CompareTo(parsedVersion2));
}

[Test]
[TestCase("release-1", "release-1", true)]
[TestCase("release-1", "release-2", false)]
public void TestEquality(string version1, string version2, bool result)
{
var parsedVersion1 = LexicographicSortedVersionParser.Parse(version1);
var parsedVersion2 = LexicographicSortedVersionParser.Parse(version2);
Assert.AreEqual(result, Equals(parsedVersion1, parsedVersion2));
}

[Test]
public void TestGetHashCode()
{
var versionString = "release-1";
var parsedVersion = LexicographicSortedVersionParser.Parse(versionString);

Assert.AreEqual(versionString.GetHashCode(), parsedVersion.GetHashCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using NUnit.Framework;
using Octopus.Versioning.Lexicographic;

namespace Octopus.Versioning.Tests.LexicographicSortedVersion;

[TestFixture]
public class LexicographicSortedVersionParserTests
{
[Test]
// Release
[TestCase("foobar", "foobar", "")]
[TestCase("2db4a87840113c", "2db4a87840113c", "")]
[TestCase("123456", "123456", "")]
[TestCase("foobar-qwerty", "foobar-qwerty", "")]
[TestCase("foobar.qwerty", "foobar.qwerty", "")]
[TestCase("foobar_qwerty", "foobar_qwerty", "")]
[TestCase("foobar-12345", "foobar-12345", "")]
// Metadata
[TestCase("foobar+12345", "foobar", "12345")]
[TestCase("foobar+123.456", "foobar", "123.456")]
[TestCase("foobar+123_456", "foobar", "123_456")]
[TestCase("foobar+123-456", "foobar", "123-456")]
[TestCase("foobar+123+456", "foobar", "123+456")]
[TestCase("foobar+1.2_3-4+5", "foobar", "1.2_3-4+5")]
[TestCase("foobar+qwerty", "foobar", "qwerty")]
[TestCase("foobar-qwerty+12345", "foobar-qwerty", "12345")]
// Fail Cases
[TestCase("!@#$%^", "", "")]
[TestCase("foobar-!@#$%", "", "")]
[TestCase("foobar-qwerty+!@#$%", "", "")]
[TestCase("foo bar", "", "")]
[TestCase("foobar-qwe ty", "", "")]
[TestCase("foobar+123 456", "", "")]
[TestCase("foo bar-qwe ty+123 456", "", "")]
[TestCase("!foobar", "", "")]
[TestCase("foo!bar", "", "")]
[TestCase("foobar!", "", "")]
public void ShouldParseSuccessfully(string input, string expectedRelease, string expectedMetadata)
{
_ = new LexicographicSortedVersionParser().TryParse(input, out var parsedVersion);
AssertVersionNumbersAreZero(parsedVersion);
Assert.AreEqual(expectedRelease, parsedVersion.Release);
Assert.AreEqual(expectedMetadata, parsedVersion.Metadata);
}

[Test]
public void ShouldThrowExceptionOnEmptyInput()
{
var input = "";
Assert.Catch<ArgumentException>(() => new LexicographicSortedVersionParser().Parse(input));
}

[Test]
public void ShouldThrowExceptionOnWhiteSpaceInput()
{
var input = " ";
Assert.Catch<ArgumentException>(() => new LexicographicSortedVersionParser().Parse(input));
}

[Test]
public void ShouldThrowExceptionOnNullInput()
{
Assert.Catch<ArgumentException>(() => new LexicographicSortedVersionParser().Parse(null));
}

[Test]
public void ShouldThrowExceptionOnFailureToParse()
{
var input = "bad versions string";
Assert.Catch<ArgumentException>(() => new LexicographicSortedVersionParser().Parse(input));
}

void AssertVersionNumbersAreZero(Lexicographic.LexicographicSortedVersion version)
{
Assert.AreEqual(0, version.Major);
Assert.AreEqual(0, version.Minor);
Assert.AreEqual(0, version.Patch);
Assert.AreEqual(0, version.Revision);
}
}
136 changes: 136 additions & 0 deletions source/Octopus.Versioning/Lexicographic/LexicographicSortedVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Octopus.Versioning.Lexicographic
{
public class LexicographicSortedVersion: IVersion
{
public LexicographicSortedVersion(string release, string? metadata, string? originalString)
{
Metadata = metadata;
Release = release;
OriginalString = originalString ?? string.Empty;
}

public int Major => 0;
public int Minor => 0;
public int Patch => 0;
public int Revision => 0;
public bool IsPrerelease => false;
public IEnumerable<string> ReleaseLabels => Enumerable.Empty<string>();
public string? Metadata { get; }
public bool HasMetadata => !string.IsNullOrWhiteSpace(Metadata);
public string Release { get; }
public string OriginalString { get; }

public VersionFormat Format => VersionFormat.Lexicographic;

public int CompareTo(object obj)
{
if (!(obj is IVersion objVersion))
return -1;

if (string.Compare(Release.AlphaNumericOnly(), (objVersion.Release ?? string.Empty).AlphaNumericOnly(), StringComparison.Ordinal) != 0)
return CompareReleaseLabels(Release.AlphaNumericOnly().Split('.', '-', '_'), (objVersion.Release ?? string.Empty).AlphaNumericOnly().Split('.', '-', '_'));

return 0;
}

public override string ToString()
{
return OriginalString;
}

public override bool Equals(object obj)
{
if (obj is IVersion objVersion)
return CompareTo(objVersion) == 0;

return false;
}

public override int GetHashCode()
{
return Release.GetHashCode();
}

/// <summary>
/// Compares sets of release labels.
/// </summary>
static int CompareReleaseLabels(IEnumerable<string> version1, IEnumerable<string> version2)
{
var result = 0;

using var a = version1.GetEnumerator();
using var b = version2.GetEnumerator();

var aExists = a.MoveNext();
var bExists = b.MoveNext();

while (aExists || bExists)
{
if (!aExists && bExists)
return -1;

if (aExists && !bExists)
return 1;

// compare the labels
result = CompareRelease(a.Current, b.Current);

if (result != 0)
return result;

aExists = a.MoveNext();
bExists = b.MoveNext();
}

return result;
}

/// <summary>
/// Release labels are compared as numbers if they are numeric, otherwise they will be compared
/// as strings.
/// </summary>
static int CompareRelease(string version1, string version2)
{
var version1Num = 0;
var version2Num = 0;
var result = 0;

// check if the identifiers are numeric
var v1IsNumeric = int.TryParse(version1, out version1Num);
var v2IsNumeric = int.TryParse(version2, out version2Num);

// if both are numeric compare them as numbers
if (v1IsNumeric && v2IsNumeric)
{
result = version1Num.CompareTo(version2Num);
}
else if (v1IsNumeric || v2IsNumeric)
{
// numeric labels come before alpha labels
if (v1IsNumeric)
result = -1;
else
result = 1;
}
else
{
// Ignoring 2.0.0 case sensitive compare. Everything will be compared case insensitively as 2.0.1 specifies.
var stringCompareResult = StringComparer.OrdinalIgnoreCase.Compare(version1, version2);
if (stringCompareResult < 0)
{
result = -1;
}
else if (stringCompareResult > 0)
{
result = 1;
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Text.RegularExpressions;

namespace Octopus.Versioning.Lexicographic
{
public class LexicographicSortedVersionParser
{
const string Release = "release";
const string Meta = "buildmetadata";

static readonly Regex VersionRegex = new Regex($@"^(?<{Release}>([A-Za-z0-9]*?)([.\-_\\]([A-Za-z0-9.\-_\\]*?)?)?)?" +
$@"(?:\+(?<{Meta}>[A-Za-z0-9_\-.\\+]*?))?\s*$"
);

public LexicographicSortedVersion Parse(string? version)
{
if (string.IsNullOrWhiteSpace(version))
throw new ArgumentException("The version can not be an empty string");

var sanitisedVersion = version ?? string.Empty;
// SemVerFactory treated the original string as if it had no spaces at all
var noSpaces = sanitisedVersion.Replace(" ", "");

// We parse on the original string. This *does not* tolerate spaces in prerelease fields or metadata
// just like SemVerFactory.
var result = VersionRegex.Match(sanitisedVersion);

if (!result.Success)
throw new ArgumentException("The supplied version was not valid");

return new LexicographicSortedVersion(
result.Groups[Release].Success ? result.Groups[Release].Value : string.Empty,
result.Groups[Meta].Success ? result.Groups[Meta].Value : string.Empty,
noSpaces
);
}

public bool TryParse(string version, out LexicographicSortedVersion parsedVersion)
{
try
{
parsedVersion = Parse(version);
return true;
}
catch
{
parsedVersion = new LexicographicSortedVersion(
string.Empty,
string.Empty,
null
);
return false;
}
}
}
}
22 changes: 22 additions & 0 deletions source/Octopus.Versioning/VersionFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Octopus.Versioning.Docker;
using Octopus.Versioning.Lexicographic;
using Octopus.Versioning.Maven;
using Octopus.Versioning.Octopus;
using Octopus.Versioning.Semver;
Expand All @@ -19,6 +20,8 @@ public static IVersion CreateVersion(string input, VersionFormat format)
return CreateDockerTag(input);
case VersionFormat.Octopus:
return CreateOctopusVersion(input);
case VersionFormat.Lexicographic:
return CreateLexicographicSortedVersion(input);
default:
return CreateSemanticVersion(input);
}
Expand All @@ -34,6 +37,8 @@ public static IVersion CreateVersion(string input, VersionFormat format)
return TryCreateDockerTag(input);
case VersionFormat.Octopus:
return TryCreateOctopusVersion(input);
case VersionFormat.Lexicographic:
return TryCreateLexicographicSortedVersion(input);
default:
return TryCreateSemanticVersion(input);
}
Expand Down Expand Up @@ -150,5 +155,22 @@ public static IVersion CreateOctopusVersion(string input)
return null;
}
}

public static IVersion CreateLexicographicSortedVersion(string input)
{
return new LexicographicSortedVersionParser().Parse(input);
}

public static IVersion? TryCreateLexicographicSortedVersion(string input)
{
try
{
return CreateLexicographicSortedVersion(input);
}
catch
{
return null;
}
}
}
}
Loading

0 comments on commit 08377a1

Please sign in to comment.