diff options
Diffstat (limited to 'src/main/java/sevenUnits/utils')
-rw-r--r-- | src/main/java/sevenUnits/utils/NameSymbol.java | 308 | ||||
-rw-r--r-- | src/main/java/sevenUnits/utils/Nameable.java | 79 | ||||
-rw-r--r-- | src/main/java/sevenUnits/utils/ObjectProduct.java | 48 | ||||
-rw-r--r-- | src/main/java/sevenUnits/utils/SemanticVersionNumber.java | 691 | ||||
-rw-r--r-- | src/main/java/sevenUnits/utils/UncertainDouble.java | 24 |
5 files changed, 1139 insertions, 11 deletions
diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java new file mode 100644 index 0000000..7ef2967 --- /dev/null +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -0,0 +1,308 @@ +/** + * Copyright (C) 2019 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.utils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * A class that can be used to specify names and a symbol for a unit. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class NameSymbol { + public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), + Optional.empty(), new HashSet<>()); + + /** + * Creates a {@code NameSymbol}, ensuring that if primaryName is null and + * otherNames is not empty, one name is moved from otherNames to primaryName + * + * Ensure that otherNames is not a copy of the inputted argument. + */ + private static final NameSymbol create(final String name, + final String symbol, final Set<String> otherNames) { + final Optional<String> primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + final Iterator<String> it = otherNames.iterator(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), + otherNames); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and no other + * names. + * + * @param name name to use + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name or symbol is null + */ + public static final NameSymbol of(final String name, final String symbol) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final Set<String> otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Objects.requireNonNull(otherNames, + "otherNames must not be null."))); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final String... otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, + "otherNames must not be null.")))); + } + + /** + * Gets a {@code NameSymbol} with a primary name, no symbol, and no other + * names. + * + * @param name name to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name is null + */ + public static final NameSymbol ofName(final String name) { + return new NameSymbol(Optional.of(name), Optional.empty(), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + * <p> + * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final Set<String> otherNames) { + return NameSymbol.create(name, symbol, + otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + * <p> + * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final String... otherNames) { + return create(name, symbol, otherNames == null ? new HashSet<>() + : new HashSet<>(Arrays.asList(otherNames))); + } + + /** + * Gets a {@code NameSymbol} with a symbol and no names. + * + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if symbol is null + */ + public static final NameSymbol ofSymbol(final String symbol) { + return new NameSymbol(Optional.empty(), Optional.of(symbol), + new HashSet<>()); + } + + private final Optional<String> primaryName; + private final Optional<String> symbol; + + private final Set<String> otherNames; + + /** + * Creates the {@code NameSymbol}. + * + * @param primaryName primary name of unit + * @param symbol symbol used to represent unit + * @param otherNames other names and/or spellings, should be a mutable copy + * of the argument + * @since 2019-10-21 + */ + private NameSymbol(final Optional<String> primaryName, + final Optional<String> symbol, final Set<String> otherNames) { + this.primaryName = primaryName; + this.symbol = symbol; + otherNames.remove(null); + this.otherNames = Collections.unmodifiableSet(otherNames); + + if (this.primaryName.isEmpty()) { + assert this.otherNames.isEmpty(); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof NameSymbol)) + return false; + final NameSymbol other = (NameSymbol) obj; + if (this.otherNames == null) { + if (other.otherNames != null) + return false; + } else if (!this.otherNames.equals(other.otherNames)) + return false; + if (this.primaryName == null) { + if (other.primaryName != null) + return false; + } else if (!this.primaryName.equals(other.primaryName)) + return false; + if (this.symbol == null) { + if (other.symbol != null) + return false; + } else if (!this.symbol.equals(other.symbol)) + return false; + return true; + } + + /** + * @return otherNames + * @since 2019-10-21 + */ + public final Set<String> getOtherNames() { + return this.otherNames; + } + + /** + * @return primaryName + * @since 2019-10-21 + */ + public final Optional<String> getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-10-21 + */ + public final Optional<String> getSymbol() { + return this.symbol; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.otherNames == null ? 0 : this.otherNames.hashCode()); + result = prime * result + + (this.primaryName == null ? 0 : this.primaryName.hashCode()); + result = prime * result + + (this.symbol == null ? 0 : this.symbol.hashCode()); + return result; + } + + /** + * @return true iff this {@code NameSymbol} contains no names or symbols. + */ + public final boolean isEmpty() { + // if primaryName is empty, otherNames must also be empty + return this.primaryName.isEmpty() && this.symbol.isEmpty(); + } + + @Override + public String toString() { + if (this.isEmpty()) + return "NameSymbol.EMPTY"; + else if (this.primaryName.isPresent() && this.symbol.isPresent()) + return this.primaryName.orElseThrow() + " (" + + this.symbol.orElseThrow() + ")"; + else + return this.primaryName.orElseGet(this.symbol::orElseThrow); + } + + /** + * Creates and returns a copy of this {@code NameSymbol} with the provided + * extra name. If this {@code NameSymbol} has a primary name, the provided + * name will become an other name, otherwise it will become the primary name. + * + * @since 2022-04-19 + */ + public final NameSymbol withExtraName(String name) { + if (this.primaryName.isPresent()) { + final var otherNames = new HashSet<>(this.otherNames); + otherNames.add(name); + return NameSymbol.ofNullable(this.primaryName.orElse(null), + this.symbol.orElse(null), otherNames); + } else + return NameSymbol.ofNullable(name, this.symbol.orElse(null)); + } +}
\ No newline at end of file diff --git a/src/main/java/sevenUnits/utils/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java new file mode 100644 index 0000000..e469d04 --- /dev/null +++ b/src/main/java/sevenUnits/utils/Nameable.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2020 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.utils; + +import java.util.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a name for the object - if there's a primary name, it's that, + * otherwise the symbol, otherwise "Unnamed" + * @since 2022-02-26 + */ + default String getName() { + final NameSymbol ns = this.getNameSymbol(); + return ns.getPrimaryName().or(ns::getSymbol).orElse("Unnamed"); + } + + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set<String> getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional<String> getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return a short name for the object - if there's a symbol, it's that, + * otherwise the symbol, otherwise "Unnamed" + * @since 2022-02-26 + */ + default String getShortName() { + final NameSymbol ns = this.getNameSymbol(); + return ns.getSymbol().or(ns::getPrimaryName).orElse("Unnamed"); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional<String> getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 5b1b739..66bb773 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -33,7 +33,7 @@ import java.util.function.Function; * @author Adrien Hopkins * @since 2019-10-16 */ -public final class ObjectProduct<T> { +public class ObjectProduct<T> implements Nameable { /** * Returns an empty ObjectProduct of a certain type * @@ -83,15 +83,32 @@ public final class ObjectProduct<T> { final Map<T, Integer> exponents; /** - * Creates the {@code ObjectProduct}. + * The object's name and symbol + */ + private final NameSymbol nameSymbol; + + /** + * Creates a {@code ObjectProduct} without a name/symbol. * * @param exponents objects that make up this product * @since 2019-10-16 */ - private ObjectProduct(final Map<T, Integer> exponents) { + ObjectProduct(final Map<T, Integer> exponents) { + this(exponents, NameSymbol.EMPTY); + } + + /** + * Creates the {@code ObjectProduct}. + * + * @param exponents objects that make up this product + * @param nameSymbol name and symbol of object product + * @since 2019-10-16 + */ + ObjectProduct(final Map<T, Integer> exponents, NameSymbol nameSymbol) { this.exponents = Collections.unmodifiableMap( ConditionalExistenceCollections.conditionalExistenceMap(exponents, e -> !Integer.valueOf(0).equals(e.getValue()))); + this.nameSymbol = nameSymbol; } /** @@ -171,6 +188,11 @@ public final class ObjectProduct<T> { } @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + @Override public int hashCode() { return Objects.hash(this.exponents); } @@ -235,16 +257,19 @@ public final class ObjectProduct<T> { /** * Converts this product to a string using the objects' - * {@link Object#toString()} method. If objects have a long toString - * representation, it is recommended to use {@link #toString(Function)} - * instead to shorten the returned string. + * {@link Object#toString()} method (or {@link Nameable#getShortName} if + * available). If objects have a long toString representation, it is + * recommended to use {@link #toString(Function)} instead to shorten the + * returned string. * * <p> * {@inheritDoc} */ @Override public String toString() { - return this.toString(Object::toString); + return this + .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName() + : o.toString()); } /** @@ -280,4 +305,13 @@ public final class ObjectProduct<T> { return positiveString + negativeString; } + + /** + * @return named version of this {@code ObjectProduct}, using data from + * {@code nameSymbol} + * @since 2021-12-15 + */ + public ObjectProduct<T> withName(NameSymbol nameSymbol) { + return new ObjectProduct<>(this.exponents, nameSymbol); + } } diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java new file mode 100644 index 0000000..06417c5 --- /dev/null +++ b/src/main/java/sevenUnits/utils/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.utils; + +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.compareIdentifiers(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 compareIdentifiers(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.compareIdentifiers(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/utils/UncertainDouble.java b/src/main/java/sevenUnits/utils/UncertainDouble.java index fe41104..ac523b3 100644 --- a/src/main/java/sevenUnits/utils/UncertainDouble.java +++ b/src/main/java/sevenUnits/utils/UncertainDouble.java @@ -46,6 +46,21 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { + "(?:\\s*(?:±|\\+-)\\s*" + NUMBER_REGEX + ")?"); /** + * Gets an UncertainDouble from a double string. The uncertainty of the + * double will be one of the lowest decimal place of the number. For example, + * "12345.678" will become 12345.678 ± 0.001. + * + * @throws NumberFormatException if the argument is not a number + * + * @since 2022-04-18 + */ + public static final UncertainDouble fromRoundedString(String s) { + final BigDecimal value = new BigDecimal(s); + final double uncertainty = Math.pow(10, -value.scale()); + return UncertainDouble.of(value.doubleValue(), uncertainty); + } + + /** * Parses a string in the form of {@link UncertainDouble#toString(boolean)} * and returns the corresponding {@code UncertainDouble} instance. * <p> @@ -348,7 +363,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { */ @Override public final String toString() { - return this.toString(!this.isExact()); + return this.toString(!this.isExact(), RoundingMode.HALF_EVEN); } /** @@ -379,7 +394,8 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { * * @since 2020-09-07 */ - public final String toString(boolean showUncertainty) { + public final String toString(boolean showUncertainty, + RoundingMode roundingMode) { String valueString, uncertaintyString; // generate the string representation of value and uncertainty @@ -394,9 +410,9 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { final int displayScale = this.getDisplayScale(); final BigDecimal roundedUncertainty = bigUncertainty - .setScale(displayScale, RoundingMode.HALF_EVEN); + .setScale(displayScale, roundingMode); final BigDecimal roundedValue = bigValue.setScale(displayScale, - RoundingMode.HALF_EVEN); + roundingMode); valueString = roundedValue.toString(); uncertaintyString = roundedUncertainty.toString(); |