From 4f754b0bc136b972607aa1b39f2358bd98dc4e1c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 15 Dec 2021 17:52:43 -0500 Subject: Designed the API for a new GUI Currently "supports" unit conversion, expression conversion and settings loading --- .../java/sevenUnits/utils/NamedObjectProduct.java | 46 ++++++++++++++++++++++ src/main/java/sevenUnits/utils/ObjectProduct.java | 15 ++++++- 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/main/java/sevenUnits/utils/NamedObjectProduct.java (limited to 'src/main/java/sevenUnits/utils') diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java new file mode 100644 index 0000000..514f0b1 --- /dev/null +++ b/src/main/java/sevenUnits/utils/NamedObjectProduct.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2021 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 . + */ +package sevenUnits.utils; + +import java.util.Map; + +import sevenUnits.unit.NameSymbol; +import sevenUnits.unit.Nameable; + +/** + * An ObjectProduct with name(s) and/or a symbol. Can be created with the + * {@link ObjectProduct#withName} method. + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public class NamedObjectProduct extends ObjectProduct + implements Nameable { + private final NameSymbol nameSymbol; + + NamedObjectProduct(final Map exponents, + final NameSymbol nameSymbol) { + super(exponents); + this.nameSymbol = nameSymbol; + } + + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; + } + +} diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 5b1b739..d4f88b9 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -26,6 +26,8 @@ import java.util.Objects; import java.util.Set; import java.util.function.Function; +import sevenUnits.unit.NameSymbol; + /** * An immutable product of multiple objects of a type, such as base units. The * objects can be multiplied and exponentiated. @@ -33,7 +35,7 @@ import java.util.function.Function; * @author Adrien Hopkins * @since 2019-10-16 */ -public final class ObjectProduct { +public class ObjectProduct { /** * Returns an empty ObjectProduct of a certain type * @@ -88,7 +90,7 @@ public final class ObjectProduct { * @param exponents objects that make up this product * @since 2019-10-16 */ - private ObjectProduct(final Map exponents) { + ObjectProduct(final Map exponents) { this.exponents = Collections.unmodifiableMap( ConditionalExistenceCollections.conditionalExistenceMap(exponents, e -> !Integer.valueOf(0).equals(e.getValue()))); @@ -280,4 +282,13 @@ public final class ObjectProduct { return positiveString + negativeString; } + + /** + * @return named version of this {@code ObjectProduct}, using data from + * {@code nameSymbol} + * @since 2021-12-15 + */ + public NamedObjectProduct withName(NameSymbol nameSymbol) { + return new NamedObjectProduct<>(this.exponents, nameSymbol); + } } -- cgit v1.2.3 From b7eee33a5b162b4057d04d28f45738e3048bf01d Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 24 Feb 2022 16:44:13 -0500 Subject: Moved SemanticVersionNumber to sevenUnits.utils --- src/main/java/sevenUnits/ProgramInfo.java | 2 + .../java/sevenUnits/SemanticVersionNumber.java | 691 --------------------- .../sevenUnits/utils/SemanticVersionNumber.java | 691 +++++++++++++++++++++ src/test/java/sevenUnits/SemanticVersionTest.java | 399 ------------ .../java/sevenUnits/utils/SemanticVersionTest.java | 399 ++++++++++++ src/test/java/sevenUnitsGUI/package-info.java | 23 - 6 files changed, 1092 insertions(+), 1113 deletions(-) delete mode 100644 src/main/java/sevenUnits/SemanticVersionNumber.java create mode 100644 src/main/java/sevenUnits/utils/SemanticVersionNumber.java delete mode 100644 src/test/java/sevenUnits/SemanticVersionTest.java create mode 100644 src/test/java/sevenUnits/utils/SemanticVersionTest.java delete mode 100644 src/test/java/sevenUnitsGUI/package-info.java (limited to 'src/main/java/sevenUnits/utils') diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 876367d..6407d7c 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -16,6 +16,8 @@ */ package sevenUnits; +import sevenUnits.utils.SemanticVersionNumber; + /** * Information about 7Units * diff --git a/src/main/java/sevenUnits/SemanticVersionNumber.java b/src/main/java/sevenUnits/SemanticVersionNumber.java deleted file mode 100644 index 01aeb27..0000000 --- a/src/main/java/sevenUnits/SemanticVersionNumber.java +++ /dev/null @@ -1,691 +0,0 @@ -/** - * 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 . - */ -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 Semantic Versioning - * scheme - *

- * Each version number has three main parts: - *

    - *
  1. The major version, which increments when backwards incompatible changes - * are made - *
  2. The minor version, which increments when backwards compatible feature - * changes are made - *
  3. The patch version, which increments when backwards compatible bug fixes - * are made - *
- * - * @since 2022-02-19 - */ -public final class SemanticVersionNumber - implements Comparable { - /** - * A builder that can be used to create complex version numbers. - *

- * 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 preReleaseIdentifiers; - private final List 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 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 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. - *

- * This ordering is consistent with equals, unlike - * {@code SemanticVersionNumber}'s natural ordering. - */ - public static final Comparator 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 a, List 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 preRelease; - if (m.group(4) == null) { - preRelease = List.of(); - } else { - preRelease = Arrays.asList(m.group(4).split("\\.")); - } - - // build metadata - final List 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. - *

- * 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 preReleaseIdentifiers; - private final List 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 preReleaseIdentifiers, List 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 buildMetadata() { - return Collections.unmodifiableList(this.buildMetadata); - } - - /** - * Compares two version numbers according to the official Semantic Versioning - * order. - *

- * 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. - *

- */ - @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: - *

- * If this function returns true, then there should be no problems - * upgrading code written for this version to version {@code other} as long - * as: - *

    - *
  • Semantic Versioning is being used properly - *
  • Your code doesn't depend on unintended features (if it does, it isn't - * necessarily compatible with any other version) - *
- * If this function returns false, you may have to change your code to - * upgrade it to {@code other} - * - *

- * Two version numbers that are identical (ignoring build metadata) are - * always compatible. Different version numbers are compatible as long as: - *

    - *
  • The major version number is not 0 (if it is, the API is considered - * unstable and any upgrade can be backwards compatible) - *
  • The major version number is the same (changing the major version - * number implies bacwards incompatible changes) - *
  • This version comes before the other one in the official precedence - * order (downgrading can remove features you depend on) - *
- * - * @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 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. - *

- * 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 The official SemVer specification - */ - @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/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 . + */ +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 Semantic Versioning + * scheme + *

+ * Each version number has three main parts: + *

    + *
  1. The major version, which increments when backwards incompatible changes + * are made + *
  2. The minor version, which increments when backwards compatible feature + * changes are made + *
  3. The patch version, which increments when backwards compatible bug fixes + * are made + *
+ * + * @since 2022-02-19 + */ +public final class SemanticVersionNumber + implements Comparable { + /** + * A builder that can be used to create complex version numbers. + *

+ * 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 preReleaseIdentifiers; + private final List 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 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 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. + *

+ * This ordering is consistent with equals, unlike + * {@code SemanticVersionNumber}'s natural ordering. + */ + public static final Comparator 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 a, List 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 preRelease; + if (m.group(4) == null) { + preRelease = List.of(); + } else { + preRelease = Arrays.asList(m.group(4).split("\\.")); + } + + // build metadata + final List 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. + *

+ * 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 preReleaseIdentifiers; + private final List 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 preReleaseIdentifiers, List 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 buildMetadata() { + return Collections.unmodifiableList(this.buildMetadata); + } + + /** + * Compares two version numbers according to the official Semantic Versioning + * order. + *

+ * 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. + *

+ */ + @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: + *

+ * If this function returns true, then there should be no problems + * upgrading code written for this version to version {@code other} as long + * as: + *

    + *
  • Semantic Versioning is being used properly + *
  • Your code doesn't depend on unintended features (if it does, it isn't + * necessarily compatible with any other version) + *
+ * If this function returns false, you may have to change your code to + * upgrade it to {@code other} + * + *

+ * Two version numbers that are identical (ignoring build metadata) are + * always compatible. Different version numbers are compatible as long as: + *

    + *
  • The major version number is not 0 (if it is, the API is considered + * unstable and any upgrade can be backwards compatible) + *
  • The major version number is the same (changing the major version + * number implies bacwards incompatible changes) + *
  • This version comes before the other one in the official precedence + * order (downgrading can remove features you depend on) + *
+ * + * @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 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. + *

+ * 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 The official SemVer specification + */ + @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/test/java/sevenUnits/SemanticVersionTest.java b/src/test/java/sevenUnits/SemanticVersionTest.java deleted file mode 100644 index 9202ef9..0000000 --- a/src/test/java/sevenUnits/SemanticVersionTest.java +++ /dev/null @@ -1,399 +0,0 @@ -/** - * 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 . - */ -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)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(List.of("")), - "Empty string tolerated by builder.buildMetadata(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(List.of("")), - "Invalid string tolerated by builder.buildMetadata(List)"); - - // 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)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(List.of("")), - "Empty string tolerated by builder.preRelease(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(List.of("")), - "Invalid string tolerated by builder.preRelease(List)"); - - // 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"); - } -} diff --git a/src/test/java/sevenUnits/utils/SemanticVersionTest.java b/src/test/java/sevenUnits/utils/SemanticVersionTest.java new file mode 100644 index 0000000..877b258 --- /dev/null +++ b/src/test/java/sevenUnits/utils/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 . + */ +package sevenUnits.utils; + +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.utils.SemanticVersionNumber.BUILD_METADATA_COMPARATOR; +import static sevenUnits.utils.SemanticVersionNumber.builder; +import static sevenUnits.utils.SemanticVersionNumber.fromString; +import static sevenUnits.utils.SemanticVersionNumber.isValidVersionString; +import static sevenUnits.utils.SemanticVersionNumber.preRelease; +import static sevenUnits.utils.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)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Empty string tolerated by builder.buildMetadata(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Invalid string tolerated by builder.buildMetadata(List)"); + + // 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)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Empty string tolerated by builder.preRelease(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Invalid string tolerated by builder.preRelease(List)"); + + // 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"); + } +} diff --git a/src/test/java/sevenUnitsGUI/package-info.java b/src/test/java/sevenUnitsGUI/package-info.java deleted file mode 100644 index 96bdbd9..0000000 --- a/src/test/java/sevenUnitsGUI/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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 . - */ -/** - * Tests for the new 7Units GUI - * - * @author Adrien Hopkins - * @since 2022-01-29 - */ -package sevenUnitsGUI; \ No newline at end of file -- cgit v1.2.3 From 07c86e02be29aa3d3d878adce62c5c0a9a458e47 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 26 Feb 2022 09:53:24 -0500 Subject: Implemented unit conversion, with a few problems TabbedView now displays its units, but with their toString method which shows their definition in addition to their name --- CHANGELOG.org | 3 + .../sevenUnits/converterGUI/SevenUnitsGUI.java | 2 +- src/main/java/sevenUnits/unit/BaseDimension.java | 50 ++-- src/main/java/sevenUnits/unit/BaseUnit.java | 2 + src/main/java/sevenUnits/unit/BritishImperial.java | 2 + src/main/java/sevenUnits/unit/FunctionalUnit.java | 1 + .../java/sevenUnits/unit/FunctionalUnitlike.java | 1 + src/main/java/sevenUnits/unit/LinearUnit.java | 1 + src/main/java/sevenUnits/unit/Metric.java | 1 + src/main/java/sevenUnits/unit/MultiUnit.java | 1 + src/main/java/sevenUnits/unit/NameSymbol.java | 280 ------------------- src/main/java/sevenUnits/unit/Nameable.java | 59 ---- src/main/java/sevenUnits/unit/Unit.java | 2 + src/main/java/sevenUnits/unit/UnitDatabase.java | 23 +- src/main/java/sevenUnits/unit/UnitPrefix.java | 1 + src/main/java/sevenUnits/unit/UnitValue.java | 2 + src/main/java/sevenUnits/unit/Unitlike.java | 2 + src/main/java/sevenUnits/unit/UnitlikeValue.java | 2 + src/main/java/sevenUnits/utils/NameSymbol.java | 299 +++++++++++++++++++++ src/main/java/sevenUnits/utils/Nameable.java | 59 ++++ .../java/sevenUnits/utils/NamedObjectProduct.java | 7 +- src/main/java/sevenUnits/utils/ObjectProduct.java | 6 +- src/main/java/sevenUnitsGUI/Presenter.java | 202 +++++++++++++- src/main/java/sevenUnitsGUI/TabbedView.java | 14 +- .../java/sevenUnitsGUI/UnitConversionView.java | 3 +- src/main/java/sevenUnitsGUI/ViewBot.java | 7 +- .../java/sevenUnits/unit/UnitDatabaseTest.java | 1 + src/test/java/sevenUnits/unit/UnitTest.java | 1 + src/test/java/sevenUnitsGUI/PresenterTest.java | 11 +- 29 files changed, 636 insertions(+), 409 deletions(-) delete mode 100644 src/main/java/sevenUnits/unit/NameSymbol.java delete mode 100644 src/main/java/sevenUnits/unit/Nameable.java create mode 100644 src/main/java/sevenUnits/utils/NameSymbol.java create mode 100644 src/main/java/sevenUnits/utils/Nameable.java (limited to 'src/main/java/sevenUnits/utils') diff --git a/CHANGELOG.org b/CHANGELOG.org index 2abee52..3bc46c2 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -4,8 +4,11 @@ *** Added - Added tests for the GUI - Added an object for the version numbers (SemanticVersionNumber) + - Added some toString methods to NameSymbol *** Changed - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve + - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional instead of String, even though they will always succeed. + - The UnitDatabase's units and dimensions are now always named - Tweaked the look of the unit and expression conversion sections of the view ** v0.3.2 - [2021-12-02 Thu] *** Added diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index 9c6ae0a..55e1546 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -70,12 +70,12 @@ import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.Metric; -import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; import sevenUnits.unit.UnitValue; import sevenUnits.utils.ConditionalExistenceCollections; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java index d5e98ca..bcd57d9 100644 --- a/src/main/java/sevenUnits/unit/BaseDimension.java +++ b/src/main/java/sevenUnits/unit/BaseDimension.java @@ -18,70 +18,58 @@ package sevenUnits.unit; import java.util.Objects; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; + /** * A dimension that defines a {@code BaseUnit} * * @author Adrien Hopkins * @since 2019-10-16 */ -public final class BaseDimension { +public final class BaseDimension implements Nameable { /** * Gets a {@code BaseDimension} with the provided name and symbol. * - * @param name - * name of dimension - * @param symbol - * symbol used for dimension + * @param name name of dimension + * @param symbol symbol used for dimension * @return dimension * @since 2019-10-16 */ public static BaseDimension valueOf(final String name, final String symbol) { return new BaseDimension(name, symbol); } - + /** * The name of the dimension. */ private final String name; /** - * The symbol used by the dimension. Symbols should be short, generally one or two characters. + * The symbol used by the dimension. Symbols should be short, generally one + * or two characters. */ private final String symbol; - + /** * Creates the {@code BaseDimension}. * - * @param name - * name of unit - * @param symbol - * symbol of unit - * @throws NullPointerException - * if any argument is null + * @param name name of unit + * @param symbol symbol of unit + * @throws NullPointerException if any argument is null * @since 2019-10-16 */ private BaseDimension(final String name, final String symbol) { this.name = Objects.requireNonNull(name, "name must not be null."); this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); } - - /** - * @return name - * @since 2019-10-16 - */ - public final String getName() { - return this.name; - } - - /** - * @return symbol - * @since 2019-10-16 - */ - public final String getSymbol() { - return this.symbol; + + @Override + public NameSymbol getNameSymbol() { + return NameSymbol.of(this.name, this.symbol); } - + @Override public String toString() { - return String.format("%s (%s)", this.getName(), this.getSymbol()); + return String.format("%s (%s)", this.name, this.symbol); } } diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java index ee2c277..dba7f52 100644 --- a/src/main/java/sevenUnits/unit/BaseUnit.java +++ b/src/main/java/sevenUnits/unit/BaseUnit.java @@ -20,6 +20,8 @@ import java.util.HashSet; import java.util.Objects; import java.util.Set; +import sevenUnits.utils.NameSymbol; + /** * A unit that other units are defined by. *

diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java index 743beeb..c6e65fb 100644 --- a/src/main/java/sevenUnits/unit/BritishImperial.java +++ b/src/main/java/sevenUnits/unit/BritishImperial.java @@ -16,6 +16,8 @@ */ package sevenUnits.unit; +import sevenUnits.utils.NameSymbol; + /** * A static utility class that contains units in the British Imperial system. * diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java index df457e4..720b0af 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnit.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Objects; import java.util.function.DoubleUnaryOperator; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java index 2ee9e19..d6046c0 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 25c2e2e..deefc9a 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Objects; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 3c4d291..78e3769 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -18,6 +18,7 @@ package sevenUnits.unit; import java.util.Set; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java index 83cdb03..bc240e3 100644 --- a/src/main/java/sevenUnits/unit/MultiUnit.java +++ b/src/main/java/sevenUnits/unit/MultiUnit.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/NameSymbol.java b/src/main/java/sevenUnits/unit/NameSymbol.java deleted file mode 100644 index 3e26138..0000000 --- a/src/main/java/sevenUnits/unit/NameSymbol.java +++ /dev/null @@ -1,280 +0,0 @@ -/** - * 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 . - */ -package sevenUnits.unit; - -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 a copy of the inputted argument. - */ - private static final NameSymbol create(final String name, - final String symbol, final Set otherNames) { - final Optional primaryName; - - if (name == null && !otherNames.isEmpty()) { - // get primary name and remove it from savedNames - final Iterator 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 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. - *

- * If any argument is null, this static factory replaces it with an empty - * Optional or empty Set. - *

- * 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 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. - *

- * If any argument is null, this static factory replaces it with an empty - * Optional or empty Set. - *

- * 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 primaryName; - private final Optional symbol; - - private final Set 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 primaryName, - final Optional symbol, final Set 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 getOtherNames() { - return this.otherNames; - } - - /** - * @return primaryName - * @since 2019-10-21 - */ - public final Optional getPrimaryName() { - return this.primaryName; - } - - /** - * @return symbol - * @since 2019-10-21 - */ - public final Optional 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(); - } -} \ No newline at end of file diff --git a/src/main/java/sevenUnits/unit/Nameable.java b/src/main/java/sevenUnits/unit/Nameable.java deleted file mode 100644 index ed23687..0000000 --- a/src/main/java/sevenUnits/unit/Nameable.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * 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 . - */ -package sevenUnits.unit; - -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 {@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 getOtherNames() { - return this.getNameSymbol().getOtherNames(); - } - - /** - * @return preferred name of object - * @since 2020-09-07 - */ - default Optional getPrimaryName() { - return this.getNameSymbol().getPrimaryName(); - } - - /** - * @return short symbol representing object - * @since 2020-09-07 - */ - default Optional getSymbol() { - return this.getNameSymbol().getSymbol(); - } -} diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 005b6f7..9866e9c 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -22,6 +22,8 @@ import java.util.Objects; import java.util.function.DoubleUnaryOperator; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 18ac619..b029539 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -47,6 +47,8 @@ import java.util.regex.Pattern; import sevenUnits.utils.ConditionalExistenceCollections; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -1197,7 +1199,7 @@ public final class UnitDatabase { * @since 2019-03-14 * @since v0.2.0 */ - private final Map> dimensions; + private final Map> dimensions; /** * A map mapping strings to units (including prefixes) @@ -1313,9 +1315,16 @@ public final class UnitDatabase { */ public void addDimension(final String name, final ObjectProduct dimension) { - this.dimensions.put( - Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(dimension, "dimension must not be null.")); + Objects.requireNonNull(name, "name may not be null"); + Objects.requireNonNull(dimension, "dimension may not be null"); + if (dimension instanceof NamedObjectProduct) { + this.dimensions.put(name, + (NamedObjectProduct) dimension); + } else { + final NamedObjectProduct namedDimension = dimension + .withName(NameSymbol.ofName(name)); + this.dimensions.put(name, namedDimension); + } } /** @@ -1367,7 +1376,7 @@ public final class UnitDatabase { throw e; } - this.addDimension(name, dimension); + this.addDimension(name, dimension.withName(NameSymbol.ofName(name))); } } @@ -1463,7 +1472,7 @@ public final class UnitDatabase { throw e; } - this.addUnit(name, unit); + this.addUnit(name, unit.withName(NameSymbol.ofName(name))); } } } @@ -1510,7 +1519,7 @@ public final class UnitDatabase { * @since 2019-04-13 * @since v0.2.0 */ - public Map> dimensionMap() { + public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index 308f4b0..bf9d1fd 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.Set; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; /** * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java index f6d18f8..339263d 100644 --- a/src/main/java/sevenUnits/unit/UnitValue.java +++ b/src/main/java/sevenUnits/unit/UnitValue.java @@ -19,6 +19,8 @@ package sevenUnits.unit; import java.util.Objects; import java.util.Optional; +import sevenUnits.utils.NameSymbol; + /** * A value expressed in a unit. * diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java index d2dcbbb..68de2c2 100644 --- a/src/main/java/sevenUnits/unit/Unitlike.java +++ b/src/main/java/sevenUnits/unit/Unitlike.java @@ -22,6 +22,8 @@ import java.util.Objects; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java index edc13ca..26354b1 100644 --- a/src/main/java/sevenUnits/unit/UnitlikeValue.java +++ b/src/main/java/sevenUnits/unit/UnitlikeValue.java @@ -18,6 +18,8 @@ package sevenUnits.unit; import java.util.Optional; +import sevenUnits.utils.NameSymbol; + /** * * @since 2020-09-07 diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java new file mode 100644 index 0000000..aea274b --- /dev/null +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -0,0 +1,299 @@ +/** + * 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 . + */ +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 a copy of the inputted argument. + */ + private static final NameSymbol create(final String name, + final String symbol, final Set otherNames) { + final Optional primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + final Iterator 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 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. + *

+ * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + *

+ * 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 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. + *

+ * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + *

+ * 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 primaryName; + private final Optional symbol; + + private final Set 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 primaryName, + final Optional symbol, final Set 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 getOtherNames() { + return this.otherNames; + } + + /** + * @return primaryName + * @since 2019-10-21 + */ + public final Optional getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-10-21 + */ + public final Optional 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(); + } + + /** + * @return a short version of this NameSymbol (defaults to symbol instead of + * primary name) + * @since 2022-02-26 + */ + public String shortName() { + return this.symbol.or(this::getPrimaryName).orElse("EMPTY"); + } + + @Override + public String toString() { + if (this.isEmpty()) + return "NameSymbol.EMPTY"; + else if (this.primaryName.isPresent() && this.symbol.isPresent()) + return this.primaryName + " (" + this.symbol + ")"; + else + return this.primaryName.orElseGet(this.symbol::orElseThrow); + } +} \ 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..3cfc05a --- /dev/null +++ b/src/main/java/sevenUnits/utils/Nameable.java @@ -0,0 +1,59 @@ +/** + * 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 . + */ +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 {@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 getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java index 514f0b1..9c3079c 100644 --- a/src/main/java/sevenUnits/utils/NamedObjectProduct.java +++ b/src/main/java/sevenUnits/utils/NamedObjectProduct.java @@ -18,9 +18,6 @@ package sevenUnits.utils; import java.util.Map; -import sevenUnits.unit.NameSymbol; -import sevenUnits.unit.Nameable; - /** * An ObjectProduct with name(s) and/or a symbol. Can be created with the * {@link ObjectProduct#withName} method. @@ -43,4 +40,8 @@ public class NamedObjectProduct extends ObjectProduct return this.nameSymbol; } + @Override + public String toString() { + return this.nameSymbol.toString() + ", " + super.toString(); + } } diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index d4f88b9..926ce10 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -26,8 +26,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.Function; -import sevenUnits.unit.NameSymbol; - /** * An immutable product of multiple objects of a type, such as base units. The * objects can be multiplied and exponentiated. @@ -246,7 +244,9 @@ public class ObjectProduct { */ @Override public String toString() { - return this.toString(Object::toString); + return this.toString(o -> o instanceof Nameable + ? ((Nameable) o).getNameSymbol().shortName() + : o.toString()); } /** diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 23a631d..be02364 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -19,17 +19,26 @@ package sevenUnitsGUI; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; import java.util.Scanner; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.BritishImperial; +import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -40,6 +49,44 @@ import sevenUnits.utils.UncertainDouble; * @since 2021-12-15 */ public final class Presenter { + /** The default place where settings are stored. */ + private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** The default place where units are stored. */ + private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; + /** The default place where dimensions are stored. */ + private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; + /** The default place where exceptions are stored. */ + private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; + + /** + * Adds default units and dimensions to a database. + * + * @param database database to add to + * @since 2019-04-14 + * @since v0.2.0 + */ + private static void addDefaults(final UnitDatabase database) { + database.addUnit("metre", Metric.METRE); + database.addUnit("kilogram", Metric.KILOGRAM); + database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000)); + database.addUnit("second", Metric.SECOND); + database.addUnit("ampere", Metric.AMPERE); + database.addUnit("kelvin", Metric.KELVIN); + database.addUnit("mole", Metric.MOLE); + database.addUnit("candela", Metric.CANDELA); + database.addUnit("bit", Metric.BIT); + database.addUnit("unit", Metric.ONE); + // nonlinear units - must be loaded manually + database.addUnit("tempCelsius", Metric.CELSIUS); + database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); + + // load initial dimensions + database.addDimension("LENGTH", Metric.Dimensions.LENGTH); + database.addDimension("MASS", Metric.Dimensions.MASS); + database.addDimension("TIME", Metric.Dimensions.TIME); + database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE); + } + /** * @return text in About file * @since 2022-02-19 @@ -85,6 +132,25 @@ public final class Presenter { return Presenter.class.getResourceAsStream(filepath); } + /** + * Accepts a collection and returns a set with the unique elements in that + * collection + * + * @param type of element in collection + * @param collection collection to uniquify + * @return unique collection + * @since 2022-02-26 + */ + private static Set unique(Collection collection) { + final Set uniqueSet = new HashSet<>(); + for (final E e : collection) { + if (!uniqueSet.contains(e)) { + uniqueSet.add(e); + } + } + return uniqueSet; + } + /** * @return {@code line} with any comments removed. * @since 2021-03-13 @@ -126,6 +192,13 @@ public final class Presenter { */ private Predicate> prefixRepetitionRule; + /** + * The set of units that is considered neither metric nor nonmetric for the + * purposes of the metric-imperial one-way conversion. These units are + * included in both From and To, even if One Way Conversion is enabled. + */ + private final Set metricExceptions; + /** * If this is true, views that show units as a list will have metric units * removed from the From unit list and imperial/USC units removed from the To @@ -148,19 +221,42 @@ public final class Presenter { public Presenter(View view) { this.view = view; this.database = new UnitDatabase(); + addDefaults(this.database); + + // load units and prefixes + try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { + this.database.loadUnitsFromStream(units); + } catch (final IOException e) { + throw new AssertionError("Loading of unitsfile.txt failed.", e); + } + + // load dimensions + try (final InputStream dimensions = inputStream( + DEFAULT_DIMENSIONS_FILEPATH)) { + this.database.loadDimensionsFromStream(dimensions); + } catch (final IOException e) { + throw new AssertionError("Loading of dimensionfile.txt failed.", e); + } + + // load metric exceptions + try { + this.metricExceptions = new HashSet<>(); + try (InputStream exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH); + Scanner scanner = new Scanner(exceptions)) { + while (scanner.hasNextLine()) { + final String line = Presenter + .withoutComments(scanner.nextLine()); + if (!line.isBlank()) { + this.metricExceptions.add(line); + } + } + } + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } } - /** - * Sets the dimension of the view's From and To units. - * - * @throws UnsupportedOperationException if the view does not support - * unit-based conversion (does not - * implement - * {@link UnitConversionView}) - * @since 2021-12-15 - */ - public void applyDimensionFilter() {} - /** * Gets settings from the view and applies them to both view and presenter. * @@ -190,7 +286,52 @@ public final class Presenter { * {@link UnitConversionView}) * @since 2021-12-15 */ - public void convertUnits() {} + public void convertUnits() { + if (this.view instanceof UnitConversionView) { + final UnitConversionView ucview = (UnitConversionView) this.view; + + final Optional fromUnitOptional = ucview.getFromSelection(); + final Optional toUnitOptional = ucview.getToSelection(); + final OptionalDouble valueOptional = ucview.getInputValue(); + + // ensure everything is obtained + final Unit fromUnit, toUnit; + final double value; + if (fromUnitOptional.isPresent()) { + fromUnit = fromUnitOptional.orElseThrow(); + } else { + this.view.showErrorMessage("Unit Conversion Error", + "Please specify a From unit"); + return; + } + if (toUnitOptional.isPresent()) { + toUnit = toUnitOptional.orElseThrow(); + } else { + this.view.showErrorMessage("Unit Conversion Error", + "Please specify a To unit"); + return; + } + if (valueOptional.isPresent()) { + value = valueOptional.orElseThrow(); + } else { + this.view.showErrorMessage("Unit Conversion Error", + "Please specify a valid value"); + return; + } + + if (!fromUnit.canConvertTo(toUnit)) + throw new AssertionError( + "From and To units incompatible (should be impossible)"); + + // convert! + final UnitValue initialValue = UnitValue.of(fromUnit, value); + final UnitValue converted = initialValue.convertTo(toUnit); + ucview.showUnitConversionOutput( + String.format("%s = %s", initialValue, converted)); + } else + throw new UnsupportedOperationException( + "This function can only be called when the view is a UnitConversionView."); + } /** * Loads settings from the user's settings file and applies them to the view. @@ -199,6 +340,21 @@ public final class Presenter { */ public void loadSettings() {} + /** + * Completes creation of the presenter. This part of the initialization + * depends on the view's functions, so it cannot be run if the components + * they depend on are not created yet. + * + * @since 2022-02-26 + */ + public void postViewInitialize() { + // unit conversion specific stuff + if (this.view instanceof UnitConversionView) { + final UnitConversionView ucview = (UnitConversionView) this.view; + ucview.setDimensions(unique(this.database.dimensionMap().values())); + } + } + void prefixSelected() {} /** @@ -227,4 +383,26 @@ public final class Presenter { } void unitNameSelected() {} + + /** + * Updates the view's From and To units, if it has some + * + * @since 2021-12-15 + */ + public void updateView() { + if (this.view instanceof UnitConversionView) { + final UnitConversionView ucview = (UnitConversionView) this.view; + final ObjectProduct viewDimension = ucview + .getSelectedDimension().orElseThrow(); + + final Set units = this.database + .unitMapPrefixless(this.showDuplicateUnits).entrySet().stream() + .map(Map.Entry::getValue) + .filter(u -> viewDimension.equals(u.getDimension())) + .collect(Collectors.toSet()); + + ucview.setFromUnits(units); + ucview.setToUnits(units); + } + } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index e92b661..c3a05e2 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import javax.swing.BorderFactory; @@ -212,7 +213,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.dimensionSelector = new JComboBox<>(); inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START); this.dimensionSelector - .addItemListener(e -> this.presenter.applyDimensionFilter()); + .addItemListener(e -> this.presenter.updateView()); final JLabel arrowLabel = new JLabel("-->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); @@ -326,9 +327,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); // ============ FINALIZE CREATION OF VIEW ============ + this.presenter.postViewInitialize(); this.frame.pack(); this.frame.setVisible(true); - } /** @@ -541,8 +542,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public String getInputValue() { - return this.valueInput.getText(); + public OptionalDouble getInputValue() { + final String text = this.valueInput.getText(); + try { + return OptionalDouble.of(Double.parseDouble(text)); + } catch (final NumberFormatException e) { + return OptionalDouble.empty(); + } } @Override diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index 5fd5a82..e411a44 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -17,6 +17,7 @@ package sevenUnitsGUI; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import sevenUnits.unit.BaseDimension; @@ -48,7 +49,7 @@ public interface UnitConversionView extends View { * string provided by the user) * @since 2021-12-15 */ - String getInputValue(); + OptionalDouble getInputValue(); /** * @return selected dimension diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 0c0d189..bc5302b 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import sevenUnits.unit.BaseDimension; @@ -47,7 +48,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { /** * The user-provided string representing the value in {@code fromSelection} */ - private String inputValue; + private OptionalDouble inputValue; /** The unit selected in the From selection */ private Optional fromSelection; /** The unit selected in the To selection */ @@ -102,7 +103,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public String getInputValue() { + public OptionalDouble getInputValue() { return this.inputValue; } @@ -190,7 +191,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @param inputValue the inputValue to set * @since 2022-01-29 */ - public void setInputValue(String inputValue) { + public void setInputValue(OptionalDouble inputValue) { this.inputValue = inputValue; } diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java index 2276d7c..b8669cb 100644 --- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java +++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java @@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.UncertainDouble; /** diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java index bb2e6a4..f174e7c 100644 --- a/src/test/java/sevenUnits/unit/UnitTest.java +++ b/src/test/java/sevenUnits/unit/UnitTest.java @@ -27,6 +27,7 @@ import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.Test; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.UncertainDouble; /** diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 3e7c2b5..ff1450b 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -21,14 +21,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import org.junit.jupiter.api.Test; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Metric; -import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Unit; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.NamedObjectProduct; /** @@ -44,12 +45,12 @@ public final class PresenterTest { Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); /** - * Test for {@link Presenter#applyDimensionFilter()} + * Test for {@link Presenter#updateView()} * * @since 2022-02-12 */ @Test - void testApplyDimensionFilter() { + void testUpdateView() { // setup final ViewBot viewBot = new ViewBot(); final Presenter presenter = new Presenter(viewBot); @@ -61,7 +62,7 @@ public final class PresenterTest { Optional.of(this.testDimensions.iterator().next())); // filter to length units only, then get the filtered sets of units - presenter.applyDimensionFilter(); + presenter.updateView(); final Set fromUnits = viewBot.getFromUnits(); final Set toUnits = viewBot.getToUnits(); @@ -118,7 +119,7 @@ public final class PresenterTest { viewBot.setToUnits(this.testUnits); viewBot.setFromSelection(Optional.of(Metric.METRE)); viewBot.setToSelection(Optional.of(Metric.KILOMETRE)); - viewBot.setInputValue("10000.0"); + viewBot.setInputValue(OptionalDouble.of(10000.0)); // convert units presenter.convertUnits(); -- cgit v1.2.3 From 934213e08e85cc20bd994d0f39567426c21b89eb Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 26 Feb 2022 11:15:04 -0500 Subject: Implemented expression conversion, tests now pass --- CHANGELOG.org | 2 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 11 +++ src/main/java/sevenUnits/utils/NameSymbol.java | 9 -- src/main/java/sevenUnits/utils/Nameable.java | 20 ++++ src/main/java/sevenUnits/utils/ObjectProduct.java | 6 +- src/main/java/sevenUnitsGUI/Presenter.java | 77 ++++++++++----- src/main/java/sevenUnitsGUI/TabbedView.java | 15 +-- .../java/sevenUnitsGUI/UnitConversionView.java | 6 +- src/main/java/sevenUnitsGUI/ViewBot.java | 36 +++++-- src/test/java/sevenUnitsGUI/PresenterTest.java | 104 ++++++++++++--------- 10 files changed, 195 insertions(+), 91 deletions(-) (limited to 'src/main/java/sevenUnits/utils') diff --git a/CHANGELOG.org b/CHANGELOG.org index 3bc46c2..681fead 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -4,7 +4,7 @@ *** Added - Added tests for the GUI - Added an object for the version numbers (SemanticVersionNumber) - - Added some toString methods to NameSymbol + - Added some toString methods to NameSymbol and Nameable *** Changed - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional instead of String, even though they will always succeed. diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index b029539..bf6ae64 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1477,6 +1477,17 @@ public final class UnitDatabase { } } + /** + * Removes all units, prefixes and dimensions from this database. + * + * @since 2022-02-26 + */ + public void clear() { + this.dimensions.clear(); + this.prefixes.clear(); + this.prefixlessUnits.clear(); + } + /** * Tests if the database has a unit dimension with this name. * diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index aea274b..255e82f 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -278,15 +278,6 @@ public final class NameSymbol { return this.primaryName.isEmpty() && this.symbol.isEmpty(); } - /** - * @return a short version of this NameSymbol (defaults to symbol instead of - * primary name) - * @since 2022-02-26 - */ - public String shortName() { - return this.symbol.or(this::getPrimaryName).orElse("EMPTY"); - } - @Override public String toString() { if (this.isEmpty()) diff --git a/src/main/java/sevenUnits/utils/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java index 3cfc05a..e469d04 100644 --- a/src/main/java/sevenUnits/utils/Nameable.java +++ b/src/main/java/sevenUnits/utils/Nameable.java @@ -26,6 +26,16 @@ import java.util.Set; * @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 @@ -49,6 +59,16 @@ public interface Nameable { 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 diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 926ce10..830f9d7 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -244,9 +244,9 @@ public class ObjectProduct { */ @Override public String toString() { - return this.toString(o -> o instanceof Nameable - ? ((Nameable) o).getNameSymbol().shortName() - : o.toString()); + return this + .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName() + : o.toString()); } /** diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index be02364..57d353d 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; import java.util.OptionalDouble; import java.util.Scanner; @@ -34,6 +35,7 @@ import java.util.stream.Collectors; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; +import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; @@ -170,7 +172,7 @@ public final class Presenter { /** * The database that this presenter communicates with (effectively the model) */ - private final UnitDatabase database; + final UnitDatabase database; /** * The rule used for parsing input numbers. Any number-string inputted into @@ -274,7 +276,58 @@ public final class Presenter { * {@link ExpressionConversionView}) * @since 2021-12-15 */ - public void convertExpressions() {} + public void convertExpressions() { + if (this.view instanceof ExpressionConversionView) { + final ExpressionConversionView xcview = (ExpressionConversionView) this.view; + + final String fromExpression = xcview.getFromExpression(); + final String toExpression = xcview.getToExpression(); + + // expressions must not be empty + if (fromExpression.isEmpty()) { + this.view.showErrorMessage("Parse Error", + "Please enter a unit expression in the From: box."); + return; + } + if (toExpression.isEmpty()) { + this.view.showErrorMessage("Parse Error", + "Please enter a unit expression in the To: box."); + return; + } + + // evaluate expressions + final LinearUnitValue from; + final Unit to; + try { + from = this.database.evaluateUnitExpression(fromExpression); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorMessage("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); + return; + } + try { + to = this.database.getUnitFromExpression(toExpression); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorMessage("Parse Error", + "Could not recognize text in To entry: " + e.getMessage()); + return; + } + + // convert and show output + if (from.getUnit().canConvertTo(to)) { + final double value = from.asUnitValue().convertTo(to).getValue(); + xcview.showExpressionConversionOutput(fromExpression, toExpression, + value); + } else { + this.view.showErrorMessage("Conversion Error", + "Cannot convert between \"" + fromExpression + "\" and \"" + + toExpression + "\"."); + } + + } else + throw new UnsupportedOperationException( + "This function can only be called when the view is an ExpressionConversionView"); + } /** * Converts from the view's input unit to its output unit. Displays an error @@ -326,8 +379,7 @@ public final class Presenter { // convert! final UnitValue initialValue = UnitValue.of(fromUnit, value); final UnitValue converted = initialValue.convertTo(toUnit); - ucview.showUnitConversionOutput( - String.format("%s = %s", initialValue, converted)); + ucview.showUnitConversionOutput(initialValue, converted); } else throw new UnsupportedOperationException( "This function can only be called when the view is a UnitConversionView."); @@ -365,23 +417,6 @@ public final class Presenter { */ public void saveSettings() {} - /** - * Returns true if and only if the unit represented by {@code unitName} has - * the dimension represented by {@code dimensionName}. - * - * @param unitName name of unit to test - * @param dimensionName name of dimension to test - * @return whether unit has dimenision - * @since 2019-04-13 - * @since v0.2.0 - */ - boolean unitMatchesDimension(String unitName, String dimensionName) { - final Unit unit = this.database.getUnit(unitName); - final ObjectProduct dimension = this.database - .getDimension(dimensionName); - return unit.getDimension().equals(dimension); - } - void unitNameSelected() {} /** diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index c3a05e2..1d40087 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -23,6 +23,7 @@ import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.text.ParseException; import java.util.AbstractSet; import java.util.Collections; import java.util.Iterator; @@ -59,6 +60,7 @@ import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitPrefix; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; @@ -140,7 +142,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** The combo box that selects dimensions */ private final JComboBox> dimensionSelector; /** The panel for inputting values in the dimension-based converter */ - private final JTextField valueInput; + private final JFormattedTextField valueInput; /** The panel for "From" in the dimension-based converter */ private final SearchBoxList fromSearch; /** The panel for "To" in the dimension-based converter */ @@ -543,12 +545,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public OptionalDouble getInputValue() { - final String text = this.valueInput.getText(); try { - return OptionalDouble.of(Double.parseDouble(text)); - } catch (final NumberFormatException e) { + this.valueInput.commitEdit(); + } catch (final ParseException e) { return OptionalDouble.empty(); } + return OptionalDouble + .of(((Number) this.valueInput.getValue()).doubleValue()); } @Override @@ -604,8 +607,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public void showUnitConversionOutput(String outputString) { - this.unitOutput.setText(outputString); + public void showUnitConversionOutput(UnitValue input, UnitValue output) { + this.unitOutput.setText(input + " = " + output); } } diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index e411a44..67d3ddc 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -22,6 +22,7 @@ import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; @@ -94,8 +95,9 @@ public interface UnitConversionView extends View { /** * Shows the output of a unit conversion. * - * @param outputString string that shows output of conversion + * @param input input unit & value (obtained from this view) + * @param output output unit & value * @since 2021-12-24 */ - void showUnitConversionOutput(String outputString); + void showUnitConversionOutput(UnitValue input, UnitValue output); } diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index bc5302b..43d73bb 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -16,6 +16,7 @@ */ package sevenUnitsGUI; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -25,6 +26,7 @@ import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; @@ -59,8 +61,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private Set fromUnits; /** The units available in the To selection */ private Set toUnits; + /** Saved input values of all unit conversions */ + private final List unitConversionInputValues; /** Saved output values of all unit conversions */ - private List unitConversionOutputValues; + private final List unitConversionOutputValues; + /** Saved outputs of all unit expressions */ + private final List expressionConversionOutputs; /** * Creates a new {@code ViewBot} with a new presenter. @@ -69,6 +75,10 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { */ public ViewBot() { this.presenter = new Presenter(this); + + this.unitConversionInputValues = new ArrayList<>(); + this.unitConversionOutputValues = new ArrayList<>(); + this.expressionConversionOutputs = new ArrayList<>(); } /** @@ -81,7 +91,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } public List getExpressionConversionOutputs() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.expressionConversionOutputs; } @Override @@ -138,11 +148,19 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { return Collections.unmodifiableSet(this.toUnits); } + /** + * @return the unitConversionInputValues + * @since 2022-02-26 + */ + public List getUnitConversionInputValues() { + return this.unitConversionInputValues; + } + /** * @return the unitConversionOutputValues * @since 2022-02-10 */ - public List getUnitConversionOutputValues() { + public List getUnitConversionOutputValues() { return this.unitConversionOutputValues; } @@ -247,13 +265,17 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void showExpressionConversionOutput(String fromExpression, String toExpression, double value) { - System.out.printf("Expression Conversion: %s = %d * (%s)%n", - fromExpression, value, toExpression); + final String output = String.format("%s = %s %s", fromExpression, value, + toExpression); + this.expressionConversionOutputs.add(output); + System.out.println("Expression Conversion: " + output); } @Override - public void showUnitConversionOutput(String outputString) { - System.out.println("Unit conversion: " + outputString); + public void showUnitConversionOutput(UnitValue input, UnitValue output) { + this.unitConversionInputValues.add(input); + this.unitConversionOutputValues.add(output); + System.out.println("Unit conversion: " + input + " = " + output); } @Override diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index ff1450b..deb16d7 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -19,16 +19,19 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.OptionalDouble; import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.NamedObjectProduct; @@ -38,50 +41,19 @@ import sevenUnits.utils.NamedObjectProduct; * @since 2022-02-10 */ public final class PresenterTest { + private static final List unitNames( + Collection units) { + return units.stream().map(Unit::getShortName) + .collect(Collectors.toList()); + } + Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); + Set> testDimensions = Set.of( Metric.Dimensions.LENGTH.withName(NameSymbol.ofName("Length")), Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); - /** - * Test for {@link Presenter#updateView()} - * - * @since 2022-02-12 - */ - @Test - void testUpdateView() { - // setup - final ViewBot viewBot = new ViewBot(); - final Presenter presenter = new Presenter(viewBot); - - viewBot.setFromUnits(this.testUnits); - viewBot.setToUnits(this.testUnits); - viewBot.setDimensions(this.testDimensions); - viewBot.setSelectedDimension( - Optional.of(this.testDimensions.iterator().next())); - - // filter to length units only, then get the filtered sets of units - presenter.updateView(); - final Set fromUnits = viewBot.getFromUnits(); - final Set toUnits = viewBot.getToUnits(); - - // test that fromUnits/toUnits is [METRE, KILOMETRE] - // HOWEVER I don't care about the order so I'm testing it this way - assertEquals(2, fromUnits.size(), - "Invalid fromUnits (length != 2): " + fromUnits); - assertEquals(2, toUnits.size(), - "Invalid toUnits (length != 2): " + toUnits); - assertTrue(fromUnits.contains(Metric.METRE), - "Invaild fromUnits (METRE missing): " + fromUnits); - assertTrue(toUnits.contains(Metric.METRE), - "Invaild toUnits (METRE missing): " + toUnits); - assertTrue(fromUnits.contains(Metric.KILOMETRE), - "Invaild fromUnits (KILOMETRE missing): " + fromUnits); - assertTrue(toUnits.contains(Metric.KILOMETRE), - "Invaild toUnits (KILOMETRE missing): " + toUnits); - } - /** * Test method for {@link Presenter#convertExpressions} * @@ -129,10 +101,58 @@ public final class PresenterTest { * here (that's for the backend tests), I'm just testing that it correctly * calls the unit conversion system */ - final String expected = String - .valueOf(Metric.METRE.convertTo(Metric.KILOMETRE, 10000.0)); + final UnitValue expectedInput = UnitValue.of(Metric.METRE, 10000.0); + final UnitValue expectedOutput = expectedInput + .convertTo(Metric.KILOMETRE); + + final List inputs = viewBot.getUnitConversionInputValues(); + final List outputs = viewBot.getUnitConversionOutputValues(); + assertEquals(expectedInput, inputs.get(inputs.size() - 1)); + assertEquals(expectedOutput, outputs.get(outputs.size() - 1)); + } + + /** + * Test for {@link Presenter#updateView()} + * + * @since 2022-02-12 + */ + @Test + void testUpdateView() { + // setup + final ViewBot viewBot = new ViewBot(); + final Presenter presenter = new Presenter(viewBot); + + // override default database units + presenter.database.clear(); + for (final Unit unit : this.testUnits) { + presenter.database.addUnit(unit.getPrimaryName().orElseThrow(), unit); + } + + // set from and to units + viewBot.setFromUnits(this.testUnits); + viewBot.setToUnits(this.testUnits); + viewBot.setDimensions(this.testDimensions); + viewBot.setSelectedDimension( + Optional.of(this.testDimensions.iterator().next())); + + // filter to length units only, then get the filtered sets of units + presenter.updateView(); + final Set fromUnits = viewBot.getFromUnits(); + final Set toUnits = viewBot.getToUnits(); - final List outputs = viewBot.getUnitConversionOutputValues(); - assertEquals(expected, outputs.get(outputs.size() - 1)); + // test that fromUnits/toUnits is [METRE, KILOMETRE] + // HOWEVER I don't care about the order so I'm testing it this way + assertEquals(2, fromUnits.size(), + "Invalid fromUnits (length != 2): " + unitNames(fromUnits)); + assertEquals(2, toUnits.size(), + "Invalid toUnits (length != 2): " + unitNames(toUnits)); + assertTrue(fromUnits.contains(Metric.METRE), + "Invaild fromUnits (METRE missing): " + unitNames(fromUnits)); + assertTrue(toUnits.contains(Metric.METRE), + "Invaild toUnits (METRE missing): " + unitNames(toUnits)); + assertTrue(fromUnits.contains(Metric.KILOMETRE), + "Invaild fromUnits (KILOMETRE missing): " + unitNames(fromUnits)); + assertTrue(toUnits.contains(Metric.KILOMETRE), + "Invaild toUnits (KILOMETRE missing): " + unitNames(toUnits)); } } -- cgit v1.2.3 From c5b209d48ef38b733e3fd8fd8ef86ae13a552821 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 10 Mar 2022 06:55:59 -0500 Subject: Simplified toString of common unit classes --- CHANGELOG.org | 1 + src/main/java/sevenUnits/unit/LinearUnit.java | 20 ++++-------- src/main/java/sevenUnits/unit/Unit.java | 36 +++++++++++++++++----- .../java/sevenUnits/utils/NamedObjectProduct.java | 6 +++- src/main/java/sevenUnitsGUI/TabbedView.java | 2 +- 5 files changed, 41 insertions(+), 24 deletions(-) (limited to 'src/main/java/sevenUnits/utils') diff --git a/CHANGELOG.org b/CHANGELOG.org index 681fead..7c53032 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -9,6 +9,7 @@ - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional instead of String, even though they will always succeed. - The UnitDatabase's units and dimensions are now always named + - The toString method of the common unit classes is now simpler. Alternate toString functions that describe the full unit are provided. - Tweaked the look of the unit and expression conversion sections of the view ** v0.3.2 - [2021-12-02 Thu] *** Added diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index deefc9a..3a28261 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -370,6 +370,12 @@ public final class LinearUnit extends Unit { this.getConversionFactor() * multiplier.getConversionFactor()); } + @Override + public String toDefinitionString() { + return Double.toString(this.conversionFactor) + " " + + this.getBase().toString(BaseUnit::getShortName); + } + /** * Returns this unit but to an exponent. * @@ -383,20 +389,6 @@ public final class LinearUnit extends Unit { Math.pow(this.conversionFactor, exponent)); } - /** - * @return a string providing a definition of this unit - * @since 2019-10-21 - */ - @Override - public String toString() { - return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() - ? String.format(" (%s)", this.getSymbol().get()) - : "") - + ", " + Double.toString(this.conversionFactor) + " * " - + this.getBase().toString(u -> u.getSymbol().get()); - } - @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 9866e9c..b80ccbd 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -24,6 +24,7 @@ import java.util.function.DoubleUnaryOperator; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; +import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; /** @@ -349,16 +350,35 @@ public abstract class Unit implements Nameable { .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); } + /** + * @return a string representing this unit's definition + * @since 2022-03-10 + */ + public String toDefinitionString() { + if (this.unitBase instanceof NamedObjectProduct) + return "derived from " + + ((NamedObjectProduct) this.unitBase).getName(); + else + return "derived from " + + this.getBase().toString(BaseUnit::getShortName); + } + + /** + * @return a string containing both this unit's name and its definition + * @since 2022-03-10 + */ + public final String toFullString() { + return this.toString() + " (" + this.toDefinitionString() + ")"; + } + @Override public String toString() { - return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() - ? String.format(" (%s)", this.getSymbol().get()) - : "") - + ", derived from " - + this.getBase().toString(u -> u.getSymbol().get()) - + (this.getOtherNames().isEmpty() ? "" - : ", also called " + String.join(", ", this.getOtherNames())); + if (this.nameSymbol.getPrimaryName().isPresent() + && this.nameSymbol.getSymbol().isPresent()) + return this.nameSymbol.getPrimaryName().orElseThrow() + " (" + + this.nameSymbol.getSymbol().orElseThrow() + ")"; + else + return this.getName(); } /** diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java index 9c3079c..89b2fad 100644 --- a/src/main/java/sevenUnits/utils/NamedObjectProduct.java +++ b/src/main/java/sevenUnits/utils/NamedObjectProduct.java @@ -40,8 +40,12 @@ public class NamedObjectProduct extends ObjectProduct return this.nameSymbol; } + public final String toDefinitionString() { + return super.toString(); + } + @Override public String toString() { - return this.nameSymbol.toString() + ", " + super.toString(); + return this.nameSymbol.toString(); } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 1d40087..0461cb6 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -71,7 +71,7 @@ import sevenUnits.utils.ObjectProduct; */ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** - * A List-like view of a JComboBox's items + * A Set-like view of a JComboBox's items * * @param type of item in list * -- cgit v1.2.3 From 91f87da88f98de996e167f0ff6809356f6d57e11 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 10 Mar 2022 15:14:33 -0500 Subject: Named the metric dimensions & fixed NameSymbol.toString --- src/main/java/sevenUnits/unit/Metric.java | 153 ++++++++++++++++--------- src/main/java/sevenUnits/utils/NameSymbol.java | 3 +- src/main/resources/dimensionfile.txt | 8 +- src/test/java/sevenUnitsGUI/PresenterTest.java | 29 +++-- 4 files changed, 117 insertions(+), 76 deletions(-) (limited to 'src/main/java/sevenUnits/utils') diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 78e3769..7ede085 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Set; import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; /** @@ -114,32 +115,39 @@ public final class Metric { public static final class Dimensions { public static final ObjectProduct EMPTY = ObjectProduct .empty(); - public static final ObjectProduct LENGTH = ObjectProduct - .oneOf(BaseDimensions.LENGTH); - public static final ObjectProduct MASS = ObjectProduct - .oneOf(BaseDimensions.MASS); - public static final ObjectProduct TIME = ObjectProduct - .oneOf(BaseDimensions.TIME); - public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct - .oneOf(BaseDimensions.ELECTRIC_CURRENT); - public static final ObjectProduct TEMPERATURE = ObjectProduct - .oneOf(BaseDimensions.TEMPERATURE); - public static final ObjectProduct QUANTITY = ObjectProduct - .oneOf(BaseDimensions.QUANTITY); - public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct - .oneOf(BaseDimensions.LUMINOUS_INTENSITY); - public static final ObjectProduct INFORMATION = ObjectProduct - .oneOf(BaseDimensions.INFORMATION); - public static final ObjectProduct CURRENCY = ObjectProduct - .oneOf(BaseDimensions.CURRENCY); + public static final NamedObjectProduct LENGTH = ObjectProduct + .oneOf(BaseDimensions.LENGTH) + .withName(NameSymbol.of("Length", "L")); + public static final NamedObjectProduct MASS = ObjectProduct + .oneOf(BaseDimensions.MASS).withName(NameSymbol.of("Mass", "M")); + public static final NamedObjectProduct TIME = ObjectProduct + .oneOf(BaseDimensions.TIME).withName(NameSymbol.of("Time", "T")); + public static final NamedObjectProduct ELECTRIC_CURRENT = ObjectProduct + .oneOf(BaseDimensions.ELECTRIC_CURRENT) + .withName(NameSymbol.of("Current", "I")); + public static final NamedObjectProduct TEMPERATURE = ObjectProduct + .oneOf(BaseDimensions.TEMPERATURE) + .withName(NameSymbol.of("Temperature", "\u0398")); + public static final NamedObjectProduct QUANTITY = ObjectProduct + .oneOf(BaseDimensions.QUANTITY) + .withName(NameSymbol.of("Quantity", "N")); + public static final NamedObjectProduct LUMINOUS_INTENSITY = ObjectProduct + .oneOf(BaseDimensions.LUMINOUS_INTENSITY) + .withName(NameSymbol.of("Luminous Intensity", "J")); + public static final NamedObjectProduct INFORMATION = ObjectProduct + .oneOf(BaseDimensions.INFORMATION) + .withName(NameSymbol.ofName("Information")); + public static final NamedObjectProduct CURRENCY = ObjectProduct + .oneOf(BaseDimensions.CURRENCY) + .withName(NameSymbol.ofName("Currency")); // derived dimensions without named SI units public static final ObjectProduct AREA = LENGTH .times(LENGTH); public static final ObjectProduct VOLUME = AREA .times(LENGTH); - public static final ObjectProduct VELOCITY = LENGTH - .dividedBy(TIME); + public static final NamedObjectProduct VELOCITY = LENGTH + .dividedBy(TIME).withName(NameSymbol.ofName("Velocity")); public static final ObjectProduct ACCELERATION = VELOCITY .dividedBy(TIME); public static final ObjectProduct WAVENUMBER = EMPTY @@ -403,54 +411,89 @@ public final class Metric { .withName(NameSymbol.of("exbi", "Ei")); // a few prefixed units - public static final LinearUnit MICROMETRE = Metric.METRE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIMETRE = Metric.METRE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOMETRE = Metric.METRE.withPrefix(Metric.KILO); - public static final LinearUnit MEGAMETRE = Metric.METRE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROMETRE = Metric.METRE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIMETRE = Metric.METRE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOMETRE = Metric.METRE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAMETRE = Metric.METRE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROLITRE = Metric.LITRE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLILITRE = Metric.LITRE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOLITRE = Metric.LITRE.withPrefix(Metric.KILO); - public static final LinearUnit MEGALITRE = Metric.LITRE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROLITRE = Metric.LITRE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLILITRE = Metric.LITRE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOLITRE = Metric.LITRE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGALITRE = Metric.LITRE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROSECOND = Metric.SECOND.withPrefix(Metric.MICRO); - public static final LinearUnit MILLISECOND = Metric.SECOND.withPrefix(Metric.MILLI); - public static final LinearUnit KILOSECOND = Metric.SECOND.withPrefix(Metric.KILO); - public static final LinearUnit MEGASECOND = Metric.SECOND.withPrefix(Metric.MEGA); + public static final LinearUnit MICROSECOND = Metric.SECOND + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLISECOND = Metric.SECOND + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOSECOND = Metric.SECOND + .withPrefix(Metric.KILO); + public static final LinearUnit MEGASECOND = Metric.SECOND + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROGRAM = Metric.GRAM.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIGRAM = Metric.GRAM.withPrefix(Metric.MILLI); - public static final LinearUnit MEGAGRAM = Metric.GRAM.withPrefix(Metric.MEGA); + public static final LinearUnit MICROGRAM = Metric.GRAM + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIGRAM = Metric.GRAM + .withPrefix(Metric.MILLI); + public static final LinearUnit MEGAGRAM = Metric.GRAM + .withPrefix(Metric.MEGA); - public static final LinearUnit MICRONEWTON = Metric.NEWTON.withPrefix(Metric.MICRO); - public static final LinearUnit MILLINEWTON = Metric.NEWTON.withPrefix(Metric.MILLI); - public static final LinearUnit KILONEWTON = Metric.NEWTON.withPrefix(Metric.KILO); - public static final LinearUnit MEGANEWTON = Metric.NEWTON.withPrefix(Metric.MEGA); + public static final LinearUnit MICRONEWTON = Metric.NEWTON + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLINEWTON = Metric.NEWTON + .withPrefix(Metric.MILLI); + public static final LinearUnit KILONEWTON = Metric.NEWTON + .withPrefix(Metric.KILO); + public static final LinearUnit MEGANEWTON = Metric.NEWTON + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROJOULE = Metric.JOULE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIJOULE = Metric.JOULE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOJOULE = Metric.JOULE.withPrefix(Metric.KILO); - public static final LinearUnit MEGAJOULE = Metric.JOULE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROJOULE = Metric.JOULE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIJOULE = Metric.JOULE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOJOULE = Metric.JOULE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAJOULE = Metric.JOULE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROWATT = Metric.WATT.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIWATT = Metric.WATT.withPrefix(Metric.MILLI); - public static final LinearUnit KILOWATT = Metric.WATT.withPrefix(Metric.KILO); - public static final LinearUnit MEGAWATT = Metric.WATT.withPrefix(Metric.MEGA); + public static final LinearUnit MICROWATT = Metric.WATT + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIWATT = Metric.WATT + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOWATT = Metric.WATT + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAWATT = Metric.WATT + .withPrefix(Metric.MEGA); public static final LinearUnit MICROCOULOMB = Metric.COULOMB .withPrefix(Metric.MICRO); public static final LinearUnit MILLICOULOMB = Metric.COULOMB .withPrefix(Metric.MILLI); - public static final LinearUnit KILOCOULOMB = Metric.COULOMB.withPrefix(Metric.KILO); - public static final LinearUnit MEGACOULOMB = Metric.COULOMB.withPrefix(Metric.MEGA); + public static final LinearUnit KILOCOULOMB = Metric.COULOMB + .withPrefix(Metric.KILO); + public static final LinearUnit MEGACOULOMB = Metric.COULOMB + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROAMPERE = Metric.AMPERE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIAMPERE = Metric.AMPERE.withPrefix(Metric.MILLI); + public static final LinearUnit MICROAMPERE = Metric.AMPERE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIAMPERE = Metric.AMPERE + .withPrefix(Metric.MILLI); - public static final LinearUnit MICROVOLT = Metric.VOLT.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIVOLT = Metric.VOLT.withPrefix(Metric.MILLI); - public static final LinearUnit KILOVOLT = Metric.VOLT.withPrefix(Metric.KILO); - public static final LinearUnit MEGAVOLT = Metric.VOLT.withPrefix(Metric.MEGA); + public static final LinearUnit MICROVOLT = Metric.VOLT + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIVOLT = Metric.VOLT + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOVOLT = Metric.VOLT + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAVOLT = Metric.VOLT + .withPrefix(Metric.MEGA); public static final LinearUnit KILOOHM = Metric.OHM.withPrefix(Metric.KILO); public static final LinearUnit MEGAOHM = Metric.OHM.withPrefix(Metric.MEGA); diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 255e82f..41cf41d 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -283,7 +283,8 @@ public final class NameSymbol { if (this.isEmpty()) return "NameSymbol.EMPTY"; else if (this.primaryName.isPresent() && this.symbol.isPresent()) - return this.primaryName + " (" + this.symbol + ")"; + return this.primaryName.orElseThrow() + " (" + + this.symbol.orElseThrow() + ")"; else return this.primaryName.orElseGet(this.symbol::orElseThrow); } diff --git a/src/main/resources/dimensionfile.txt b/src/main/resources/dimensionfile.txt index 3485de5..a946677 100644 --- a/src/main/resources/dimensionfile.txt +++ b/src/main/resources/dimensionfile.txt @@ -12,7 +12,7 @@ TIME ! TEMPERATURE ! # Derived Dimensions -AREA LENGTH^2 -VOLUME LENGTH^3 -VELOCITY LENGTH / TIME -ENERGY MASS * VELOCITY^2 \ No newline at end of file +Area LENGTH^2 +Volume LENGTH^3 +Velocity LENGTH / TIME +Energy MASS * Velocity^2 \ No newline at end of file diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index deb16d7..82842d8 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -32,7 +32,6 @@ import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitValue; -import sevenUnits.utils.NameSymbol; import sevenUnits.utils.NamedObjectProduct; /** @@ -41,19 +40,18 @@ import sevenUnits.utils.NamedObjectProduct; * @since 2022-02-10 */ public final class PresenterTest { + static final Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, + Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); + + static final Set> testDimensions = Set + .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY); + private static final List unitNames( Collection units) { return units.stream().map(Unit::getShortName) .collect(Collectors.toList()); } - Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, - Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); - - Set> testDimensions = Set.of( - Metric.Dimensions.LENGTH.withName(NameSymbol.ofName("Length")), - Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); - /** * Test method for {@link Presenter#convertExpressions} * @@ -87,8 +85,8 @@ public final class PresenterTest { final ViewBot viewBot = new ViewBot(); final Presenter presenter = new Presenter(viewBot); - viewBot.setFromUnits(this.testUnits); - viewBot.setToUnits(this.testUnits); + viewBot.setFromUnits(testUnits); + viewBot.setToUnits(testUnits); viewBot.setFromSelection(Optional.of(Metric.METRE)); viewBot.setToSelection(Optional.of(Metric.KILOMETRE)); viewBot.setInputValue(OptionalDouble.of(10000.0)); @@ -124,16 +122,15 @@ public final class PresenterTest { // override default database units presenter.database.clear(); - for (final Unit unit : this.testUnits) { + for (final Unit unit : testUnits) { presenter.database.addUnit(unit.getPrimaryName().orElseThrow(), unit); } // set from and to units - viewBot.setFromUnits(this.testUnits); - viewBot.setToUnits(this.testUnits); - viewBot.setDimensions(this.testDimensions); - viewBot.setSelectedDimension( - Optional.of(this.testDimensions.iterator().next())); + viewBot.setFromUnits(testUnits); + viewBot.setToUnits(testUnits); + viewBot.setDimensions(testDimensions); + viewBot.setSelectedDimension(Optional.of(Metric.Dimensions.LENGTH)); // filter to length units only, then get the filtered sets of units presenter.updateView(); -- cgit v1.2.3 From 4ad68a29f84538d3fb19eec8e0622731f5a5d7c8 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 12 Apr 2022 15:17:12 -0500 Subject: Removed NamedObjectProduct in favour of the regular ObjectProduct --- src/main/java/sevenUnits/unit/Metric.java | 21 +++++---- src/main/java/sevenUnits/unit/Unit.java | 4 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 12 +++-- src/main/java/sevenUnits/utils/NameSymbol.java | 2 +- .../java/sevenUnits/utils/NamedObjectProduct.java | 51 ---------------------- src/main/java/sevenUnits/utils/ObjectProduct.java | 30 +++++++++++-- src/test/java/sevenUnitsGUI/PresenterTest.java | 4 +- 7 files changed, 46 insertions(+), 78 deletions(-) delete mode 100644 src/main/java/sevenUnits/utils/NamedObjectProduct.java (limited to 'src/main/java/sevenUnits/utils') diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 7ede085..05e82ba 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -19,7 +19,6 @@ package sevenUnits.unit; import java.util.Set; import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; /** @@ -115,29 +114,29 @@ public final class Metric { public static final class Dimensions { public static final ObjectProduct EMPTY = ObjectProduct .empty(); - public static final NamedObjectProduct LENGTH = ObjectProduct + public static final ObjectProduct LENGTH = ObjectProduct .oneOf(BaseDimensions.LENGTH) .withName(NameSymbol.of("Length", "L")); - public static final NamedObjectProduct MASS = ObjectProduct + public static final ObjectProduct MASS = ObjectProduct .oneOf(BaseDimensions.MASS).withName(NameSymbol.of("Mass", "M")); - public static final NamedObjectProduct TIME = ObjectProduct + public static final ObjectProduct TIME = ObjectProduct .oneOf(BaseDimensions.TIME).withName(NameSymbol.of("Time", "T")); - public static final NamedObjectProduct ELECTRIC_CURRENT = ObjectProduct + public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct .oneOf(BaseDimensions.ELECTRIC_CURRENT) .withName(NameSymbol.of("Current", "I")); - public static final NamedObjectProduct TEMPERATURE = ObjectProduct + public static final ObjectProduct TEMPERATURE = ObjectProduct .oneOf(BaseDimensions.TEMPERATURE) .withName(NameSymbol.of("Temperature", "\u0398")); - public static final NamedObjectProduct QUANTITY = ObjectProduct + public static final ObjectProduct QUANTITY = ObjectProduct .oneOf(BaseDimensions.QUANTITY) .withName(NameSymbol.of("Quantity", "N")); - public static final NamedObjectProduct LUMINOUS_INTENSITY = ObjectProduct + public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct .oneOf(BaseDimensions.LUMINOUS_INTENSITY) .withName(NameSymbol.of("Luminous Intensity", "J")); - public static final NamedObjectProduct INFORMATION = ObjectProduct + public static final ObjectProduct INFORMATION = ObjectProduct .oneOf(BaseDimensions.INFORMATION) .withName(NameSymbol.ofName("Information")); - public static final NamedObjectProduct CURRENCY = ObjectProduct + public static final ObjectProduct CURRENCY = ObjectProduct .oneOf(BaseDimensions.CURRENCY) .withName(NameSymbol.ofName("Currency")); @@ -146,7 +145,7 @@ public final class Metric { .times(LENGTH); public static final ObjectProduct VOLUME = AREA .times(LENGTH); - public static final NamedObjectProduct VELOCITY = LENGTH + public static final ObjectProduct VELOCITY = LENGTH .dividedBy(TIME).withName(NameSymbol.ofName("Velocity")); public static final ObjectProduct ACCELERATION = VELOCITY .dividedBy(TIME); diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 826b59b..14478ba 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -354,8 +354,8 @@ public abstract class Unit implements Nameable { * @since 2022-03-10 */ public String toDefinitionString() { - if (this.unitBase instanceof Nameable) - return "derived from " + ((Nameable) this.unitBase).getName(); + if (!this.unitBase.getNameSymbol().isEmpty()) + return "derived from " + this.unitBase.getName(); else return "derived from " + this.getBase().toString(BaseUnit::getShortName); diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index bf6ae64..5591d7d 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -48,7 +48,6 @@ import sevenUnits.utils.ConditionalExistenceCollections; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -1199,7 +1198,7 @@ public final class UnitDatabase { * @since 2019-03-14 * @since v0.2.0 */ - private final Map> dimensions; + private final Map> dimensions; /** * A map mapping strings to units (including prefixes) @@ -1317,11 +1316,10 @@ public final class UnitDatabase { final ObjectProduct dimension) { Objects.requireNonNull(name, "name may not be null"); Objects.requireNonNull(dimension, "dimension may not be null"); - if (dimension instanceof NamedObjectProduct) { - this.dimensions.put(name, - (NamedObjectProduct) dimension); + if (!dimension.getNameSymbol().equals(NameSymbol.EMPTY)) { + this.dimensions.put(name, dimension); } else { - final NamedObjectProduct namedDimension = dimension + final ObjectProduct namedDimension = dimension .withName(NameSymbol.ofName(name)); this.dimensions.put(name, namedDimension); } @@ -1530,7 +1528,7 @@ public final class UnitDatabase { * @since 2019-04-13 * @since v0.2.0 */ - public Map> dimensionMap() { + public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 41cf41d..955f980 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -38,7 +38,7 @@ public final class NameSymbol { * 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 a copy of the inputted argument. + * Ensure that otherNames is not a copy of the inputted argument. */ private static final NameSymbol create(final String name, final String symbol, final Set otherNames) { diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java deleted file mode 100644 index 89b2fad..0000000 --- a/src/main/java/sevenUnits/utils/NamedObjectProduct.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (C) 2021 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 . - */ -package sevenUnits.utils; - -import java.util.Map; - -/** - * An ObjectProduct with name(s) and/or a symbol. Can be created with the - * {@link ObjectProduct#withName} method. - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -public class NamedObjectProduct extends ObjectProduct - implements Nameable { - private final NameSymbol nameSymbol; - - NamedObjectProduct(final Map exponents, - final NameSymbol nameSymbol) { - super(exponents); - this.nameSymbol = nameSymbol; - } - - @Override - public NameSymbol getNameSymbol() { - return this.nameSymbol; - } - - public final String toDefinitionString() { - return super.toString(); - } - - @Override - public String toString() { - return this.nameSymbol.toString(); - } -} diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 830f9d7..110bdc1 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 class ObjectProduct { +public class ObjectProduct implements Nameable { /** * Returns an empty ObjectProduct of a certain type * @@ -83,15 +83,32 @@ public class ObjectProduct { final Map 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 */ ObjectProduct(final Map 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 exponents, NameSymbol nameSymbol) { this.exponents = Collections.unmodifiableMap( ConditionalExistenceCollections.conditionalExistenceMap(exponents, e -> !Integer.valueOf(0).equals(e.getValue()))); + this.nameSymbol = nameSymbol; } /** @@ -170,6 +187,11 @@ public class ObjectProduct { return this.exponents.getOrDefault(dimension, 0); } + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; + } + @Override public int hashCode() { return Objects.hash(this.exponents); @@ -288,7 +310,7 @@ public class ObjectProduct { * {@code nameSymbol} * @since 2021-12-15 */ - public NamedObjectProduct withName(NameSymbol nameSymbol) { - return new NamedObjectProduct<>(this.exponents, nameSymbol); + public ObjectProduct withName(NameSymbol nameSymbol) { + return new ObjectProduct<>(this.exponents, nameSymbol); } } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index dc2fb57..3fe7e47 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -30,7 +30,7 @@ import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitValue; import sevenUnits.utils.Nameable; -import sevenUnits.utils.NamedObjectProduct; +import sevenUnits.utils.ObjectProduct; /** * @author Adrien Hopkins @@ -41,7 +41,7 @@ public final class PresenterTest { static final Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); - static final Set> testDimensions = Set + static final Set> testDimensions = Set .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY); private static final Set names(Set units) { -- cgit v1.2.3 From 855cdf83b91bd3061662e563db6656408cc24a12 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 16 Apr 2022 17:00:52 -0500 Subject: Implemented the unit & prefix viewers --- CHANGELOG.org | 2 +- src/main/java/sevenUnits/unit/LinearUnit.java | 5 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 4 +- src/main/java/sevenUnits/utils/ObjectProduct.java | 7 +- src/main/java/sevenUnitsGUI/Presenter.java | 98 ++++++++++++++++++++--- src/main/java/sevenUnitsGUI/TabbedView.java | 15 ++-- src/main/java/sevenUnitsGUI/ViewBot.java | 29 ++++--- src/test/java/sevenUnitsGUI/PresenterTest.java | 11 ++- 8 files changed, 133 insertions(+), 38 deletions(-) (limited to 'src/main/java/sevenUnits/utils') diff --git a/CHANGELOG.org b/CHANGELOG.org index 7c53032..61d9333 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -8,7 +8,7 @@ *** Changed - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional instead of String, even though they will always succeed. - - The UnitDatabase's units and dimensions are now always named + - The UnitDatabase's units, prefixes and dimensions are now always named - The toString method of the common unit classes is now simpler. Alternate toString functions that describe the full unit are provided. - Tweaked the look of the unit and expression conversion sections of the view ** v0.3.2 - [2021-12-02 Thu] diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 3a28261..103b7f6 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -372,8 +372,9 @@ public final class LinearUnit extends Unit { @Override public String toDefinitionString() { - return Double.toString(this.conversionFactor) + " " - + this.getBase().toString(BaseUnit::getShortName); + return Double.toString(this.conversionFactor) + + (this.getBase().equals(ObjectProduct.empty()) ? "" + : " " + this.getBase().toString(BaseUnit::getShortName)); } /** diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 5591d7d..7b02ac7 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1458,7 +1458,9 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - this.addPrefix(name.substring(0, name.length() - 1), prefix); + final String prefixName = name.substring(0, name.length() - 1); + this.addPrefix(prefixName, + prefix.withName(NameSymbol.ofName(prefixName))); } else { // it's a unit, get the unit final Unit unit; diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 110bdc1..66bb773 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -257,9 +257,10 @@ public class ObjectProduct implements Nameable { /** * 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. * *

* {@inheritDoc} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 5c8ce53..981af21 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -33,13 +33,17 @@ import java.util.stream.Collectors; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.BaseUnit; import sevenUnits.unit.BritishImperial; +import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; +import sevenUnits.unit.UnitType; import sevenUnits.unit.UnitValue; +import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -382,6 +386,21 @@ public final class Presenter { return this.showDuplicateUnits; } + /** + * Gets a name for this dimension using the database + * + * @param dimension dimension to name + * @return name of dimension + * @since 2022-04-16 + */ + final String getDimensionName(ObjectProduct dimension) { + // find this dimension in the database and get its name + // if it isn't there, use the dimension's toString instead + return this.database.dimensionMap().values().stream() + .filter(d -> d.equals(dimension)).findAny().map(Nameable::getName) + .orElse(dimension.toString(Nameable::getName)); + } + /** * @return the rule that is used by this presenter to convert numbers into * strings @@ -402,14 +421,25 @@ public final class Presenter { } /** - * @return true iff the One-Way Conversion feature is available (views that - * show units as a list will have metric units removed from the From - * unit list and imperial/USC units removed from the To unit list) - * - * @since 2022-03-30 + * @return type of unit {@code u} + * @since 2022-04-16 */ - public boolean oneWayConversionEnabled() { - return this.oneWayConversionEnabled; + private final UnitType getUnitType(Unit u) { + // determine if u is an exception + final var primaryName = u.getPrimaryName(); + final var symbol = u.getSymbol(); + final boolean isException = primaryName.isPresent() + && this.metricExceptions.contains(primaryName.orElseThrow()) + || symbol.isPresent() + && this.metricExceptions.contains(symbol.orElseThrow()); + + // determine unit type + if (isException) + return UnitType.SEMI_METRIC; + else if (u.isMetric()) + return UnitType.METRIC; + else + return UnitType.NON_METRIC; } /** @@ -421,6 +451,17 @@ public final class Presenter { */ void loadSettings(Path settingsFile) {} + /** + * @return true iff the One-Way Conversion feature is available (views that + * show units as a list will have metric units removed from the From + * unit list and imperial/USC units removed from the To unit list) + * + * @since 2022-03-30 + */ + public boolean oneWayConversionEnabled() { + return this.oneWayConversionEnabled; + } + /** * Completes creation of the presenter. This part of the initialization * depends on the view's functions, so it cannot be run if the components @@ -434,9 +475,24 @@ public final class Presenter { final UnitConversionView ucview = (UnitConversionView) this.view; ucview.setDimensionNames(this.database.dimensionMap().keySet()); } + + // load units & prefixes into viewers + this.view.setViewableUnitNames( + this.database.unitMapPrefixless(this.showDuplicateUnits).keySet()); + this.view.setViewablePrefixNames(this.database.prefixMap().keySet()); } - void prefixSelected() {} + void prefixSelected() { + final Optional selectedPrefixName = this.view + .getViewedPrefixName(); + final Optional selectedPrefix = selectedPrefixName + .map(name -> this.database.containsPrefixName(name) + ? this.database.getPrefix(name) + : null); + selectedPrefix + .ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(), + String.valueOf(prefix.getMultiplier()))); + } /** * Saves the presenter's settings to the user settings file. @@ -487,13 +543,37 @@ public final class Presenter { this.updateView(); } + /** + * Shows a unit in the unit viewer + * + * @param u unit to show + * @since 2022-04-16 + */ + private final void showUnit(Unit u) { + final var nameSymbol = u.getNameSymbol(); + final boolean isBase = u instanceof BaseUnit + || u instanceof LinearUnit && ((LinearUnit) u).isBase(); + final var definition = isBase ? "(Base unit)" : u.toDefinitionString(); + final var dimensionString = this.getDimensionName(u.getDimension()); + final var unitType = this.getUnitType(u); + this.view.showUnit(nameSymbol, definition, dimensionString, unitType); + } + /** * Runs whenever a unit name is selected in the unit viewer. Gets the * description of a unit and displays it. * * @since 2022-04-10 */ - void unitNameSelected() {} + void unitNameSelected() { + // get selected unit, if it's there and valid + final Optional selectedUnitName = this.view.getViewedUnitName(); + final Optional selectedUnit = selectedUnitName + .map(unitName -> this.database.containsUnitName(unitName) + ? this.database.getUnit(unitName) + : null); + selectedUnit.ifPresent(this::showUnit); + } /** * Updates the view's From and To units, if it has some diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index fd48965..d0eb32f 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -578,12 +578,12 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public Optional getViewedPrefixName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.prefixNameList.getSelectedValue(); } @Override public Optional getViewedUnitName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.unitNameList.getSelectedValue(); } @Override @@ -606,12 +606,12 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public void setViewablePrefixNames(Set prefixNames) { - throw new UnsupportedOperationException("Not implemented yet"); + this.prefixNameList.setItems(prefixNames); } @Override public void setViewableUnitNames(Set unitNames) { - throw new UnsupportedOperationException("Not implemented yet"); + this.unitNameList.setItems(unitNames); } @Override @@ -628,13 +628,16 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public void showPrefix(NameSymbol name, String multiplierString) { - throw new UnsupportedOperationException("Not implemented yet"); + this.prefixTextBox.setText( + String.format("%s%nMultiplier: %s", name, multiplierString)); } @Override public void showUnit(NameSymbol name, String definition, String dimensionName, UnitType type) { - throw new UnsupportedOperationException("Not implemented yet"); + this.unitTextBox.setText( + String.format("%s%nDefinition: %s%nDimension: %s%nType: %s", name, + definition, dimensionName, type)); } @Override diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 988d1bc..dd9869d 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -216,6 +216,11 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { /** The units available in the To selection */ private Set toUnits; + /** The selected unit in the unit viewer */ + private Optional unitViewerSelection; + /** The selected unit in the prefix viewer */ + private Optional prefixViewerSelection; + /** Saved outputs of all unit conversions */ private final List unitConversions; /** Saved outputs of all unit expressions */ @@ -314,12 +319,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public Optional getViewedPrefixName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.prefixViewerSelection; } @Override public Optional getViewedUnitName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.unitViewerSelection; } /** @@ -423,26 +428,24 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void setViewablePrefixNames(Set prefixNames) { - throw new UnsupportedOperationException("Not implemented yet"); + // do nothing, ViewBot supports selecting any prefix } @Override public void setViewableUnitNames(Set unitNames) { - throw new UnsupportedOperationException("Not implemented yet"); + // do nothing, ViewBot supports selecting any unit } - public void setViewedPrefixName( - @SuppressWarnings("unused") Optional viewedPrefixName) { - throw new UnsupportedOperationException("Not implemented yet"); + public void setViewedPrefixName(Optional viewedPrefixName) { + this.prefixViewerSelection = viewedPrefixName; } public void setViewedPrefixName(String viewedPrefixName) { this.setViewedPrefixName(Optional.of(viewedPrefixName)); } - public void setViewedUnitName( - @SuppressWarnings("unused") Optional viewedUnitName) { - throw new UnsupportedOperationException("Not implemented yet"); + public void setViewedUnitName(Optional viewedUnitName) { + this.unitViewerSelection = viewedUnitName; } public void setViewedUnitName(String viewedUnitName) { @@ -462,13 +465,15 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void showPrefix(NameSymbol name, String multiplierString) { - throw new UnsupportedOperationException("Not implemented yet"); + this.prefixViewingRecords + .add(new PrefixViewingRecord(name, multiplierString)); } @Override public void showUnit(NameSymbol name, String definition, String dimensionName, UnitType type) { - throw new UnsupportedOperationException("Not implemented yet"); + this.unitViewingRecords + .add(new UnitViewingRecord(name, definition, dimensionName, type)); } @Override diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 85ebe09..8446a90 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -212,7 +212,8 @@ public final class PresenterTest { presenter.prefixSelected(); // just in case // get correct values - final var expectedNameSymbol = Metric.KILO.getNameSymbol(); + final var expectedNameSymbol = presenter.database.getPrefix("kilo") + .getNameSymbol(); final var expectedMultiplierString = String .valueOf(Metric.KILO.getMultiplier()); @@ -298,9 +299,11 @@ public final class PresenterTest { // automatically // get correct values - final var expectedNameSymbol = Metric.METRE.getNameSymbol(); - final var expectedDefinition = Metric.METRE.toDefinitionString(); - final var expectedDimensionName = Metric.METRE.getDimension().getName(); + final var expectedNameSymbol = presenter.database.getUnit("metre") + .getNameSymbol(); + final var expectedDefinition = "(Base unit)"; + final var expectedDimensionName = presenter + .getDimensionName(Metric.METRE.getDimension()); final var expectedUnitType = UnitType.METRIC; // test for correctness -- cgit v1.2.3 From f0541a955b6e4b12d808cffec0874f50a004e8b9 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 18 Apr 2022 17:01:54 -0500 Subject: Implemented rounding and duplicate-removal settings into the new GUI --- CHANGELOG.org | 2 + .../sevenUnits/converterGUI/SevenUnitsGUI.java | 2 +- src/main/java/sevenUnits/unit/LinearUnitValue.java | 10 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 6 +- .../java/sevenUnits/utils/UncertainDouble.java | 24 +- src/main/java/sevenUnitsGUI/Presenter.java | 56 ++++- .../java/sevenUnitsGUI/StandardDisplayRules.java | 220 +++++++++++++--- src/main/java/sevenUnitsGUI/TabbedView.java | 280 +++++++++++++++++---- src/test/java/sevenUnits/unit/UnitTest.java | 14 +- .../java/sevenUnits/utils/UncertainDoubleTest.java | 11 + src/test/java/sevenUnitsGUI/PresenterTest.java | 14 +- 11 files changed, 518 insertions(+), 121 deletions(-) (limited to 'src/main/java/sevenUnits/utils') diff --git a/CHANGELOG.org b/CHANGELOG.org index 61d9333..c164d1f 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -10,6 +10,8 @@ - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional instead of String, even though they will always succeed. - The UnitDatabase's units, prefixes and dimensions are now always named - The toString method of the common unit classes is now simpler. Alternate toString functions that describe the full unit are provided. + - UncertainDouble and LinearUnitValue accept a RoundingMode in their complicated toString functions. + - Rounding rules are now in their own classes - Tweaked the look of the unit and expression conversion sections of the view ** v0.3.2 - [2021-12-02 Thu] *** Added diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index 55e1546..e10bab4 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -488,7 +488,7 @@ final class SevenUnitsGUI { case SIGNIFICANT_DIGITS: return this.getRoundedString(value.asUnitValue()); case SCIENTIFIC: - return value.toString(showUncertainty); + return value.toString(showUncertainty, RoundingMode.HALF_EVEN); default: throw new AssertionError("Invalid switch condition."); } diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java index a50e1f5..f91d30b 100644 --- a/src/main/java/sevenUnits/unit/LinearUnitValue.java +++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java @@ -16,6 +16,7 @@ */ package sevenUnits.unit; +import java.math.RoundingMode; import java.util.Objects; import java.util.Optional; @@ -300,7 +301,7 @@ public final class LinearUnitValue { @Override public String toString() { - return this.toString(!this.value.isExact()); + return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN); } /** @@ -315,7 +316,8 @@ public final class LinearUnitValue { * * @since 2020-07-26 */ - public String toString(final boolean showUncertainty) { + public String toString(final boolean showUncertainty, + RoundingMode roundingMode) { final Optional primaryName = this.unit.getPrimaryName(); final Optional symbol = this.unit.getSymbol(); final String chosenName = symbol.orElse(primaryName.orElse(null)); @@ -325,10 +327,10 @@ public final class LinearUnitValue { // get rounded strings // if showUncertainty is true, add brackets around the string final String valueString = (showUncertainty ? "(" : "") - + this.value.toString(showUncertainty) + + this.value.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); final String baseValueString = (showUncertainty ? "(" : "") - + baseValue.toString(showUncertainty) + + baseValue.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); // create string diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 7b02ac7..a4f0c44 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -19,7 +19,6 @@ package sevenUnits.unit; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; import java.util.AbstractSet; @@ -1705,11 +1704,8 @@ public final class UnitDatabase { LinearUnitValue getLinearUnitValue(final String name) { try { // try to parse it as a number - otherwise it is not a number! - final BigDecimal number = new BigDecimal(name); - - final double uncertainty = Math.pow(10, -number.scale()); return LinearUnitValue.of(Metric.ONE, - UncertainDouble.of(number.doubleValue(), uncertainty)); + UncertainDouble.fromRoundedString(name)); } catch (final NumberFormatException e) { return LinearUnitValue.getExact(this.getLinearUnit(name), 1); } 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 @@ -45,6 +45,21 @@ public final class UncertainDouble implements Comparable { // optional "± [number]" + "(?:\\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. @@ -348,7 +363,7 @@ public final class UncertainDouble implements Comparable { */ @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 { * * @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 { 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(); diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 981af21..85a0ddc 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -242,6 +242,9 @@ public final class Presenter { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); } + + // set default settings temporarily + this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); } /** @@ -293,9 +296,21 @@ public final class Presenter { // convert and show output if (from.getUnit().canConvertTo(to)) { - final double value = from.asUnitValue().convertTo(to).getValue(); + final UncertainDouble uncertainValue; + + // uncertainty is meaningless for non-linear units, so we will have + // to erase uncertainty information for them + if (to instanceof LinearUnit) { + final var toLinear = (LinearUnit) to; + uncertainValue = from.convertTo(toLinear).getValue(); + } else { + final double value = from.asUnitValue().convertTo(to).getValue(); + uncertainValue = UncertainDouble.of(value, 0); + } + final UnitConversionRecord uc = UnitConversionRecord.valueOf( - fromExpression, toExpression, "", String.valueOf(value)); + fromExpression, toExpression, "", + this.numberDisplayRule.apply(uncertainValue)); xcview.showExpressionConversionOutput(uc); } else { this.view.showErrorMessage("Conversion Error", @@ -324,7 +339,7 @@ public final class Presenter { final Optional fromUnitOptional = ucview.getFromSelection(); final Optional toUnitOptional = ucview.getToSelection(); - final String valueString = ucview.getInputValue(); + final String inputValueString = ucview.getInputValue(); // extract values from optionals final String fromUnitString, toUnitString; @@ -345,7 +360,7 @@ public final class Presenter { // convert strings to data, checking if anything is invalid final Unit fromUnit, toUnit; - final double value; + final UncertainDouble uncertainValue; if (this.database.containsUnitName(fromUnitString)) { fromUnit = this.database.getUnit(fromUnitString); @@ -356,23 +371,42 @@ public final class Presenter { } else throw this.viewError("Nonexistent To unit: %s", toUnitString); try { - value = Double.parseDouble(valueString); + uncertainValue = UncertainDouble + .fromRoundedString(inputValueString); } catch (final NumberFormatException e) { this.view.showErrorMessage("Value Error", - "Invalid value " + valueString); + "Invalid value " + inputValueString); return; } if (!fromUnit.canConvertTo(toUnit)) throw this.viewError("Could not convert between %s and %s", fromUnit, toUnit); - - // convert! - final UnitValue initialValue = UnitValue.of(fromUnit, value); - final UnitValue converted = initialValue.convertTo(toUnit); + + // convert - we will need to erase uncertainty for non-linear units, so + // we need to treat linear and non-linear units differently + final String outputValueString; + if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) { + final LinearUnit fromLinear = (LinearUnit) fromUnit; + final LinearUnit toLinear = (LinearUnit) toUnit; + final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear, + uncertainValue); + final LinearUnitValue converted = initialValue.convertTo(toLinear); + + outputValueString = this.numberDisplayRule + .apply(converted.getValue()); + } else { + final UnitValue initialValue = UnitValue.of(fromUnit, + uncertainValue.value()); + final UnitValue converted = initialValue.convertTo(toUnit); + + outputValueString = this.numberDisplayRule + .apply(UncertainDouble.of(converted.getValue(), 0)); + } ucview.showUnitConversionOutput( - UnitConversionRecord.fromValues(initialValue, converted)); + UnitConversionRecord.valueOf(fromUnitString, toUnitString, + inputValueString, outputValueString)); } else throw new UnsupportedOperationException( "This function can only be called when the view is a UnitConversionView."); diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java index f6272c8..0c0ba8e 100644 --- a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * 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 @@ -16,67 +16,192 @@ */ package sevenUnitsGUI; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import java.util.function.Function; +import java.util.regex.Pattern; import sevenUnits.utils.UncertainDouble; /** - * The default rules for displaying numbers. - * - * Unless otherwise stated, all of this class's functions throw - * {@link NullPointerException} when they receive a null parameter. + * A static utility class that can be used to make display rules for the + * presenter. * - * @since 2021-12-24 + * @since 2022-04-18 */ -final class StandardDisplayRules { +public final class StandardDisplayRules { /** - * Rounds using UncertainDouble's toString method. + * A rule that rounds to a fixed number of decimal places. + * + * @since 2022-04-18 */ - private static final Function SCIENTIFIC_ROUNDING_RULE = new Function<>() { + public static final class FixedDecimals + implements Function { + public static final Pattern TO_STRING_PATTERN = Pattern + .compile("Round to (\\d+) decimal places"); + /** + * The number of places to round to. + */ + private final int decimalPlaces; + + /** + * @param decimalPlaces + * @since 2022-04-18 + */ + private FixedDecimals(int decimalPlaces) { + this.decimalPlaces = decimalPlaces; + } + @Override public String apply(UncertainDouble t) { - return t.toString(false); + final var toRound = new BigDecimal(t.value()); + return toRound.setScale(this.decimalPlaces, RoundingMode.HALF_EVEN) + .toPlainString(); + } + + /** + * @return the number of decimal places this rule rounds to + * @since 2022-04-18 + */ + public int decimalPlaces() { + return this.decimalPlaces; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof FixedDecimals)) + return false; + final FixedDecimals other = (FixedDecimals) obj; + if (this.decimalPlaces != other.decimalPlaces) + return false; + return true; + } + + @Override + public int hashCode() { + return 31 + this.decimalPlaces; } @Override public String toString() { - return "Scientific Rounding"; + return "Round to " + this.decimalPlaces + " decimal places"; } - }; + } /** - * Gets a display rule that rounds numbers to a fixed number of decimal - * places. + * A rule that rounds to a fixed number of significant digits. * - * @param decimalPlaces number of decimal places - * @return display rule - * @since 2022-04-16 + * @since 2022-04-18 */ - public static final Function getFixedPlacesRule( - int decimalPlaces) { - throw new UnsupportedOperationException("Not implemented yet"); + public static final class FixedPrecision + implements Function { + public static final Pattern TO_STRING_PATTERN = Pattern + .compile("Round to (\\d+) significant figures"); + + /** + * The number of significant figures to round to. + */ + private final MathContext mathContext; + + /** + * @param significantFigures + * @since 2022-04-18 + */ + private FixedPrecision(int significantFigures) { + this.mathContext = new MathContext(significantFigures, + RoundingMode.HALF_EVEN); + } + + @Override + public String apply(UncertainDouble t) { + final var toRound = new BigDecimal(t.value()); + return toRound.round(this.mathContext).toString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof FixedPrecision)) + return false; + final FixedPrecision other = (FixedPrecision) obj; + if (this.mathContext == null) { + if (other.mathContext != null) + return false; + } else if (!this.mathContext.equals(other.mathContext)) + return false; + return true; + } + + @Override + public int hashCode() { + return 127 + + (this.mathContext == null ? 0 : this.mathContext.hashCode()); + } + + /** + * @return the number of significant figures this rule rounds to + * @since 2022-04-18 + */ + public int significantFigures() { + return this.mathContext.getPrecision(); + } + + @Override + public String toString() { + return "Round to " + this.mathContext.getPrecision() + + " significant figures"; + } } /** - * Gets a display rule that rounds numbers to a fixed number of significant - * figures. + * A rounding rule that rounds based on UncertainDouble's toString method. + * This means the output will have around as many significant figures as the + * input. * - * @param significantFigures number of significant figures - * @return display rule - * @since 2022-04-16 + * @since 2022-04-18 + */ + public static final class UncertaintyBased + implements Function { + private UncertaintyBased() {} + + @Override + public String apply(UncertainDouble t) { + return t.toString(false, RoundingMode.HALF_EVEN); + } + + @Override + public String toString() { + return "Uncertainty-Based Rounding"; + } + } + + /** + * For now, I want this to be a singleton. I might want to add a parameter + * later, so I won't make it an enum. + */ + private static final UncertaintyBased UNCERTAINTY_BASED_ROUNDING_RULE = new UncertaintyBased(); + + /** + * @param decimalPlaces decimal places to round to + * @return a rounding rule that rounds to fixed number of decimal places + * @since 2022-04-18 */ - public static final Function getFixedPrecisionRule( - int significantFigures) { - throw new UnsupportedOperationException("Not implemented yet"); + public static final FixedDecimals fixedDecimals(int decimalPlaces) { + return new FixedDecimals(decimalPlaces); } /** - * @return a rule that rounds using UncertainDouble's own toString(false) - * function. - * @since 2021-12-24 + * @param significantFigures significant figures to round to + * @return a rounding rule that rounds to a fixed number of significant + * figures + * @since 2022-04-18 */ - public static final Function getScientificRule() { - return SCIENTIFIC_ROUNDING_RULE; + public static final FixedPrecision fixedPrecision(int significantFigures) { + return new FixedPrecision(significantFigures); } /** @@ -90,11 +215,32 @@ final class StandardDisplayRules { */ public static final Function getStandardRule( String ruleToString) { - throw new UnsupportedOperationException("Not implemented yet"); + if (UNCERTAINTY_BASED_ROUNDING_RULE.toString().equals(ruleToString)) + return UNCERTAINTY_BASED_ROUNDING_RULE; + + // test if it is a fixed-places rule + final var placesMatch = FixedDecimals.TO_STRING_PATTERN + .matcher(ruleToString); + if (placesMatch.matches()) + return new FixedDecimals(Integer.valueOf(placesMatch.group(1))); + + // test if it is a fixed-sig-fig rule + final var sigFigMatch = FixedPrecision.TO_STRING_PATTERN + .matcher(ruleToString); + if (sigFigMatch.matches()) + return new FixedPrecision(Integer.valueOf(sigFigMatch.group(1))); + + throw new IllegalArgumentException( + "Provided string does not match any given rules."); } - private StandardDisplayRules() { - throw new AssertionError( - "This is a static utility class, you may not get instances of it."); + /** + * @return an UncertainDouble-based rounding rule + * @since 2022-04-18 + */ + public static final UncertaintyBased uncertaintyBased() { + return UNCERTAINTY_BASED_ROUNDING_RULE; } + + private StandardDisplayRules() {} } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index d0eb32f..3a951ef 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -20,16 +20,18 @@ import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; +import java.awt.event.ItemEvent; import java.awt.event.KeyEvent; -import java.text.DecimalFormat; -import java.text.NumberFormat; import java.util.AbstractSet; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Optional; +import java.util.OptionalInt; import java.util.Set; +import java.util.function.Function; import javax.swing.BorderFactory; import javax.swing.BoxLayout; @@ -37,7 +39,6 @@ import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; -import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; @@ -58,6 +59,7 @@ import javax.swing.border.TitledBorder; import sevenUnits.ProgramInfo; import sevenUnits.unit.UnitType; import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.UncertainDouble; /** * A View that separates its functions into multiple tabs @@ -111,7 +113,99 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } - private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + /** + * The standard types of rounding, corresponding to the options on the + * TabbedView's settings panel. + * + * @since 2022-04-18 + */ + private static enum StandardRoundingType { + /** + * Rounds to a fixed number of significant digits. Precision is used, + * representing the number of significant digits to round to. + */ + SIGNIFICANT_DIGITS(true) { + @Override + public Function getRuleFromPrecision( + int precision) { + return StandardDisplayRules.fixedPrecision(precision); + } + }, + /** + * Rounds to a fixed number of decimal places. Precision is used, + * representing the number of decimal places to round to. + */ + DECIMAL_PLACES(true) { + @Override + public Function getRuleFromPrecision( + int precision) { + return StandardDisplayRules.fixedDecimals(precision); + } + }, + /** + * Rounds according to UncertainDouble's toString method. The specified + * precision is ignored. + */ + UNCERTAINTY(false) { + @Override + public Function getRuleFromPrecision( + int precision) { + return StandardDisplayRules.uncertaintyBased(); + } + }; + + /** + * If true, this type of rounding rule requires you to specify a + * precision. + */ + private final boolean requiresPrecision; + + /** + * @param canCustomizePrecision + * @since 2022-04-18 + */ + private StandardRoundingType(boolean requiresPrecision) { + this.requiresPrecision = requiresPrecision; + } + + /** + * Gets a rounding rule of this type. + * + * @param precision the rounding type's precision. If + * {@link #requiresPrecision} is false, this field will + * be ignored. + * @return rounding rule + * @since 2022-04-18 + */ + public abstract Function getRuleFromPrecision( + int precision); + + /** + * Tries to get this rule without specifying precision. + * + * @throws UnsupportedOperationException if this rule requires specifying + * precision + * @since 2022-04-18 + */ + public final Function getRuleWithoutPrecision() { + if (this.requiresPrecision()) + throw new UnsupportedOperationException("Rounding type " + this + + " requires you to specify precision."); + else + // random number to mess with anyone who lies about whether or not + // precision is required + return this.getRuleFromPrecision(-623546735); + } + + /** + * @return whether or not this rounding type requires you to specify an + * integer precision + * @since 2022-04-18 + */ + public boolean requiresPrecision() { + return this.requiresPrecision; + } + } /** * Creates a TabbedView. @@ -137,7 +231,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** The combo box that selects dimensions */ private final JComboBox dimensionSelector; /** The panel for inputting values in the dimension-based converter */ - private final JFormattedTextField valueInput; + private final JTextField valueInput; /** The panel for "From" in the dimension-based converter */ private final SearchBoxList fromSearch; /** The panel for "To" in the dimension-based converter */ @@ -163,6 +257,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; + // SETTINGS STUFF + private StandardRoundingType roundingType; + private int precision; + /** * Creates the view and makes it visible to the user * @@ -229,7 +327,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { final JLabel valuePrompt = new JLabel("Value to convert: "); outputPanel.add(valuePrompt, BorderLayout.LINE_START); - this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); + this.valueInput = new JTextField(); outputPanel.add(this.valueInput, BorderLayout.CENTER); // conversion button @@ -352,61 +450,89 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // rounding rule selection final ButtonGroup roundingRuleButtons = new ButtonGroup(); + this.roundingType = this.getPresenterRoundingType() + .orElseThrow(() -> new AssertionError( + "Presenter loaded non-standard rounding rule")); + this.precision = this.getPresenterPrecision().orElse(6); final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); + // sigDigSlider needs to be first so that the rounding-type buttons can + // show and hide it + final JLabel sliderLabel = new JLabel("Precision:"); + sliderLabel.setVisible( + this.roundingType != StandardRoundingType.UNCERTAINTY); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JSlider sigDigSlider = new JSlider(0, 12); + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) + .setAnchor(GridBagConstraints.LINE_START).build()); + + sigDigSlider.setMajorTickSpacing(4); + sigDigSlider.setMinorTickSpacing(1); + sigDigSlider.setSnapToTicks(true); + sigDigSlider.setPaintTicks(true); + sigDigSlider.setPaintLabels(true); + + sigDigSlider.setVisible( + this.roundingType != StandardRoundingType.UNCERTAINTY); + sigDigSlider.setValue(this.precision); + + sigDigSlider.addChangeListener(e -> { + this.precision = sigDigSlider.getValue(); + this.updatePresenterRoundingRule(); + }); + + // significant digit rounding final JRadioButton fixedPrecision = new JRadioButton( "Fixed Precision"); -// if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { -// fixedPrecision.setSelected(true); -// } -// fixedPrecision.addActionListener(e -> this.presenter -// .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + if (this.roundingType == StandardRoundingType.SIGNIFICANT_DIGITS) { + fixedPrecision.setSelected(true); + } + fixedPrecision.addActionListener(e -> { + this.roundingType = StandardRoundingType.SIGNIFICANT_DIGITS; + sliderLabel.setVisible(true); + sigDigSlider.setVisible(true); + this.updatePresenterRoundingRule(); + }); roundingRuleButtons.add(fixedPrecision); roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); + // decimal place rounding final JRadioButton fixedDecimals = new JRadioButton( "Fixed Decimal Places"); -// if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { -// fixedDecimals.setSelected(true); -// } -// fixedDecimals.addActionListener(e -> this.presenter -// .setRoundingType(RoundingType.DECIMAL_PLACES)); + if (this.roundingType == StandardRoundingType.DECIMAL_PLACES) { + fixedDecimals.setSelected(true); + } + fixedDecimals.addActionListener(e -> { + this.roundingType = StandardRoundingType.DECIMAL_PLACES; + sliderLabel.setVisible(true); + sigDigSlider.setVisible(true); + this.updatePresenterRoundingRule(); + }); roundingRuleButtons.add(fixedDecimals); roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); + // scientific rounding final JRadioButton relativePrecision = new JRadioButton( - "Scientific Precision"); -// if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { -// relativePrecision.setSelected(true); -// } -// relativePrecision.addActionListener( -// e -> this.presenter.setRoundingType(RoundingType.SCIENTIFIC)); + "Uncertainty-Based Rounding"); + if (this.roundingType == StandardRoundingType.UNCERTAINTY) { + relativePrecision.setSelected(true); + } + relativePrecision.addActionListener(e -> { + this.roundingType = StandardRoundingType.UNCERTAINTY; + sliderLabel.setVisible(false); + sigDigSlider.setVisible(false); + this.updatePresenterRoundingRule(); + }); roundingRuleButtons.add(relativePrecision); roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) .setAnchor(GridBagConstraints.LINE_START).build()); - - final JLabel sliderLabel = new JLabel("Precision:"); - roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JSlider sigDigSlider = new JSlider(0, 12); - roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) - .setAnchor(GridBagConstraints.LINE_START).build()); - - sigDigSlider.setMajorTickSpacing(4); - sigDigSlider.setMinorTickSpacing(1); - sigDigSlider.setSnapToTicks(true); - sigDigSlider.setPaintTicks(true); - sigDigSlider.setPaintLabels(true); -// sigDigSlider.setValue(this.presenter.precision); - -// sigDigSlider.addChangeListener( -// e -> this.presenter.setPrecision(sigDigSlider.getValue())); } // ============ PREFIX REPETITION SETTINGS ============ @@ -501,17 +627,18 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { miscPanel.setLayout(new GridBagLayout()); final JCheckBox oneWay = new JCheckBox("Convert One Way Only"); -// oneWay.setSelected(this.presenter.oneWay); -// oneWay.addItemListener( -// e -> this.presenter.setOneWay(e.getStateChange() == 1)); + oneWay.setSelected(this.presenter.oneWayConversionEnabled()); + oneWay.addItemListener(e -> this.presenter.setOneWayConversionEnabled( + e.getStateChange() == ItemEvent.SELECTED)); miscPanel.add(oneWay, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); final JCheckBox showAllVariations = new JCheckBox( "Show Duplicates in \"Convert Units\""); -// showAllVariations.setSelected(this.presenter.includeDuplicateUnits); -// showAllVariations.addItemListener(e -> this.presenter -// .setIncludeDuplicateUnits(e.getStateChange() == 1)); + showAllVariations.setSelected(this.presenter.duplicateUnitsShown()); + showAllVariations + .addItemListener(e -> this.presenter.setShowDuplicateUnits( + e.getStateChange() == ItemEvent.SELECTED)); miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); @@ -552,6 +679,43 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return this.valueInput.getText(); } + /** + * @return the precision of the presenter's rounding rule, if that is + * meaningful + * @since 2022-04-18 + */ + private OptionalInt getPresenterPrecision() { + final var presenterRule = this.presenter.getNumberDisplayRule(); + if (presenterRule instanceof StandardDisplayRules.FixedDecimals) + return OptionalInt + .of(((StandardDisplayRules.FixedDecimals) presenterRule) + .decimalPlaces()); + else if (presenterRule instanceof StandardDisplayRules.FixedPrecision) + return OptionalInt + .of(((StandardDisplayRules.FixedPrecision) presenterRule) + .significantFigures()); + else + return OptionalInt.empty(); + } + + /** + * Determines which rounding type the presenter is currently using, if any. + * + * @since 2022-04-18 + */ + private Optional getPresenterRoundingType() { + final var presenterRule = this.presenter.getNumberDisplayRule(); + if (Objects.equals(presenterRule, + StandardDisplayRules.uncertaintyBased())) + return Optional.of(StandardRoundingType.UNCERTAINTY); + else if (presenterRule instanceof StandardDisplayRules.FixedDecimals) + return Optional.of(StandardRoundingType.DECIMAL_PLACES); + else if (presenterRule instanceof StandardDisplayRules.FixedPrecision) + return Optional.of(StandardRoundingType.SIGNIFICANT_DIGITS); + else + return Optional.empty(); + } + @Override public Optional getSelectedDimensionName() { final String selectedItem = (String) this.dimensionSelector @@ -644,4 +808,28 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { public void showUnitConversionOutput(UnitConversionRecord uc) { this.unitOutput.setText(uc.toString()); } + + /** + * Sets the presenter's rounding rule to the one specified by the current + * settings + * + * @since 2022-04-18 + */ + private void updatePresenterRoundingRule() { + final Function roundingRule; + switch (this.roundingType) { + case DECIMAL_PLACES: + roundingRule = StandardDisplayRules.fixedDecimals(this.precision); + break; + case SIGNIFICANT_DIGITS: + roundingRule = StandardDisplayRules.fixedPrecision(this.precision); + break; + case UNCERTAINTY: + roundingRule = StandardDisplayRules.uncertaintyBased(); + break; + default: + throw new AssertionError(); + } + this.presenter.setNumberDisplayRule(roundingRule); + } } diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java index f174e7c..d3699ca 100644 --- a/src/test/java/sevenUnits/unit/UnitTest.java +++ b/src/test/java/sevenUnits/unit/UnitTest.java @@ -21,6 +21,7 @@ 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 java.math.RoundingMode; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @@ -164,8 +165,9 @@ class UnitTest { UncertainDouble.of(10, 0.24)); assertEquals("(10.0 ± 0.2) m", value.toString()); - assertEquals("(10.0 ± 0.2) m", value.toString(true)); - assertEquals("10.0 m", value.toString(false)); + assertEquals("(10.0 ± 0.2) m", + value.toString(true, RoundingMode.HALF_EVEN)); + assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN)); } /** @@ -179,8 +181,9 @@ class UnitTest { UncertainDouble.of(10, 0)); assertEquals("10.0 m", value.toString()); - assertEquals("(10.0 ± 0.0) m", value.toString(true)); - assertEquals("10.0 m", value.toString(false)); + assertEquals("(10.0 ± 0.0) m", + value.toString(true, RoundingMode.HALF_EVEN)); + assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN)); } /** @@ -194,7 +197,8 @@ class UnitTest { Metric.METRE.withName(NameSymbol.EMPTY), UncertainDouble.of(10, 0.24)); - assertEquals("10.0 unnamed unit (= 10.0 m)", value.toString(false)); + assertEquals("10.0 unnamed unit (= 10.0 m)", + value.toString(false, RoundingMode.HALF_EVEN)); } /** diff --git a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java index c891f20..0e18461 100644 --- a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java +++ b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java @@ -19,6 +19,7 @@ package sevenUnits.utils; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static sevenUnits.utils.UncertainDouble.fromRoundedString; import static sevenUnits.utils.UncertainDouble.fromString; import static sevenUnits.utils.UncertainDouble.of; @@ -66,6 +67,16 @@ class UncertainDoubleTest { x.toExponentExact(Math.E).value()); } + /** + * Test for {@link UncertainDouble#fromRoundedString} + * + * @since 2022-04-18 + */ + @Test + final void testFromRoundedString() { + assertEquals(of(12345.678, 0.001), fromRoundedString("12345.678")); + } + @Test final void testFromString() { // valid strings diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 8446a90..f52d846 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -60,10 +60,9 @@ public final class PresenterTest { * @since 2022-04-16 */ private static final Stream> getRoundingRules() { - final var SCIENTIFIC_ROUNDING = StandardDisplayRules.getScientificRule(); - final var INTEGER_ROUNDING = StandardDisplayRules.getFixedPlacesRule(0); - final var SIG_FIG_ROUNDING = StandardDisplayRules - .getFixedPrecisionRule(4); + final var SCIENTIFIC_ROUNDING = StandardDisplayRules.uncertaintyBased(); + final var INTEGER_ROUNDING = StandardDisplayRules.fixedDecimals(0); + final var SIG_FIG_ROUNDING = StandardDisplayRules.fixedPrecision(4); return Stream.of(SCIENTIFIC_ROUNDING, INTEGER_ROUNDING, SIG_FIG_ROUNDING); } @@ -264,20 +263,19 @@ public final class PresenterTest { // set and save custom settings presenter.setOneWayConversionEnabled(true); presenter.setShowDuplicateUnits(true); - presenter.setNumberDisplayRule( - StandardDisplayRules.getFixedPrecisionRule(11)); + presenter.setNumberDisplayRule(StandardDisplayRules.fixedPrecision(11)); presenter.saveSettings(TEST_SETTINGS); // overwrite custom settings presenter.setOneWayConversionEnabled(false); presenter.setShowDuplicateUnits(false); - presenter.setNumberDisplayRule(StandardDisplayRules.getScientificRule()); + presenter.setNumberDisplayRule(StandardDisplayRules.uncertaintyBased()); // load settings & test that they're the same presenter.loadSettings(TEST_SETTINGS); assertTrue(presenter.oneWayConversionEnabled()); assertTrue(presenter.duplicateUnitsShown()); - assertEquals(StandardDisplayRules.getFixedPlacesRule(11), + assertEquals(StandardDisplayRules.fixedPrecision(11), presenter.getNumberDisplayRule()); } -- cgit v1.2.3 From 0aacba9fc8a9140fdf331172ad66afe280d09b5e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 19 Apr 2022 16:10:44 -0500 Subject: Implemented prefix settings, saving & loading of settings Also fixed some bugs: - Presenter now has default values for its settings in case they don't load properly - UnitDatabase ensures its units, prefixes and dimensions have all of the names you give it --- src/main/java/sevenUnits/unit/UnitDatabase.java | 29 ++-- src/main/java/sevenUnits/utils/NameSymbol.java | 17 ++ .../sevenUnitsGUI/DefaultPrefixRepetitionRule.java | 95 +++++++++++ src/main/java/sevenUnitsGUI/Presenter.java | 175 ++++++++++++++++++--- src/main/java/sevenUnitsGUI/TabbedView.java | 77 ++++++--- .../java/sevenUnitsGUI/UnitConversionRecord.java | 19 +++ src/main/java/sevenUnitsGUI/View.java | 6 + src/main/java/sevenUnitsGUI/ViewBot.java | 23 +-- src/test/java/sevenUnitsGUI/PresenterTest.java | 25 +-- src/test/resources/test-settings.txt | 4 + 10 files changed, 384 insertions(+), 86 deletions(-) create mode 100644 src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java create mode 100644 src/test/resources/test-settings.txt (limited to 'src/main/java/sevenUnits/utils') diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 71676a1..12b78a7 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1315,13 +1315,9 @@ public final class UnitDatabase { final ObjectProduct dimension) { Objects.requireNonNull(name, "name may not be null"); Objects.requireNonNull(dimension, "dimension may not be null"); - if (!dimension.getNameSymbol().equals(NameSymbol.EMPTY)) { - this.dimensions.put(name, dimension); - } else { - final ObjectProduct namedDimension = dimension - .withName(NameSymbol.ofName(name)); - this.dimensions.put(name, namedDimension); - } + final ObjectProduct namedDimension = dimension + .withName(dimension.getNameSymbol().withExtraName(name)); + this.dimensions.put(name, namedDimension); } /** @@ -1373,7 +1369,7 @@ public final class UnitDatabase { throw e; } - this.addDimension(name, dimension.withName(NameSymbol.ofName(name))); + this.addDimension(name, dimension); } } @@ -1387,8 +1383,11 @@ public final class UnitDatabase { * @since v0.1.0 */ public void addPrefix(final String name, final UnitPrefix prefix) { + Objects.requireNonNull(prefix, "prefix may not be null"); + final var namedPrefix = prefix + .withName(prefix.getNameSymbol().withExtraName(name)); this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(prefix, "prefix must not be null.")); + namedPrefix); } /** @@ -1401,9 +1400,11 @@ public final class UnitDatabase { * @since v0.1.0 */ public void addUnit(final String name, final Unit unit) { + Objects.requireNonNull(unit, "unit may not be null"); + final var namedUnit = unit + .withName(unit.getNameSymbol().withExtraName(name)); this.prefixlessUnits.put( - Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(unit, "unit must not be null.")); + Objects.requireNonNull(name, "name must not be null."), namedUnit); } /** @@ -1458,8 +1459,7 @@ public final class UnitDatabase { throw e; } final String prefixName = name.substring(0, name.length() - 1); - this.addPrefix(prefixName, - prefix.withName(NameSymbol.ofName(prefixName))); + this.addPrefix(prefixName, prefix); } else { // it's a unit, get the unit final Unit unit; @@ -1470,8 +1470,7 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - - this.addUnit(name, unit.withName(NameSymbol.ofName(name))); + this.addUnit(name, unit); } } } diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 955f980..7ef2967 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -288,4 +288,21 @@ public final class NameSymbol { 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/sevenUnitsGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..b56356d --- /dev/null +++ b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,95 @@ +/** + * @since 2020-08-26 + */ +package sevenUnitsGUI; + +import java.util.List; +import java.util.function.Predicate; + +import sevenUnits.unit.Metric; +import sevenUnits.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +public enum DefaultPrefixRepetitionRule implements Predicate> { + NO_REPETITION { + @Override + public boolean test(List prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List prefixes) { + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!Metric.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!Metric.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (Metric.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (Metric.YOTTA.equals(prefix) || Metric.YOCTO.equals(prefix)) { + part = 0; + } else if (Metric.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; + } + }; +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index f4f3e3a..fd050b7 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -16,8 +16,10 @@ */ package sevenUnitsGUI; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; @@ -30,7 +32,6 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; @@ -139,13 +140,25 @@ public final class Presenter { return Presenter.class.getResourceAsStream(filepath); } + /** + * @return true iff a and b have any elements in common + * @since 2022-04-19 + */ + private static final boolean sharesAnyElements(Set a, Set b) { + for (final Object e : a) { + if (b.contains(e)) + return true; + } + return false; + } + /** * @return {@code line} with any comments removed. * @since 2021-03-13 */ private static final String withoutComments(String line) { final int index = line.indexOf('#'); - return index == -1 ? line : line.substring(index); + return index == -1 ? line : line.substring(0, index); } // ====== SETTINGS ====== @@ -171,14 +184,15 @@ public final class Presenter { * of unit conversions will be put into this function, and the resulting * string will be used in the output. */ - private Function numberDisplayRule; + private Function numberDisplayRule = StandardDisplayRules + .uncertaintyBased(); /** * A predicate that determines whether or not a certain combination of * prefixes is allowed. If it returns false, a combination of prefixes will * not be allowed. Prefixes are put in the list from right to left. */ - private Predicate> prefixRepetitionRule; + private Predicate> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION; /** * The set of units that is considered neither metric nor nonmetric for the @@ -192,13 +206,13 @@ public final class Presenter { * removed from the From unit list and imperial/USC units removed from the To * unit list. */ - private boolean oneWayConversionEnabled; + private boolean oneWayConversionEnabled = false; /** * If this is false, duplicate units and prefixes will be removed from the * unit view in views that show units as a list to choose from. */ - private boolean showDuplicates; + private boolean showDuplicates = false; /** * Creates a Presenter @@ -245,7 +259,19 @@ public final class Presenter { } // set default settings temporarily - this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); + this.loadSettings(DEFAULT_SETTINGS_FILEPATH); + + // a Predicate that returns true iff the argument is a full base unit + final Predicate isFullBase = unit -> unit instanceof LinearUnit + && ((LinearUnit) unit).isBase(); + + // print out unit counts + System.out.printf( + "Successfully loaded %d units with %d unit names (%d base units).%n", + this.database.unitMapPrefixless(false).size(), + this.database.unitMapPrefixless(true).size(), + this.database.unitMapPrefixless(false).values().stream() + .filter(isFullBase).count()); } /** @@ -455,19 +481,36 @@ public final class Presenter { return this.numberParsingRule; } + /** + * @return the rule that determines whether a set of prefixes is valid + * @since 2022-04-19 + */ + public Predicate> getPrefixRepetitionRule() { + return this.prefixRepetitionRule; + } + + /** + * @return the view associated with this presenter + * @since 2022-04-19 + */ + public View getView() { + return this.view; + } + /** * @return whether or not the provided unit is semi-metric (i.e. an * exception) * @since 2022-04-16 */ - private final boolean isSemiMetric(Unit u) { + final boolean isSemiMetric(Unit u) { // determine if u is an exception final var primaryName = u.getPrimaryName(); final var symbol = u.getSymbol(); return primaryName.isPresent() && this.metricExceptions.contains(primaryName.orElseThrow()) || symbol.isPresent() - && this.metricExceptions.contains(symbol.orElseThrow()); + && this.metricExceptions.contains(symbol.orElseThrow()) + || sharesAnyElements(this.metricExceptions, u.getOtherNames()); } /** @@ -477,7 +520,48 @@ public final class Presenter { * @param settingsFile file settings should be loaded from * @since 2021-12-15 */ - void loadSettings(Path settingsFile) {} + void loadSettings(Path settingsFile) { + try { + // read file line by line + final int lineNum = 0; + for (final String line : Files.readAllLines(settingsFile)) { + final int equalsIndex = line.indexOf('='); + if (equalsIndex == -1) + throw new IllegalStateException( + "Settings file is malformed at line " + lineNum); + + final String param = line.substring(0, equalsIndex); + final String value = line.substring(equalsIndex + 1); + + switch (param) { + // set manually to avoid the unnecessary saving of the non-manual + // methods + case "number_display_rule": + this.numberDisplayRule = StandardDisplayRules + .getStandardRule(value); + break; + case "prefix_rule": + this.prefixRepetitionRule = DefaultPrefixRepetitionRule + .valueOf(value); + this.database.setPrefixRepetitionRule(this.prefixRepetitionRule); + break; + case "one_way": + this.oneWayConversionEnabled = Boolean.valueOf(value); + break; + case "include_duplicates": + this.showDuplicates = Boolean.valueOf(value); + break; + default: + System.err.printf("Warning: unrecognized setting \"%s\".%n", + param); + break; + } + } + if (this.view.getPresenter() != null) { + this.updateView(); + } + } catch (final IOException e) {} + } /** * @return true iff the One-Way Conversion feature is available (views that @@ -519,13 +603,38 @@ public final class Presenter { String.valueOf(prefix.getMultiplier()))); } + /** + * Saves the presenter's current settings to its default filepath. + * + * @since 2022-04-19 + */ + public void saveSettings() { + this.saveSettings(DEFAULT_SETTINGS_FILEPATH); + } + /** * Saves the presenter's settings to the user settings file. * * @param settingsFile file settings should be saved to * @since 2021-12-15 */ - void saveSettings(Path settingsFile) {} + void saveSettings(Path settingsFile) { + try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) { + writer.write(String.format("number_display_rule=%s\n", + this.numberDisplayRule)); + writer.write( + String.format("prefix_rule=%s\n", this.prefixRepetitionRule)); + writer.write( + String.format("one_way=%s\n", this.oneWayConversionEnabled)); + writer.write( + String.format("include_duplicates=%s\n", this.showDuplicates)); + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorMessage("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + } + } /** * @param numberDisplayRule the new rule that will be used by this presenter @@ -559,6 +668,17 @@ public final class Presenter { this.updateView(); } + /** + * @param prefixRepetitionRule the rule that determines whether a set of + * prefixes is valid + * @since 2022-04-19 + */ + public void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + this.database.setPrefixRepetitionRule(prefixRepetitionRule); + } + /** * @param showDuplicateUnits whether or not duplicate units should be shown * @since 2022-03-30 @@ -608,9 +728,7 @@ public final class Presenter { public void updateView() { if (this.view instanceof UnitConversionView) { final UnitConversionView ucview = (UnitConversionView) this.view; - final ObjectProduct viewDimension = this.database - .getDimension(((UnitConversionView) this.view) - .getSelectedDimensionName().orElseThrow()); + final var selectedDimensionName = ucview.getSelectedDimensionName(); // load units & prefixes into viewers this.view.setViewableUnitNames( @@ -619,29 +737,34 @@ public final class Presenter { this.database.prefixMap(this.showDuplicates).keySet()); // get From and To units - Stream fromUnits = this.database - .unitMapPrefixless(this.showDuplicates).entrySet().stream() - .map(Map.Entry::getValue) - .filter(u -> viewDimension.equals(u.getDimension())); + var fromUnits = this.database.unitMapPrefixless(this.showDuplicates) + .entrySet().stream(); + var toUnits = this.database.unitMapPrefixless(this.showDuplicates) + .entrySet().stream(); - Stream toUnits = this.database - .unitMapPrefixless(this.showDuplicates).entrySet().stream() - .map(Map.Entry::getValue) - .filter(u -> viewDimension.equals(u.getDimension())); + // filter by dimension, if one is selected + if (selectedDimensionName.isPresent()) { + final var viewDimension = this.database + .getDimension(selectedDimensionName.orElseThrow()); + fromUnits = fromUnits.filter( + u -> viewDimension.equals(u.getValue().getDimension())); + toUnits = toUnits.filter( + u -> viewDimension.equals(u.getValue().getDimension())); + } // filter by unit type, if desired if (this.oneWayConversionEnabled) { - fromUnits = fromUnits.filter(u -> UnitType.getType(u, + fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(), this::isSemiMetric) != UnitType.METRIC); - toUnits = toUnits.filter(u -> UnitType.getType(u, + toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(), this::isSemiMetric) != UnitType.NON_METRIC); } // set unit names ucview.setFromUnitNames( - fromUnits.map(Unit::getName).collect(Collectors.toSet())); + fromUnits.map(Map.Entry::getKey).collect(Collectors.toSet())); ucview.setToUnitNames( - toUnits.map(Unit::getName).collect(Collectors.toSet())); + toUnits.map(Map.Entry::getKey).collect(Collectors.toSet())); } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 098c374..c8e69ee 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -543,39 +543,49 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { .setBorder(new TitledBorder("Prefix Repetition Settings")); prefixRepetitionPanel.setLayout(new GridBagLayout()); + final var prefixRule = this.getPresenterPrefixRule() + .orElseThrow(() -> new AssertionError( + "Presenter loaded non-standard prefix rule")); + // prefix rules final ButtonGroup prefixRuleButtons = new ButtonGroup(); final JRadioButton noRepetition = new JRadioButton("No Repetition"); -// if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { -// noRepetition.setSelected(true); -// } -// noRepetition -// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( -// DefaultPrefixRepetitionRule.NO_REPETITION)); + if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { + noRepetition.setSelected(true); + } + noRepetition.addActionListener(e -> { + this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_REPETITION); + this.presenter.saveSettings(); + }); prefixRuleButtons.add(noRepetition); prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton noRestriction = new JRadioButton("No Restriction"); -// if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { -// noRestriction.setSelected(true); -// } -// noRestriction -// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( -// DefaultPrefixRepetitionRule.NO_RESTRICTION)); + if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { + noRestriction.setSelected(true); + } + noRestriction.addActionListener(e -> { + this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_RESTRICTION); + this.presenter.saveSettings(); + }); prefixRuleButtons.add(noRestriction); prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton customRepetition = new JRadioButton( "Complex Repetition"); -// if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { -// customRepetition.setSelected(true); -// } -// customRepetition -// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( -// DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); + if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { + customRepetition.setSelected(true); + } + customRepetition.addActionListener(e -> { + this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION); + this.presenter.saveSettings(); + }); prefixRuleButtons.add(customRepetition); prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); @@ -628,16 +638,22 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { final JCheckBox oneWay = new JCheckBox("Convert One Way Only"); oneWay.setSelected(this.presenter.oneWayConversionEnabled()); - oneWay.addItemListener(e -> this.presenter.setOneWayConversionEnabled( - e.getStateChange() == ItemEvent.SELECTED)); + oneWay.addItemListener(e -> { + this.presenter.setOneWayConversionEnabled( + e.getStateChange() == ItemEvent.SELECTED); + this.presenter.saveSettings(); + }); miscPanel.add(oneWay, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); final JCheckBox showAllVariations = new JCheckBox( "Show Duplicate Units & Prefixes"); showAllVariations.setSelected(this.presenter.duplicatesShown()); - showAllVariations.addItemListener(e -> this.presenter - .setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED)); + showAllVariations.addItemListener(e -> { + this.presenter + .setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED); + this.presenter.saveSettings(); + }); miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); @@ -678,6 +694,11 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return this.valueInput.getText(); } + @Override + public Presenter getPresenter() { + return this.presenter; + } + /** * @return the precision of the presenter's rounding rule, if that is * meaningful @@ -697,6 +718,17 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return OptionalInt.empty(); } + /** + * @return presenter's prefix repetition rule + * @since 2022-04-19 + */ + private Optional getPresenterPrefixRule() { + final var prefixRule = this.presenter.getPrefixRepetitionRule(); + return prefixRule instanceof DefaultPrefixRepetitionRule + ? Optional.of((DefaultPrefixRepetitionRule) prefixRule) + : Optional.empty(); + } + /** * Determines which rounding type the presenter is currently using, if any. * @@ -830,5 +862,6 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { throw new AssertionError(); } this.presenter.setNumberDisplayRule(roundingRule); + this.presenter.saveSettings(); } } diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java index 60675e2..f951f44 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java @@ -16,6 +16,9 @@ */ package sevenUnitsGUI; +import java.math.RoundingMode; + +import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.UnitValue; /** @@ -24,6 +27,22 @@ import sevenUnits.unit.UnitValue; * @since 2022-04-09 */ public final class UnitConversionRecord { + /** + * Gets a {@code UnitConversionRecord} from two linear unit values + * + * @param input input unit & value + * @param output output unit & value + * @return unit conversion record + * @since 2022-04-09 + */ + public static UnitConversionRecord fromLinearValues(LinearUnitValue input, + LinearUnitValue output) { + return UnitConversionRecord.valueOf(input.getUnit().getName(), + output.getUnit().getName(), + input.getValue().toString(false, RoundingMode.HALF_EVEN), + output.getValue().toString(false, RoundingMode.HALF_EVEN)); + } + /** * Gets a {@code UnitConversionRecord} from two unit values * diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java index da3749e..011e87f 100644 --- a/src/main/java/sevenUnitsGUI/View.java +++ b/src/main/java/sevenUnitsGUI/View.java @@ -29,6 +29,12 @@ import sevenUnits.utils.NameSymbol; * @since 2021-12-15 */ public interface View { + /** + * @return the presenter associated with this view + * @since 2022-04-19 + */ + Presenter getPresenter(); + /** * @return name of prefix currently being viewed * @since 2022-04-10 diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index dd9869d..9253ae5 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -196,30 +196,30 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private final Presenter presenter; /** The dimensions available to select from */ - private Set dimensionNames; + private Set dimensionNames = Set.of(); /** The expression in the From field */ - private String fromExpression; + private String fromExpression = ""; /** The expression in the To field */ - private String toExpression; + private String toExpression = ""; /** * The user-provided string representing the value in {@code fromSelection} */ - private String inputValue; + private String inputValue = ""; /** The unit selected in the From selection */ - private Optional fromSelection; + private Optional fromSelection = Optional.empty(); /** The unit selected in the To selection */ - private Optional toSelection; + private Optional toSelection = Optional.empty(); /** The currently selected dimension */ - private Optional selectedDimensionName; + private Optional selectedDimensionName = Optional.empty(); /** The units available in the From selection */ - private Set fromUnits; + private Set fromUnits = Set.of(); /** The units available in the To selection */ - private Set toUnits; + private Set toUnits = Set.of(); /** The selected unit in the unit viewer */ - private Optional unitViewerSelection; + private Optional unitViewerSelection = Optional.empty(); /** The selected unit in the prefix viewer */ - private Optional prefixViewerSelection; + private Optional prefixViewerSelection = Optional.empty(); /** Saved outputs of all unit conversions */ private final List unitConversions; @@ -289,6 +289,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the presenter associated with tihs view * @since 2022-01-29 */ + @Override public Presenter getPresenter() { return this.presenter; } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 362a5c9..f639329 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -19,6 +19,7 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.math.RoundingMode; import java.nio.file.Path; import java.util.List; import java.util.Set; @@ -32,10 +33,10 @@ import org.junit.jupiter.params.provider.MethodSource; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; +import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitType; -import sevenUnits.unit.UnitValue; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; @@ -91,7 +92,7 @@ public final class PresenterTest { // test result final List outputs = viewBot .expressionConversionList(); - assertEquals("10000.0 m = 10.0 km", + assertEquals("10000.0 m = 10.00000 km", outputs.get(outputs.size() - 1).toString()); } @@ -120,12 +121,14 @@ public final class PresenterTest { * here (that's for the backend tests), I'm just testing that it correctly * calls the unit conversion system */ - final UnitValue expectedInput = UnitValue.of(Metric.METRE, 10000.0); - final UnitValue expectedOutput = expectedInput + final LinearUnitValue expectedInput = LinearUnitValue.of(Metric.METRE, + UncertainDouble.fromRoundedString("10000.0")); + final LinearUnitValue expectedOutput = expectedInput .convertTo(Metric.KILOMETRE); - final UnitConversionRecord expectedUC = UnitConversionRecord - .fromValues(expectedInput, expectedOutput); - + final UnitConversionRecord expectedUC = UnitConversionRecord.valueOf( + expectedInput.getUnit().getName(), + expectedOutput.getUnit().getName(), "10000.0", + expectedOutput.getValue().toString(false, RoundingMode.HALF_EVEN)); assertEquals(List.of(expectedUC), viewBot.unitConversionList()); } @@ -182,13 +185,11 @@ public final class PresenterTest { // test that units are removed from each side when one-way conversion is // enabled presenter.setOneWayConversionEnabled(true); - presenter.updateView(); - assertEquals(metricNames, viewBot.getFromUnitNames()); - assertEquals(nonMetricNames, viewBot.getToUnitNames()); + assertEquals(nonMetricNames, viewBot.getFromUnitNames()); + assertEquals(metricNames, viewBot.getToUnitNames()); // test that units are kept when one-way conversion is disabled presenter.setOneWayConversionEnabled(false); - presenter.updateView(); assertEquals(allNames, viewBot.getFromUnitNames()); assertEquals(allNames, viewBot.getToUnitNames()); } @@ -243,7 +244,7 @@ public final class PresenterTest { // test the result of the rounding final String expectedOutputString = roundingRule - .apply(UncertainDouble.of(12.3456789, 0)); + .apply(UncertainDouble.fromRoundedString("12.3456789")); final String actualOutputString = viewBot.unitConversionList().get(0) .outputValueString(); assertEquals(expectedOutputString, actualOutputString); diff --git a/src/test/resources/test-settings.txt b/src/test/resources/test-settings.txt new file mode 100644 index 0000000..932221e --- /dev/null +++ b/src/test/resources/test-settings.txt @@ -0,0 +1,4 @@ +number_display_rule=Round to 11 significant figures +prefix_rule=NO_RESTRICTION +one_way=true +include_duplicates=true -- cgit v1.2.3 From 42c7e596747873afeb572be551dd991986beb51c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 18 May 2022 09:38:49 -0500 Subject: Added front-end code information to the design document --- docs/design.org | 31 +++++++-- docs/design.pdf | Bin 242954 -> 256907 bytes docs/design.tex | 73 +++++++++++++++------ .../sevenUnits/utils/SemanticVersionNumber.java | 8 +-- src/main/java/sevenUnitsGUI/FilterComparator.java | 10 ++- src/main/java/sevenUnitsGUI/MutablePredicate.java | 70 -------------------- 6 files changed, 92 insertions(+), 100 deletions(-) delete mode 100644 src/main/java/sevenUnitsGUI/MutablePredicate.java (limited to 'src/main/java/sevenUnits/utils') diff --git a/docs/design.org b/docs/design.org index be345f2..8bd27e6 100644 --- a/docs/design.org +++ b/docs/design.org @@ -1,14 +1,12 @@ #+TITLE: 7Units Design Document -#+SUBTITLE: For version 0.3.1 -#+DATE: 2021 August 26 +#+SUBTITLE: For version 0.4.0-alpha.1 +#+DATE: 2022 May 18 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX_HEADER: \usepackage{xurl} #+LaTeX: \newpage * Introduction 7Units is a program that can convert between units. This document details the internal design of 7Units, intended to be used by current and future developers. - - The frontend code is currently subject to change, so it is not included in the current version of this document. * Unit System Design Any code related to the backend unit system is stored in the ~sevenUnits.unit~ package. @@ -56,6 +54,31 @@ Dimension files are similar, only for dimensions instead of units and prefixes. #+LaTeX: \newpage +* Front-End Design + The front-end of 7Units is based on an MVP model. There are two major frontend classes, the *View* and the *Presenter*. +** The View + The ~View~ is the part of the frontend code that directly interacts with the user. It handles input and output, but does not do any processing. Processing is handled by the presenter and the backend code. + + The ~View~ is an interface, not a single class, so that I can easily create multiple views without having to rewrite any processing code. This allows me to easily prototype changes to the GUI without messing with existing code. + + In addition, I have decided to move some functions of the ~View~ into two subinterfaces, ~UnitConversionView~ and ~ExpressionConversionView~. This is because 7Units supports two kinds of unit conversion: unit conversion (select two compatible units, specify a value then convert) and expression conversion (enter two expressions and convert the first to a multiple of the second). Putting these functions into subinterfaces allows a ~View~ to do one type of conversion without forcing it to support the other. + + There are currently two implementations of the ~View~: + - TabbedView :: A Swing GUI implementation that uses tabs to separate the two types of conversion. The default GUI used by 7Units. + - ViewBot :: A simulated view that allows programs to set the output of its public methods (i.e. every getter in ~View~ has a setter in ~ViewBot~). Intended for testing, and is already used by ~PresenterTest~. + Both of these ~View~ implementations implement ~UnitConversionView~ and ~ExpressionConversionView~. +** The Presenter + The ~Presenter~ is an intermediary between the ~View~ and the backend code. It accepts the user's input and passes it to the backend, then accepts the backend's output and passes it to the frontend for user viewing. Its main functions do not have arguments or return values; instead it takes input from and provides output to the ~View~ via its public methods. +** Utility Classes + The frontend has many miscellaneous utility classes. Many of them are package-private. Here is a list of them, with a brief description of what they do and where they are used: + - DefaultPrefixRepetitionRule :: An enum containing the available rules determining when you can repeat prefixes. Used by the ~TabbedView~ for selecting the rule and by the ~Presenter~ for loading it from a file. + - DelegateListModel :: A ~javax.swing.ListModel~ implementation that delegates all of its methods to a ~List~. Implements ~List~ by also delegating to the underlying list. Used by the ~SearchBoxList~ to create an easily mutable ~ListModel~. + - FilterComparator :: A ~Comparator~ that sorts objects according to whether or not they match a filter. Used by the ~SearchBoxList~ for item sorting. + - GridBagBuilder :: A convenience class for generating ~GridBagConstraints~ objects for Swing's ~GridBagLayout~. Used by ~TabbedView~ for constructing the GUI. + - SearchBoxList :: A Swing component that combines a text field and a ~JList~ to create a searchable list. Used by the ~TabbedView~'s unit conversion mode. + - StandardDisplayRules :: A static utility class that allows you to generate display/rounding rules. Used by ~TabbedView~ for generating these rules and ~Presenter~ for loading them from a file. + - UnitConversionRecord :: A record-like object that contains the results of a unit or expression conversion. Used by ~UnitConversionView~ and ~ExpressionConversionView~ for accepting the results to be displayed. +#+LaTeX: \newpage * Utility Classes 7Units has a few general "utility" classes. They aren't directly related to units, but are used in the units system. ** ObjectProduct diff --git a/docs/design.pdf b/docs/design.pdf index fd1f2f1..0c4bb53 100644 Binary files a/docs/design.pdf and b/docs/design.pdf differ diff --git a/docs/design.tex b/docs/design.tex index 178a1c8..6523206 100644 --- a/docs/design.tex +++ b/docs/design.tex @@ -1,4 +1,4 @@ -% Created 2021-08-26 Thu 11:25 +% Created 2022-05-18 Wed 09:37 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -16,9 +16,9 @@ \usepackage{hyperref} \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} \usepackage{xurl} -\date{2021 August 26} +\date{2022 May 18} \title{7Units Design Document\\\medskip -\large For version 0.3.1} +\large For version 0.4.0-alpha.1} \hypersetup{ pdfauthor={}, pdftitle={7Units Design Document}, @@ -34,12 +34,10 @@ \newpage \section{Introduction} -\label{sec:orgc81fddd} +\label{sec:orgeddaf9e} 7Units is a program that can convert between units. This document details the internal design of 7Units, intended to be used by current and future developers. - -The frontend code is currently subject to change, so it is not included in the current version of this document. \section{Unit System Design} -\label{sec:org9d0655d} +\label{sec:org67c461a} Any code related to the backend unit system is stored in the \texttt{sevenUnits.unit} package. Here is a class diagram of the system. Unimportant methods, methods inherited from Object, getters and setters have been omitted. @@ -48,11 +46,11 @@ Here is a class diagram of the system. Unimportant methods, methods inherited f \end{center} \newpage \subsection{Dimensions} -\label{sec:org1e98dda} -Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:orgc7c5740]{ObjectProduct}, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions. +\label{sec:org8ff098d} +Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:org364a46d]{ObjectProduct}, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions. \subsection{Unit Classes} -\label{sec:orgd5ff0fb} -Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:orgc7c5740]{ObjectProduct} (referred to as the base) that they are based on, a dimension (ObjectProduct), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable. +\label{sec:org8192d2b} +Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:org364a46d]{ObjectProduct} (referred to as the base) that they are based on, a dimension (ObjectProduct), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable. Units also have two conversion functions - one which converts from a value expressed in this unit to its base unit, and another which converts from a value expressed in the base unit to this unit. In \texttt{Unit}, they are defined as two abstract methods. This allows you to convert from any unit to any other (as long as they have the same base, i.e. you aren't converting metres to pounds). To convert from A to B, first convert from A to its base, then convert from the base to B. @@ -77,36 +75,69 @@ There are a few more classes which play small roles in the unit system: \item[{USCustomary}] A static utility class with instances of common units in the US Customary system (not to be confused with the British Imperial system; it has the same unit names but the values of a few units are different). \end{description} \subsection{Prefixes} -\label{sec:org1aaa92c} +\label{sec:org6469484} A \texttt{UnitPrefix} is a simple object that can multiply a \texttt{LinearUnit} by a value. It can calculate a new name for the unit by combining its name and the unit's name (symbols are done similarly). It can do multiplication, division and exponentation with a number, as well as multiplication and division with another prefix; all of these work by changing the prefix's multiplier. \subsection{The Unit Database} -\label{sec:org7c6cf6d} +\label{sec:orgeeef8d3} The \texttt{UnitDatabase} class stores all of the unit, prefix and dimension data used by this program. It is not a representation of an actual database, just a class that stores lots of data. Units are stored using a custom \texttt{Map} implementation (\texttt{PrefixedUnitMap}) which maps unit names to units. It is backed by two maps: one for units (without prefixes) and one for prefixes. It is programmed to include prefixes (so if units includes "metre" and prefixes includes "kilo", this map will include "kilometre", mapping it to a unit representing a kilometre). It is immutable, but you can modify the underlying maps, which is reflected in the \texttt{PrefixedUnitMap}. Other than that, it is a normal map implementation. Prefixes and dimensions are stored in normal maps. \subsubsection{Parsing Expressions} -\label{sec:org8392990} -Each \texttt{UnitDatabase} instance has four \hyperref[sec:orgb091347]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating. +\label{sec:org338b57f} +Each \texttt{UnitDatabase} instance has four \hyperref[sec:org89370fa]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating. \subsubsection{Parsing Files} -\label{sec:orgd4ad341} +\label{sec:org4a8e841} There are two types of data files: unit and dimension. Unit files contain data about units and prefixes. Each line contains the name of a unit or prefix (prefixes end in a dash, units don't) followed by an expression which defines it, separated by one or more space characters (this behaviour is defined by the static regular expression \texttt{NAME\_EXPRESSION}). Unit files are parsed line by line, each line being run through the \texttt{addUnitOrPrefixFromLine} method, which splits a line into name and expression, determines whether it's a unit or a prefix, and parses the expression. Because all units are defined by others, base units need to be defined with a special expression "!"; \textbf{these units should be added to the database before parsing the file}. Dimension files are similar, only for dimensions instead of units and prefixes. \newpage +\section{Front-End Design} +\label{sec:orgda7974b} +The front-end of 7Units is based on an MVP model. There are two major frontend classes, the \textbf{View} and the \textbf{Presenter}. +\subsection{The View} +\label{sec:orgc7df86d} +The \texttt{View} is the part of the frontend code that directly interacts with the user. It handles input and output, but does not do any processing. Processing is handled by the presenter and the backend code. + +The \texttt{View} is an interface, not a single class, so that I can easily create multiple views without having to rewrite any processing code. This allows me to easily prototype changes to the GUI without messing with existing code. + +In addition, I have decided to move some functions of the \texttt{View} into two subinterfaces, \texttt{UnitConversionView} and \texttt{ExpressionConversionView}. This is because 7Units supports two kinds of unit conversion: unit conversion (select two compatible units, specify a value then convert) and expression conversion (enter two expressions and convert the first to a multiple of the second). Putting these functions into subinterfaces allows a \texttt{View} to do one type of conversion without forcing it to support the other. + +There are currently two implementations of the \texttt{View}: +\begin{description} +\item[{TabbedView}] A Swing GUI implementation that uses tabs to separate the two types of conversion. The default GUI used by 7Units. +\item[{ViewBot}] A simulated view that allows programs to set the output of its public methods (i.e. every getter in \texttt{View} has a setter in \texttt{ViewBot}). Intended for testing, and is already used by \texttt{PresenterTest}. +\end{description} +Both of these \texttt{View} implementations implement \texttt{UnitConversionView} and \texttt{ExpressionConversionView}. +\subsection{The Presenter} +\label{sec:org737be7a} +The \texttt{Presenter} is an intermediary between the \texttt{View} and the backend code. It accepts the user's input and passes it to the backend, then accepts the backend's output and passes it to the frontend for user viewing. Its main functions do not have arguments or return values; instead it takes input from and provides output to the \texttt{View} via its public methods. +\subsection{Utility Classes} +\label{sec:org81a3701} +The frontend has many miscellaneous utility classes. Many of them are package-private. Here is a list of them, with a brief description of what they do and where they are used: +\begin{description} +\item[{DefaultPrefixRepetitionRule}] An enum containing the available rules determining when you can repeat prefixes. Used by the \texttt{TabbedView} for selecting the rule and by the \texttt{Presenter} for loading it from a file. +\item[{DelegateListModel}] A \texttt{javax.swing.ListModel} implementation that delegates all of its methods to a \texttt{List}. Implements \texttt{List} by also delegating to the underlying list. Used by the \texttt{SearchBoxList} to create an easily mutable \texttt{ListModel}. +\item[{FilterComparator}] A \texttt{Comparator} that sorts objects according to whether or not they match a filter. Used by the \texttt{SearchBoxList} for item sorting. +\item[{GridBagBuilder}] A convenience class for generating \texttt{GridBagConstraints} objects for Swing's \texttt{GridBagLayout}. Used by \texttt{TabbedView} for constructing the GUI. +\item[{SearchBoxList}] A Swing component that combines a text field and a \texttt{JList} to create a searchable list. Used by the \texttt{TabbedView}'s unit conversion mode. +\item[{StandardDisplayRules}] A static utility class that allows you to generate display/rounding rules. Used by \texttt{TabbedView} for generating these rules and \texttt{Presenter} for loading them from a file. +\item[{UnitConversionRecord}] A record-like object that contains the results of a unit or expression conversion. Used by \texttt{UnitConversionView} and \texttt{ExpressionConversionView} for accepting the results to be displayed. +\end{description} +\newpage \section{Utility Classes} -\label{sec:org251cb6a} +\label{sec:orge44a23f} 7Units has a few general "utility" classes. They aren't directly related to units, but are used in the units system. \subsection{ObjectProduct} -\label{sec:orgc7c5740} +\label{sec:org364a46d} An \texttt{ObjectProduct} represents a "product" of elements of some type. The units system uses them to represent coherent units as a product of base units, and dimensions as a product of base dimensions. Internally, it is represented using a map mapping objects to their exponents in the product. For example, the unit "kg m\textsuperscript{2} / s\textsuperscript{2}" (i.e. a Joule) would be represented with a map like \texttt{[kg: 1, m: 2, s: -2]}. \subsection{ExpressionParser} -\label{sec:orgb091347} +\label{sec:org89370fa} The \texttt{ExpressionParser} class is used to parse the unit, prefix and dimension expressions that are used throughout 7Units. An expression is something like "(2 m + 30 J / N) * 8 s)". Each instance represents a type of expression, containing a way to obtain values (such as numbers or units) from the text and operations that can be done on these values (such as addition, subtraction or multiplication). Each operation also has a priority, which controls the order of operations (i.e. multiplication gets a higher priority than addition). \texttt{ExpressionParser} has a parameterized type \texttt{T}, which represents the type of the value used in the expression. The expression parser currently only supports one type of value per expression; in the expressions used by 7Units numbers are treated as a kind of unit or prefix. Operators are represented by internal types; the system distinguishes between unary operators (those that take a single value, like negation) and binary operators (those that take 2 values, like +, -, * or /). @@ -123,13 +154,13 @@ Expressions are parsed in 2 steps: After evaluating the last token, there should be one value left in the stack - the answer. If there isn't, the original expression was malformed. \end{enumerate} \subsection{Math Classes} -\label{sec:orgb4a476a} +\label{sec:org04a7e76} There are two simple math classes in 7Units: \begin{description} \item[{\texttt{UncertainDouble}}] Like a \texttt{double}, but with an uncertainty (e.g. \(2.0 \pm 0.4\)). The operations are like those of the regular Double, only they also calculate the uncertainty of the final value. They also have "exact" versions to help interoperation between \texttt{double} and \texttt{UncertainDouble}. It is used by the converter's Scientific Precision setting. \item[{\texttt{DecimalComparison}}] A static utility class that contains a few alternate equals() methods for \texttt{double} and \texttt{UncertainDouble}. These methods allow a slight (configurable) difference between values to still be considered equal, to fight roundoff error. \end{description} \subsection{Collection Classes} -\label{sec:org4a3e6a1} +\label{sec:orgb67e633} The \texttt{ConditionalExistenceCollections} class contains wrapper implementations of \texttt{Collection}, \texttt{Iterator}, \texttt{Map} and \texttt{Set}. These implementations ignore elements that do not pass a certain condition - if an element fails the condition, \texttt{contains} will return false, the iterator will skip past it, it won't be counted in \texttt{size}, etc. even if it exists in the original collection. Effectively, any element of the original collection that fails the test does not exist. \end{document} diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java index 06417c5..a663a11 100644 --- a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java +++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java @@ -533,8 +533,8 @@ public final class SemanticVersionNumber // now we just compare pre-release identifiers // (remember: build metadata is ignored) - return SemanticVersionNumber.compareIdentifiers(this.preReleaseIdentifiers, - o.preReleaseIdentifiers); + return SemanticVersionNumber.compareIdentifiers( + this.preReleaseIdentifiers, o.preReleaseIdentifiers); } /** @@ -557,9 +557,9 @@ public final class SemanticVersionNumber * always compatible. Different version numbers are compatible as long as: *

    *
  • The major version number is not 0 (if it is, the API is considered - * unstable and any upgrade can be backwards compatible) + * unstable and any upgrade can be backwards incompatible) *
  • The major version number is the same (changing the major version - * number implies bacwards incompatible changes) + * number implies backwards incompatible changes) *
  • This version comes before the other one in the official precedence * order (downgrading can remove features you depend on) *
diff --git a/src/main/java/sevenUnitsGUI/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java index f34d0c0..c0a67e8 100644 --- a/src/main/java/sevenUnitsGUI/FilterComparator.java +++ b/src/main/java/sevenUnitsGUI/FilterComparator.java @@ -90,11 +90,19 @@ final class FilterComparator implements Comparator { */ public FilterComparator(final String filter, final Comparator comparator, final boolean caseSensitive) { - this.filter = Objects.requireNonNull(filter, "filter must not be null."); + Objects.requireNonNull(filter, "filter must not be null."); + this.filter = caseSensitive ? filter : filter.toLowerCase(); this.comparator = comparator; this.caseSensitive = caseSensitive; } + /** + * Compares two objects according to whether or not they match a filter. + * Objects whose string representation starts with the filter's text go + * first, then those that contain it but don't start with it, then those that + * don't contain it. Objects in the same order here are sorted by their + * string representation's compareTo or the provided comparator. + */ @Override public int compare(final T arg0, final T arg1) { // if this is case insensitive, make them lowercase diff --git a/src/main/java/sevenUnitsGUI/MutablePredicate.java b/src/main/java/sevenUnitsGUI/MutablePredicate.java deleted file mode 100644 index 6cb8689..0000000 --- a/src/main/java/sevenUnitsGUI/MutablePredicate.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * 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 . - */ -package sevenUnitsGUI; - -import java.util.function.Predicate; - -/** - * A container for a predicate, which can be changed later. - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ -final class MutablePredicate implements Predicate { - /** - * The predicate stored in this {@code MutablePredicate} - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private Predicate predicate; - - /** - * Creates the {@code MutablePredicate}. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public MutablePredicate(final Predicate predicate) { - this.predicate = predicate; - } - - /** - * @return predicate - * @since 2019-04-13 - * @since v0.2.0 - */ - public final Predicate getPredicate() { - return this.predicate; - } - - /** - * @param predicate - * new value of predicate - * @since 2019-04-13 - * @since v0.2.0 - */ - public final void setPredicate(final Predicate predicate) { - this.predicate = predicate; - } - - @Override - public boolean test(final T t) { - return this.predicate.test(t); - } -} -- cgit v1.2.3 From 39668f4b274f0e7996f65b4f432a48ae0d88daca Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 8 Jul 2022 12:46:45 -0500 Subject: Bumped version number to 0.4.0b1 & added @since --- CHANGELOG.org | 2 +- README.org | 2 +- docs/design.org | 2 +- docs/design.pdf | Bin 348080 -> 348051 bytes docs/design.tex | 68 ++++++++++----------- docs/manual.org | 6 +- docs/manual.pdf | Bin 173138 -> 173293 bytes docs/manual.tex | 36 +++++------ screenshots/main-interface-settings.png | Bin 25953 -> 25953 bytes src/main/java/sevenUnits/ProgramInfo.java | 4 +- src/main/java/sevenUnits/unit/BaseDimension.java | 5 +- src/main/java/sevenUnits/utils/NameSymbol.java | 1 + .../sevenUnits/utils/SemanticVersionNumber.java | 25 ++++++++ .../sevenUnitsGUI/ExpressionConversionView.java | 4 ++ src/main/java/sevenUnitsGUI/Main.java | 6 +- src/main/java/sevenUnitsGUI/PrefixSearchRule.java | 12 ++++ .../java/sevenUnitsGUI/StandardDisplayRules.java | 8 +++ .../java/sevenUnitsGUI/UnitConversionRecord.java | 8 +++ .../java/sevenUnitsGUI/UnitConversionView.java | 12 ++++ src/main/java/sevenUnitsGUI/View.java | 10 +++ src/main/java/sevenUnitsGUI/ViewBot.java | 1 + 21 files changed, 150 insertions(+), 62 deletions(-) (limited to 'src/main/java/sevenUnits/utils') diff --git a/CHANGELOG.org b/CHANGELOG.org index 7000828..de873b3 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,6 +1,6 @@ * Changelog All notable changes in this project will be shown in this file. -** Unreleased +** v0.4.0 (Unreleased) *** Added - Added tests for the GUI - Added an object for the version numbers (SemanticVersionNumber) diff --git a/README.org b/README.org index f891bdc..171b4d9 100644 --- a/README.org +++ b/README.org @@ -1,4 +1,4 @@ -* 7Units v0.4.0a1 +* 7Units v0.4.0b1 (this project uses Semantic Versioning) ** What is it? This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in. diff --git a/docs/design.org b/docs/design.org index 467c018..0e4ca92 100644 --- a/docs/design.org +++ b/docs/design.org @@ -1,5 +1,5 @@ #+TITLE: 7Units Design Document -#+SUBTITLE: For version 0.4.0-alpha.1 +#+SUBTITLE: For version 0.4.0-beta.1 #+DATE: 2022 July 8 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX_HEADER: \usepackage{xurl} diff --git a/docs/design.pdf b/docs/design.pdf index 56d73a6..99fbb3b 100644 Binary files a/docs/design.pdf and b/docs/design.pdf differ diff --git a/docs/design.tex b/docs/design.tex index 5023fb3..9434cc1 100644 --- a/docs/design.tex +++ b/docs/design.tex @@ -1,4 +1,4 @@ -% Created 2022-07-08 Fri 10:05 +% Created 2022-07-08 Fri 12:43 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -18,7 +18,7 @@ \usepackage{xurl} \date{2022 July 8} \title{7Units Design Document\\\medskip -\large For version 0.4.0-alpha.1} +\large For version 0.4.0-beta.1} \hypersetup{ pdfauthor={}, pdftitle={7Units Design Document}, @@ -34,35 +34,35 @@ \newpage \section{Introduction} -\label{sec:orgd3381a6} +\label{sec:orga266915} 7Units is a program that can convert between units. This document details the internal design of 7Units, intended to be used by current and future developers. \section{System Overview} -\label{sec:org9bd9474} +\label{sec:org6d969dd} \begin{figure}[htbp] \centering \includegraphics[height=144px]{./diagrams/overview-diagram.plantuml.png} \caption{A big-picture diagram of 7Units, containing all of the major classes.} \end{figure} \subsection{Packages of 7Units} -\label{sec:org5c8f3ee} +\label{sec:orged5584a} 7Units splits its code into three main packages: \begin{description} -\item[{\texttt{sevenUnits.unit}}] The \hyperref[sec:org1171d15]{unit system} +\item[{\texttt{sevenUnits.unit}}] The \hyperref[sec:orgf969629]{unit system} \item[{\texttt{sevenUnits.utils}}] Extra classes that aid the unit system. -\item[{\texttt{sevenUnitsGUI}}] The \hyperref[sec:orgb716c37]{front end} code +\item[{\texttt{sevenUnitsGUI}}] The \hyperref[sec:org431a987]{front end} code \end{description} \texttt{sevenUnits.unit} depends on \texttt{sevenUnits.utils}, while \texttt{sevenUnitsGUI} depends on both \texttt{sevenUnits} packages. There is only one class that isn't in any of these packages, \texttt{sevenUnits.VersionInfo}. \subsection{Major Classes of 7Units} -\label{sec:org22c5367} +\label{sec:org147769f} \begin{description} -\item[{\hyperref[sec:orge128590]{sevenUnits.unit.Unit}}] The class representing a unit -\item[{\hyperref[sec:orgc8dc0e4]{sevenUnits.unit.UnitDatabase}}] A class that stores collections of units, prefixes and dimensions. -\item[{\hyperref[sec:orgf3ee40e]{sevenUnitsGUI.View}}] The class that handles interaction between the user and the program. -\item[{\hyperref[sec:orgcf5cc70]{sevenUnitsGUI.Presenter}}] The class that handles communication between the \texttt{View} and the unit system. +\item[{\hyperref[sec:org93aab3e]{sevenUnits.unit.Unit}}] The class representing a unit +\item[{\hyperref[sec:org0a33326]{sevenUnits.unit.UnitDatabase}}] A class that stores collections of units, prefixes and dimensions. +\item[{\hyperref[sec:org5460ab6]{sevenUnitsGUI.View}}] The class that handles interaction between the user and the program. +\item[{\hyperref[sec:org4f735c1]{sevenUnitsGUI.Presenter}}] The class that handles communication between the \texttt{View} and the unit system. \end{description} \newpage \subsection{Process of Unit Conversion} -\label{sec:org73700d8} +\label{sec:org65b1400} \begin{figure}[htbp] \centering \includegraphics[width=.9\linewidth]{./diagrams/convert-units.plantuml.png} @@ -77,7 +77,7 @@ \end{enumerate} \newpage \subsection{Process of Expression Conversion} -\label{sec:org3e8ae17} +\label{sec:orgc9346ba} The process of expression conversion is similar to that of unit conversion. \begin{figure}[htbp] \centering @@ -93,7 +93,7 @@ The process of expression conversion is similar to that of unit conversion. \end{enumerate} \newpage \section{Unit System Design} -\label{sec:org1171d15} +\label{sec:orgf969629} Any code related to the backend unit system is stored in the \texttt{sevenUnits.unit} package. Here is a class diagram of the system. Unimportant methods, methods inherited from Object, getters and setters have been omitted. @@ -104,11 +104,11 @@ Here is a class diagram of the system. Unimportant methods, methods inherited f \end{figure} \newpage \subsection{Dimensions} -\label{sec:org14cd421} -Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:orgc3e831c]{ObjectProduct}, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions. +\label{sec:orgda7eb73} +Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:org9c5f1fc]{ObjectProduct}, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions. \subsection{Unit Classes} -\label{sec:orge128590} -Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:orgc3e831c]{ObjectProduct} (referred to as the base) that they are based on, a dimension (ObjectProduct), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable. +\label{sec:org93aab3e} +Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:org9c5f1fc]{ObjectProduct} (referred to as the base) that they are based on, a dimension (ObjectProduct), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable. Units also have two conversion functions - one which converts from a value expressed in this unit to its base unit, and another which converts from a value expressed in the base unit to this unit. In \texttt{Unit}, they are defined as two abstract methods. This allows you to convert from any unit to any other (as long as they have the same base, i.e. you aren't converting metres to pounds). To convert from A to B, first convert from A to its base, then convert from the base to B. @@ -133,20 +133,20 @@ There are a few more classes which play small roles in the unit system: \item[{USCustomary}] A static utility class with instances of common units in the US Customary system (not to be confused with the British Imperial system; it has the same unit names but the values of a few units are different). \end{description} \subsection{Prefixes} -\label{sec:org1bd7050} +\label{sec:org40fa3a0} A \texttt{UnitPrefix} is a simple object that can multiply a \texttt{LinearUnit} by a value. It can calculate a new name for the unit by combining its name and the unit's name (symbols are done similarly). It can do multiplication, division and exponentation with a number, as well as multiplication and division with another prefix; all of these work by changing the prefix's multiplier. \subsection{The Unit Database} -\label{sec:orgc8dc0e4} +\label{sec:org0a33326} The \texttt{UnitDatabase} class stores all of the unit, prefix and dimension data used by this program. It is not a representation of an actual database, just a class that stores lots of data. Units are stored using a custom \texttt{Map} implementation (\texttt{PrefixedUnitMap}) which maps unit names to units. It is backed by two maps: one for units (without prefixes) and one for prefixes. It is programmed to include prefixes (so if units includes "metre" and prefixes includes "kilo", this map will include "kilometre", mapping it to a unit representing a kilometre). It is immutable, but you can modify the underlying maps, which is reflected in the \texttt{PrefixedUnitMap}. Other than that, it is a normal map implementation. Prefixes and dimensions are stored in normal maps. \subsubsection{Parsing Expressions} -\label{sec:orgb7ee1da} -Each \texttt{UnitDatabase} instance has four \hyperref[sec:orgd351c2f]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating. +\label{sec:orgb3362c7} +Each \texttt{UnitDatabase} instance has four \hyperref[sec:org7f49fac]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating. \subsubsection{Parsing Files} -\label{sec:org072dc65} +\label{sec:org5f50970} There are two types of data files: unit and dimension. Unit files contain data about units and prefixes. Each line contains the name of a unit or prefix (prefixes end in a dash, units don't) followed by an expression which defines it, separated by one or more space characters (this behaviour is defined by the static regular expression \texttt{NAME\_EXPRESSION}). Unit files are parsed line by line, each line being run through the \texttt{addUnitOrPrefixFromLine} method, which splits a line into name and expression, determines whether it's a unit or a prefix, and parses the expression. Because all units are defined by others, base units need to be defined with a special expression "!"; \textbf{these units should be added to the database before parsing the file}. @@ -154,10 +154,10 @@ Unit files contain data about units and prefixes. Each line contains the name o Dimension files are similar, only for dimensions instead of units and prefixes. \newpage \section{Front-End Design} -\label{sec:orgb716c37} +\label{sec:org431a987} The front-end of 7Units is based on an MVP model. There are two major frontend classes, the \textbf{View} and the \textbf{Presenter}. \subsection{The View} -\label{sec:orgf3ee40e} +\label{sec:org5460ab6} The \texttt{View} is the part of the frontend code that directly interacts with the user. It handles input and output, but does not do any processing. Processing is handled by the presenter and the backend code. The \texttt{View} is an interface, not a single class, so that I can easily create multiple views without having to rewrite any processing code. This allows me to easily prototype changes to the GUI without messing with existing code. @@ -171,10 +171,10 @@ There are currently two implementations of the \texttt{View}: \end{description} Both of these \texttt{View} implementations implement \texttt{UnitConversionView} and \texttt{ExpressionConversionView}. \subsection{The Presenter} -\label{sec:orgcf5cc70} +\label{sec:org4f735c1} The \texttt{Presenter} is an intermediary between the \texttt{View} and the backend code. It accepts the user's input and passes it to the backend, then accepts the backend's output and passes it to the frontend for user viewing. Its main functions do not have arguments or return values; instead it takes input from and provides output to the \texttt{View} via its public methods. \subsubsection{Rules} -\label{sec:org9542834} +\label{sec:org29011d5} The \texttt{Presenter} has a set of function-object rules that determine some of its behaviours. Each corresponds to a setting in the \texttt{View}, but they can be set to other values via the \texttt{Presenter}'s setters (although nonstandard rules cannot be saved and loaded): \begin{description} \item[{numberDisplayRule}] A function that determines how numbers are displayed. This controls the rounding rules. @@ -184,7 +184,7 @@ The \texttt{Presenter} has a set of function-object rules that determine some of These rules have been made this way to enable an incredible level of customization of these behaviours. Because any function object with the correct arguments and return type is accepted, these rules (especially the number display rule) can do much more than the default behaviours. \subsection{Utility Classes} -\label{sec:orgc49ece0} +\label{sec:orga401626} The frontend has many miscellaneous utility classes. Many of them are package-private. Here is a list of them, with a brief description of what they do and where they are used: \begin{description} \item[{DefaultPrefixRepetitionRule}] An enum containing the available rules determining when you can repeat prefixes. Used by the \texttt{TabbedView} for selecting the rule and by the \texttt{Presenter} for loading it from a file. @@ -197,15 +197,15 @@ The frontend has many miscellaneous utility classes. Many of them are package-p \end{description} \newpage \section{Utility Classes} -\label{sec:orgc90957f} +\label{sec:org9889589} 7Units has a few general "utility" classes. They aren't directly related to units, but are used in the units system. \subsection{ObjectProduct} -\label{sec:orgc3e831c} +\label{sec:org9c5f1fc} An \texttt{ObjectProduct} represents a "product" of elements of some type. The units system uses them to represent coherent units as a product of base units, and dimensions as a product of base dimensions. Internally, it is represented using a map mapping objects to their exponents in the product. For example, the unit "kg m\textsuperscript{2} / s\textsuperscript{2}" (i.e. a Joule) would be represented with a map like \texttt{[kg: 1, m: 2, s: -2]}. \subsection{ExpressionParser} -\label{sec:orgd351c2f} +\label{sec:org7f49fac} The \texttt{ExpressionParser} class is used to parse the unit, prefix and dimension expressions that are used throughout 7Units. An expression is something like "(2 m + 30 J / N) * 8 s)". Each instance represents a type of expression, containing a way to obtain values (such as numbers or units) from the text and operations that can be done on these values (such as addition, subtraction or multiplication). Each operation also has a priority, which controls the order of operations (i.e. multiplication gets a higher priority than addition). \texttt{ExpressionParser} has a parameterized type \texttt{T}, which represents the type of the value used in the expression. The expression parser currently only supports one type of value per expression; in the expressions used by 7Units numbers are treated as a kind of unit or prefix. Operators are represented by internal types; the system distinguishes between unary operators (those that take a single value, like negation) and binary operators (those that take 2 values, like +, -, * or /). @@ -222,13 +222,13 @@ Expressions are parsed in 2 steps: After evaluating the last token, there should be one value left in the stack - the answer. If there isn't, the original expression was malformed. \end{enumerate} \subsection{Math Classes} -\label{sec:orgdadbc0d} +\label{sec:org48c9af9} There are two simple math classes in 7Units: \begin{description} \item[{\texttt{UncertainDouble}}] Like a \texttt{double}, but with an uncertainty (e.g. \(2.0 \pm 0.4\)). The operations are like those of the regular Double, only they also calculate the uncertainty of the final value. They also have "exact" versions to help interoperation between \texttt{double} and \texttt{UncertainDouble}. It is used by the converter's Scientific Precision setting. \item[{\texttt{DecimalComparison}}] A static utility class that contains a few alternate equals() methods for \texttt{double} and \texttt{UncertainDouble}. These methods allow a slight (configurable) difference between values to still be considered equal, to fight roundoff error. \end{description} \subsection{Collection Classes} -\label{sec:org7421746} +\label{sec:org9065607} The \texttt{ConditionalExistenceCollections} class contains wrapper implementations of \texttt{Collection}, \texttt{Iterator}, \texttt{Map} and \texttt{Set}. These implementations ignore elements that do not pass a certain condition - if an element fails the condition, \texttt{contains} will return false, the iterator will skip past it, it won't be counted in \texttt{size}, etc. even if it exists in the original collection. Effectively, any element of the original collection that fails the test does not exist. \end{document} diff --git a/docs/manual.org b/docs/manual.org index 6de3b93..bcaaf6b 100644 --- a/docs/manual.org +++ b/docs/manual.org @@ -1,5 +1,5 @@ #+TITLE: 7Units User Manual -#+SUBTITLE: For Version 0.4.0-alpha.1 +#+SUBTITLE: For Version 0.4.0-beta.1 #+DATE: 2022 July 8 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} @@ -9,7 +9,7 @@ * System Requirements - Works on all major operating systems \\ *NOTE:* All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown. - - Java version 11-15 required + - Java version 11+ required # installation instructions go here - wait until git repository is fixed/set up #+LaTeX: \newpage * How to Use 7Units @@ -47,7 +47,7 @@ [[../screenshots/sample-conversion-results-expression-converter.png]] * 7Units Settings All settings can be accessed in the tab with the gear icon. - #+CAPTION: The settings menu, as of version 0.4.0-alpha.1 + #+CAPTION: The settings menu, as of version 0.4.0-beta.1 #+ATTR_LaTeX: :height 250px [[../screenshots/main-interface-settings.png]] ** Rounding Settings diff --git a/docs/manual.pdf b/docs/manual.pdf index 71cb886..430eb09 100644 Binary files a/docs/manual.pdf and b/docs/manual.pdf differ diff --git a/docs/manual.tex b/docs/manual.tex index 4b8e4ab..bc80a69 100644 --- a/docs/manual.tex +++ b/docs/manual.tex @@ -1,4 +1,4 @@ -% Created 2022-07-08 Fri 10:17 +% Created 2022-07-08 Fri 12:44 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -17,7 +17,7 @@ \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} \date{2022 July 8} \title{7Units User Manual\\\medskip -\large For Version 0.4.0-alpha.1} +\large For Version 0.4.0-beta.1} \hypersetup{ pdfauthor={}, pdftitle={7Units User Manual}, @@ -32,21 +32,21 @@ \newpage \section{Introduction and Purpose} -\label{sec:org26b964e} +\label{sec:org40df1fc} 7Units is a program that can be used to convert units. This document outlines how to use the program. \section{System Requirements} -\label{sec:orgfb95788} +\label{sec:org5bf24ac} \begin{itemize} \item Works on all major operating systems \\ \textbf{NOTE:} All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown. -\item Java version 11-15 required +\item Java version 11+ required \end{itemize} \newpage \section{How to Use 7Units} -\label{sec:orgc48d5d5} +\label{sec:org0303c2c} \subsection{Simple Unit Conversion} -\label{sec:orgd56d395} +\label{sec:orgfdea557} \begin{enumerate} \item Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure \ref{main-interface-dimension}: \begin{figure}[htbp] @@ -71,7 +71,7 @@ \end{figure} \end{enumerate} \subsection{Complex Unit Conversion} -\label{sec:org79999e9} +\label{sec:orgaebd362} \begin{enumerate} \item Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure \ref{main-interface-expression}: \begin{figure}[htbp] @@ -79,7 +79,7 @@ \includegraphics[height=250px]{../screenshots/main-interface-expression-converter.png} \caption{\label{main-interface-expression}Taken in version 0.3.0} \end{figure} -\item Enter a \hyperref[sec:org28ae9bb]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}". +\item Enter a \hyperref[sec:org1bb92a1]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}". \item Enter a unit name (or another unit expression) in the To box. \item Press the Convert button. This will calculate the value of the first expression, and convert it to a multiple of the second unit (or expression). \begin{figure}[htbp] @@ -89,15 +89,15 @@ \end{figure} \end{enumerate} \section{7Units Settings} -\label{sec:org6032cec} +\label{sec:orgcab9094} All settings can be accessed in the tab with the gear icon. \begin{figure}[htbp] \centering \includegraphics[height=250px]{../screenshots/main-interface-settings.png} -\caption{The settings menu, as of version 0.4.0-alpha.1} +\caption{The settings menu, as of version 0.4.0-beta.1} \end{figure} \subsection{Rounding Settings} -\label{sec:orgbe67478} +\label{sec:orgbb50019} These settings control how the output of a unit conversion is rounded. \begin{description} \item[{Fixed Precision}] Round to a fixed number of \href{https://en.wikipedia.org/wiki/Significant\_figures}{significant digits}. The number of significant digits is controlled by the precision slider below. @@ -105,7 +105,7 @@ These settings control how the output of a unit conversion is rounded. \item[{Scientific Precision}] Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider. \end{description} \subsection{Prefix Repetition Settings} -\label{sec:org3207fad} +\label{sec:org48d4cc7} These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre) \begin{description} \item[{No Repetition}] Units may only have one prefix. @@ -120,7 +120,7 @@ These settings control when you are allowed to repeat unit prefixes (e.g. kiloki \end{itemize} \end{description} \subsection{Search Settings} -\label{sec:orge76e8f6} +\label{sec:orgce39699} These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile). \begin{description} \item[{Never Include Prefixed Units}] Prefixed units will only be shown if they are explicitly added to the unitfile. @@ -128,15 +128,15 @@ These settings control which prefixes are shown in the "Convert Units" tab. Onl \item[{Include All Single Prefixes}] Every coherent unit will have every prefixed version of it included in the list. \end{description} \subsection{Miscellaneous Settings} -\label{sec:org5613324} +\label{sec:org2332067} \begin{description} \item[{Convert One Way Only}] In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units\footnote{7Units's definition of "metric" is stricter than the SI, but all of the common units that are commonly considered metric but not included in 7Units's definition are included in the exceptions file.} will be shown on the right. Units listed in the exceptions file (\texttt{src/main/resources/metric\_exceptions.txt}) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected. \item[{Show Duplicates in "Convert Units"}] If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab. \end{description} \section{Appendices} -\label{sec:org03406a3} +\label{sec:orgd294b53} \subsection{Unit Expressions} -\label{sec:org28ae9bb} +\label{sec:org1bb92a1} A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence): \begin{itemize} \item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent @@ -146,6 +146,6 @@ A unit expression is simply a math expression where the values being operated on Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \textdegree{} C \subsection{Other Expressions} -\label{sec:orgb03cf2c} +\label{sec:org2f36819} There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future. \end{document} diff --git a/screenshots/main-interface-settings.png b/screenshots/main-interface-settings.png index c29f65c..39b95f4 100644 Binary files a/screenshots/main-interface-settings.png and b/screenshots/main-interface-settings.png differ diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index f32d2c7..6ebe66c 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -26,9 +26,9 @@ import sevenUnits.utils.SemanticVersionNumber; */ public final class ProgramInfo { - /** The version number (0.4.0-alpha+dev) */ + /** The version number (0.4.0-beta.1) */ public static final SemanticVersionNumber VERSION = SemanticVersionNumber - .preRelease(0, 4, 0, "alpha", 1); + .preRelease(0, 4, 0, "beta", 1); private ProgramInfo() { // this class is only for static variables, you shouldn't be able to diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java index bcd57d9..820d48c 100644 --- a/src/main/java/sevenUnits/unit/BaseDimension.java +++ b/src/main/java/sevenUnits/unit/BaseDimension.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 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 @@ -63,6 +63,9 @@ public final class BaseDimension implements Nameable { this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); } + /** + * @since v0.4.0 + */ @Override public NameSymbol getNameSymbol() { return NameSymbol.of(this.name, this.symbol); diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 7ef2967..9388f63 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -294,6 +294,7 @@ public final class NameSymbol { * 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 v0.4.0 * @since 2022-04-19 */ public final NameSymbol withExtraName(String name) { diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java index a663a11..e80e16e 100644 --- a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java +++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java @@ -39,6 +39,7 @@ import java.util.regex.Pattern; * are made * * + * @since v0.4.0 * @since 2022-02-19 */ public final class SemanticVersionNumber @@ -51,6 +52,7 @@ public final class SemanticVersionNumber * throw NullPointerExceptions, everything else throws * IllegalArgumentException. * + * @since v0.4.0 * @since 2022-02-19 */ public static final class Builder { @@ -67,6 +69,7 @@ public final class 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 v0.4.0 * @since 2022-02-19 */ private Builder(int major, int minor, int patch) { @@ -79,6 +82,7 @@ public final class SemanticVersionNumber /** * @return version number created by this builder + * @since v0.4.0 * @since 2022-02-19 */ public SemanticVersionNumber build() { @@ -91,6 +95,7 @@ public final class SemanticVersionNumber * * @param identifiers build metadata * @return this builder + * @since v0.4.0 * @since 2022-02-19 */ public Builder buildMetadata(List identifiers) { @@ -110,6 +115,7 @@ public final class SemanticVersionNumber * * @param identifiers build metadata * @return this builder + * @since v0.4.0 * @since 2022-02-19 */ public Builder buildMetadata(String... identifiers) { @@ -148,6 +154,7 @@ public final class SemanticVersionNumber * * @param identifiers pre-release identifier(s) to add * @return this builder + * @since v0.4.0 * @since 2022-02-19 */ public Builder preRelease(int... identifiers) { @@ -166,6 +173,7 @@ public final class SemanticVersionNumber * * @param identifiers pre-release identifier(s) to add * @return this builder + * @since v0.4.0 * @since 2022-02-19 */ public Builder preRelease(List identifiers) { @@ -185,6 +193,7 @@ public final class SemanticVersionNumber * * @param identifiers pre-release identifier(s) to add * @return this builder + * @since v0.4.0 * @since 2022-02-19 */ public Builder preRelease(String... identifiers) { @@ -205,6 +214,7 @@ public final class SemanticVersionNumber * @param identifier1 first identifier * @param identifier2 second identifier * @return this builder + * @since v0.4.0 * @since 2022-02-19 */ public Builder preRelease(String identifier1, int identifier2) { @@ -270,6 +280,7 @@ public final class SemanticVersionNumber * @param patch patch version number of final version * @return version number builder * @throws IllegalArgumentException if any argument is negative + * @since v0.4.0 * @since 2022-02-19 */ public static final SemanticVersionNumber.Builder builder(int major, @@ -293,6 +304,7 @@ public final class SemanticVersionNumber * @param b second list * @return result of comparison as in a comparator * @see Comparator + * @since v0.4.0 * @since 2022-02-20 */ private static final int compareIdentifiers(List a, List b) { @@ -353,6 +365,7 @@ public final class SemanticVersionNumber * * @param versionString string to parse * @return {@code SemanticVersionNumber} instance + * @since v0.4.0 * @since 2022-02-19 * @see {@link #toString} */ @@ -396,6 +409,7 @@ public final class SemanticVersionNumber * * @param versionString string to test * @return true iff string is valid + * @since v0.4.0 * @since 2022-02-19 */ public static final boolean isValidVersionString(String versionString) { @@ -415,6 +429,7 @@ public final class SemanticVersionNumber * @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 v0.4.0 * @since 2022-02-19 */ public static final SemanticVersionNumber preRelease(int major, int minor, @@ -452,6 +467,7 @@ public final class SemanticVersionNumber * @param patch patch version number * @return {@code SemanticVersionNumber} instance * @throws IllegalArgumentException if any argument is negative + * @since v0.4.0 * @since 2022-02-19 */ public static final SemanticVersionNumber stableVersion(int major, int minor, @@ -484,6 +500,7 @@ public final class SemanticVersionNumber * @param patch patch version number * @param preReleaseIdentifiers pre-release version data * @param buildMetadata build metadata + * @since v0.4.0 * @since 2022-02-19 */ private SemanticVersionNumber(int major, int minor, int patch, @@ -497,6 +514,7 @@ public final class SemanticVersionNumber /** * @return build metadata (empty if there is none) + * @since v0.4.0 * @since 2022-02-19 */ public List buildMetadata() { @@ -567,6 +585,7 @@ public final class SemanticVersionNumber * @param other version to compare with * @return true if you can definitely upgrade to {@code other} without * changing code + * @since v0.4.0 * @since 2022-02-20 */ public boolean compatibleWith(SemanticVersionNumber other) { @@ -620,6 +639,7 @@ public final class SemanticVersionNumber /** * @return true iff this version is stable (major version > 0 and not a * pre-release) + * @since v0.4.0 * @since 2022-02-19 */ public boolean isStable() { @@ -629,6 +649,7 @@ public final class SemanticVersionNumber /** * @return the MAJOR version number, incremented when you make backwards * incompatible API changes + * @since v0.4.0 * @since 2022-02-19 */ public int majorVersion() { @@ -638,6 +659,7 @@ public final class SemanticVersionNumber /** * @return the MINOR version number, incremented when you add backwards * compatible functionality + * @since v0.4.0 * @since 2022-02-19 */ public int minorVersion() { @@ -647,6 +669,7 @@ public final class SemanticVersionNumber /** * @return the PATCH version number, incremented when you make backwards * compatible bug fixes + * @since v0.4.0 * @since 2022-02-19 */ public int patchVersion() { @@ -656,6 +679,7 @@ public final class SemanticVersionNumber /** * @return identifiers describing this pre-release (empty if not a * pre-release) + * @since v0.4.0 * @since 2022-02-19 */ public List preReleaseIdentifiers() { @@ -674,6 +698,7 @@ public final class SemanticVersionNumber * 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". * + * @since v0.4.0 * @see The official SemVer specification */ @Override diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java index 872ca10..5c39788 100644 --- a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java +++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java @@ -20,17 +20,20 @@ package sevenUnitsGUI; * A View that can convert unit expressions * * @author Adrien Hopkins + * @since v0.4.0 * @since 2021-12-15 */ public interface ExpressionConversionView extends View { /** * @return unit expression to convert from + * @since v0.4.0 * @since 2021-12-15 */ String getFromExpression(); /** * @return unit expression to convert to + * @since v0.4.0 * @since 2021-12-15 */ String getToExpression(); @@ -39,6 +42,7 @@ public interface ExpressionConversionView extends View { * Shows the output of an expression conversion to the user. * * @param uc unit conversion to show + * @since v0.4.0 * @since 2021-12-15 */ void showExpressionConversionOutput(UnitConversionRecord uc); diff --git a/src/main/java/sevenUnitsGUI/Main.java b/src/main/java/sevenUnitsGUI/Main.java index b5a896f..998b373 100644 --- a/src/main/java/sevenUnitsGUI/Main.java +++ b/src/main/java/sevenUnitsGUI/Main.java @@ -19,12 +19,16 @@ package sevenUnitsGUI; /** * The main code for the 7Units GUI * + * @since v0.4.0 * @since 2022-04-19 */ public final class Main { /** - * @param args + * The main method that starts 7Units + * + * @param args commandline arguments + * @since v0.4.0 * @since 2022-04-19 */ public static void main(String[] args) { diff --git a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java index 2928082..87f14a8 100644 --- a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java +++ b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java @@ -33,24 +33,31 @@ import sevenUnits.unit.UnitPrefix; * A search rule that applies a certain set of prefixes to a unit. It always * includes the original unit in the output map. * + * @since v0.4.0 * @since 2022-07-06 */ public final class PrefixSearchRule implements Function, Map> { /** * A rule that does not add any prefixed versions of units. + * + * @since v0.4.0 */ public static final PrefixSearchRule NO_PREFIXES = getUniversalRule( Set.of()); /** * A rule that gives every unit a common set of prefixes. + * + * @since v0.4.0 */ public static final PrefixSearchRule COMMON_PREFIXES = getCoherentOnlyRule( Set.of(Metric.MILLI, Metric.KILO)); /** * A rule that gives every unit all metric prefixes. + * + * @since v0.4.0 */ public static final PrefixSearchRule ALL_METRIC_PREFIXES = getCoherentOnlyRule( Metric.ALL_PREFIXES); @@ -62,6 +69,7 @@ public final class PrefixSearchRule implements * * @param prefixes prefixes to apply * @return prefix rule + * @since v0.4.0 * @since 2022-07-06 */ public static final PrefixSearchRule getCoherentOnlyRule( @@ -75,6 +83,7 @@ public final class PrefixSearchRule implements * * @param prefixes prefixes to apply * @return prefix rule + * @since v0.4.0 * @since 2022-07-06 */ public static final PrefixSearchRule getUniversalRule( @@ -95,6 +104,7 @@ public final class PrefixSearchRule implements /** * @param prefixes * @param metricOnly + * @since v0.4.0 * @since 2022-07-06 */ public PrefixSearchRule(Set prefixes, @@ -140,6 +150,7 @@ public final class PrefixSearchRule implements /** * @return the allowUnit + * @since v0.4.0 * @since 2022-07-06 */ public Predicate getAllowUnit() { @@ -148,6 +159,7 @@ public final class PrefixSearchRule implements /** * @return the prefixes that are applied by this rule + * @since v0.4.0 * @since 2022-07-06 */ public Set getPrefixes() { diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java index 0c0ba8e..cc69d31 100644 --- a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -28,12 +28,14 @@ import sevenUnits.utils.UncertainDouble; * A static utility class that can be used to make display rules for the * presenter. * + * @since v0.4.0 * @since 2022-04-18 */ public final class StandardDisplayRules { /** * A rule that rounds to a fixed number of decimal places. * + * @since v0.4.0 * @since 2022-04-18 */ public static final class FixedDecimals @@ -94,6 +96,7 @@ public final class StandardDisplayRules { /** * A rule that rounds to a fixed number of significant digits. * + * @since v0.4.0 * @since 2022-04-18 */ public static final class FixedPrecision @@ -162,6 +165,7 @@ public final class StandardDisplayRules { * This means the output will have around as many significant figures as the * input. * + * @since v0.4.0 * @since 2022-04-18 */ public static final class UncertaintyBased @@ -188,6 +192,7 @@ public final class StandardDisplayRules { /** * @param decimalPlaces decimal places to round to * @return a rounding rule that rounds to fixed number of decimal places + * @since v0.4.0 * @since 2022-04-18 */ public static final FixedDecimals fixedDecimals(int decimalPlaces) { @@ -198,6 +203,7 @@ public final class StandardDisplayRules { * @param significantFigures significant figures to round to * @return a rounding rule that rounds to a fixed number of significant * figures + * @since v0.4.0 * @since 2022-04-18 */ public static final FixedPrecision fixedPrecision(int significantFigures) { @@ -211,6 +217,7 @@ public final class StandardDisplayRules { * @return display rule * @throws IllegalArgumentException if the provided string is not that of a * standard rule. + * @since v0.4.0 * @since 2021-12-24 */ public static final Function getStandardRule( @@ -236,6 +243,7 @@ public final class StandardDisplayRules { /** * @return an UncertainDouble-based rounding rule + * @since v0.4.0 * @since 2022-04-18 */ public static final UncertaintyBased uncertaintyBased() { diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java index f951f44..fa64ee9 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java @@ -24,6 +24,7 @@ import sevenUnits.unit.UnitValue; /** * A record of a conversion between units or expressions * + * @since v0.4.0 * @since 2022-04-09 */ public final class UnitConversionRecord { @@ -33,6 +34,7 @@ public final class UnitConversionRecord { * @param input input unit & value * @param output output unit & value * @return unit conversion record + * @since v0.4.0 * @since 2022-04-09 */ public static UnitConversionRecord fromLinearValues(LinearUnitValue input, @@ -49,6 +51,7 @@ public final class UnitConversionRecord { * @param input input unit & value * @param output output unit & value * @return unit conversion record + * @since v0.4.0 * @since 2022-04-09 */ public static UnitConversionRecord fromValues(UnitValue input, @@ -67,6 +70,7 @@ public final class UnitConversionRecord { * @param inputValueString string representing input value * @param outputValueString string representing output value * @return unit conversion record + * @since v0.4.0 * @since 2022-04-09 */ public static UnitConversionRecord valueOf(String fromName, String toName, @@ -143,6 +147,7 @@ public final class UnitConversionRecord { /** * @return name of unit or expression that was converted from + * @since v0.4.0 * @since 2022-04-09 */ public String fromName() { @@ -166,6 +171,7 @@ public final class UnitConversionRecord { /** * @return string representing input value + * @since v0.4.0 * @since 2022-04-09 */ public String inputValueString() { @@ -174,6 +180,7 @@ public final class UnitConversionRecord { /** * @return string representing output value + * @since v0.4.0 * @since 2022-04-09 */ public String outputValueString() { @@ -182,6 +189,7 @@ public final class UnitConversionRecord { /** * @return name of unit or expression that was converted to + * @since v0.4.0 * @since 2022-04-09 */ public String toName() { diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index 6a95aa5..0d07823 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -23,23 +23,27 @@ import java.util.Set; * A View that supports single unit-based conversion * * @author Adrien Hopkins + * @since v0.4.0 * @since 2021-12-15 */ public interface UnitConversionView extends View { /** * @return dimensions available for filtering + * @since v0.4.0 * @since 2022-01-29 */ Set getDimensionNames(); /** * @return name of unit to convert from + * @since v0.4.0 * @since 2021-12-15 */ Optional getFromSelection(); /** * @return list of names of units available to convert from + * @since v0.4.0 * @since 2022-03-30 */ Set getFromUnitNames(); @@ -47,24 +51,28 @@ public interface UnitConversionView extends View { /** * @return value to convert between the units (specifically, the numeric * string provided by the user) + * @since v0.4.0 * @since 2021-12-15 */ String getInputValue(); /** * @return selected dimension + * @since v0.4.0 * @since 2021-12-15 */ Optional getSelectedDimensionName(); /** * @return name of unit to convert to + * @since v0.4.0 * @since 2021-12-15 */ Optional getToSelection(); /** * @return list of names of units available to convert to + * @since v0.4.0 * @since 2022-03-30 */ Set getToUnitNames(); @@ -73,6 +81,7 @@ public interface UnitConversionView extends View { * Sets the available dimensions for filtering. * * @param dimensionNames names of dimensions to use + * @since v0.4.0 * @since 2021-12-15 */ void setDimensionNames(Set dimensionNames); @@ -83,6 +92,7 @@ public interface UnitConversionView extends View { * that allow the user to select units from a list. * * @param unitNames names of units to convert from + * @since v0.4.0 * @since 2021-12-15 */ void setFromUnitNames(Set unitNames); @@ -93,6 +103,7 @@ public interface UnitConversionView extends View { * that allow the user to select units from a list. * * @param unitNames names of units to convert to + * @since v0.4.0 * @since 2021-12-15 */ void setToUnitNames(Set unitNames); @@ -102,6 +113,7 @@ public interface UnitConversionView extends View { * * @param input input unit & value (obtained from this view) * @param output output unit & value + * @since v0.4.0 * @since 2021-12-24 */ void showUnitConversionOutput(UnitConversionRecord uc); diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java index b2d2b94..bb810ec 100644 --- a/src/main/java/sevenUnitsGUI/View.java +++ b/src/main/java/sevenUnitsGUI/View.java @@ -26,11 +26,13 @@ import sevenUnits.utils.NameSymbol; * An object that controls user interaction with 7Units * * @author Adrien Hopkins + * @since v0.4.0 * @since 2021-12-15 */ public interface View { /** * @return a new tabbed view + * @since v0.4.0 * @since 2022-04-19 */ static View createTabbedView() { @@ -39,18 +41,21 @@ public interface View { /** * @return the presenter associated with this view + * @since v0.4.0 * @since 2022-04-19 */ Presenter getPresenter(); /** * @return name of prefix currently being viewed + * @since v0.4.0 * @since 2022-04-10 */ Optional getViewedPrefixName(); /** * @return name of unit currently being viewed + * @since v0.4.0 * @since 2022-04-10 */ Optional getViewedUnitName(); @@ -60,6 +65,7 @@ public interface View { * viewer * * @param prefixNames prefix names to view + * @since v0.4.0 * @since 2022-04-10 */ void setViewablePrefixNames(Set prefixNames); @@ -68,6 +74,7 @@ public interface View { * Sets the list of units that are available to be viewed in a unit viewer * * @param unitNames unit names to view + * @since v0.4.0 * @since 2022-04-10 */ void setViewableUnitNames(Set unitNames); @@ -78,6 +85,7 @@ public interface View { * @param title title of error message; on any view that uses an error * dialog, this should be the title of the error dialog. * @param message error message + * @since v0.4.0 * @since 2021-12-15 */ void showErrorMessage(String title, String message); @@ -87,6 +95,7 @@ public interface View { * * @param name name(s) and symbol of prefix * @param multiplierString string representation of prefix multiplier + * @since v0.4.0 * @since 2022-04-10 */ void showPrefix(NameSymbol name, String multiplierString); @@ -98,6 +107,7 @@ public interface View { * @param definition unit's definition string * @param dimensionName name of unit's dimension * @param type type of unit (metric/semi-metric/non-metric) + * @since v0.4.0 * @since 2022-04-10 */ void showUnit(NameSymbol name, String definition, String dimensionName, diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index a3ba7a2..e7304c4 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -32,6 +32,7 @@ import sevenUnits.utils.Nameable; * for testing. Getters and setters work as expected. * * @author Adrien Hopkins + * @since v0.4.0 * @since 2022-01-29 */ public final class ViewBot -- cgit v1.2.3