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 }