1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System
.Globalization
;
6 using System
.Diagnostics
;
8 using System
.Runtime
.CompilerServices
;
9 using System
.Diagnostics
.CodeAnalysis
;
13 // A Version object contains four hierarchical numeric components: major, minor,
14 // build and revision. Build and revision may be unspecified, which is represented
15 // internally as a -1. By definition, an unspecified component matches anything
16 // (both unspecified and specified), and an unspecified component is "less than" any
17 // specified component.
20 [System
.Runtime
.CompilerServices
.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
21 public sealed class Version
: ICloneable
, IComparable
, IComparable
<Version
?>,
22 #nullable disable // see comment on String
27 // AssemblyName depends on the order staying the same
28 private readonly int _Major
; // Do not rename (binary serialization)
29 private readonly int _Minor
; // Do not rename (binary serialization)
30 private readonly int _Build
= -1; // Do not rename (binary serialization)
31 private readonly int _Revision
= -1; // Do not rename (binary serialization)
33 public Version(int major
, int minor
, int build
, int revision
)
36 throw new ArgumentOutOfRangeException(nameof(major
), SR
.ArgumentOutOfRange_Version
);
39 throw new ArgumentOutOfRangeException(nameof(minor
), SR
.ArgumentOutOfRange_Version
);
42 throw new ArgumentOutOfRangeException(nameof(build
), SR
.ArgumentOutOfRange_Version
);
45 throw new ArgumentOutOfRangeException(nameof(revision
), SR
.ArgumentOutOfRange_Version
);
53 public Version(int major
, int minor
, int build
)
56 throw new ArgumentOutOfRangeException(nameof(major
), SR
.ArgumentOutOfRange_Version
);
59 throw new ArgumentOutOfRangeException(nameof(minor
), SR
.ArgumentOutOfRange_Version
);
62 throw new ArgumentOutOfRangeException(nameof(build
), SR
.ArgumentOutOfRange_Version
);
70 public Version(int major
, int minor
)
73 throw new ArgumentOutOfRangeException(nameof(major
), SR
.ArgumentOutOfRange_Version
);
76 throw new ArgumentOutOfRangeException(nameof(minor
), SR
.ArgumentOutOfRange_Version
);
82 public Version(string version
)
84 Version v
= Version
.Parse(version
);
88 _Revision
= v
.Revision
;
97 private Version(Version version
)
99 Debug
.Assert(version
!= null);
101 _Major
= version
._Major
;
102 _Minor
= version
._Minor
;
103 _Build
= version
._Build
;
104 _Revision
= version
._Revision
;
107 public object Clone()
109 return new Version(this);
112 // Properties for setting and getting version numbers
113 public int Major
=> _Major
;
115 public int Minor
=> _Minor
;
117 public int Build
=> _Build
;
119 public int Revision
=> _Revision
;
121 public short MajorRevision
=> (short)(_Revision
>> 16);
123 public short MinorRevision
=> (short)(_Revision
& 0xFFFF);
125 public int CompareTo(object? version
)
132 if (version
is Version v
)
137 throw new ArgumentException(SR
.Arg_MustBeVersion
);
140 public int CompareTo(Version
? value)
143 object.ReferenceEquals(value, this) ? 0 :
145 _Major
!= value._Major
? (_Major
> value._Major
? 1 : -1) :
146 _Minor
!= value._Minor
? (_Minor
> value._Minor
? 1 : -1) :
147 _Build
!= value._Build
? (_Build
> value._Build
? 1 : -1) :
148 _Revision
!= value._Revision
? (_Revision
> value._Revision
? 1 : -1) :
152 public override bool Equals(object? obj
)
154 return Equals(obj
as Version
);
157 public bool Equals(Version
? obj
)
159 return object.ReferenceEquals(obj
, this) ||
161 _Major
== obj
._Major
&&
162 _Minor
== obj
._Minor
&&
163 _Build
== obj
._Build
&&
164 _Revision
== obj
._Revision
);
167 public override int GetHashCode()
169 // Let's assume that most version numbers will be pretty small and just
170 // OR some lower order bits together.
174 accumulator
|= (_Major
& 0x0000000F) << 28;
175 accumulator
|= (_Minor
& 0x000000FF) << 20;
176 accumulator
|= (_Build
& 0x000000FF) << 12;
177 accumulator
|= (_Revision
& 0x00000FFF);
182 public override string ToString() =>
183 ToString(DefaultFormatFieldCount
);
185 public string ToString(int fieldCount
) =>
186 fieldCount
== 0 ? string.Empty
:
187 fieldCount
== 1 ? _Major
.ToString() :
188 StringBuilderCache
.GetStringAndRelease(ToCachedStringBuilder(fieldCount
));
190 public bool TryFormat(Span
<char> destination
, out int charsWritten
) =>
191 TryFormat(destination
, DefaultFormatFieldCount
, out charsWritten
);
193 public bool TryFormat(Span
<char> destination
, int fieldCount
, out int charsWritten
)
200 else if (fieldCount
== 1)
202 return _Major
.TryFormat(destination
, out charsWritten
);
205 StringBuilder sb
= ToCachedStringBuilder(fieldCount
);
206 if (sb
.Length
<= destination
.Length
)
208 sb
.CopyTo(0, destination
, sb
.Length
);
209 StringBuilderCache
.Release(sb
);
210 charsWritten
= sb
.Length
;
214 StringBuilderCache
.Release(sb
);
219 bool ISpanFormattable
.TryFormat(Span
<char> destination
, out int charsWritten
, ReadOnlySpan
<char> format
, IFormatProvider
? provider
)
221 // format and provider are ignored.
222 return TryFormat(destination
, out charsWritten
);
225 private int DefaultFormatFieldCount
=>
227 _Revision
== -1 ? 3 :
230 private StringBuilder
ToCachedStringBuilder(int fieldCount
)
232 // Note: As we always have positive numbers then it is safe to convert the number to string
233 // regardless of the current culture as we'll not have any punctuation marks in the number.
237 StringBuilder sb
= StringBuilderCache
.Acquire();
247 throw new ArgumentException(SR
.Format(SR
.ArgumentOutOfRange_Bounds_Lower_Upper
, "0", "2"), nameof(fieldCount
));
252 StringBuilder sb
= StringBuilderCache
.Acquire();
263 throw new ArgumentException(SR
.Format(SR
.ArgumentOutOfRange_Bounds_Lower_Upper
, "0", "3"), nameof(fieldCount
));
268 StringBuilder sb
= StringBuilderCache
.Acquire();
275 sb
.Append(_Revision
);
279 throw new ArgumentException(SR
.Format(SR
.ArgumentOutOfRange_Bounds_Lower_Upper
, "0", "4"), nameof(fieldCount
));
283 public static Version
Parse(string input
)
287 throw new ArgumentNullException(nameof(input
));
290 return ParseVersion(input
.AsSpan(), throwOnFailure
: true)!;
293 public static Version
Parse(ReadOnlySpan
<char> input
) =>
294 ParseVersion(input
, throwOnFailure
: true)!;
296 public static bool TryParse(string? input
, [NotNullWhen(true)] out Version
? result
)
304 return (result
= ParseVersion(input
.AsSpan(), throwOnFailure
: false)) != null;
307 public static bool TryParse(ReadOnlySpan
<char> input
, [NotNullWhen(true)] out Version
? result
) =>
308 (result
= ParseVersion(input
, throwOnFailure
: false)) != null;
310 private static Version
? ParseVersion(ReadOnlySpan
<char> input
, bool throwOnFailure
)
312 // Find the separator between major and minor. It must exist.
313 int majorEnd
= input
.IndexOf('.');
316 if (throwOnFailure
) throw new ArgumentException(SR
.Arg_VersionString
, nameof(input
));
320 // Find the ends of the optional minor and build portions.
321 // We musn't have any separators after build.
323 int minorEnd
= input
.Slice(majorEnd
+ 1).IndexOf('.');
326 minorEnd
+= (majorEnd
+ 1);
327 buildEnd
= input
.Slice(minorEnd
+ 1).IndexOf('.');
330 buildEnd
+= (minorEnd
+ 1);
331 if (input
.Slice(buildEnd
+ 1).Contains('.'))
333 if (throwOnFailure
) throw new ArgumentException(SR
.Arg_VersionString
, nameof(input
));
339 int minor
, build
, revision
;
341 // Parse the major version
342 if (!TryParseComponent(input
.Slice(0, majorEnd
), nameof(input
), throwOnFailure
, out int major
))
349 // If there's more than a major and minor, parse the minor, too.
350 if (!TryParseComponent(input
.Slice(majorEnd
+ 1, minorEnd
- majorEnd
- 1), nameof(input
), throwOnFailure
, out minor
))
357 // major.minor.build.revision
359 TryParseComponent(input
.Slice(minorEnd
+ 1, buildEnd
- minorEnd
- 1), nameof(build
), throwOnFailure
, out build
) &&
360 TryParseComponent(input
.Slice(buildEnd
+ 1), nameof(revision
), throwOnFailure
, out revision
) ?
361 new Version(major
, minor
, build
, revision
) :
367 return TryParseComponent(input
.Slice(minorEnd
+ 1), nameof(build
), throwOnFailure
, out build
) ?
368 new Version(major
, minor
, build
) :
375 return TryParseComponent(input
.Slice(majorEnd
+ 1), nameof(input
), throwOnFailure
, out minor
) ?
376 new Version(major
, minor
) :
381 private static bool TryParseComponent(ReadOnlySpan
<char> component
, string componentName
, bool throwOnFailure
, out int parsedComponent
)
385 if ((parsedComponent
= int.Parse(component
, NumberStyles
.Integer
, CultureInfo
.InvariantCulture
)) < 0)
387 throw new ArgumentOutOfRangeException(componentName
, SR
.ArgumentOutOfRange_Version
);
392 return int.TryParse(component
, NumberStyles
.Integer
, CultureInfo
.InvariantCulture
, out parsedComponent
) && parsedComponent
>= 0;
395 // Force inline as the true/false ternary takes it above ALWAYS_INLINE size even though the asm ends up smaller
396 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
397 public static bool operator ==(Version
? v1
, Version
? v2
)
399 // Test "right" first to allow branch elimination when inlined for null checks (== null)
400 // so it can become a simple test
403 // return true/false not the test result https://github.com/dotnet/coreclr/issues/914
404 return (v1
is null) ? true : false;
407 // Quick reference equality test prior to calling the virtual Equality
408 return ReferenceEquals(v2
, v1
) ? true : v2
.Equals(v1
);
411 public static bool operator !=(Version
? v1
, Version
? v2
)
416 public static bool operator <(Version
? v1
, Version
? v2
)
420 return !(v2
is null);
423 return (v1
.CompareTo(v2
) < 0);
426 public static bool operator <=(Version
? v1
, Version
? v2
)
433 return (v1
.CompareTo(v2
) <= 0);
436 public static bool operator >(Version
? v1
, Version
? v2
)
441 public static bool operator >=(Version
? v1
, Version
? v2
)