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 	* Define BrightProof_ForceSTD version for force using standard library functions.
46 	* Standard library functions is slower, but more safe to use.
47 	* Usualy you don't have to use it.
48 	* Params:
49 	*	i = input string
50 	* Throws: SemVerException if there is any syntax errors.
51 	*/
52 	pure this(string i) {
53 		import std.string : indexOf;
54 		import std.conv : to;
55 
56 		auto MajorDot = indexOf(i, '.', 0);
57 		auto MinorDot = indexOf(i, '.', MajorDot + 1);
58 		auto PreReleaseStart = indexOf(i, '-', MinorDot + 1);
59 		auto BuildStart = indexOf(i, '+', PreReleaseStart + 1);
60 
61 		if((MajorDot == -1) || (MinorDot == -1)) {
62 			// If there is no 2 dots - this is not complete semver.
63 			throw new SemVerException("There is no major, minor or patch");
64 		} else if(MajorDot < 1) {
65 			// If first symbol is a dot, there is no Major.
66 			throw new SemVerException("There is no major version number");
67 		} else if((MinorDot < 1) || (MinorDot - MajorDot < 2)) {
68 			// If there is nothing between MajorDot and MinorDot.
69 			throw new SemVerException("There is no minor version number");
70 		} else if(
71 			((PreReleaseStart < 1) && (i.length - MinorDot < 2)) ||
72 			((PreReleaseStart >= 0) && (PreReleaseStart - MinorDot < 2))) {
73 			// There is no Patch, if there is nothing after MinorDot.
74 			// and string end or `-`.
75 			throw new SemVerException("There is no patch version number");
76 		} else if(
77 			((BuildStart < 1) && (i.length - PreReleaseStart < 2)) ||
78 			((BuildStart >= 0) && (BuildStart - PreReleaseStart < 2))) {
79 			// There is nothing in PreRelease, if nothing follow `-` .
80 				throw new SemVerException("There is no prerelease version string");
81 		} else if(i.length - BuildStart < 2) {
82 			// There is no in Build, if string ends after `+`.
83 			throw new SemVerException("There is no build version string");
84 		}
85 
86 		// Now we know where Major, Minor, Patch, PreRelease, Build starts and ends.
87 		try {
88 			Major = to!size_t(i[0..MajorDot]);
89 		} catch {
90 			throw new SemVerException("There is a non-number characters in major");
91 		}
92 
93 		try {
94 			Minor = to!size_t(i[MajorDot+1..MinorDot]);
95 		} catch {
96 			throw new SemVerException("There is a non-number characters in minor");
97 		}
98 
99 		if(PreReleaseStart != -1) {
100 			try {
101 				Patch = to!size_t(i[MinorDot+1..PreReleaseStart]);
102 			} catch {
103 				throw new SemVerException("There is a non-number in patch");
104 			}
105 			if(BuildStart != -1) {
106 				PreRelease = i[PreReleaseStart+1..BuildStart];
107 				Build = i[BuildStart+1..$];
108 			} else {
109 				PreRelease = i[PreReleaseStart+1..$];
110 			}
111 		} else {
112 			if(BuildStart != -1) {
113 				try {
114 					Patch = to!size_t(i[MinorDot+1..BuildStart]);
115 				} catch {
116 					throw new SemVerException("There is a non-number in patch");
117 				}
118 				Build = i[BuildStart+1..$];
119 			} else {
120 				try {
121 					Patch = to!size_t(i[MinorDot+1..$]);
122 				} catch {
123 					throw new SemVerException("There is a non-number in patch");
124 				}
125 			}
126 		}
127 	}
128 
129 	/**
130 	* Next Major/Minor/Patch version
131 	* Increments numbers with semver rules.
132 	* Example:
133 	* 	1.2.3 -> nextMajor -> 2.0.0
134 	* 	1.2.3 -> nextMinor -> 1.3.0
135 	* 	1.2.3 -> nextPatch -> 1.2.4
136 	* 	1.2.3-rc.1+build.5 -> nextPatch -> 1.2.4
137 	*/
138 	@safe @nogc pure nothrow void nextMajor() {
139 		Major++;
140 		Minor = Patch = 0;
141 		PreRelease = Build = "";
142 	}
143 	/// ditto
144 	@safe @nogc pure nothrow void nextMinor() {
145 		Minor++;
146 		Patch = 0;
147 		PreRelease = Build = "";
148 	}
149 	/// ditto
150 	@safe @nogc pure nothrow void nextPatch() {
151 		Patch++;
152 		PreRelease = Build = "";
153 	}
154 
155 	/**
156 	* Convert SemVer to string
157 	* Returns: SemVer in string (MAJOR.MINOR.PATCH-PRERELEASE+BUILD)
158 	*/
159 	@safe pure string toString() {
160 		import std.array : appender;
161 		import std.format : formattedWrite;
162 
163 		auto writer = appender!string();
164 		formattedWrite(writer, "%d.%d.%d", Major, Minor, Patch);
165 		if(PreRelease != "")
166 			formattedWrite(writer, "-%s", PreRelease);
167 		if(Build != "")
168 			formattedWrite(writer, "+%s", Build);
169 		return writer.data;
170 	}
171 
172 	/**
173 	* true, if this == b
174 	*/
175 	@safe @nogc pure nothrow const bool opEquals()(auto ref const SemVer b) {
176 		return (this.Major == b.Major) &&
177 			(this.Minor == b.Minor) &&
178 			(this.Patch == b.Patch) &&
179 			(this.PreRelease == b.PreRelease);
180 	}
181 
182 	/**
183 	* Compares two SemVer structs.
184 	*/
185 	@safe const int opCmp(ref const SemVer b) {
186 		import natcmp;
187 
188 		if(this == b)
189 			return 0;
190 
191 		if(this.Major != b.Major)
192 			return this.Major < b.Major ? -1 : 1;
193 		else if(this.Minor != b.Minor)
194 			return this.Minor < b.Minor ? -1 : 1;
195 		else if(this.Major != b.Major)
196 			return this.Major < b.Major ? -1 : 1;
197 
198 		if((this.PreRelease != "") && (b.PreRelease != "")) {
199 			int result = compareNatural(this.PreRelease, b.PreRelease);
200 			if(result) {
201 				return result;
202 			}
203 		} else if(this.PreRelease != "") {
204 			return -1;
205 		} else if(b.PreRelease != "") {
206 			return 1;
207 		}
208 
209 		throw new SemVerException("I don't know, how you got that error: SemVer is not an equal, but also not an different");
210 	}
211 	/// ditto
212 	@safe const int opCmp(in SemVer b) {
213 		return this.opCmp(b);
214 	}
215 	///
216 	unittest {
217 		assert(SemVer("1.0.0-alpha") < SemVer("1.0.0-alpha.1"));
218 		assert(SemVer("1.0.0-alpha.1") < SemVer("1.0.0-alpha.beta"));
219 		assert(SemVer("1.0.0-alpha.beta") < SemVer("1.0.0-beta"));
220 		assert(SemVer("1.0.0-beta") < SemVer("1.0.0-beta.2"));
221 		assert(SemVer("1.0.0-beta.2") < SemVer("1.0.0-beta.11"));
222 		assert(SemVer("1.0.0-beta.11") < SemVer("1.0.0-rc.1"));
223 		assert(SemVer("1.0.0-rc.1") < SemVer("1.0.0"));
224 		assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0+build.9"));
225 		assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0-rc.1+build.5"));
226 		assert(SemVer("1.0.0-rc.1+build.5") == SemVer("1.0.0-rc.1+build.5"));
227 	}
228 }