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 }