-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSemanticVersion.cs
More file actions
203 lines (178 loc) · 8.45 KB
/
Copy pathSemanticVersion.cs
File metadata and controls
203 lines (178 loc) · 8.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// —————————————————————————————————————————————
//?
//!? 📜 SemanticVersion.cs
//!? 🖋️ Galacticai 📅 2022 - 2023
//! ⚖️ GPL-3.0-or-later
//? 🔗 Dependencies: No special Dependencies
//?
// —————————————————————————————————————————————
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
namespace GalacticLib.Semantic;
/// <summary> Semantic version matching the guidelines in <see href="https://semver.org"/> </summary>
public class SemanticVersion {
/// <summary> Collection of version regex-related things </summary>
public static class VersionRegex {
public const string Number
= "[0-9]";
public const string AlphanumericDash
= "[0-9A-Za-z-]";
public const string AlphanumericDashDot
= "[0-9A-Za-z-.]";
/// <summary> (X.Y.Z)-BuildType+Build </summary>
public const string XYZ
= $@"({Number}+)\.({Number}+)\.({Number}+)";
/// <summary> X.Y.Z-(BuildType)+Build </summary>
public const string BuildType
= $@"(?:-({AlphanumericDash}+(?:\.{AlphanumericDashDot}+)*))";
/// <summary> X.Y.Z-BuildType+(Build) </summary>
public const string Build
= $@"?(?:\+{AlphanumericDashDot}+)?";
/// <summary> X.Y.Z-BuildType+Build </summary>
public const string Complete = XYZ + BuildType + Build;
}
/// <summary> Check whether a string is a version that follows the semtantic version guidelines </summary>
/// <param name="versionString"> Target <see cref="string"/> </param>
/// <returns> true if the string matches the <see cref="Complete"/> semtantic version regex </returns>
public static bool IsSemantic(string versionString)
=> Regex.IsMatch(versionString.Trim(), VersionRegex.Complete);
/// <summary> The name of the version part
/// <br/> Major.Minor.Patch-BuildType+Build </summary>
public enum VersionPartType {
Major, Minor, Patch, BuildType, Build
}
/// <summary> Collection of preset BuildTypes </summary>
public static class BuildTypes {
public const string
Alpha = "alpha", Beta = "beta",
Dev = "dev", Development = "development",
Pre = "pre", PreRelease = "pre-release",
Release = "release", ReleaseCandidate = "rc",
Stable = "stable", Unstable = "unstable",
Test = "test", Testing = "testing";
}
private int _Major;
/// <summary> (Major).Minor.Patch-BuildType+Build </summary>
public int Major { get => _Major; set => _Major = value < 0 ? 0 : value; }
private int _Minor;
/// <summary> Major.(Minor).Patch-BuildType+Build </summary>
public int Minor { get => _Minor; set => _Minor = value < 0 ? 0 : value; }
private int _Patch;
/// <summary> Major.Minor.(Patch)-BuildType+Build </summary>
public int Patch { get => _Patch; set => _Patch = value < 0 ? 0 : value; }
/// <summary> Major.Minor.Patch-(BuildType)+Build </summary>
public string BuildType { get; set; }
/// <summary> Just a different label for <see cref="BuildType"/> </summary>
public string Prerelease { get => BuildType; set => BuildType = value; }
/// <summary> Major.Minor.Patch-BuildType+(Build) </summary>
public string Build { get; set; }
/// <summary> Semantic version matching the guidelines in semver.org </summary>
public SemanticVersion(
int major = 0, int minor = 0, int patch = 0,
string buildType = "", string build = "") {
Major = major;
Minor = minor;
Patch = patch;
BuildType = buildType;
Build = build;
}
/// <summary> Semantic version matching the guidelines in semver.org
/// <br/> From a <see cref="string"/> </summary>
public SemanticVersion(string versionString)
: this(versionString, false) { }
/// <summary> Semantic version matching the guidelines in semver.org
/// <br/> From a <see cref="string"/> </summary>
public SemanticVersion(string versionString, bool forceSemantic)
: this() {
if (forceSemantic && !IsSemantic(versionString))
throw new ArgumentException("The provided version string does not follow the semantic version guidelines");
versionString = versionString.Trim();
string?[] parts = [null, null, null, null, null];
int partIndex = 0;
StringBuilder partBuilder = new();
bool IsXYZ() => partIndex <= (int)VersionPartType.Patch;
void ApplyPart() {
string part = parts[partIndex] = partBuilder.ToString();
if (!string.IsNullOrEmpty(part)) {
VersionPartType partType = (VersionPartType)partIndex;
PropertyInfo? property
= typeof(Version).GetProperty(partType.ToString())
?? throw new Exception(
"(BUG!) Version part type name is invalid."
+ Environment.NewLine + "If you see this, please contact the developer."
);
//? Make int for XYZ
if (IsXYZ()) {
_ = int.TryParse(part, out int partValue); //!? no if, so it always sets a value or 0
property.SetValue(this, partValue);
} else property.SetValue(this, part); //? keep as {string?} for BuildType, Build
}
partIndex++;
partBuilder = new();
}
for (int i = 0; i < versionString.Length && partIndex < parts.Length; i++) {
char c = versionString[i];
if (c == ' ') { ApplyPart(); break; }
if (IsXYZ()) {
if (char.IsDigit(c)) { partBuilder.Append(c); continue; }
if (c == '.') { ApplyPart(); continue; }
ApplyPart();
partIndex = (int)VersionPartType.BuildType;
if (c != '-' && c != '+') partBuilder.Append(c);
} else {
if (c == '+' && partIndex < (int)VersionPartType.Build) {
ApplyPart(); continue;
}
if (!Regex.IsMatch(c.ToString(), VersionRegex.AlphanumericDashDot)) {
ApplyPart(); break;
}
partBuilder.Append(c);
}
if (i == versionString.Length - 1) ApplyPart();
}
}
public static Version Parse(string versionString)
=> new(versionString);
/// <summary> Convert to <see cref="System.Version"/>
/// <br/> ⚠️ Warning: <see cref="System.Version"/> does not support the <see cref="BuildType"/> and <see cref="Build"/> so they are lost in the conversion process
/// <br/> Note: <see cref="Version"/> does not support <see cref="System.Version.Revision"/> so it is replaced by 0 </summary>
/// <returns> Major.Minor.Patch.0 (as <see cref="System.Version"/>) </returns>
public virtual System.Version ToSystemVersion()
=> new(Major, Minor, Patch, 0);
public override int GetHashCode() {
HashCode code = new();
code.Add(Major);
code.Add(Minor);
code.Add(Patch);
code.Add(BuildType);
code.Add(Build);
return code.ToHashCode();
}
#nullable enable
public override bool Equals(object? obj)
=> ReferenceEquals(this, obj)
|| (obj is SemanticVersion other
&& Major == other.Major
&& Minor == other.Minor
&& Patch == other.Patch
&& BuildType == other.BuildType
&& Build == other.Build);
#nullable restore
/// <summary> Convert to <see cref="string"/> </summary>
/// <returns> "Major.Minor.Patch-BuildType+Build"
/// <br/> Note: BuildType and Build might not always be present </returns>
public override string ToString() {
StringBuilder versionSB = new();
versionSB.Append(Major).Append('.').Append(Minor).Append('.').Append(Patch);
if (!string.IsNullOrEmpty(BuildType))
versionSB.Append('-').Append(BuildType);
if (!string.IsNullOrEmpty(Build))
versionSB.Append('+').Append(Build);
return versionSB.ToString();
}
public static implicit operator SemanticVersion(string versionString)
=> new(versionString, false);
public static implicit operator string(SemanticVersion semanticVersion)
=> semanticVersion.ToString();
}