1 /*
2                                     __
3                                    / _|
4   __ _ _   _ _ __ ___  _ __ __ _  | |_ ___  ___ ___
5  / _` | | | | '__/ _ \| '__/ _` | |  _/ _ \/ __/ __|
6 | (_| | |_| | | | (_) | | | (_| | | || (_) \__ \__ \
7  \__,_|\__,_|_|  \___/|_|  \__,_| |_| \___/|___/___/
8 
9 Copyright (C) 2018-2019 Aurora Free Open Source Software.
10 Copyright (C) 2013-2016 rejectedsoftware e.K.
11 Copyright (C) 2013-2016 Sönke Ludwig
12 Copyright (C) 2018-2019 Luís Ferreira <luis@aurorafoss.org>
13 
14 This file is part of the Aurora Free Open Source Software. This
15 organization promote free and open source software that you can
16 redistribute and/or modify under the terms of the GNU Lesser General
17 Public License Version 3 as published by the Free Software Foundation or
18 (at your option) any later version approved by the Aurora Free Open Source
19 Software Organization. The license is available in the package root path
20 as 'LICENSE' file. Please review the following information to ensure the
21 GNU Lesser General Public License version 3 requirements will be met:
22 https://www.gnu.org/licenses/lgpl.html .
23 
24 Alternatively, this file may be used under the terms of the GNU General
25 Public License version 3 or later as published by the Free Software
26 Foundation. Please review the following information to ensure the GNU
27 General Public License requirements will be met:
28 http://www.gnu.org/licenses/gpl-3.0.html.
29 
30 NOTE: All products, services or anything associated to trademarks and
31 service marks used or referenced on this file are the property of their
32 respective companies/owners or its subsidiaries. Other names and brands
33 may be claimed as the property of others.
34 
35 For more info about intellectual property visit: aurorafoss.org or
36 directly send an email to: contact (at) aurorafoss.org .
37 
38 This file was based on DUB and Ruby Gems.
39 More about DUB: https://github.com/dlang/dub
40 More about Ruby Gems: https://github.com/rubygems/rubygems
41 */
42 
43 /++
44 Semantic Versioning
45 
46 This file defines semantic versioning validators and structs for versioning
47 
48 Authors: Luís Ferreira <luis@aurorafoss.org>
49 Copyright: All rights reserved, Aurora Free Open Source Software
50 License: GNU Lesser General Public License (Version 3, 29 June 2007)
51 Date: 2018-2019
52 +/
53 module aurorafw.core.semver;
54 
55 import std.conv : to, ConvException;
56 import std.traits : isIntegral;
57 import std.ascii : isDigit;
58 import std.uni : isNumber;
59 import aurorafw.stdx.array : replaceFirst;
60 import std..string;
61 import std.range.primitives;
62 import std.format;
63 import std.algorithm;
64 import std.exception : enforce;
65 import core.exception;
66 
67 version(unittest) import aurorafw.unit.assertion;
68 import aurorafw.stdx..string : indexOfAny;
69 
70 /** Version struct
71  *
72  * Data struct to store version with MAJOR.MINOR.PATCH format.
73  *
74  * Examples:
75  * --------------------
76  * Version foo = Version(3, 1);
77  * assert(foo.to!string == "3.1.0");
78  * --------------------
79  */
80 @safe pure
81 struct Version {
82 	/// Major version
83 	public uint major;
84 	/// Minor version
85 	public uint minor;
86 	/// Patch version
87 	public uint patch;
88 
89 
90 	/// Default constructor
91 	@safe pure @nogc
92 	public this(M = uint, N = uint, P = uint)(M major = uint.init, N minor = uint.init, P patch = uint.init)
93 	if(isIntegral!M && isIntegral!N && isIntegral!P)
94 	{
95 		this.major = major;
96 		this.minor = minor;
97 		this.patch = patch;
98 	}
99 
100 
101 	/** Construct from string
102 	 * Use common version strings and convert to
103 	 * MAJOR.MINOR.PATCH version scheme if its possible
104 	 *
105 	 * Throws: FormatException if format is invalid
106 	 */
107 	@safe pure
108 	public this(string str)
109 	{
110 		if(str.empty)
111 			return;
112 
113 		if(str.length > 0 && str[0] == 'v')
114 			str = str[1 .. $];
115 
116 		auto strSplitted = str.split('.');
117 
118 		try {
119 			foreach(i, verStr; strSplitted)
120 			{
121 				if((i + 1 == strSplitted.length && i <= 2) || i == 2)
122 				{
123 					auto verStrReplaced = verStr.replaceFirst('+', '-');
124 					auto verStrSplitted = verStrReplaced.split('-');
125 					this[i] = verStrSplitted.front.to!uint;
126 					if(verStrSplitted.length > 1 &&
127 						verStrReplaced == verStr &&
128 						verStrSplitted[1].all!(isNumber) &&
129 						i != 2)
130 					{
131 						this[2] = verStrSplitted[1].to!uint;
132 					}
133 				} else if(i >= 3)
134 				{
135 					return;
136 				}
137 				else {
138 					this[i] = verStr.to!uint;
139 				}
140 			}
141 		}
142 		catch(Exception e)
143 		{
144 			throw new FormatException("Invalid version format", e.file, e.line);
145 		}
146 
147 	}
148 
149 
150 	@safe pure
151 	uint[] opIndex()
152 	{
153 		return [major, minor, patch];
154 	}
155 
156 
157 	@safe pure
158 	uint opIndex(size_t idx)
159 	{
160 		enforce(idx < 3, "Invalid index");
161 
162 		return opIndex[idx];
163 	}
164 
165 
166 	@safe pure
167 	uint opIndexAssign(uint val, size_t idx)
168 	{
169 		enforce(idx < 3, "Invalid index");
170 
171 		final switch(idx)
172 		{
173 			case 0: return this.major = val;
174 			case 1: return this.minor = val;
175 			case 2: return this.patch = val;
176 		}
177 	}
178 
179 	@safe pure
180 	int opCmp(ref const Version v) const // for l-values
181 	{
182 		return (this.major != v.major) ? this.major - v.major :
183 				(this.minor != v.minor) ? this.minor - v.minor :
184 				(this.patch != v.patch) ? this.patch - v.patch :
185 				0;
186 	}
187 
188 	@safe pure
189 	int opCmp(const Version v) const // for r-values
190 	{
191 		return opCmp(v);
192 	}
193 
194 	@safe pure
195 	public string toString()
196 	{
197 		return major.to!string ~ "." ~ minor.to!string ~ "." ~ patch.to!string;
198 	}
199 }
200 
201 
202 ///
203 @safe pure
204 @("Versioning: Check for .init in ctor")
205 unittest
206 {
207 	Version ver = Version();
208 	assertEquals(0, ver.major);
209 	assertEquals(0, ver.minor);
210 	assertEquals(0, ver.patch);
211 
212 	ver = Version(1);
213 	assertEquals(1, ver.major);
214 	assertEquals(0, ver.minor);
215 	assertEquals(0, ver.patch);
216 
217 	ver = Version(2, 1);
218 	assertEquals(2, ver.major);
219 	assertEquals(1, ver.minor);
220 	assertEquals(0, ver.patch);
221 
222 	ver = Version(1, 2, 4);
223 	assertEquals(1, ver.major);
224 	assertEquals(2, ver.minor);
225 	assertEquals(4, ver.patch);
226 }
227 
228 
229 ///
230 @safe pure
231 @("Versioning: index operator")
232 unittest
233 {
234 	Version ver = Version(1, 4, 5);
235 	assertEquals(1, ver[0]);
236 	assertEquals(4, ver[1]);
237 	assertEquals(5, ver[2]);
238 
239 	ver[2] = 2;
240 	assertEquals(Version(1, 4, 2), ver);
241 
242 	expectThrows(ver[3] = 0);
243 	expectThrows(ver[3]);
244 }
245 
246 
247 ///
248 @safe pure
249 @("Versioning: ctor from string")
250 unittest
251 {
252 	Version ver = Version("");
253 	assertEquals(Version(), ver);
254 
255 	// supress minor and patch
256 	ver = Version("1");
257 	assertEquals(Version(1), ver);
258 
259 	// supress patch
260 	ver = Version("2.1");
261 	assertEquals(Version(2, 1, 0), ver);
262 
263 	ver = Version("1.2.4");
264 	assertEquals(Version(1, 2, 4), ver);
265 
266 	ver = Version("v1.2.4");
267 	assertEquals(Version(1, 2, 4), ver);
268 
269 	ver = Version("1.2.4-alpha.1");
270 	assertEquals(Version(1, 2, 4), ver);
271 
272 	ver = Version("1.2-1");
273 	assertEquals(Version(1, 2, 1), ver);
274 
275 	ver = Version("1.2.4-1");
276 	assertEquals(Version(1, 2, 4), ver);
277 
278 	ver = Version("2-1");
279 	assertEquals(Version(2, 0, 1), ver);
280 
281 	ver = Version("1.2+543");
282 	assertEquals(Version(1, 2), ver);
283 
284 	ver = Version("1.2.1+543");
285 	assertEquals(Version(1, 2, 1), ver);
286 
287 	expectThrows(Version("-alpha.1"));
288 	expectThrows(Version(".-rc.1"));
289 	expectThrows(Version("1.."));
290 	expectThrows(Version("vv1.1"));
291 	expectThrows(Version("1.1-"));
292 	expectThrows(Version("-1"));
293 	expectThrows(Version("-"));
294 	expectThrows(Version("alpha"));
295 	expectThrows(Version("."));
296 }
297 
298 
299 ///
300 @safe pure
301 @("Versioning: string conversion")
302 unittest {
303 	Version ver = Version(1, 3, 4);
304 	assertEquals("1.3.4", ver.to!string);
305 
306 	ver = Version(1, 3);
307 	assertEquals("1.3.0", ver.to!string);
308 }
309 
310 ///
311 @safe pure
312 @("Versioning: boolean operations")
313 unittest {
314 	assertGreaterThan(Version(2), Version(1));
315 	assertGreaterThan(Version(2), Version(1, 2, 3));
316 	assertLessThan(Version(2), Version(3));
317 
318 	assertFalse(Version(1) < Version(1) || Version(1) > Version(1));
319 	assertTrue(Version(1) >= Version(1) && Version(1) <= Version(1));
320 
321 	assertEquals(Version(2), Version(2));
322 	assertGreaterThan(Version(1, 0, 1), Version(1, 0, 0));
323 	assertLessThan(Version(1, 0, 32), Version(1, 1, 0));
324 }
325 
326 
327 /**
328  * Check if the given version is semver compatible
329  */
330 @safe pure
331 bool isValidVersion(string str)
332 {
333 	try {
334 		Version(str);
335 		return true;
336 	} catch (FormatException )
337 	{
338 		return false;
339 	}
340 }
341 
342 @("Versioning: check valid version")
343 @safe pure
344 unittest {
345 	enum testCTFE = isValidVersion("1.0.0");
346 
347 	assertTrue(isValidVersion("1.0.0"));
348 	assertFalse(isValidVersion("1. -"));
349 
350 	//aditional tests
351 	assertTrue(isValidVersion("01.9.0"));
352 	assertTrue(isValidVersion("1.09.0"));
353 	assertTrue(isValidVersion("1.9.00"));
354 	assertTrue(isValidVersion("1.0.0-alpha"));
355 	assertTrue(isValidVersion("1.0.0-alpha.1"));
356 	assertTrue(isValidVersion("1.0.0-0.3.7"));
357 	assertTrue(isValidVersion("1.0.0-x.7.z.92"));
358 	assertTrue(isValidVersion("1.0.0-x.7-z.92"));
359 	assertTrue(isValidVersion("1.0.0-00.3.7"));
360 	assertTrue(isValidVersion("1.0.0-0.03.7"));
361 	assertTrue(isValidVersion("1.0.0-alpha+001"));
362 	assertTrue(isValidVersion("1.0.0+20130313144700"));
363 	assertTrue(isValidVersion("1.0.0-beta+exp.sha.5114f85"));
364 	assertFalse(isValidVersion(" 1.0.0"));
365 	assertFalse(isValidVersion("1. 0.0"));
366 	assertFalse(isValidVersion("1.0 .0"));
367 	assertFalse(isValidVersion("1.0.0 "));
368 }
369 
370 /**
371  * Validates a version string according to the SemVer specification.
372  */
373 @safe pure @nogc
374 bool isSemVer(string ver)
375 {
376 	// a
377 	auto sepi = ver.indexOf('.');
378 	if (sepi < 0) return false;
379 	if (!isSemVerNumber(ver[0 .. sepi])) return false;
380 	ver = ver[sepi+1 .. $];
381 
382 	// c
383 	sepi = ver.indexOf('.');
384 	if (sepi < 0) return false;
385 	if (!isSemVerNumber(ver[0 .. sepi])) return false;
386 	ver = ver[sepi+1 .. $];
387 
388 	// c
389 	sepi = ver.indexOfAny("-+");
390 	if (sepi < 0) sepi = ver.length;
391 	if (!isSemVerNumber(ver[0 .. sepi])) return false;
392 	ver = ver[sepi .. $];
393 
394 	@safe pure @nogc
395 	bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
396 	{
397 		bool isValidIdentifier(string str, bool allow_leading_zeros = false)
398 		pure @nogc {
399 			if (str.length < 1) return false;
400 
401 			bool numeric = true;
402 			foreach (ch; str) {
403 				switch (ch) {
404 					default: return false;
405 					case 'a': .. case 'z':
406 					case 'A': .. case 'Z':
407 					case '-':
408 						numeric = false;
409 						break;
410 					case '0': .. case '9':
411 						break;
412 				}
413 			}
414 
415 			if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false;
416 
417 			return true;
418 		}
419 
420 		if (str.length == 0) return false;
421 		while (str.length) {
422 			auto end = str.indexOf('.');
423 			if (end < 0) end = str.length;
424 			if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false;
425 			if (end < str.length) str = str[end+1 .. $];
426 			else break;
427 		}
428 		return true;
429 	}
430 
431 	// prerelease tail
432 	if (ver.length > 0 && ver[0] == '-') {
433 		ver = ver[1 .. $];
434 		sepi = ver.indexOf('+');
435 		if (sepi < 0) sepi = ver.length;
436 		if (!isValidIdentifierChain(ver[0 .. sepi])) return false;
437 		ver = ver[sepi .. $];
438 	}
439 
440 	// build tail
441 	if (ver.length > 0 && ver[0] == '+') {
442 		ver = ver[1 .. $];
443 		if (!isValidIdentifierChain(ver, true)) return false;
444 		ver = null;
445 	}
446 
447 	assert(ver.length == 0);
448 	return true;
449 }
450 
451 
452 ///
453 @("Versioning: check valid semantic version")
454 @safe pure
455 unittest {
456 	enum testCTFE = isSemVer("1.0.0");
457 
458 	assertTrue(isSemVer("1.9.0"));
459 	assertTrue(isSemVer("0.10.0"));
460 	assertFalse(isSemVer("01.9.0"));
461 	assertFalse(isSemVer("1.09.0"));
462 	assertFalse(isSemVer("1.9.00"));
463 	assertTrue(isSemVer("1.0.0-alpha"));
464 	assertTrue(isSemVer("1.0.0-alpha.1"));
465 	assertTrue(isSemVer("1.0.0-0.3.7"));
466 	assertTrue(isSemVer("1.0.0-x.7.z.92"));
467 	assertTrue(isSemVer("1.0.0-x.7-z.92"));
468 	assertFalse(isSemVer("1.0.0-00.3.7"));
469 	assertFalse(isSemVer("1.0.0-0.03.7"));
470 	assertTrue(isSemVer("1.0.0-alpha+001"));
471 	assertTrue(isSemVer("1.0.0+20130313144700"));
472 	assertTrue(isSemVer("1.0.0-beta+exp.sha.5114f85"));
473 	assertFalse(isSemVer(" 1.0.0"));
474 	assertFalse(isSemVer("1. 0.0"));
475 	assertFalse(isSemVer("1.0 .0"));
476 	assertFalse(isSemVer("1.0.0 "));
477 	assertFalse(isSemVer("1.0.0-a_b"));
478 	assertFalse(isSemVer("1.0.0+"));
479 	assertFalse(isSemVer("1.0.0-"));
480 	assertFalse(isSemVer("1.0.0-+a"));
481 	assertFalse(isSemVer("1.0.0-a+"));
482 	assertFalse(isSemVer("1.0"));
483 	assertFalse(isSemVer("1.0-1.0"));
484 }
485 
486 
487 /**
488  * Takes a partial version and expands it to a valid SemVer version.
489  *
490  * This function corresponds to the semantivs of the "~>" comparison operator's
491  * lower bound.
492  *
493  * See_Also: `bumpVersion`
494  */
495 @safe pure
496 string expandVersion(string ver)
497 {
498 	auto mi = ver.indexOfAny("+-");
499 	auto sub = "";
500 	if (mi > 0) {
501 		sub = ver[mi..$];
502 		ver = ver[0..mi];
503 	}
504 	auto splitted = () @trusted { return split(ver, "."); } ();
505 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
506 	while (splitted.length < 3) splitted ~= "0";
507 	return splitted.join(".") ~ sub;
508 }
509 
510 
511 ///
512 @("Versioning: expand version")
513 @safe pure
514 unittest {
515 	assertEquals("1.0.0", expandVersion("1"));
516 	assertEquals("1.0.0", expandVersion("1.0"));
517 	assertEquals("1.0.0", expandVersion("1.0.0"));
518 	// These are rather excotic variants...
519 	assertEquals("1.0.0-pre.release", expandVersion("1-pre.release"));
520 	assertEquals("1.0.0+meta", expandVersion("1+meta"));
521 	assertEquals("1.0.0-pre.release+meta", expandVersion("1-pre.release+meta"));
522 }
523 
524 
525 /**
526  * Determines if a given valid SemVer version has a pre-release suffix.
527  */
528 @safe pure
529 bool isPreReleaseVersion(string ver)
530 in { assert(isSemVer(ver)); }
531 body {
532 	foreach (i; 0 .. 2) {
533 		auto di = ver.indexOf('.');
534 		assert(di > 0);
535 		ver = ver[di+1 .. $];
536 	}
537 	auto di = ver.indexOf('-');
538 	if (di < 0) return false;
539 	return isSemVerNumber(ver[0 .. di]);
540 }
541 
542 ///
543 @("Versioning: check pre-release")
544 @safe pure
545 unittest {
546 	assertTrue(isPreReleaseVersion("1.0.0-alpha"));
547 	assertTrue(isPreReleaseVersion("1.0.0-alpha+b1"));
548 	assertTrue(isPreReleaseVersion("0.9.0-beta.1"));
549 	assertFalse(isPreReleaseVersion("0.9.0"));
550 	assertFalse(isPreReleaseVersion("0.9.0+b1"));
551 }
552 
553 
554 
555 /**
556  * Compares the precedence of two SemVer version strings.
557 
558  * The version strings must be validated using `isSemVer` before being
559  * passed to this function. Note that the build meta data suffix (if any) is
560  * being ignored when comparing version numbers.
561 
562  * Returns:
563  *      Returns a negative number if `a` is a lower version than `b`, `0` if they are
564  *      equal, and a positive number otherwise.
565  */
566 @safe pure @nogc nothrow
567 int compareSemVer(string a, string b)
568 {
569 	@safe pure @nogc nothrow
570 	int compareIdentifier(ref string a, ref string b)
571 	{
572 		bool anumber = true;
573 		bool bnumber = true;
574 		bool aempty = true, bempty = true;
575 		int res = 0;
576 		while (true) {
577 			if (a[0] != b[0] && res == 0) res = a[0] - b[0];
578 			if (anumber && (a[0] < '0' || a[0] > '9')) anumber = false;
579 			if (bnumber && (b[0] < '0' || b[0] > '9')) bnumber = false;
580 			a = a[1 .. $]; b = b[1 .. $];
581 			aempty = !a.length || a[0] == '.' || a[0] == '+';
582 			bempty = !b.length || b[0] == '.' || b[0] == '+';
583 			if (aempty || bempty) break;
584 		}
585 
586 		if (anumber && bnumber) {
587 			if (aempty != bempty) return bempty - aempty;
588 			return res;
589 		} else {
590 			if (anumber && aempty) return -1;
591 			if (bnumber && bempty) return 1;
592 
593 			static assert('0' < 'a' && '0' < 'A');
594 			if (res != 0) return res;
595 			return bempty - aempty;
596 		}
597 	}
598 
599 	@safe pure @nogc nothrow
600 	int compareNumber(ref string a, ref string b)
601 	{
602 		int res = 0;
603 		while (true) {
604 			if (a[0] != b[0] && res == 0) res = a[0] - b[0];
605 			a = a[1 .. $]; b = b[1 .. $];
606 			auto aempty = !a.length || (a[0] < '0' || a[0] > '9');
607 			auto bempty = !b.length || (b[0] < '0' || b[0] > '9');
608 			if (aempty != bempty) return bempty - aempty;
609 			if (aempty) return res;
610 		}
611 	}
612 
613 	// compare a.b.c numerically
614 	if (auto ret = compareNumber(a, b)) return ret;
615 	assert(a[0] == '.' && b[0] == '.');
616 	a = a[1 .. $]; b = b[1 .. $];
617 	if (auto ret = compareNumber(a, b)) return ret;
618 	assert(a[0] == '.' && b[0] == '.');
619 	a = a[1 .. $]; b = b[1 .. $];
620 	if (auto ret = compareNumber(a, b)) return ret;
621 
622 	// give precedence to non-prerelease versions
623 	bool apre = a.length > 0 && a[0] == '-';
624 	bool bpre = b.length > 0 && b[0] == '-';
625 	if (apre != bpre) return bpre - apre;
626 	if (!apre) return 0;
627 
628 	// compare the prerelease tail lexicographically
629 	do {
630 		a = a[1 .. $]; b = b[1 .. $];
631 		if (auto ret = compareIdentifier(a, b)) return ret;
632 	} while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+');
633 
634 	// give longer prerelease tails precedence
635 	bool aempty = a.length == 0 || a[0] == '+';
636 	bool bempty = b.length == 0 || b[0] == '+';
637 	if (aempty == bempty) {
638 		assert(aempty);
639 		return 0;
640 	}
641 	return bempty - aempty;
642 }
643 
644 ///
645 @("Versioning: semver comparison")
646 @safe pure
647 unittest {
648 	assertEquals(0, compareSemVer("1.0.0", "1.0.0"));
649 	assertEquals(0, compareSemVer("1.0.0+b1", "1.0.0+b2"));
650 	assertLessThan(compareSemVer("1.0.0", "2.0.0"), 0);
651 	assertLessThan(compareSemVer("1.0.0-beta", "1.0.0"), 0);
652 	assertGreaterThan(compareSemVer("1.0.1", "1.0.0"), 0);
653 
654 	void assertLess(string a, string b) {
655 		assertLessThan(compareSemVer(a, b), 0);
656 		assertGreaterThan(compareSemVer(b, a), 0);
657 		assertEquals(0, compareSemVer(a, a));
658 		assertEquals(0, compareSemVer(b, b));
659 	}
660 	assertLess("1.0.0", "2.0.0");
661 	assertLess("2.0.0", "2.1.0");
662 	assertLess("2.1.0", "2.1.1");
663 	assertLess("1.0.0-alpha", "1.0.0");
664 	assertLess("1.0.0-alpha", "1.0.0-alpha.1");
665 	assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta");
666 	assertLess("1.0.0-alpha.beta", "1.0.0-beta");
667 	assertLess("1.0.0-beta", "1.0.0-beta.2");
668 	assertLess("1.0.0-beta.2", "1.0.0-beta.11");
669 	assertLess("1.0.0-beta.11", "1.0.0-rc.1");
670 	assertLess("1.0.0-rc.1", "1.0.0");
671 	assertEquals(0, compareSemVer("1.0.0", "1.0.0+1.2.3"));
672 	assertEquals(0, compareSemVer("1.0.0", "1.0.0+1.2.3-2"));
673 	assertEquals(0, compareSemVer("1.0.0+asdasd", "1.0.0+1.2.3"));
674 	assertLess("2.0.0", "10.0.0");
675 	assertLess("1.0.0-2", "1.0.0-10");
676 	assertLess("1.0.0-99", "1.0.0-1a");
677 	assertLess("1.0.0-99", "1.0.0-a");
678 	assertLess("1.0.0-alpha", "1.0.0-alphb");
679 	assertLess("1.0.0-alphz", "1.0.0-alphz0");
680 	assertLess("1.0.0-alphZ", "1.0.0-alpha");
681 }
682 
683 
684 /**
685  * Increments a given (partial) version number to the next higher version.
686  *
687  * Prerelease and build metadata information is ignored. The given version
688  * can skip the minor and patch digits. If no digits are skipped, the next
689  * minor version will be selected. If the patch or minor versions are skipped,
690  * the next major version will be selected.
691  *
692  * This function corresponds to the semantivs of the "~>" comparison operator's
693  * upper bound.
694  *
695  * The semantics of this are the same as for the "approximate" version
696  * specifier from rubygems.
697  * (https://github.com/rubygems/rubygems/tree/81d806d818baeb5dcb6398ca631d772a003d078e/lib/rubygems/version.rb)
698  *
699  * See_Also: `expandVersion`
700  */
701 @safe pure
702 string bumpVersion(string ver)
703 {
704 	// Cut off metadata and prerelease information.
705 	auto mi = ver.indexOfAny("+-");
706 	if (mi > 0) ver = ver[0..mi];
707 	// Increment next to last version from a[.b[.c]].
708 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
709 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
710 	auto to_inc = splitted.length == 3? 1 : 0;
711 	splitted = splitted[0 .. to_inc+1];
712 	splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1);
713 	// Fill up to three compontents to make valid SemVer version.
714 	while (splitted.length < 3) splitted ~= "0";
715 	return splitted.join(".");
716 }
717 
718 
719 ///
720 @("Versioning: bump version")
721 @safe pure
722 unittest {
723 	assertEquals("1.0.0", bumpVersion("0"));
724 	assertEquals("1.0.0", bumpVersion("0.0"));
725 	assertEquals("0.1.0", bumpVersion("0.0.0"));
726 	assertEquals("1.3.0", bumpVersion("1.2.3"));
727 	assertEquals("1.3.0", bumpVersion("1.2.3+metadata"));
728 	assertEquals("1.3.0", bumpVersion("1.2.3-pre.release"));
729 	assertEquals("1.3.0", bumpVersion("1.2.3-pre.release+metadata"));
730 }
731 
732 
733 @safe pure @nogc nothrow
734 private bool isSemVerNumber(string str)
735 {
736 	if (str.length < 1) return false;
737 	foreach (ch; str)
738 		if (ch < '0' || ch > '9')
739 			return false;
740 
741 	if (str[0] == '0' && str.length > 1) return false;
742 
743 	return true;
744 }