// Look Ma! Recursion everywhere!
public sealed partial class VersionMgmt
{
#region Properties
private char _separator = '.';
// NOTE: If you want to increase this ( i.e. >5 ), the AsInt accessor needs modification!
private const byte MAX_DEPTH = 5;
#endregion
#region Constructors
/// <summary>Instantiate the class via the <seealso cref="Parse(string)"/> static function.</summary>
private VersionMgmt() { }
public VersionMgmt( params uint[] values )
{
if (values is not null && values.Length > 0)
{
// NOTE: C# Ranges are exclusive of the last value specified so, "..6" = FIVE items!
if (values.Length >= MAX_DEPTH) values = values[ ..(MAX_DEPTH + 1)];
this.Value = values[ 0 ];
if ( values.Length > 1)
this.Add( new VersionMgmt( values[ 1.. ] ) );
}
}
#endregion
#region Accessors
public VersionMgmt this[ int index ]
{
get
{
if ( index < 0 ) index = Length - 1;
if ( index >= Length ) throw new ArgumentOutOfRangeException( $"The supplied index, {index} exceeds the size of this Version value ({Length})." );
return index == 0 ? this : HasChild ? this.Child[ index - 1 ] : null;
}
}
public uint Value { get; private set; } = 1;
private bool HasChild => this.Child is not null;
private bool IsRoot => this.Parent is null;
private VersionMgmt Root => IsRoot ? this : this.Parent.Root;
private VersionMgmt Parent { get; set; } = null;
public int Length => HasChild ? Child.Length + 1 : 1;
private VersionMgmt Child { get; set; } = null;
public char Separator
{
get => this._separator;
set { if ( ".:/-".Contains( value ) ) { this._separator = value; } }
}
public string Suffix { get; set; } = string.Empty;
/// <summary>Facilitates basic serialization by encoding the version as a 64-bit unsigned integer.</summary>
/// <remarks>
/// When using this function, the maximum value that can be stored for each segment is:<br/>
/// Segment 1-3: 0-1023 ( 0x003ff / 10-bits ea)<br/>
/// Segment 4-5: 0-131070 ( 0x1ffff / 17-bits ea)<br/>
/// Max version value: 1023.1023.1023.131070.131070 (64 bits)
/// </remarks>
public ulong AsInt
{
get => Length switch
{
< 3 => (this.Value & 0x1ffff) | (HasChild ? (Child.Value << 17) : 0), // range 0 - 131,070
_ => (this.Value & 0x003ff) | Child.Value << 10, // range 0 - 1023
};
set
{
this.Value = (uint)(Length < 3 ? (value & 0x03ff) : (value & 0x1ffff));
if ( HasChild ) this.Child.AsInt = Length < 3 ? value >> 10 : value >> 17;
}
}
#endregion
#region Operators
public static implicit operator Version( VersionMgmt source ) => source is null ? new Version() : new Version( source.ToString( '.', 4 ) );
public static implicit operator VersionMgmt( Version source ) => source is null ? new( 1,0,0,0 ) : Parse( source.ToString() );
public static implicit operator VersionMgmt( ulong source ) => new() { AsInt = source };
public static implicit operator ulong( VersionMgmt source ) => source is null ? 0 : source.AsInt;
public static implicit operator uint[]( VersionMgmt source ) => source is null ? [] : source.ToUIntArray();
public static implicit operator VersionMgmt( uint[] source ) => source is null || source.Length == 0 ? new( 1, 0, 0, 0 ) : new VersionMgmt( source );
#endregion
#region Methods
/// <summary>Faclitates incrementing the version number.</summary>
/// <param name="value">The amount by which to increment it.</param>
/// <param name="depth">The 0-based index of the version element to increment.</param>
/// <returns>The new value of the element incremented.</returns>
/// <remarks>If the supplied <paramref name="depth"/> is greater than the length of the version,
/// the last element will be incremented. If it's a negative number, the depth counts from the end backwards
/// (i.e. -1 = last value, -2 = second last, etc.)
/// </remarks>
public VersionMgmt Increment( uint value = 1, int depth = -1 )
{
if ( depth < 0 ) depth = Math.Max(0,Length + depth);
if ( (depth > 0) && HasChild )
this.Child.Increment( value, --depth );
else
this.Value += value;
return this;
}
/// <summary>Adds a supplied child node to the version.</summary>
/// <param name="child">The child node to add.</param>
/// <returns>The index of the newly added child.</returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentNullException"></exception>
/// <remarks>
/// A version may only comprise up to MAX_DEPTH nodes. Attempting to add beyond this limit
/// generates an <seealso cref="InvalidOperationException"/>.<br/>New nodes <i>are always appended to the end</i>
/// of the version sequence, regardless of which node actually calls this method.</remarks>
private int Add( VersionMgmt child )
{
if ( Root.Length >= MAX_DEPTH ) throw new InvalidOperationException( "This version is full." );
ArgumentNullException.ThrowIfNull( child );
if ( this.Child is null )
{
child.Parent = this;
this.Child = child;
}
else
this.Child.Add( child );
return Length;
}
private int Add( string value ) => this.Add( Parse( value ) );
/// <summary>Returns the Version as an array of individual <see cref="VersionMgmt"/> objects.</summary>
/// <remarks><b>NOTE</b>: If the passed <paramref name="depth"/> value is zero, the returned result will be a zero-length array!</remarks>
public VersionMgmt[] ToArray( byte depth = MAX_DEPTH )
{
if (depth == 0) return [];
VersionMgmt[] result = new VersionMgmt[ Math.Min( Length, depth ) ];
for (int i = 0; i < result.Length; i++)
result[ i ] = this[ i ];
return result;
}
/// <summary>Returns the value of the version as an array of <see cref="uint"/> values.</summary>
public uint[] ToUIntArray( byte depth = MAX_DEPTH )
{
if (depth == 0) return [];
var segments = ToArray( depth );
var result = new uint[ segments.Length ];
for (int i = 0; i < segments.Length; i++ ) result[i] = segments[ i ].Value;
return result;
}
/// <summary>The full version value with its natural separators and appended suffix.</summary>
public override string ToString() => this.ToString( -1 );
/// <summary>The full version value with its natural separators and optional appended suffix.</summary>
public string ToString( bool suppressSuffix ) => this.ToString( -1, suppressSuffix );
/// <summary>Facilitates creating a subset string of the full version value, limited to a specified depth.</summary>
/// <param name="maxDepth">The <i>maximum</i> number of elements to include in the result. If this exceeds the number of elements, only they will be returned.</param>
/// <returns>A string containing the number of version elements managed up to the depth specified, using the natural separation characters of the stored version.</returns>
/// <remarks>If the specified depth is negative, or exceeds the length of the managed version value, the entire value will be returned.</remarks>
public string ToString( int maxDepth, bool suppressSuffix = false ) => this.ToString( this._separator, maxDepth ) + (suppressSuffix ? "" : Suffix );
/// <summary>Facilitates returning the version string with a designated separator, to a specified depth.</summary>
/// <param name="divider">What character to use as a separator. <b>This overrides the stored/natural separator values!</b></param>
/// <param name="maxDepth">The maximum number of version elements to include.</param>
internal string ToString( char divider, int maxDepth = -1 )
{
if (maxDepth < 0) maxDepth = Math.Max(0,Length + maxDepth);
return $"{Value}" + ((maxDepth > 0) && HasChild ? $"{divider}" + this.Child.ToString( divider, maxDepth - 1 ) : "");
}
/// <summary>Given a string, searches for a valid version number, and parses it into a <seealso cref="VersionMgmt"/> object.</summary>
/// <remarks>
/// To prevent abuse, the parser only reads the first <b>six</b> (6) version elements it finds in the supplied string. If no
/// valid version values can be found in the supplied string, an <seealso cref="ArgumentException"/> will be thrown. If
/// </remarks>
public static VersionMgmt Parse( string source, uint increment = 0, int depth = -1 )
{
VersionMgmt result;
source ??= string.Empty;
string suffix = VersionMgmtSuffixCapture_Rx().Match( source ).Value.Trim();
if ( !string.IsNullOrWhiteSpace( source = VersionMgmtCleaner_Rx().Replace( source, "" ) ) )
{
Match m = VersionMgmtValidation_Rx().Match( source );
if ( m.Success && m.Groups[ "ver" ].Success )
{
m = VersionMgmtParser2_Rx().Match( m.Groups[ "ver" ].Value );
if ( m.Success && m.Groups[ "value" ].Success )
{
result = new()
{
Value = uint.Parse( m.Groups[ "value" ].Value ),
Suffix = suffix,
};
if ( m.Groups[ "div" ].Success )
{
result.Separator = m.Groups[ "div" ].Value[ 0 ];
if ( m.Groups[ "remainder" ].Success ) result.Add( m.Groups[ "remainder" ].Value );
}
if ( increment > 0 ) result.Increment( increment, depth );
return result;
}
}
else
{
if (VersionMgmtParser3_Rx().IsMatch( source ))
{
result = new() { Value = uint.Parse( source ) };
if (increment > 0) result.Increment( increment,depth );
return result;
}
}
}
throw new ArgumentException( "No valid versions were found within the supplied string!" );
}
/// <summary>Tries to parse the supplied string.</summary>
/// <param name="source">The string to attempt to parse.</param>
/// <param name="result">If successful, a new <see cref="VersionMgmt"/> object derived from the supplied <paramref name="source"/>.</param>
/// <param name="increment">(Optional): If provided, will increment the generated version according to the <see cref="VersionMgmt.Increment(uint, int)"/> rules.</param>
/// <param name="depth">(Optional): See: <seealso cref="VersionMgmt.Increment(uint, int)"/>.</param>
/// <returns><b>TRUE</b> if parsing of the supplied string was successful.</returns>
public static bool TryParse( string source, out VersionMgmt result, uint increment = 0, int depth = -1 )
{
result = null;
try
{
result = Parse( source, increment, depth );
return true;
}
catch (OverflowException) { }
catch (ArgumentException) { }
return false;
}
/// <summary>Tests a supplied string and reports if a valid version value was detected within it.</summary>
/// <returns><b>TRUE</b> if the supplied string contains a parseable version value.</returns>
public static bool ContainsVersion( string source )
{
if ( string.IsNullOrWhiteSpace( source ) ) return false;
//return Regex.IsMatch( source, @"(?<ver>(?:[\d]+[.:/-]){1,5}[\d]+)", RegexOptions.None );
return VersionMgmtValidation_Rx().IsMatch( source );
}
[GeneratedRegex( @"(?<ver>(?:[\d]+[.:/-]){1,5}[\d]+)", RegexOptions.None )]
public static partial Regex VersionMgmtValidation_Rx();
[GeneratedRegex( @"^(?<value>[\d]+)(?<div>[.:/-])?(?<remainder>.+)?$", RegexOptions.None )]
private static partial Regex VersionMgmtParser2_Rx();
[GeneratedRegex( @"^[\d]+$", RegexOptions.None )] private static partial Regex VersionMgmtParser3_Rx();
[GeneratedRegex( @"(?:[^\d]+$|[^\d./:-])" , RegexOptions.None )] private static partial Regex VersionMgmtCleaner_Rx();
[GeneratedRegex( @" ?[^\S]+$" )] private static partial Regex VersionMgmtSuffixCapture_Rx();
#endregion
}
This is a simple, self-contained Version management class for C#.
It handles variable-length versions from just 1, up-to 5 numeric segments (plus an unmanaged alphanumeric suffix), supports incrementing, optional customized suffixes, and integrated parsing/translation to/from ulong, System.Version, string, and uint[]