summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAdrien Hopkins <ahopk127@my.yorku.ca>2022-02-20 10:30:55 -0500
committerAdrien Hopkins <ahopk127@my.yorku.ca>2022-02-20 10:30:55 -0500
commit63740b955b5baf955cac4f720a4c75f576d645f4 (patch)
treec9b20a2e47ef99afac18f7c6df097231e2e26f75 /src
parent540b798e397fb787fd81c8e6e636a2343655a42f (diff)
Made the version number an object, changing it to 0.4.0-alpha+dev
Diffstat (limited to 'src')
-rw-r--r--src/main/java/sevenUnits/ProgramInfo.java4
-rw-r--r--src/main/java/sevenUnits/SemanticVersionNumber.java691
-rw-r--r--src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java4
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java2
-rw-r--r--src/test/java/sevenUnits/SemanticVersionTest.java399
5 files changed, 1096 insertions, 4 deletions
diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java
index 0d67824..876367d 100644
--- a/src/main/java/sevenUnits/ProgramInfo.java
+++ b/src/main/java/sevenUnits/ProgramInfo.java
@@ -24,7 +24,9 @@ package sevenUnits;
*/
public final class ProgramInfo {
- public static final String VERSION = "0.4.0-dev";
+ /** The version number (0.4.0-alpha+dev) */
+ public static final SemanticVersionNumber VERSION = SemanticVersionNumber
+ .builder(0, 4, 0).preRelease("alpha").buildMetadata("dev").build();
private ProgramInfo() {
// this class is only for static variables, you shouldn't be able to
diff --git a/src/main/java/sevenUnits/SemanticVersionNumber.java b/src/main/java/sevenUnits/SemanticVersionNumber.java
new file mode 100644
index 0000000..01aeb27
--- /dev/null
+++ b/src/main/java/sevenUnits/SemanticVersionNumber.java
@@ -0,0 +1,691 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A version number in the <a href="https://semver.org">Semantic Versioning</a>
+ * scheme
+ * <p>
+ * Each version number has three main parts:
+ * <ol>
+ * <li>The major version, which increments when backwards incompatible changes
+ * are made
+ * <li>The minor version, which increments when backwards compatible feature
+ * changes are made
+ * <li>The patch version, which increments when backwards compatible bug fixes
+ * are made
+ * </ol>
+ *
+ * @since 2022-02-19
+ */
+public final class SemanticVersionNumber
+ implements Comparable<SemanticVersionNumber> {
+ /**
+ * A builder that can be used to create complex version numbers.
+ * <p>
+ * Note: None of this builder's methods tolerate null arguments, arrays
+ * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls
+ * throw NullPointerExceptions, everything else throws
+ * IllegalArgumentException.
+ *
+ * @since 2022-02-19
+ */
+ public static final class Builder {
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final List<String> preReleaseIdentifiers;
+ private final List<String> buildMetadata;
+
+ /**
+ * Creates a builder which can be used to create a
+ * {@code SemanticVersionNumber}
+ *
+ * @param major major version number of final version
+ * @param minor minor version number of final version
+ * @param patch patch version number of final version
+ * @since 2022-02-19
+ */
+ private Builder(int major, int minor, int patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.preReleaseIdentifiers = new ArrayList<>();
+ this.buildMetadata = new ArrayList<>();
+ }
+
+ /**
+ * @return version number created by this builder
+ * @since 2022-02-19
+ */
+ public SemanticVersionNumber build() {
+ return new SemanticVersionNumber(this.major, this.minor, this.patch,
+ this.preReleaseIdentifiers, this.buildMetadata);
+ }
+
+ /**
+ * Adds one or more build metadata identifiers
+ *
+ * @param identifiers build metadata
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder buildMetadata(List<String> identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.buildMetadata.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more build metadata identifiers
+ *
+ * @param identifiers build metadata
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder buildMetadata(String... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.buildMetadata.add(identifier);
+ }
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof Builder))
+ return false;
+ final Builder other = (Builder) obj;
+ return Objects.equals(this.buildMetadata, other.buildMetadata)
+ && this.major == other.major && this.minor == other.minor
+ && this.patch == other.patch && Objects.equals(
+ this.preReleaseIdentifiers, other.preReleaseIdentifiers);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.buildMetadata, this.major, this.minor,
+ this.patch, this.preReleaseIdentifiers);
+ }
+
+ /**
+ * Adds one or more numeric identifiers to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(int... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final int identifier : identifiers) {
+ if (identifier < 0)
+ throw new IllegalArgumentException(
+ "Numeric identifiers may not be negative");
+ this.preReleaseIdentifiers.add(Integer.toString(identifier));
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more pre-release identifier(s) to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(List<String> identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.preReleaseIdentifiers.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more pre-release identifier(s) to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(String... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.preReleaseIdentifiers.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a string identifier and an integer identifer to pre-release data
+ *
+ * @param identifier1 first identifier
+ * @param identifier2 second identifier
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(String identifier1, int identifier2) {
+ Objects.requireNonNull(identifier1, "identifier1 may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier1).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier1));
+ if (identifier2 < 0)
+ throw new IllegalArgumentException(
+ "Integer identifier cannot be negative");
+ this.preReleaseIdentifiers.add(identifier1);
+ this.preReleaseIdentifiers.add(Integer.toString(identifier2));
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "Semantic Version Builder: " + this.build().toString();
+ }
+ }
+
+ /**
+ * An alternative comparison method for version numbers. This uses the
+ * version's natural order, but the build metadata will be compared (using
+ * the same rules as pre-release identifiers) if everything else is equal.
+ * <p>
+ * This ordering is consistent with equals, unlike
+ * {@code SemanticVersionNumber}'s natural ordering.
+ */
+ public static final Comparator<SemanticVersionNumber> BUILD_METADATA_COMPARATOR = new Comparator<>() {
+ @Override
+ public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) {
+ Objects.requireNonNull(o1, "o1 may not be null");
+ Objects.requireNonNull(o2, "o2 may not be null");
+ final int naturalComparison = o1.compareTo(o2);
+ if (naturalComparison == 0)
+ return SemanticVersionNumber.compare(o1.buildMetadata,
+ o2.buildMetadata);
+ else
+ return naturalComparison;
+ };
+ };
+
+ /** The alphanumeric pattern all identifiers must follow */
+ private static final Pattern VALID_IDENTIFIER = Pattern
+ .compile("[0-9A-Za-z-]+");
+
+ /** The numeric pattern which causes special behaviour */
+ private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+");
+
+ /** The pattern for a version number */
+ private static final Pattern VERSION_NUMBER = Pattern
+ .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main
+ // version
+ + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release
+ + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data
+
+ /**
+ * Creates a builder that can be used to create a version number
+ *
+ * @param major major version number of final version
+ * @param minor minor version number of final version
+ * @param patch patch version number of final version
+ * @return version number builder
+ * @throws IllegalArgumentException if any argument is negative
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber.Builder builder(int major,
+ int minor, int patch) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ return new SemanticVersionNumber.Builder(major, minor, patch);
+ }
+
+ /**
+ * Compares two lists of strings based on SemVer's precedence rules
+ *
+ * @param a first list
+ * @param b second list
+ * @return result of comparison as in a comparator
+ * @see Comparator
+ * @since 2022-02-20
+ */
+ private static final int compare(List<String> a, List<String> b) {
+ // test pre-release size
+ final int aSize = a.size();
+ final int bSize = b.size();
+
+ // no identifiers is greater than any identifiers
+ if (aSize != 0 && bSize == 0)
+ return -1;
+ else if (aSize == 0 && bSize != 0)
+ return 1;
+
+ // test identifiers one by one
+ for (int i = 0; i < Math.min(aSize, bSize); i++) {
+ final String aElement = a.get(i);
+ final String bElement = b.get(i);
+
+ if (NUMERIC_IDENTIFER.matcher(aElement).matches()) {
+ if (NUMERIC_IDENTIFER.matcher(bElement).matches()) {
+ // both are numbers, compare them
+ final int aNumber = Integer.parseInt(aElement);
+ final int bNumber = Integer.parseInt(bElement);
+
+ if (aNumber < bNumber)
+ return -1;
+ else if (aNumber > bNumber)
+ return 1;
+ } else
+ // aElement is a number and bElement is not a number
+ // by the rules, a goes before b
+ return -1;
+ } else {
+ if (NUMERIC_IDENTIFER.matcher(bElement).matches())
+ // aElement is not a number but bElement is
+ // by the rules, a goes after b
+ return 1;
+ else {
+ // both are not numbers, compare them
+ final int comparison = aElement.compareTo(bElement);
+ if (comparison != 0)
+ return comparison;
+ }
+ }
+ }
+
+ // we just tested the stuff that's in common, maybe someone has more
+ if (aSize < bSize)
+ return -1;
+ else if (aSize > bSize)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * Gets a version number from a string in the official format
+ *
+ * @param versionString string to parse
+ * @return {@code SemanticVersionNumber} instance
+ * @since 2022-02-19
+ * @see {@link #toString}
+ */
+ public static final SemanticVersionNumber fromString(String versionString) {
+ // parse & validate version string
+ Objects.requireNonNull(versionString, "versionString may not be null");
+ final Matcher m = VERSION_NUMBER.matcher(versionString);
+ if (!m.matches())
+ throw new IllegalArgumentException(
+ String.format("Provided string \"%s\" is not a version number",
+ versionString));
+
+ // main parts
+ final int major = Integer.parseInt(m.group(1));
+ final int minor = Integer.parseInt(m.group(2));
+ final int patch = Integer.parseInt(m.group(3));
+
+ // pre release
+ final List<String> preRelease;
+ if (m.group(4) == null) {
+ preRelease = List.of();
+ } else {
+ preRelease = Arrays.asList(m.group(4).split("\\."));
+ }
+
+ // build metadata
+ final List<String> buildMetadata;
+ if (m.group(5) == null) {
+ buildMetadata = List.of();
+ } else {
+ buildMetadata = Arrays.asList(m.group(5).split("\\."));
+ }
+
+ // return number
+ return new SemanticVersionNumber(major, minor, patch, preRelease,
+ buildMetadata);
+ }
+
+ /**
+ * Tests whether a string is a valid Semantic Version string
+ *
+ * @param versionString string to test
+ * @return true iff string is valid
+ * @since 2022-02-19
+ */
+ public static final boolean isValidVersionString(String versionString) {
+ return VERSION_NUMBER.matcher(versionString).matches();
+ }
+
+ /**
+ * Creates a simple pre-release version number of the form
+ * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4).
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @param preReleaseType first pre-release element
+ * @param preReleaseNumber second pre-release element
+ * @return {@code SemanticVersionNumber} instance
+ * @throws IllegalArgumentException if any argument is negative or if the
+ * preReleaseType is null, empty or not
+ * alphanumeric (0-9, A-Z, a-z, - only)
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber preRelease(int major, int minor,
+ int patch, String preReleaseType, int preReleaseNumber) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ Objects.requireNonNull(preReleaseType, "preReleaseType may not be null");
+ if (!VALID_IDENTIFIER.matcher(preReleaseType).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\".", preReleaseType));
+ if (preReleaseNumber < 0)
+ throw new IllegalArgumentException(
+ "Pre-release number must be non-negative.");
+ return new SemanticVersionNumber(major, minor, patch,
+ List.of(preReleaseType, Integer.toString(preReleaseNumber)),
+ List.of());
+ }
+
+ /**
+ * Creates a {@code SemanticVersionNumber} instance without pre-release
+ * identifiers or build metadata.
+ * <p>
+ * Note: this method allows you to create versions with major version number
+ * 0, even though these versions would not be considered stable.
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @return {@code SemanticVersionNumber} instance
+ * @throws IllegalArgumentException if any argument is negative
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber stableVersion(int major, int minor,
+ int patch) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ return new SemanticVersionNumber(major, minor, patch, List.of(),
+ List.of());
+ }
+
+ // parts of the version number
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final List<String> preReleaseIdentifiers;
+ private final List<String> buildMetadata;
+
+ /**
+ * Creates a version number
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @param preReleaseIdentifiers pre-release version data
+ * @param buildMetadata build metadata
+ * @since 2022-02-19
+ */
+ private SemanticVersionNumber(int major, int minor, int patch,
+ List<String> preReleaseIdentifiers, List<String> buildMetadata) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.preReleaseIdentifiers = preReleaseIdentifiers;
+ this.buildMetadata = buildMetadata;
+ }
+
+ /**
+ * @return build metadata (empty if there is none)
+ * @since 2022-02-19
+ */
+ public List<String> buildMetadata() {
+ return Collections.unmodifiableList(this.buildMetadata);
+ }
+
+ /**
+ * Compares two version numbers according to the official Semantic Versioning
+ * order.
+ * <p>
+ * Note: this ordering is not consistent with equals. Specifically, two
+ * versions that are identical except for their build metadata will be
+ * considered different by equals but the same by this method. This is
+ * required to follow the official Semantic Versioning specification.
+ * <p>
+ */
+ @Override
+ public int compareTo(SemanticVersionNumber o) {
+ // test the three big numbers in order first
+ if (this.major < o.major)
+ return -1;
+ else if (this.major > o.major)
+ return 1;
+
+ if (this.minor < o.minor)
+ return -1;
+ else if (this.minor > o.minor)
+ return 1;
+
+ if (this.patch < o.patch)
+ return -1;
+ else if (this.patch > o.patch)
+ return 1;
+
+ // now we just compare pre-release identifiers
+ // (remember: build metadata is ignored)
+ return SemanticVersionNumber.compare(this.preReleaseIdentifiers,
+ o.preReleaseIdentifiers);
+ }
+
+ /**
+ * Determines the compatibility of code written for this version to
+ * {@code other}. More specifically:
+ * <p>
+ * If this function returns <b>true</b>, then there should be no problems
+ * upgrading code written for this version to version {@code other} as long
+ * as:
+ * <ul>
+ * <li>Semantic Versioning is being used properly
+ * <li>Your code doesn't depend on unintended features (if it does, it isn't
+ * necessarily compatible with any other version)
+ * </ul>
+ * If this function returns <b>false</b>, you may have to change your code to
+ * upgrade it to {@code other}
+ *
+ * <p>
+ * Two version numbers that are identical (ignoring build metadata) are
+ * always compatible. Different version numbers are compatible as long as:
+ * <ul>
+ * <li>The major version number is not 0 (if it is, the API is considered
+ * unstable and any upgrade can be backwards compatible)
+ * <li>The major version number is the same (changing the major version
+ * number implies bacwards incompatible changes)
+ * <li>This version comes before the other one in the official precedence
+ * order (downgrading can remove features you depend on)
+ * </ul>
+ *
+ * @param other version to compare with
+ * @return true if you can definitely upgrade to {@code other} without
+ * changing code
+ * @since 2022-02-20
+ */
+ public boolean compatibleWith(SemanticVersionNumber other) {
+ Objects.requireNonNull(other, "other may not be null");
+
+ return this.compareTo(other) == 0 || this.major != 0
+ && this.major == other.major && this.compareTo(other) < 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof SemanticVersionNumber))
+ return false;
+ final SemanticVersionNumber other = (SemanticVersionNumber) obj;
+ if (this.buildMetadata == null) {
+ if (other.buildMetadata != null)
+ return false;
+ } else if (!this.buildMetadata.equals(other.buildMetadata))
+ return false;
+ if (this.major != other.major)
+ return false;
+ if (this.minor != other.minor)
+ return false;
+ if (this.patch != other.patch)
+ return false;
+ if (this.preReleaseIdentifiers == null) {
+ if (other.preReleaseIdentifiers != null)
+ return false;
+ } else if (!this.preReleaseIdentifiers
+ .equals(other.preReleaseIdentifiers))
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode());
+ result = prime * result + this.major;
+ result = prime * result + this.minor;
+ result = prime * result + this.patch;
+ result = prime * result + (this.preReleaseIdentifiers == null ? 0
+ : this.preReleaseIdentifiers.hashCode());
+ return result;
+ }
+
+ /**
+ * @return true iff this version is stable (major version > 0 and not a
+ * pre-release)
+ * @since 2022-02-19
+ */
+ public boolean isStable() {
+ return this.major > 0 && this.preReleaseIdentifiers.isEmpty();
+ }
+
+ /**
+ * @return the MAJOR version number, incremented when you make backwards
+ * incompatible API changes
+ * @since 2022-02-19
+ */
+ public int majorVersion() {
+ return this.major;
+ }
+
+ /**
+ * @return the MINOR version number, incremented when you add backwards
+ * compatible functionality
+ * @since 2022-02-19
+ */
+ public int minorVersion() {
+ return this.minor;
+ }
+
+ /**
+ * @return the PATCH version number, incremented when you make backwards
+ * compatible bug fixes
+ * @since 2022-02-19
+ */
+ public int patchVersion() {
+ return this.patch;
+ }
+
+ /**
+ * @return identifiers describing this pre-release (empty if not a
+ * pre-release)
+ * @since 2022-02-19
+ */
+ public List<String> preReleaseIdentifiers() {
+ return Collections.unmodifiableList(this.preReleaseIdentifiers);
+ }
+
+ /**
+ * Converts a version number to a string using the official SemVer format.
+ * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If
+ * pre-release identifiers are present, they are separated by periods and
+ * added after a '-'. If build metadata is present, it is separated by
+ * periods and added after a '+'. Pre-release identifiers go before version
+ * metadata.
+ * <p>
+ * For example, the version with major number 3, minor number 2, patch number
+ * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19"
+ * has a string representation "3.2.1-alpha.1+2022-02-19".
+ *
+ * @see <a href="https://semver.org">The official SemVer specification</a>
+ */
+ @Override
+ public String toString() {
+ String versionString = String.format("%d.%d.%d", this.major, this.minor,
+ this.patch);
+ if (!this.preReleaseIdentifiers.isEmpty()) {
+ versionString += "-" + String.join(".", this.preReleaseIdentifiers);
+ }
+ if (!this.buildMetadata.isEmpty()) {
+ versionString += "+" + String.join(".", this.buildMetadata);
+ }
+ return versionString;
+ }
+}
diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java
index e21c25f..9c6ae0a 100644
--- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java
+++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java
@@ -1221,8 +1221,8 @@ final class SevenUnitsGUI {
final String infoText = Presenter
.getLinesFromResource("/about.txt").stream()
.map(Presenter::withoutComments)
- .collect(Collectors.joining("\n"))
- .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION);
+ .collect(Collectors.joining("\n")).replaceAll(
+ "\\[VERSION\\]", ProgramInfo.VERSION.toString());
infoTextArea.setText(infoText);
}
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
index 07671e4..23a631d 100644
--- a/src/main/java/sevenUnitsGUI/Presenter.java
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -47,7 +47,7 @@ public final class Presenter {
static final String getAboutText() {
return Presenter.getLinesFromResource("/about.txt").stream()
.map(Presenter::withoutComments).collect(Collectors.joining("\n"))
- .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION);
+ .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString());
}
/**
diff --git a/src/test/java/sevenUnits/SemanticVersionTest.java b/src/test/java/sevenUnits/SemanticVersionTest.java
new file mode 100644
index 0000000..9202ef9
--- /dev/null
+++ b/src/test/java/sevenUnits/SemanticVersionTest.java
@@ -0,0 +1,399 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static sevenUnits.SemanticVersionNumber.BUILD_METADATA_COMPARATOR;
+import static sevenUnits.SemanticVersionNumber.builder;
+import static sevenUnits.SemanticVersionNumber.fromString;
+import static sevenUnits.SemanticVersionNumber.isValidVersionString;
+import static sevenUnits.SemanticVersionNumber.preRelease;
+import static sevenUnits.SemanticVersionNumber.stableVersion;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link SemanticVersionNumber}
+ *
+ * @since 2022-02-19
+ */
+public final class SemanticVersionTest {
+ /**
+ * Test for {@link SemanticVersionNumber#compatible}
+ *
+ * @since 2022-02-20
+ */
+ @Test
+ public void testCompatibility() {
+ assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)),
+ "1.0.0 not compatible with 1.0.5");
+ assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)),
+ "1.3.1 not compatible with 1.4.0");
+
+ // 0.y.z should not be compatible with any other version
+ assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)),
+ "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)");
+
+ // upgrading major version should = incompatible
+ assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)),
+ "1.0.0 compatible with 2.0.0");
+
+ // dowgrade should = incompatible
+ assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)),
+ "1.1.0 compatible with 1.0.0");
+ }
+
+ /**
+ * Tests {@link SemanticVersionNumber#toString} for complex version numbers
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testComplexToString() {
+ final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
+ .build();
+ assertEquals("1.2.3-1.2.3", v1.toString());
+ final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123)
+ .buildMetadata("2022-02-19").build();
+ assertEquals("4.5.6-abc.123+2022-02-19", v2.toString());
+ final SemanticVersionNumber v3 = builder(1, 0, 0)
+ .preRelease("x-y-z", "--").build();
+ assertEquals("1.0.0-x-y-z.--", v3.toString());
+ }
+
+ /**
+ * Tests that complex version can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testComplexVersions() {
+ final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
+ .build();
+ assertEquals(1, v1.majorVersion());
+ assertEquals(2, v1.minorVersion());
+ assertEquals(3, v1.patchVersion());
+ assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers());
+ assertEquals(List.of(), v1.buildMetadata());
+
+ final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123)
+ .buildMetadata("2022-02-19").build();
+ assertEquals(4, v2.majorVersion());
+ assertEquals(5, v2.minorVersion());
+ assertEquals(6, v2.patchVersion());
+ assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers());
+ assertEquals(List.of("2022-02-19"), v2.buildMetadata());
+
+ final SemanticVersionNumber v3 = builder(1, 0, 0)
+ .preRelease("x-y-z", "--").build();
+ assertEquals(1, v3.majorVersion());
+ assertEquals(0, v3.minorVersion());
+ assertEquals(0, v3.patchVersion());
+ assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers());
+ assertEquals(List.of(), v3.buildMetadata());
+ }
+
+ /**
+ * Test that semantic version strings can be parsed correctly
+ *
+ * @since 2022-02-19
+ * @see SemanticVersionNumber#fromString
+ * @see SemanticVersionNumber#isValidVersionString
+ */
+ @Test
+ public void testFromString() {
+ // test that the regex can match version strings
+ assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid");
+ assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid");
+ assertTrue(isValidVersionString("2.0.0-a.1"),
+ "2.0.0-a.1 is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0-a.b.c.d"),
+ "1.0.0-a.b.c.d is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0+abc"),
+ "1.0.0+abc is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0-abc+def"),
+ "1.0.0-abc+def is treated as invalid");
+
+ // test that invalid versions don't match
+ assertFalse(isValidVersionString("1.0"),
+ "1.0 is treated as valid (patch should be required)");
+ assertFalse(isValidVersionString("1.A.0"),
+ "1.A.0 is treated as valid (main versions must be numbers)");
+ assertFalse(isValidVersionString("1.0.0-"),
+ "1.0.0- is treated as valid (pre-release must not be empty)");
+ assertFalse(isValidVersionString("1.0.0+"),
+ "1.0.0+ is treated as valid (build metadata must not be empty)");
+
+ // test that versions can be parsed
+ assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"),
+ "Could not parse 1.0.0");
+ assertEquals(
+ builder(1, 2, 3).preRelease("abc", "56", "def")
+ .buildMetadata("2022abc99").build(),
+ fromString("1.2.3-abc.56.def+2022abc99"),
+ "Could not parse 1.2.3-abc.56.def+2022abc99");
+ }
+
+ /**
+ * Ensures it is impossible to create invalid version numbers
+ */
+ @Test
+ public void testInvalidVersionNumbers() {
+ // stableVersion()
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(1, 0, -1),
+ "Negative patch tolerated by stableVersion");
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(1, -2, 1),
+ "Negative minor version number tolerated by stableVersion");
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(-3, 0, 7),
+ "Negative major version number tolerated by stableVersion");
+
+ // preRelease()
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, -1, "test", 2),
+ "Negative patch tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, -2, 1, "test", 2),
+ "Negative minor version number tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(-3, 0, 7, "test", 2),
+ "Negative major version number tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "test", -1),
+ "Negative pre release number tolerated by preRelease");
+ assertThrows(NullPointerException.class,
+ () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "", 1),
+ "Empty string tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "abc+cde", 1),
+ "Invalid string tolerated by preRelease");
+
+ // builder()
+ assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1),
+ "Negative patch tolerated by builder");
+ assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1),
+ "Negative minor version number tolerated by builder");
+ assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7),
+ "Negative major version number tolerated by builder");
+
+ final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3);
+ // note: builder.buildMetadata(null) doesn't even compile lol
+ // builder.buildMetadata
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.buildMetadata(null, "abc"),
+ "Null tolerated by builder.buildMetadata(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(""),
+ "Empty string tolerated by builder.buildMetadata(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata("c%4"),
+ "Invalid string tolerated by builder.buildMetadata(String...)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.buildMetadata(List.of("abc", null)),
+ "Null tolerated by builder.buildMetadata(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(List.of("")),
+ "Empty string tolerated by builder.buildMetadata(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(List.of("")),
+ "Invalid string tolerated by builder.buildMetadata(List<String>)");
+
+ // builder.preRelease
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(null, "abc"),
+ "Null tolerated by builder.preRelease(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(""),
+ "Empty string tolerated by builder.preRelease(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("c%4"),
+ "Invalid string tolerated by builder.preRelease(String...)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(List.of("abc", null)),
+ "Null tolerated by builder.preRelease(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(List.of("")),
+ "Empty string tolerated by builder.preRelease(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(List.of("")),
+ "Invalid string tolerated by builder.preRelease(List<String>)");
+
+ // the overloadings that accept numeric arguments
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(-1),
+ "Negative number tolerated by builder.preRelease(int...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("abc", -1),
+ "Negative number tolerated by builder.preRelease(String, int)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(null, 1),
+ "Null tolerated by builder.preRelease(String, int)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("", 1),
+ "Empty string tolerated by builder.preRelease(String, int)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("#$#c", 1),
+ "Invalid string tolerated by builder.preRelease(String, int)");
+
+ // ensure all these attempts didn't change the builder
+ assertEquals(builder(1, 2, 3), testBuilder,
+ "Attempts at making invalid version number succeeded despite throwing errors");
+ }
+
+ /**
+ * Test for {@link SemanticVersionNumber#isStable}
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testIsStable() {
+ assertTrue(stableVersion(1, 0, 0).isStable(),
+ "1.0.0 should be stable but is not");
+ assertFalse(stableVersion(0, 1, 2).isStable(),
+ "0.1.2 should not be stable but is");
+ assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(),
+ "1.2.3a5 should not be stable but is");
+ assertTrue(
+ builder(9, 9, 99)
+ .buildMetadata("lots-of-metadata", "abc123", "2022").build()
+ .isStable(),
+ "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not");
+ }
+
+ /**
+ * Tests that the versions are ordered by
+ * {@link SemanticVersionNumber#compareTo} according to official rules. Tests
+ * all of the versions compared in section 11 of the SemVer 2.0.0 document
+ * and some more.
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testOrder() {
+ final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha")
+ .build(); // 1.0.0-alpha
+ final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1
+ final SemanticVersionNumber v100ab = builder(1, 0, 0)
+ .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta
+ final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta")
+ .build(); // 1.0.0-alpha
+ final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2
+ final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11
+ final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ final SemanticVersionNumber v100plus = builder(1, 0, 0)
+ .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah
+ final SemanticVersionNumber v200 = stableVersion(2, 0, 0);
+ final SemanticVersionNumber v201 = stableVersion(2, 0, 1);
+ final SemanticVersionNumber v210 = stableVersion(2, 1, 0);
+ final SemanticVersionNumber v211 = stableVersion(2, 1, 1);
+ final SemanticVersionNumber v300 = stableVersion(3, 0, 0);
+
+ // test order of version numbers
+ assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1");
+ assertTrue(v100a1.compareTo(v100ab) < 0,
+ "1.0.0-alpha.1 >= 1.0.0-alpha.beta");
+ assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta");
+ assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2");
+ assertTrue(v100b2.compareTo(v100b11) < 0,
+ "1.0.0-beta.2 >= 1.0.0-beta.11");
+ assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1");
+ assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0");
+ assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0");
+ assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1");
+ assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0");
+ assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1");
+ assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0");
+
+ // test symmetry - assume previous tests passed
+ assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha");
+ assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1");
+ assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1");
+
+ // test transitivity
+ assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11");
+ assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0");
+ assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0");
+ assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0");
+
+ // test metadata is ignored
+ assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored");
+ // test metadata is NOT ignored by alternative comparator
+ assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0,
+ "Build metadata ignored by BUILD_METADATA_COMPARATOR");
+ }
+
+ /**
+ * Tests that simple stable versions can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleStableVersions() {
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ assertEquals(1, v100.majorVersion());
+ assertEquals(0, v100.minorVersion());
+ assertEquals(0, v100.patchVersion());
+
+ final SemanticVersionNumber v925 = stableVersion(9, 2, 5);
+ assertEquals(9, v925.majorVersion());
+ assertEquals(2, v925.minorVersion());
+ assertEquals(5, v925.patchVersion());
+ }
+
+ /**
+ * Tests that {@link SemanticVersionNumber#toString} works for simple version
+ * numbers
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleToString() {
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ assertEquals("1.0.0", v100.toString());
+
+ final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1);
+ assertEquals("8.4.5-alpha.1", v845a1.toString());
+ }
+
+ /**
+ * Tests that simple unstable versions can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleUnstableVersions() {
+ final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1);
+ assertEquals(3, v350a1.majorVersion(),
+ "Incorrect major version for v3.5.0a1");
+ assertEquals(5, v350a1.minorVersion(),
+ "Incorrect minor version for v3.5.0a1");
+ assertEquals(0, v350a1.patchVersion(),
+ "Incorrect patch version for v3.5.0a1");
+ assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(),
+ "Incorrect pre-release identifiers for v3.5.0a1");
+ }
+}