1 /**
2 * Authors: Azbuka-slovensko
3 * License: MIT, see LICENCE.md
4 * Copyright: Azbuka-slovensko 2016
5 * See_Also:
6 *	Semantic Versioning http://semver.org/
7 */
8 module BrightProof;
9 
10 /**
11 * Exception for easy error handling
12 */
13 class SemVerException : Exception {
14 	 /**
15 	* Params:
16 	* 	msg = message
17 	* 	file = file, where SemVerException have been throwed
18 	* 	line = line number in file
19 	* 	next = next exception
20 	*/
21 	@safe pure nothrow this(string msg,
22 		string file = __FILE__,
23 		size_t line = __LINE__,
24 		Throwable next = null) {
25 			super(msg, file, line, next);
26 	}
27 }
28 
29 /**
30 * Main struct
31 * Examples:
32 * ---
33 * SemVer("1.0.0");
34 * SemVer("1.0.0+4444");
35 * SemVer("1.0.0-eyyyyup");
36 * SemVer("1.0.0-yay+build");
37 * ---
38 */
39 struct SemVer {
40 	size_t Major, Minor, Patch;
41 	string PreRelease, Build;
42 
43 	/**
44 	* Constructor
45 	* Params:
46 	*	i = input string
47 	* Throws: SemVerException if there is any syntax errors.
48 	*/
49 	pure this(string i) {
50 		import std.string : isNumeric;
51 		import std.conv : to;
52 
53 		size_t MajorDot, MinorDot, PreReleaseStart, BuildStart;
54 
55 		for(size_t count = 0; count < i.length; count++) {
56 			switch(i[count]) {
57 				case '.':
58 					if(!MajorDot) {
59 						MajorDot = count;
60 						break;
61 					}
62 					if(!MinorDot)
63 						MinorDot = count;
64 					break;
65 				case '-':
66 					if(!BuildStart && !PreReleaseStart)
67 						PreReleaseStart = count;
68 					break;
69 				case '+':
70 					BuildStart = count;
71 					break;
72 				default: break;
73 			}
74 		}
75 
76 		if(MajorDot == 0) {
77 			// If first symbol is a dot, there is no Major.
78 			throw new SemVerException("There is no major version number");
79 		} else if(!MinorDot || (MinorDot - MajorDot < 2)) {
80 			// If there is nothing between MajorDot and MinorDot.
81 			throw new SemVerException("There is no minor version number");
82 		} else if(
83 			(!PreReleaseStart && (i.length - MinorDot < 2)) ||
84 			(!PreReleaseStart && (PreReleaseStart - MinorDot < 2))) {
85 			// There is no Patch, if there is nothing after MinorDot.
86 			// and string end or `-`.
87 			throw new SemVerException("There is no patch version number");
88 		} else if(
89 			(!BuildStart && (i.length - PreReleaseStart < 2)) ||
90 			((BuildStart > 0) && (BuildStart - PreReleaseStart < 2))) {
91 			// There is nothing in PreRelease, if nothing follow `-` .
92 				throw new SemVerException("There is no prerelease version string");
93 		} else if(i.length - BuildStart < 2) {
94 			// There is no in Build, if string ends after `+`.
95 			throw new SemVerException("There is no build version string");
96 		}
97 
98 		if(isNumeric(i[0..MajorDot])) {
99 			if((MajorDot > 1) && (to!size_t(i[0..1]) == 0))
100 				throw new SemVerException("Major cannot begin with '0'");
101 
102 			Major = to!size_t(i[0..MajorDot]);
103 		} else {
104 			throw new SemVerException("There is a non-number in major");
105 		}
106 
107 		if(isNumeric(i[MajorDot+1..MinorDot])) {
108 			if((MinorDot - MajorDot > 2) && (to!size_t(i[MajorDot+1..MajorDot+2]) == 0))
109 				throw new SemVerException("Minor cannot begin with '0'");
110 
111 			Minor = to!size_t(i[MajorDot+1..MinorDot]);
112 		} else {
113 			throw new SemVerException("There is a non-number in minor");
114 		}
115 
116 		if(PreReleaseStart) {
117 			if(isNumeric(i[MinorDot+1..PreReleaseStart])) {
118 				if((PreReleaseStart - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0))
119 					throw new SemVerException("Patch cannot begin with '0'");
120 
121 				Patch = to!size_t(i[MinorDot+1..PreReleaseStart]);
122 			} else {
123 				throw new SemVerException("There is a non-number in patch");
124 			}
125 			if(BuildStart) {
126 				PreRelease = i[PreReleaseStart+1..BuildStart];
127 			} else {
128 				PreRelease = i[PreReleaseStart+1..$];
129 			}
130 		} else {
131 			if(BuildStart) {
132 				if(isNumeric(i[MinorDot+1..BuildStart])) {
133 					if((BuildStart - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0))
134 						throw new SemVerException("Patch cannot begin with '0'");
135 
136 					Patch = to!size_t(i[MinorDot+1..BuildStart]);
137 				} else {
138 					throw new SemVerException("There is a non-number in patch");
139 				}
140 				Build = i[BuildStart+1..$];
141 			} else {
142 				if(isNumeric(i[MinorDot+1..$])) {
143 					if((i.length - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0))
144 						throw new SemVerException("Patch cannot begin with '0'");
145 
146 					Patch = to!size_t(i[MinorDot+1..$]);
147 				} else {
148 					throw new SemVerException("There is a non-number in patch");
149 				}
150 			}
151 		}
152 	}
153 
154 	/**
155 	* Next Major/Minor/Patch version
156 	* Increments numbers with semver rules.
157 	* Example:
158 	* 	1.2.3 -> nextMajor -> 2.0.0
159 	* 	1.2.3 -> nextMinor -> 1.3.0
160 	* 	1.2.3 -> nextPatch -> 1.2.4
161 	* 	1.2.3-rc.1+build.5 -> nextPatch -> 1.2.4
162 	*/
163 	@safe @nogc pure nothrow void nextMajor() {
164 		Major++;
165 		Minor = Patch = 0;
166 		PreRelease = Build = "";
167 	}
168 	/// ditto
169 	@safe @nogc pure nothrow void nextMinor() {
170 		Minor++;
171 		Patch = 0;
172 		PreRelease = Build = "";
173 	}
174 	/// ditto
175 	@safe @nogc pure nothrow void nextPatch() {
176 		Patch++;
177 		PreRelease = Build = "";
178 	}
179 
180 	/**
181 	* Convert SemVer to string
182 	* Returns: SemVer in string (MAJOR.MINOR.PATCH-PRERELEASE+BUILD)
183 	*/
184 	@safe pure string toString() {
185 		import std.array : appender;
186 		import std.format : formattedWrite;
187 
188 		auto writer = appender!string();
189 		formattedWrite(writer, "%d.%d.%d", Major, Minor, Patch);
190 		if(PreRelease != "")
191 			formattedWrite(writer, "-%s", PreRelease);
192 		if(Build != "")
193 			formattedWrite(writer, "+%s", Build);
194 		return writer.data;
195 	}
196 
197 	/**
198 	* true, if this == b
199 	*/
200 	@safe @nogc pure nothrow const bool opEquals()(auto ref const SemVer b) {
201 		return (this.Major == b.Major) &&
202 			(this.Minor == b.Minor) &&
203 			(this.Patch == b.Patch) &&
204 			(this.PreRelease == b.PreRelease);
205 	}
206 
207 	/**
208 	* Compares two SemVer structs.
209 	*/
210 	@safe const int opCmp(ref const SemVer b) {
211 		import natcmp;
212 
213 		if(this == b)
214 			return 0;
215 
216 		if(this.Major != b.Major)
217 			return this.Major < b.Major ? -1 : 1;
218 		else if(this.Minor != b.Minor)
219 			return this.Minor < b.Minor ? -1 : 1;
220 		else if(this.Major != b.Major)
221 			return this.Major < b.Major ? -1 : 1;
222 
223 		if((this.PreRelease != "") && (b.PreRelease != "")) {
224 			int result = compareNatural(this.PreRelease, b.PreRelease);
225 			if(result) {
226 				return result;
227 			}
228 		} else if(this.PreRelease != "") {
229 			return -1;
230 		} else if(b.PreRelease != "") {
231 			return 1;
232 		}
233 
234 		throw new SemVerException("I don't know, how you got that error: SemVer is not an equal, but also not an different");
235 	}
236 	/// ditto
237 	@safe const int opCmp(in SemVer b) {
238 		return this.opCmp(b);
239 	}
240 	///
241 	unittest {
242 		assert(SemVer("1.0.0-alpha") < SemVer("1.0.0-alpha.1"));
243 		assert(SemVer("1.0.0-alpha.1") < SemVer("1.0.0-alpha.beta"));
244 		assert(SemVer("1.0.0-alpha.beta") < SemVer("1.0.0-beta"));
245 		assert(SemVer("1.0.0-beta") < SemVer("1.0.0-beta.2"));
246 		assert(SemVer("1.0.0-beta.2") < SemVer("1.0.0-beta.11"));
247 		assert(SemVer("1.0.0-beta.11") < SemVer("1.0.0-rc.1"));
248 		assert(SemVer("1.0.0-rc.1") < SemVer("1.0.0"));
249 		assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0+build.9"));
250 		assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0-rc.1+build.5"));
251 		assert(SemVer("1.0.0-rc.1+build.5") == SemVer("1.0.0-rc.1+build.5"));
252 	}
253 }