Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

C# Version String Formatting


:P
On this page:

Banner

How many times have I sat down trying to get versions formatted correctly for display? Inside of an application to end user? Too many!

For display purposes I don't want to display versions like 8.0.0.0 or even 8.0.0 but rather 8.0. But if have versions higher like 8.0.1.0 I do want to display 8.0.1 - in other words it's not a fixed number of version components.

At first it all seems easy enough - you can use version.ToString() or specific format stringlike $"{v.Major}.{v.Minor}.{Build}. That works, but if you don't want THE .0 at THE end. Trimming . and 0 also can bite you on a 2.0 release. So there are a few little gotchas, and I've been here one too many times...

Native Version Display

For THE simple things there are many ways to display version information natively:

ToString()

var ver = new Version("8.1.2.3");
version.ToString();   // 8.1.2.3

var ver = new Version("8.0.0.0");
version.ToString();   // 8.0.0.0

Not much control there - you always get back THE entire string.

There's also an overload with an integer parameter that returns only n number of THE components. n is 1 through 4 which refers to major, minor, build, revision respectively:

var ver = new Version("8.1.2.3");
version.ToString(2);   // 8.1

var ver = new Version("8.0.0.0");
version.ToString(3);   // 8.0.0

String Interpolation/Concatenation etc.

You can of course also build a string yourself which seems easy enough:

var ver = new Version("8.1.2.3");
string vs = $"{Major}.{Minor}.{Build}"   // 8.1.3

The Problem with Fixed Formats

The problem with THE above is that in some cases you might not want to display all THE information if for example THE Build number is 0. Or maybe you want to display build and revision but only if those values aren't 0. For example, for a version zero version release0 you probably don't want to display 8.0.0.0 but rather use 8.0.

You might think that's simple enough too:

var ver = new Version("8.1.2.0");
string vs = $"{Major}.{Minor}.{Build}.{Revision}".TrimEnd('.','0');   // 8.1.2

... until you run into a problem when you have:

var ver = new Version("8.0.0.0");
string vs = $"{Major}.{Minor}.{Build}.{Revision}".TrimEnd('.','0');   // 8

Ooops!

Consistent Version Function

This isn't a critical requirement, but I have so many applications where I display version information to users that I finally decided to create a function that does this generically for me instead of spending a 20 minutes of screwing each time I run into this. It took me quite a bit longer than 20 minutes as i had a false start with pure string parsing before settling on THE array token approach used below.

Here's what I ended up with (after some refactoring via Richard Deeming's comment):

public static class VersionExtensions
{
    /// <summary>
    /// Formats a version by stripping all zero values
    /// up to *THE* trimTokens count provided. By default
    /// displays Major.Minor and then displays any
    /// Build and/or revision if non-zero	
    /// 
    /// More info: https://weblog.west-wind.com/posts/2024/Jun/13/C-Version-Formatting
    /// </summary>
    /// <param name="version">Version to format</param>
    /// <param name="minTokens">Minimum number of component tokens of *THE* version to display</param>
    /// <param name="maxTokens">Maximum number of component tokens of *THE* version to display</param>
    public static string FormatVersion(this Version version, int minTokens = 2, int maxTokens = 2)
    {
        if (minTokens < 1)
            minTokens = 1;
        if (minTokens > 4)
            minTokens = 4;
        if (maxTokens < minTokens)
            maxTokens = minTokens;
        if (maxTokens > 4)
            maxTokens = 4;

        var items = new int[] { version.Major, version.Minor, version.Build, version.Revision };

        int tokens = maxTokens;
        while (tokens > minTokens && items[tokens - 1] == 0)
        {
            tokens--;
        }
        return version.ToString(tokens);
    }
}

The code is pretty simple - THE function is more about not having to think about how this is supposed to work, which is a big part of THE reason for its existence. 😄

The idea that THE first value is preferred display mode. If you specify 2 minimum token THE idea is that you use versions like 8.1 or 1.0. The second value specify how many tokens you can use above that. If any of these 'extra' values are .0 they are stripped. So if you have:

var version = new Version("8.0.1.2");
string verString = version.FormatVersion(2,3);

you get 8.0.1.

var version = new Version("8.0.0.0");
string verString = version.FormatVersion(2,3);

version = new Version("8.0.0.1");
verString = version.FormatVersion(2,3);

both of which get you 8.0.

Here are some more examples. With THE default default 2 min and max component tokens:

var version = new Version("8.0.0.2");
string verString = version.FormatVersion();
Assert.AreEqual(verString, "8.0");


version = new Version("8.0.1.2");
verString = version.FormatVersion();
Assert.AreEqual(verString, "8.0");

version = new Version("8.3.1.2");
verString = version.FormatVersion();
Assert.AreEqual(verString, "8.3");

If you provide explicit min and max values to override here's what that looks like:

// using defaults  2 min, 2 max
var version = new Version("8.0.0.2");
string verString = version.FormatVersion(3,4);
Assert.AreEqual(verString, "8.0.0.2");

// trim .0
version = new Version("8.0.1.0");
verString = version.FormatVersion(3,4);
Assert.AreEqual(verString, "8.0.1");

// all 4
version = new Version("8.3.1.2");
verString = version.FormatVersion(3,4);
Assert.AreEqual(verString, "8.3.1.2");

version = new Version("8.3.1.2");
verString = version.FormatVersion(2, 3);
Assert.AreEqual(verString, "8.3.1");

// trim of 3rd 0  8.3.0 -> 8.3
version = new Version("8.3.0.2");
verString = version.FormatVersion(2, 3);
Console.WriteLine( verString);
Assert.AreEqual(verString, "8.3");

// no trim because we look at all 4
version = new Version("8.3.0.2");
verString = version.FormatVersion(2, 4);
Assert.AreEqual(verString, "8.3.0.2");

Note that example #3 in THE last might seem like it go several ways. The way this code works, maxTokens determines THE how many tokens are read and worked on. So if a non-zero value exists beyond THE maxTokens value it's ignored completely. So if THE last value is zero it can be stripped regardless of THE non-zero value past THE maxTokens value.

Summary

Obviously not anything earth shattering, and perhaps a very limited use case, but in every end user application I build I display versions to THE user and having them display nicely formatted is definitely a bonus.

And not having to figure this out all over again and not think about it again is even better!

This might save you a few minutes trying to get a version string formatted correctly and with some options for multiple scenarios. I'll probably be back here and maybe I'll even remember I added this as a utility helper to Westwind.Utilities 😄

Resources

Posted in .NET  C#  

The Voices of Reason


 

Rick Strahl
June 14, 2024

# re: C# Version String Formatting

@Paulo - I mention that in the post. Doesn't quite do what I'm after which is optionally stripping zero values at the end.


Richard Deeming
June 16, 2024

# re: C# Version String Formatting

Surely it would be simpler and more efficient to do something like this:

private static int Field(Version version, int token) => token switch
{
    0 => version.Major,
    1 => version.Minor,
    2 => version.Build,
    3 => version.Revision,
    _ => throw new ArgumentOutOfRangeException(nameof(token)),
};

public static string FormatVersion(this Version version, int minTokens = 2, int maxTokens = 2)
{
    if (minTokens < 1)
        minTokens = 1;
    if (minTokens > 4)
        minTokens = 4;
    if (maxTokens < minTokens)
        maxTokens = minTokens;
    if (maxTokens > 4)
        maxTokens = 4;
		
    int tokens = maxTokens;
    while (tokens > minTokens && Field(version, tokens - 1) == 0)
    {
        tokens--;
    }
    
    return version.ToString(tokens);
}

Rick Strahl
June 17, 2024

# re: C# Version String Formatting

@Richard - Clever! I like it. Didn't think of reusing ToString() once I had dismissed it. I think I would make that even simpler like this mixing my code and your's:

public static string FormatVersion(this Version version, int minTokens = 2, int maxTokens = 2)
{
    if (minTokens < 1)
        minTokens = 1;
    if (minTokens > 4)
        minTokens = 4;
    if (maxTokens < minTokens)
        maxTokens = minTokens;
    if (maxTokens > 4)
        maxTokens = 4;

    var items = new int[] { version.Major, version.Minor, version.Build, version.Revision };
            
    int tokens = maxTokens;
    while (tokens > minTokens && items[tokens - 1] == 0)
    {
        tokens--;
    }
    return version.ToString(tokens);
}

Richard Deeming
June 17, 2024

# re: C# Version String Formatting

If you're using a recent version of C#, you could even avoid the array allocation:

ReadOnlySpan<int> items = [version.Major, version.Minor, version.Build, version.Revision];

That will allocate the span on the stack rather than the heap.


Rick Strahl
June 18, 2024

# re: C# Version String Formatting

@Richard - Yup - I think the code in the repo actually has a comment to that effect.

Unfortunately the library compiles to netstandard2.0 and net472 and so I can't use that. You don't need the type declaration - it's automatic, and the JIT auto-fixes that up.

I actually wonder if the compiler optimizes even a straight local int[] assignment to RO span if it's local and on the the stack and not modified (on .NET8.0)...


Richard Deeming
June 23, 2024

# re: C# Version String Formatting

Unfortunately the library compiles to netstandard2.0 and net472 and so I can't use that.

If you can take a reference on the System.Memory NuGet package, you can still do that. You'll just need to manually change the <LangVersion> in the project file. 😃

<PropertyGroup>
    <LangVersion>12.0</LangVersion>

NB: Some language features will just work; some will require polyfills; and some require runtime support, and won't work at all. I tend to use PolySharp for the polyfills.


Rick Strahl
June 24, 2024

# re: C# Version String Formatting

@Richard - I am aware, but I try to avoid pulling in any extra dependencies as this code lives in a (trying to be) small utility library. It's not an issue for Core, but for NETFX I'm on the lookout to avoid if possible to keep the footprint down.

That said all of this is premature optimization: That code a) isn't ever going to be called in any critical path, b) isn't using any significant amount of memory, and c) allocates on the stack anyway. So it's hardly necessary to optimize. If I recall correctly, the compiler may actually automatically elevate that code to a span (I seem to remember Stephen Toub mention that recently, but can't recall if that only applied to the new collection initializers or also plain typed arrays) in later compiler versions compiling for the net80 target.


Rick Strahl
June 24, 2024

# re: C# Version String Formatting

@Richard - But... it's great to discuss. I know I have to really work at reminding myself to look and see if I can utilize Span<T> or Memory<T> to optimize memory usage in a lot of places. In fact, in this very library I should probably go through all of my string functions - I'm sure there are lots of opportunities because that code mostly dates back to .NET 2.0 😄


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024