![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
![[community profile]](https://www.dreamwidth.org/img/silk/identity/community.png)
My favorite single line of code: sortable ProductVersion type for .NET
Sometimes I need to take a version number - something like 5 or 6.4 or 2.25.1 - and see whether it's newer or older than another version number, or maybe just take a list of version numbers like this and put them in order.
The projects I'm working on usually have at least a bit of F# code, or a dependency on an F# library (even if most of the code is C#), and once I've taken the dependency on FSharp.Core, I might as well use it to create an equatable, sortable, and immutable "version number" type.
This might be absolute favorite line of code:
type ProductVersion = { components: int list }
That's literally all you need to make a type that stores a version number, and can be compared and sorted in the proper order (so that 10.25 comes after 10.4, for example). This is because the F# record type implements comparison using its underlying fields, in order, and the one field in this record is an F# list, which implements comparison through its elements, in order - which is exactly what we want.
Of course, you'll still want some code to convert to and from strings. Here's the full code:
type ProductVersion = { components: int list }
with
override this.ToString() = this.components |> Seq.map string |> String.concat "."
member this.IsGreaterThan(x) = this > x
member this.IsLessThan(x) = this < x
static member Parse(str: string) = { components = [ for s in str.Split('.') do int s ] }
static member Empty = { components = [] }
The ToString method replaces the default printed representation of the record type (just like in C#); here we use it to print out the version number in the standard format, as integers separated by periods. Parse is a static method to convert a string to a ProductVersion, which will fail if the format doesn't match (for example, if the version number includes letters or spaces). And Empty can be useful if you need a reference for a hypothetical version number that would come before all others.
And if you're wondering why I added IsGreaterThan and IsLessThan - it's because F# implements its greater-than and less-than operators using the .NET IComparable interface, while in C# they would need to be overridden manually, just like how the C# equality operator doesn't automatically call Equals.
You can use the type from C# like this:
var obj1 = ProductVersion.Parse("1.1");
var obj2 = ProductVersion.Parse("1.1");
Console.WriteLine(obj1 == obj2); // False
Console.WriteLine(obj1.Equals(obj2)); // True
var obj3 = ProductVersion.Parse("1.1.0");
Console.WriteLine(obj2.Equals(obj3)); // False
Console.WriteLine(obj2); // 1.1
Console.WriteLine(obj3); // 1.1.0
var obj4 = ProductVersion.Parse("1.01");
Console.WriteLine(obj4); // 1.1
Console.WriteLine(obj4.Equals(obj2)); // True
var list = new List
{
ProductVersion.Parse("3.12"),
ProductVersion.Parse("3.0"),
ProductVersion.Parse("3"),
ProductVersion.Parse("3.0.0"),
ProductVersion.Parse("3.3")
};
list.Sort();
Console.WriteLine(string.Join(", ", list)); // 3, 3.0, 3.0.0, 3.3, 3.12