Skip to content

Commit

Permalink
feat: add build metadata
Browse files Browse the repository at this point in the history
Adds a Build property to SemanticVersionObject.
Build metadata is parsed from the input string or can be set manually.
  • Loading branch information
Kampfmoehre committed Jan 26, 2024
1 parent 298a915 commit b525a5f
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 38 deletions.
62 changes: 43 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,37 @@ There is an interface `ISemanticVersion` for which comparers are implemented. Ju

You can also use the `SemanticVersionObject` class that already implements the comparer with some neat and handy additions.

Additionally `SemanticVersionObject` provides a `Build` property containing any build metadata. As per [specification](https://semver.org/#spec-item-10) build metadtaa data may only contain ASCII alphanumerics and hyphens `[0-9A-Za-z-]` and can be appended at the end of the version with a plus sign `+`.

### Constructor

When you create a new `SemanticVersionObject` instance you can give the version number parts straight away.

```csharp
var version = new SemanticVersionObject(); // 1.0.0
var version = new SemanticVersionObject(2); // 2.0.0
var version = new SemanticVersionObject(1, 1); // 1.1.0
var version = new SemanticVersionObject(1, 1, 1); // 1.1.1
var version = new SemanticVersionObject(1, 1, 1, "alpha.1"); // 1.1.1-alpha.1
SemanticVersionObject version = new(); // 1.0.0
SemanticVersionObject version = new(2); // 2.0.0
SemanticVersionObject version = new(1, 1); // 1.1.0
SemanticVersionObject version = new(1, 1, 1); // 1.1.1
SemanticVersionObject version = new(1, 1, 1, "alpha.1"); // 1.1.1-alpha.1
SemanticVersionObject version = new(1, 1, 1, "alpha.1", "298a915a"); // 1.1.1-alpha.1+298a915a
SemanticVersionObject version = new(1, 1, 1, null, "298a915a"); // 1.1.1+298a915a
```

### FromString

There is also a static `FromString` Method which will return you a `SemanticVersionObject` instance from a version string.

```csharp
var version = SemanticVersionObject.FromString("v1.0.0-beta.1");
var version = SemanticVersionObject.FromString("v1.0.0-beta.1+298a915a985daeb426a0fe7543917874d7fa2995");
```

### ToString

You can generate a version string with the `ToString` method:

```csharp
var version = new SemanticVersionObject { Major = 1, Minor = 2, Patch = 3, PreRelease = "develop.13" };
Console.WriteLine(version.ToString()); // v1.2.3-develop.13
SemanticVersionObject version = new() { Major = 1, Minor = 2, Patch = 3, PreRelease = "beta.1", Build = "298a915a" };
Console.WriteLine(version.ToString()); // v1.2.3-beta.1+298a915a
```

If you do not want the leading `v` you can use the `ToVersionString` method and give false for the `withLeadingV` parameter:
Expand All @@ -63,7 +67,7 @@ Console.WriteLine(version.ToVersionString(false)); // 1.4.0
As mentioned a default comparer is included which allows to sort version descending to have the newest version first. You can use the `Sort` LinQ method for this:

```csharp
var list = new List<SemanticVersionObject>
List<SemanticVersionObject> list = new()
{
SemanticVersionObject.FromString("1.0.0"),
SemanticVersionObject.FromString("1.1.1"),
Expand Down Expand Up @@ -113,9 +117,9 @@ Assert.Equal(
The `SemanticVersionObject` class implements the `IComparable` interface so you can use the `CompareTo` on every instance. Just pass another instance to it. The result is an integer that tells you, if the given instance is newer (1), equal (0) or older (-1).

```csharp
var oldVersion = new SemanticVersionObject(1, 0, 0);
var equalVersion = new SemanticVersionObject(1, 0, 0);
var newVersion = new SemanticVersionObject(1, 1, 0);
SemanticVersionObject oldVersion = new(1, 0, 0);
SemanticVersionObject equalVersion = new(1, 0, 0);
SemanticVersionObject newVersion = new(1, 1, 0);

Console.WriteLine(oldVersion.CompareTo(newVersion)); // 1
Console.WriteLine(newVersion.CompareTo(oldVersion)); // -1
Expand All @@ -127,8 +131,8 @@ Console.WriteLine(oldVersion.CompareTo(equalVersion)); // 0
Sorting (and comparing) takes into account if there is a prerelease or not. For example consider this:

```csharp
var x = new SemanticVersionObject(1, 0, 0);
var y = new SemanticVersionObject(1, 0, 0, "alpha.1");
SemanticVersionObject x = new(1, 0, 0);
SemanticVersionObject y = new(1, 0, 0, "alpha.1");

Console.WriteLine(x.CompareTo(y)); // -1
```
Expand All @@ -138,23 +142,43 @@ Version without a prerelease are always newer if major, minor and patch are equa
Also consider this:

```csharp
var x = new SemanticVersionObject(1, 0, 0, "beta.2");
var y = new SemanticVersionObject(1, 0, 0, "beta.10");
SemanticVersionObject x = new(1, 0, 0, "beta.2");
SemanticVersionObject y = new(1, 0, 0, "beta.10");

Console.WriteLine(x.CompareTo(y)); // 1
```

Prereleases will be split into their parts and then will be compared. Each part must be separated with a dot. So for example beta is higher than alpha but just because b is later in the alphabet than a.

```csharp
var x = new SemanticVersionObject(1, 0, 0, "alpha.1");
var y = new SemanticVersionObject(1, 0, 0, "beta.1");
var z = new SemanticVersionObject(1, 0, 0, "develop.1");
SemanticVersionObject x = new(1, 0, 0, "alpha.1");
SemanticVersionObject y = new(1, 0, 0, "beta.1");
SemanticVersionObject z = new(1, 0, 0, "develop.1");

Console.WriteLine(x.CompareTo(y)); // 1
Console.WriteLine(y.CompareTo(z)); // 1
```

As per [spec](https://semver.org/#spec-item-10) build metadata is ignored when comparing.

```csharp
List<SemanticVersionObject> list = new()
{
SemanticVersionObject.FromString("1.0.0+200"),
SemanticVersionObject.FromString("1.0.0+100"),
};

list.Sort();

Assert.Equal(
new List<SemanticVersionObject>
{
SemanticVersionObject.FromString("1.0.0+200"),
SemanticVersionObject.FromString("1.0.0+100"),
},
list);
```

### IsNewerThan, IsOlderThan

You can use the `IsNewerThan` or `IsOlderThan` method if you need a boolean value.
Expand Down
54 changes: 44 additions & 10 deletions src/DroidSolutions.Oss.SemanticVersion/SemanticVersionObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ public SemanticVersionObject(int major, int minor, int patch, string prerelease)
PreRelease = prerelease;
}

/// <summary>
/// Initializes a new instance of the <see cref="SemanticVersionObject"/> class.
/// </summary>
/// <param name="major">The major version.</param>
/// <param name="minor">The minor version.</param>
/// <param name="patch">The patch version.</param>
/// <param name="prerelease">The prerelease version.</param>
/// <param name="build">The build metadata.</param>
public SemanticVersionObject(int major, int minor, int patch, string? prerelease, string build)
{
Major = major;
Minor = minor;
Patch = patch;
PreRelease = prerelease;
Build = build;
}

/// <summary>
/// Gets or sets the major version number.
/// </summary>
Expand All @@ -78,6 +95,11 @@ public SemanticVersionObject(int major, int minor, int patch, string prerelease)
/// </summary>
public virtual string? PreRelease { get; set; }

/// <summary>
/// Gets or sets the build metadata.
/// </summary>
public virtual string? Build { get; set; }

/// <summary>
/// Checks if both given items are equal.
/// </summary>
Expand Down Expand Up @@ -152,27 +174,34 @@ public SemanticVersionObject(int major, int minor, int patch, string prerelease)
/// <returns>A <see cref="SemanticVersionObject"/> instance with the parsed version numbers. </returns>
public static SemanticVersionObject FromString(string version)
{
Regex regex = new(@"^v?(\d+).(\d+).(\d+)(-(.*))?$", RegexOptions.CultureInvariant);
// @"^v?(\d+).(\d+).(\d+)(-(.*))?$",
Regex regex = new(
@"^v?(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$",
RegexOptions.CultureInvariant);

Match match = regex.Match(version);
if (!match.Success)
{
throw new ArgumentException(
$"The given string \"{version}\" is not a valid semantic version!", nameof(version));
}

string? preRelease = null;
if (match.Groups.Count > 5 && !string.IsNullOrWhiteSpace(match.Groups[5].Value))
SemanticVersionObject result = new(
int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture),
int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture),
int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture));

if (match.Groups.ContainsKey("prerelease"))
{
preRelease = match.Groups[5].Value;
result.PreRelease = match.Groups["prerelease"].Value;
}

return new SemanticVersionObject
if (match.Groups.ContainsKey("build"))
{
Major = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture),
Minor = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture),
Patch = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture),
PreRelease = preRelease,
};
result.Build = match.Groups["build"].Value;
}

return result;
}

/// <summary>
Expand Down Expand Up @@ -233,6 +262,11 @@ public string ToVersionString(bool withLeadingV = true)
result = $"{result}-{PreRelease}";
}

if (!string.IsNullOrEmpty(Build))
{
result = $"{result}+{Build}";
}

return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,39 @@ public class SemanticVersionComparerTest
[InlineData("1.1.0", "1.0.0", -1)]
[InlineData("2.0.0", "1.0.0", -1)]
[InlineData("1.0.0", "1.0.0-develop.12", -1)]
[InlineData("1.0.0", "1.0.0+abcdefg", 0)]
[InlineData("1.0.0+111", "1.0.0+112", 0)]
public void Compare_Returns_Expected(string x, string y, int expected)
{
ISemanticVersion versionX = SemanticVersionObject.FromString(x);
ISemanticVersion versionY = SemanticVersionObject.FromString(y);

var comparer = new SemanticVersionComparer();
SemanticVersionComparer comparer = new();
Assert.Equal(expected, comparer.Compare(versionX, versionY));
}

[Theory]
[InlineData(null, "1.0.0", -1)]
[InlineData("1.0.0", null, 1)]
[InlineData(null, null, 0)]
public void Compare_HandlesNullValues(string x, string y, int expected)
public void Compare_HandlesNullValues(string? x, string? y, int expected)
{
ISemanticVersion? versionX = string.IsNullOrEmpty(x) ? null : SemanticVersionObject.FromString(x);
ISemanticVersion? versionY = string.IsNullOrEmpty(y) ? null : SemanticVersionObject.FromString(y);

var comparer = new SemanticVersionComparer();
SemanticVersionComparer comparer = new();
Assert.Equal(expected, comparer.Compare(versionX, versionY));
}

[Theory]
[InlineData("1.0.0+100", "1.0.0+200")]
[InlineData("1.0.0+abc", "1.0.0+def")]
public void Compare_IgnoresBuild(string x, string y)
{
ISemanticVersion versionX = SemanticVersionObject.FromString(x);
ISemanticVersion versionY = SemanticVersionObject.FromString(y);

SemanticVersionComparer comparer = new();
Assert.Equal(0, comparer.Compare(versionX, versionY));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ public class SemanticVersionObjectTest
public static IEnumerable<object[]> FromStringTestData() => new[]
{
new object[] { "1.0.0", new SemanticVersionObject(1, 0, 0) },
new object[] { "v1.0.0", new SemanticVersionObject(1, 0, 0) },
new object[] { "1.1.1", new SemanticVersionObject(1, 1, 1) },
new object[] { "11.111.1111", new SemanticVersionObject(11, 111, 1111) },
new object[] { "1.0.0-alpha.1", new SemanticVersionObject(1, 0, 0, "alpha.1") },
new object[] { "1.0.0-beta.12", new SemanticVersionObject(1, 0, 0, "beta.12") },
["v1.0.0", new SemanticVersionObject(1, 0, 0)],
["1.1.1", new SemanticVersionObject(1, 1, 1)],
["11.111.1111", new SemanticVersionObject(11, 111, 1111)],
["1.0.0-alpha.1", new SemanticVersionObject(1, 0, 0, "alpha.1")],
["1.0.0-beta.12", new SemanticVersionObject(1, 0, 0, "beta.12")],
[
"1.20.4+dd08e98289531187cb240db94b188c58938eb214",
new SemanticVersionObject(1, 20, 4, null, "dd08e98289531187cb240db94b188c58938eb214")
],
};

[Theory]
Expand Down Expand Up @@ -129,6 +133,8 @@ public void CompareTo_ShouldThrowIfGivenNoSemanticVersionObject()
[InlineData("v1.0.0", true, "v1.0.0")]
[InlineData("v22.222.2222", false, "22.222.2222")]
[InlineData("v3333.33.333-develop.16", true, "v3333.33.333-develop.16")]
[InlineData("2.3.4-beta.1+298a915a985daeb426a0fe7543917874d7fa2995", true, "v2.3.4-beta.1+298a915a985daeb426a0fe7543917874d7fa2995")]
[InlineData("2.3.4+298a915a985daeb426a0fe7543917874d7fa2995", false, "2.3.4+298a915a985daeb426a0fe7543917874d7fa2995")]
public void ToVersionString_ShouldWorkCorrectly(string input, bool withV, string expected)
{
var version = SemanticVersionObject.FromString(input) as SemanticVersionObject;
Expand Down Expand Up @@ -169,7 +175,9 @@ public void IsOlder_ShouldWork(string ownVersion, string otherVersion, bool expe
[InlineData("2.3.4", false)]
[InlineData("3.2.1-alpha.1", true)]
[InlineData("4.2.1-beta.15-special", true)]
[InlineData("5.0.5-develop.69-build_420", true)]
[InlineData("5.0.5-develop.69-build.420", true)]
[InlineData("5.0.5-develop.69+build.420", true)]
[InlineData("5.0.5+build.420", false)]
public void IsPreRelease_ShouldWork(string version, bool expected)
{
SemanticVersionObject semanticVersion = SemanticVersionObject.FromString(version);
Expand Down

0 comments on commit b525a5f

Please sign in to comment.