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 {
83 	/// Major version
84 	public uint major;
85 	/// Minor version
86 	public uint minor;
87 	/// Patch version
88 	public uint patch;
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 	/** Construct from string
101 	 * Use common version strings and convert to
102 	 * MAJOR.MINOR.PATCH version scheme if its possible
103 	 *
104 	 * Throws: FormatException if format is invalid
105 	 */
106 	@safe pure
107 	public this(string str)
108 	{
109 		if (str.empty)
110 			return;
111 
112 		if (str.length > 0 && str[0] == 'v')
113 			str = str[1 .. $];
114 
115 		auto strSplitted = str.split('.');
116 
117 		try
118 		{
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 				}
134 				else if (i >= 3)
135 				{
136 					return;
137 				}
138 				else
139 				{
140 					this[i] = verStr.to!uint;
141 				}
142 			}
143 		}
144 		catch (Exception e)
145 		{
146 			throw new FormatException("Invalid version format", e.file, e.line);
147 		}
148 
149 	}
150 
151 	@safe pure
152 	uint[] opIndex()
153 	{
154 		return [major, minor, patch];
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 	@safe pure
166 	uint opIndexAssign(uint val, size_t idx)
167 	{
168 		enforce(idx < 3, "Invalid index");
169 
170 		final switch (idx)
171 		{
172 		case 0:
173 			return this.major = val;
174 		case 1:
175 			return this.minor = val;
176 		case 2:
177 			return this.patch = val;
178 		}
179 	}
180 
181 	@safe pure
182 	int opCmp(ref const Version v) const  // for l-values
183 	{
184 		return (this.major != v.major) ? this.major - v.major : (this.minor != v.minor) ? this.minor - v.minor
185 			: (this.patch != v.patch) ? this.patch - v.patch : 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 @safe pure
203 @("Versioning: Check for .init in ctor")
204 unittest
205 {
206 	Version ver = Version();
207 	assertEquals(0, ver.major);
208 	assertEquals(0, ver.minor);
209 	assertEquals(0, ver.patch);
210 
211 	ver = Version(1);
212 	assertEquals(1, ver.major);
213 	assertEquals(0, ver.minor);
214 	assertEquals(0, ver.patch);
215 
216 	ver = Version(2, 1);
217 	assertEquals(2, ver.major);
218 	assertEquals(1, ver.minor);
219 	assertEquals(0, ver.patch);
220 
221 	ver = Version(1, 2, 4);
222 	assertEquals(1, ver.major);
223 	assertEquals(2, ver.minor);
224 	assertEquals(4, ver.patch);
225 }
226 
227 ///
228 @safe pure
229 @("Versioning: index operator")
230 unittest
231 {
232 	Version ver = Version(1, 4, 5);
233 	assertEquals(1, ver[0]);
234 	assertEquals(4, ver[1]);
235 	assertEquals(5, ver[2]);
236 
237 	ver[2] = 2;
238 	assertEquals(Version(1, 4, 2), ver);
239 
240 	expectThrows(ver[3] = 0);
241 	expectThrows(ver[3]);
242 }
243 
244 ///
245 @safe pure
246 @("Versioning: ctor from string")
247 unittest
248 {
249 	Version ver = Version("");
250 	assertEquals(Version(), ver);
251 
252 	// supress minor and patch
253 	ver = Version("1");
254 	assertEquals(Version(1), ver);
255 
256 	// supress patch
257 	ver = Version("2.1");
258 	assertEquals(Version(2, 1, 0), ver);
259 
260 	ver = Version("1.2.4");
261 	assertEquals(Version(1, 2, 4), ver);
262 
263 	ver = Version("v1.2.4");
264 	assertEquals(Version(1, 2, 4), ver);
265 
266 	ver = Version("1.2.4-alpha.1");
267 	assertEquals(Version(1, 2, 4), ver);
268 
269 	ver = Version("1.2-1");
270 	assertEquals(Version(1, 2, 1), ver);
271 
272 	ver = Version("1.2.4-1");
273 	assertEquals(Version(1, 2, 4), ver);
274 
275 	ver = Version("2-1");
276 	assertEquals(Version(2, 0, 1), ver);
277 
278 	ver = Version("1.2+543");
279 	assertEquals(Version(1, 2), ver);
280 
281 	ver = Version("1.2.1+543");
282 	assertEquals(Version(1, 2, 1), ver);
283 
284 	expectThrows(Version("-alpha.1"));
285 	expectThrows(Version(".-rc.1"));
286 	expectThrows(Version("1.."));
287 	expectThrows(Version("vv1.1"));
288 	expectThrows(Version("1.1-"));
289 	expectThrows(Version("-1"));
290 	expectThrows(Version("-"));
291 	expectThrows(Version("alpha"));
292 	expectThrows(Version("."));
293 }
294 
295 ///
296 @safe pure
297 @("Versioning: string conversion")
298 unittest
299 {
300 	Version ver = Version(1, 3, 4);
301 	assertEquals("1.3.4", ver.to!string);
302 
303 	ver = Version(1, 3);
304 	assertEquals("1.3.0", ver.to!string);
305 }
306 
307 ///
308 @safe pure
309 @("Versioning: boolean operations")
310 unittest
311 {
312 	assertGreaterThan(Version(2), Version(1));
313 	assertGreaterThan(Version(2), Version(1, 2, 3));
314 	assertLessThan(Version(2), Version(3));
315 
316 	assertFalse(Version(1) < Version(1) || Version(1) > Version(1));
317 	assertTrue(Version(1) >= Version(1) && Version(1) <= Version(1));
318 
319 	assertEquals(Version(2), Version(2));
320 	assertGreaterThan(Version(1, 0, 1), Version(1, 0, 0));
321 	assertLessThan(Version(1, 0, 32), Version(1, 1, 0));
322 }
323 
324 /**
325  * Check if the given version is semver compatible
326  */
327 @safe pure
328 bool isValidVersion(string str)
329 {
330 	try
331 	{
332 		Version(str);
333 		return true;
334 	}
335 	catch (FormatException)
336 	{
337 		return false;
338 	}
339 }
340 
341 @("Versioning: check valid version")
342 @safe pure
343 unittest
344 {
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)
379 		return false;
380 	if (!isSemVerNumber(ver[0 .. sepi]))
381 		return false;
382 	ver = ver[sepi + 1 .. $];
383 
384 	// c
385 	sepi = ver.indexOf('.');
386 	if (sepi < 0)
387 		return false;
388 	if (!isSemVerNumber(ver[0 .. sepi]))
389 		return false;
390 	ver = ver[sepi + 1 .. $];
391 
392 	// c
393 	sepi = ver.indexOfAny("-+");
394 	if (sepi < 0)
395 		sepi = ver.length;
396 	if (!isSemVerNumber(ver[0 .. sepi]))
397 		return false;
398 	ver = ver[sepi .. $];
399 
400 	@safe pure @nogc
401 	bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
402 	{
403 		bool isValidIdentifier(string str, bool allow_leading_zeros = false)
404 		pure @nogc
405 		{
406 			if (str.length < 1)
407 				return false;
408 
409 			bool numeric = true;
410 			foreach (ch; str)
411 			{
412 				switch (ch)
413 				{
414 				default:
415 					return false;
416 				case 'a': .. case 'z':
417 				case 'A': .. case 'Z':
418 				case '-':
419 					numeric = false;
420 					break;
421 				case '0': .. case '9':
422 					break;
423 				}
424 			}
425 
426 			if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1)
427 				return false;
428 
429 			return true;
430 		}
431 
432 		if (str.length == 0)
433 			return false;
434 		while (str.length)
435 		{
436 			auto end = str.indexOf('.');
437 			if (end < 0)
438 				end = str.length;
439 			if (!isValidIdentifier(str[0 .. end], allow_leading_zeros))
440 				return false;
441 			if (end < str.length)
442 				str = str[end + 1 .. $];
443 			else
444 				break;
445 		}
446 		return true;
447 	}
448 
449 	// prerelease tail
450 	if (ver.length > 0 && ver[0] == '-')
451 	{
452 		ver = ver[1 .. $];
453 		sepi = ver.indexOf('+');
454 		if (sepi < 0)
455 			sepi = ver.length;
456 		if (!isValidIdentifierChain(ver[0 .. sepi]))
457 			return false;
458 		ver = ver[sepi .. $];
459 	}
460 
461 	// build tail
462 	if (ver.length > 0 && ver[0] == '+')
463 	{
464 		ver = ver[1 .. $];
465 		if (!isValidIdentifierChain(ver, true))
466 			return false;
467 		ver = null;
468 	}
469 
470 	assert(ver.length == 0);
471 	return true;
472 }
473 
474 ///
475 @("Versioning: check valid semantic version")
476 @safe pure
477 unittest
478 {
479 	enum testCTFE = isSemVer("1.0.0");
480 
481 	assertTrue(isSemVer("1.9.0"));
482 	assertTrue(isSemVer("0.10.0"));
483 	assertFalse(isSemVer("01.9.0"));
484 	assertFalse(isSemVer("1.09.0"));
485 	assertFalse(isSemVer("1.9.00"));
486 	assertTrue(isSemVer("1.0.0-alpha"));
487 	assertTrue(isSemVer("1.0.0-alpha.1"));
488 	assertTrue(isSemVer("1.0.0-0.3.7"));
489 	assertTrue(isSemVer("1.0.0-x.7.z.92"));
490 	assertTrue(isSemVer("1.0.0-x.7-z.92"));
491 	assertFalse(isSemVer("1.0.0-00.3.7"));
492 	assertFalse(isSemVer("1.0.0-0.03.7"));
493 	assertTrue(isSemVer("1.0.0-alpha+001"));
494 	assertTrue(isSemVer("1.0.0+20130313144700"));
495 	assertTrue(isSemVer("1.0.0-beta+exp.sha.5114f85"));
496 	assertFalse(isSemVer(" 1.0.0"));
497 	assertFalse(isSemVer("1. 0.0"));
498 	assertFalse(isSemVer("1.0 .0"));
499 	assertFalse(isSemVer("1.0.0 "));
500 	assertFalse(isSemVer("1.0.0-a_b"));
501 	assertFalse(isSemVer("1.0.0+"));
502 	assertFalse(isSemVer("1.0.0-"));
503 	assertFalse(isSemVer("1.0.0-+a"));
504 	assertFalse(isSemVer("1.0.0-a+"));
505 	assertFalse(isSemVer("1.0"));
506 	assertFalse(isSemVer("1.0-1.0"));
507 }
508 
509 /**
510  * Takes a partial version and expands it to a valid SemVer version.
511  *
512  * This function corresponds to the semantivs of the "~>" comparison operator's
513  * lower bound.
514  *
515  * See_Also: `bumpVersion`
516  */
517 @safe pure
518 string expandVersion(string ver)
519 {
520 	auto mi = ver.indexOfAny("+-");
521 	auto sub = "";
522 	if (mi > 0)
523 	{
524 		sub = ver[mi .. $];
525 		ver = ver[0 .. mi];
526 	}
527 	auto splitted = () @trusted { return split(ver, "."); }();
528 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
529 	while (splitted.length < 3)
530 		splitted ~= "0";
531 	return splitted.join(".") ~ sub;
532 }
533 
534 ///
535 @("Versioning: expand version")
536 @safe pure
537 unittest
538 {
539 	assertEquals("1.0.0", expandVersion("1"));
540 	assertEquals("1.0.0", expandVersion("1.0"));
541 	assertEquals("1.0.0", expandVersion("1.0.0"));
542 	// These are rather excotic variants...
543 	assertEquals("1.0.0-pre.release", expandVersion("1-pre.release"));
544 	assertEquals("1.0.0+meta", expandVersion("1+meta"));
545 	assertEquals("1.0.0-pre.release+meta", expandVersion("1-pre.release+meta"));
546 }
547 
548 /**
549  * Determines if a given valid SemVer version has a pre-release suffix.
550  */
551 @safe pure
552 bool isPreReleaseVersion(string ver)
553 in
554 {
555 	assert(isSemVer(ver));
556 }
557 do
558 {
559 	foreach (i; 0 .. 2)
560 	{
561 		auto di = ver.indexOf('.');
562 		assert(di > 0);
563 		ver = ver[di + 1 .. $];
564 	}
565 	auto di = ver.indexOf('-');
566 	if (di < 0)
567 		return false;
568 	return isSemVerNumber(ver[0 .. di]);
569 }
570 
571 ///
572 @("Versioning: check pre-release")
573 @safe pure
574 unittest
575 {
576 	assertTrue(isPreReleaseVersion("1.0.0-alpha"));
577 	assertTrue(isPreReleaseVersion("1.0.0-alpha+b1"));
578 	assertTrue(isPreReleaseVersion("0.9.0-beta.1"));
579 	assertFalse(isPreReleaseVersion("0.9.0"));
580 	assertFalse(isPreReleaseVersion("0.9.0+b1"));
581 }
582 
583 /**
584  * Compares the precedence of two SemVer version strings.
585 
586  * The version strings must be validated using `isSemVer` before being
587  * passed to this function. Note that the build meta data suffix (if any) is
588  * being ignored when comparing version numbers.
589 
590  * Returns:
591  *      Returns a negative number if `a` is a lower version than `b`, `0` if they are
592  *      equal, and a positive number otherwise.
593  */
594 @safe pure @nogc nothrow
595 int compareSemVer(string a, string b)
596 {
597 	@safe pure @nogc nothrow
598 	int compareIdentifier(ref string a, ref string b)
599 	{
600 		bool anumber = true;
601 		bool bnumber = true;
602 		bool aempty = true, bempty = true;
603 		int res = 0;
604 		while (true)
605 		{
606 			if (a[0] != b[0] && res == 0)
607 				res = a[0] - b[0];
608 			if (anumber && (a[0] < '0' || a[0] > '9'))
609 				anumber = false;
610 			if (bnumber && (b[0] < '0' || b[0] > '9'))
611 				bnumber = false;
612 			a = a[1 .. $];
613 			b = b[1 .. $];
614 			aempty = !a.length || a[0] == '.' || a[0] == '+';
615 			bempty = !b.length || b[0] == '.' || b[0] == '+';
616 			if (aempty || bempty)
617 				break;
618 		}
619 
620 		if (anumber && bnumber)
621 		{
622 			if (aempty != bempty)
623 				return bempty - aempty;
624 			return res;
625 		}
626 		else
627 		{
628 			if (anumber && aempty)
629 				return -1;
630 			if (bnumber && bempty)
631 				return 1;
632 
633 			static assert('0' < 'a' && '0' < 'A');
634 			if (res != 0)
635 				return res;
636 			return bempty - aempty;
637 		}
638 	}
639 
640 	@safe pure @nogc nothrow
641 	int compareNumber(ref string a, ref string b)
642 	{
643 		int res = 0;
644 		while (true)
645 		{
646 			if (a[0] != b[0] && res == 0)
647 				res = a[0] - b[0];
648 			a = a[1 .. $];
649 			b = b[1 .. $];
650 			auto aempty = !a.length || (a[0] < '0' || a[0] > '9');
651 			auto bempty = !b.length || (b[0] < '0' || b[0] > '9');
652 			if (aempty != bempty)
653 				return bempty - aempty;
654 			if (aempty)
655 				return res;
656 		}
657 	}
658 
659 	// compare a.b.c numerically
660 	if (auto ret = compareNumber(a, b))
661 		return ret;
662 	assert(a[0] == '.' && b[0] == '.');
663 	a = a[1 .. $];
664 	b = b[1 .. $];
665 	if (auto ret = compareNumber(a, b))
666 		return ret;
667 	assert(a[0] == '.' && b[0] == '.');
668 	a = a[1 .. $];
669 	b = b[1 .. $];
670 	if (auto ret = compareNumber(a, b))
671 		return ret;
672 
673 	// give precedence to non-prerelease versions
674 	bool apre = a.length > 0 && a[0] == '-';
675 	bool bpre = b.length > 0 && b[0] == '-';
676 	if (apre != bpre)
677 		return bpre - apre;
678 	if (!apre)
679 		return 0;
680 
681 	// compare the prerelease tail lexicographically
682 	do
683 	{
684 		a = a[1 .. $];
685 		b = b[1 .. $];
686 		if (auto ret = compareIdentifier(a, b))
687 			return ret;
688 	}
689 	while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+');
690 
691 	// give longer prerelease tails precedence
692 	bool aempty = a.length == 0 || a[0] == '+';
693 	bool bempty = b.length == 0 || b[0] == '+';
694 	if (aempty == bempty)
695 	{
696 		assert(aempty);
697 		return 0;
698 	}
699 	return bempty - aempty;
700 }
701 
702 ///
703 @("Versioning: semver comparison")
704 @safe pure
705 unittest
706 {
707 	assertEquals(0, compareSemVer("1.0.0", "1.0.0"));
708 	assertEquals(0, compareSemVer("1.0.0+b1", "1.0.0+b2"));
709 	assertLessThan(compareSemVer("1.0.0", "2.0.0"), 0);
710 	assertLessThan(compareSemVer("1.0.0-beta", "1.0.0"), 0);
711 	assertGreaterThan(compareSemVer("1.0.1", "1.0.0"), 0);
712 
713 	void assertLess(string a, string b)
714 	{
715 		assertLessThan(compareSemVer(a, b), 0);
716 		assertGreaterThan(compareSemVer(b, a), 0);
717 		assertEquals(0, compareSemVer(a, a));
718 		assertEquals(0, compareSemVer(b, b));
719 	}
720 
721 	assertLess("1.0.0", "2.0.0");
722 	assertLess("2.0.0", "2.1.0");
723 	assertLess("2.1.0", "2.1.1");
724 	assertLess("1.0.0-alpha", "1.0.0");
725 	assertLess("1.0.0-alpha", "1.0.0-alpha.1");
726 	assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta");
727 	assertLess("1.0.0-alpha.beta", "1.0.0-beta");
728 	assertLess("1.0.0-beta", "1.0.0-beta.2");
729 	assertLess("1.0.0-beta.2", "1.0.0-beta.11");
730 	assertLess("1.0.0-beta.11", "1.0.0-rc.1");
731 	assertLess("1.0.0-rc.1", "1.0.0");
732 	assertEquals(0, compareSemVer("1.0.0", "1.0.0+1.2.3"));
733 	assertEquals(0, compareSemVer("1.0.0", "1.0.0+1.2.3-2"));
734 	assertEquals(0, compareSemVer("1.0.0+asdasd", "1.0.0+1.2.3"));
735 	assertLess("2.0.0", "10.0.0");
736 	assertLess("1.0.0-2", "1.0.0-10");
737 	assertLess("1.0.0-99", "1.0.0-1a");
738 	assertLess("1.0.0-99", "1.0.0-a");
739 	assertLess("1.0.0-alpha", "1.0.0-alphb");
740 	assertLess("1.0.0-alphz", "1.0.0-alphz0");
741 	assertLess("1.0.0-alphZ", "1.0.0-alpha");
742 }
743 
744 /**
745  * Increments a given (partial) version number to the next higher version.
746  *
747  * Prerelease and build metadata information is ignored. The given version
748  * can skip the minor and patch digits. If no digits are skipped, the next
749  * minor version will be selected. If the patch or minor versions are skipped,
750  * the next major version will be selected.
751  *
752  * This function corresponds to the semantivs of the "~>" comparison operator's
753  * upper bound.
754  *
755  * The semantics of this are the same as for the "approximate" version
756  * specifier from rubygems.
757  * (https://github.com/rubygems/rubygems/tree/81d806d818baeb5dcb6398ca631d772a003d078e/lib/rubygems/version.rb)
758  *
759  * See_Also: `expandVersion`
760  */
761 @safe pure
762 string bumpVersion(string ver)
763 {
764 	// Cut off metadata and prerelease information.
765 	auto mi = ver.indexOfAny("+-");
766 	if (mi > 0)
767 		ver = ver[0 .. mi];
768 	// Increment next to last version from a[.b[.c]].
769 	auto splitted = () @trusted { return split(ver, "."); }(); // DMD 2.065.0
770 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
771 	auto to_inc = splitted.length == 3 ? 1 : 0;
772 	splitted = splitted[0 .. to_inc + 1];
773 	splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1);
774 	// Fill up to three compontents to make valid SemVer version.
775 	while (splitted.length < 3)
776 		splitted ~= "0";
777 	return splitted.join(".");
778 }
779 
780 ///
781 @("Versioning: bump version")
782 @safe pure
783 unittest
784 {
785 	assertEquals("1.0.0", bumpVersion("0"));
786 	assertEquals("1.0.0", bumpVersion("0.0"));
787 	assertEquals("0.1.0", bumpVersion("0.0.0"));
788 	assertEquals("1.3.0", bumpVersion("1.2.3"));
789 	assertEquals("1.3.0", bumpVersion("1.2.3+metadata"));
790 	assertEquals("1.3.0", bumpVersion("1.2.3-pre.release"));
791 	assertEquals("1.3.0", bumpVersion("1.2.3-pre.release+metadata"));
792 }
793 
794 @safe pure @nogc nothrow
795 private bool isSemVerNumber(string str)
796 {
797 	if (str.length < 1)
798 		return false;
799 	foreach (ch; str)
800 		if (ch < '0' || ch > '9')
801 			return false;
802 
803 	if (str[0] == '0' && str.length > 1)
804 		return false;
805 
806 	return true;
807 }