summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/sevenUnits/ProgramInfo.java11
-rw-r--r--src/main/java/sevenUnits/package-info.java4
-rw-r--r--src/main/java/sevenUnits/unit/BaseDimension.java19
-rw-r--r--src/main/java/sevenUnits/unit/BaseUnit.java31
-rw-r--r--src/main/java/sevenUnits/unit/BritishImperial.java27
-rw-r--r--src/main/java/sevenUnits/unit/FunctionalUnit.java22
-rw-r--r--src/main/java/sevenUnits/unit/FunctionalUnitlike.java73
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnit.java115
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnitValue.java174
-rw-r--r--src/main/java/sevenUnits/unit/LoadingException.java122
-rw-r--r--src/main/java/sevenUnits/unit/Metric.java25
-rw-r--r--src/main/java/sevenUnits/unit/MultiUnit.java161
-rw-r--r--src/main/java/sevenUnits/unit/USCustomary.java20
-rw-r--r--src/main/java/sevenUnits/unit/Unit.java131
-rw-r--r--src/main/java/sevenUnits/unit/UnitDatabase.java835
-rw-r--r--src/main/java/sevenUnits/unit/UnitPrefix.java100
-rw-r--r--src/main/java/sevenUnits/unit/UnitType.java16
-rw-r--r--src/main/java/sevenUnits/unit/UnitValue.java74
-rw-r--r--src/main/java/sevenUnits/unit/Unitlike.java262
-rw-r--r--src/main/java/sevenUnits/unit/UnitlikeValue.java176
-rw-r--r--src/main/java/sevenUnits/unit/package-info.java4
-rw-r--r--src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java80
-rw-r--r--src/main/java/sevenUnits/utils/DecimalComparison.java55
-rw-r--r--src/main/java/sevenUnits/utils/ExpressionParser.java217
-rw-r--r--src/main/java/sevenUnits/utils/NameSymbol.java102
-rw-r--r--src/main/java/sevenUnits/utils/Nameable.java13
-rw-r--r--src/main/java/sevenUnits/utils/ObjectProduct.java88
-rw-r--r--src/main/java/sevenUnits/utils/SemanticVersionNumber.java168
-rw-r--r--src/main/java/sevenUnits/utils/UncertainDouble.java251
-rw-r--r--src/main/java/sevenUnits/utils/package-info.java4
-rw-r--r--src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java27
-rw-r--r--src/main/java/sevenUnitsGUI/DelegateListModel.java33
-rw-r--r--src/main/java/sevenUnitsGUI/ExpressionConversionView.java14
-rw-r--r--src/main/java/sevenUnitsGUI/FilterComparator.java25
-rw-r--r--src/main/java/sevenUnitsGUI/GridBagBuilder.java34
-rw-r--r--src/main/java/sevenUnitsGUI/Main.java6
-rw-r--r--src/main/java/sevenUnitsGUI/PrefixSearchRule.java33
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java1209
-rw-r--r--src/main/java/sevenUnitsGUI/SearchBoxList.java54
-rw-r--r--src/main/java/sevenUnitsGUI/StandardDisplayRules.java48
-rw-r--r--src/main/java/sevenUnitsGUI/TabbedView.java257
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionRecord.java45
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionView.java33
-rw-r--r--src/main/java/sevenUnitsGUI/View.java31
-rw-r--r--src/main/java/sevenUnitsGUI/ViewBot.java70
-rw-r--r--src/main/java/sevenUnitsGUI/package-info.java5
-rw-r--r--src/main/resources/about/en.txt (renamed from src/main/resources/about.txt)8
-rw-r--r--src/main/resources/about/fr.txt24
-rw-r--r--src/main/resources/locales/en.txt31
-rw-r--r--src/main/resources/locales/fr.txt31
-rw-r--r--src/main/resources/metric_exceptions.txt1
-rw-r--r--src/main/resources/unitsfile.txt10
-rw-r--r--src/test/java/sevenUnits/unit/MultiUnitTest.java110
-rw-r--r--src/test/java/sevenUnits/unit/UnitDatabaseTest.java324
-rw-r--r--src/test/java/sevenUnits/unit/UnitTest.java135
-rw-r--r--src/test/java/sevenUnits/unit/UnitValueTest.java240
-rw-r--r--src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java63
-rw-r--r--src/test/java/sevenUnits/utils/ExpressionParserTest.java91
-rw-r--r--src/test/java/sevenUnits/utils/NameSymbolTest.java108
-rw-r--r--src/test/java/sevenUnits/utils/ObjectProductTest.java10
-rw-r--r--src/test/java/sevenUnits/utils/SemanticVersionTest.java93
-rw-r--r--src/test/java/sevenUnits/utils/UncertainDoubleTest.java39
-rw-r--r--src/test/java/sevenUnitsGUI/I18nTest.java95
-rw-r--r--src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java20
-rw-r--r--src/test/java/sevenUnitsGUI/PrefixSearchTest.java36
-rw-r--r--src/test/java/sevenUnitsGUI/PresenterTest.java159
-rw-r--r--src/test/java/sevenUnitsGUI/RoundingTest.java35
-rw-r--r--src/test/java/sevenUnitsGUI/TabbedViewTest.java22
68 files changed, 3955 insertions, 3034 deletions
diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java
index 9c23c49..e74a99b 100644
--- a/src/main/java/sevenUnits/ProgramInfo.java
+++ b/src/main/java/sevenUnits/ProgramInfo.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2021-2024 Adrien Hopkins
+ * Copyright (C) 2021-2025 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
@@ -20,15 +20,14 @@ import sevenUnits.utils.SemanticVersionNumber;
/**
* Information about 7Units
- *
- * @since 0.3.1
+ *
* @since 2021-06-28
+ * @since v0.3.1
*/
public final class ProgramInfo {
-
- /** The version number (0.5.0) */
+ /** The version number (1.0.0) */
public static final SemanticVersionNumber VERSION = SemanticVersionNumber
- .stableVersion(0, 5, 0);
+ .stableVersion(1, 0, 0);
private ProgramInfo() {
// this class is only for static variables, you shouldn't be able to
diff --git a/src/main/java/sevenUnits/package-info.java b/src/main/java/sevenUnits/package-info.java
index 33b98fc..dc27e1f 100644
--- a/src/main/java/sevenUnits/package-info.java
+++ b/src/main/java/sevenUnits/package-info.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019-2021 Adrien Hopkins
+ * Copyright (C) 2019-2025 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,7 +16,7 @@
*/
/**
* A program that converts units.
- *
+ *
* @author Adrien Hopkins
* @version v0.3.0
* @since 2019-01-25
diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java
index 3f1f75f..11b822e 100644
--- a/src/main/java/sevenUnits/unit/BaseDimension.java
+++ b/src/main/java/sevenUnits/unit/BaseDimension.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019, 2022 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -23,26 +23,26 @@ import sevenUnits.utils.Nameable;
/**
* A dimension that defines a {@code BaseUnit}
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
+ * @since v0.3.0
*/
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
* @return dimension
* @since 2019-10-16
+ * @since v0.3.0
*/
public static BaseDimension valueOf(final String name, final String symbol) {
return new BaseDimension(name, symbol);
}
- /**
- * The name of the dimension.
- */
+ /** The name of the dimension. */
private final String name;
/**
* The symbol used by the dimension. Symbols should be short, generally one
@@ -52,20 +52,19 @@ public final class BaseDimension implements Nameable {
/**
* Creates the {@code BaseDimension}.
- *
+ *
* @param name name of unit
* @param symbol symbol of unit
* @throws NullPointerException if any argument is null
* @since 2019-10-16
+ * @since v0.3.0
*/
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.");
}
- /**
- * @since v0.4.0
- */
+ /** @since v0.4.0 */
@Override
public NameSymbol getNameSymbol() {
return NameSymbol.of(this.name, this.symbol);
diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java
index fe85a7b..13e76d9 100644
--- a/src/main/java/sevenUnits/unit/BaseUnit.java
+++ b/src/main/java/sevenUnits/unit/BaseUnit.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -28,19 +28,21 @@ import sevenUnits.utils.NameSymbol;
* Note that BaseUnits <b>must</b> have names and symbols. This is because they
* are used for toString code. Therefore, the Optionals provided by
* {@link #getPrimaryName} and {@link #getSymbol} will always contain a value.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
+ * @since v0.3.0
*/
public final class BaseUnit extends Unit {
/**
* Gets a base unit from the dimension it measures, its name and its symbol.
- *
+ *
* @param dimension dimension measured by this unit
* @param name name of unit
* @param symbol symbol of unit
* @return base unit
* @since 2019-10-16
+ * @since v0.3.0
*/
public static BaseUnit valueOf(final BaseDimension dimension,
final String name, final String symbol) {
@@ -49,31 +51,32 @@ public final class BaseUnit extends Unit {
/**
* Gets a base unit from the dimension it measures, its name and its symbol.
- *
- * @param dimension dimension measured by this unit
- * @param name name of unit
- * @param symbol symbol of unit
+ *
+ * @param dimension dimension measured by this unit
+ * @param name name of unit
+ * @param symbol symbol of unit
+ * @param otherNames other possible names of unit
* @return base unit
* @since 2019-10-21
+ * @since v0.3.0
*/
public static BaseUnit valueOf(final BaseDimension dimension,
final String name, final String symbol, final Set<String> otherNames) {
return new BaseUnit(dimension, name, symbol, otherNames);
}
- /**
- * The dimension measured by this base unit.
- */
+ /** The dimension measured by this base unit. */
private final BaseDimension dimension;
/**
* Creates the {@code BaseUnit}.
- *
+ *
* @param dimension dimension of unit
* @param primaryName name of unit
* @param symbol symbol of unit
* @throws NullPointerException if any argument is null
* @since 2019-10-16
+ * @since v0.3.0
*/
private BaseUnit(final BaseDimension dimension, final String primaryName,
final String symbol, final Set<String> otherNames) {
@@ -86,9 +89,10 @@ public final class BaseUnit extends Unit {
* Returns a {@code LinearUnit} with this unit as a base and a conversion
* factor of 1. This operation must be done in order to allow units to be
* created with operations.
- *
+ *
* @return this unit as a {@code LinearUnit}
* @since 2019-10-16
+ * @since v0.3.0
*/
public LinearUnit asLinearUnit() {
return LinearUnit.valueOf(this.getBase(), 1);
@@ -107,8 +111,9 @@ public final class BaseUnit extends Unit {
/**
* @return dimension
* @since 2019-10-16
+ * @since v0.3.0
*/
- public final BaseDimension getBaseDimension() {
+ public BaseDimension getBaseDimension() {
return this.dimension;
}
diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java
index 69a3c05..408e9e8 100644
--- a/src/main/java/sevenUnits/unit/BritishImperial.java
+++ b/src/main/java/sevenUnits/unit/BritishImperial.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -20,16 +20,21 @@ import sevenUnits.utils.NameSymbol;
/**
* A static utility class that contains units in the British Imperial system.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-21
+ * @since v0.3.0
*/
+// this class is just constants, most of which are obvious from the variable name
+// so no need to check for missing values
+@SuppressWarnings("javadoc")
public final class BritishImperial {
/**
* Imperial units that measure area
- *
+ *
* @author Adrien Hopkins
* @since 2019-11-08
+ * @since v0.3.0
*/
public static final class Area {
public static final LinearUnit SQUARE_FOOT = Length.FOOT.toExponent(2);
@@ -42,9 +47,10 @@ public final class BritishImperial {
/**
* Imperial units that measure length
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-28
+ * @since v0.3.0
*/
public static final class Length {
/**
@@ -55,11 +61,18 @@ public final class BritishImperial {
public static final LinearUnit FOOT = YARD.dividedBy(3);
public static final LinearUnit INCH = FOOT.dividedBy(12);
public static final LinearUnit THOU = INCH.dividedBy(1000);
+ /** A chain, equal to 22 yards. */
public static final LinearUnit CHAIN = YARD.times(22);
+ /** A furlong, equal to 10 chains or 220 yards. */
public static final LinearUnit FURLONG = CHAIN.times(10);
+ /** A mile, equal to 8 furlongs or 1760 yards. */
public static final LinearUnit MILE = FURLONG.times(8);
+ /** A league, equal to 3 miles. */
public static final LinearUnit LEAGUE = MILE.times(3);
+ /**
+ * A nautical mile, around 1 arcminute around the Earth's circumference.
+ */
public static final LinearUnit NAUTICAL_MILE = Metric.METRE.times(1852);
public static final LinearUnit CABLE = NAUTICAL_MILE.dividedBy(10);
public static final LinearUnit FATHOM = CABLE.dividedBy(100);
@@ -70,9 +83,10 @@ public final class BritishImperial {
/**
* British Imperial units that measure mass.
- *
+ *
* @author Adrien Hopkins
* @since 2019-11-08
+ * @since v0.3.0
*/
public static final class Mass {
public static final LinearUnit POUND = Metric.GRAM.times(453.59237);
@@ -88,9 +102,10 @@ public final class BritishImperial {
/**
* British Imperial units that measure volume
- *
+ *
* @author Adrien Hopkins
* @since 2019-11-08
+ * @since v0.3.0
*/
public static final class Volume {
public static final LinearUnit FLUID_OUNCE = Metric.LITRE
diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java
index 6de446f..1d55b42 100644
--- a/src/main/java/sevenUnits/unit/FunctionalUnit.java
+++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -24,30 +24,33 @@ import sevenUnits.utils.ObjectProduct;
/**
* A unit that uses functional objects to convert to and from its base.
- *
+ *
* @author Adrien Hopkins
* @since 2019-05-22
+ * @since v0.3.0
*/
final class FunctionalUnit extends Unit {
/**
* A function that accepts a value expressed in the unit's base and returns
* that value expressed in this unit.
- *
+ *
* @since 2019-05-22
+ * @since v0.3.0
*/
private final DoubleUnaryOperator converterFrom;
/**
* A function that accepts a value expressed in the unit and returns that
* value expressed in the unit's base.
- *
+ *
* @since 2019-05-22
+ * @since v0.3.0
*/
private final DoubleUnaryOperator converterTo;
/**
* Creates the {@code FunctionalUnit}.
- *
+ *
* @param base unit's base
* @param converterFrom function that accepts a value expressed in the unit's
* base and returns that value expressed in this unit.
@@ -55,6 +58,7 @@ final class FunctionalUnit extends Unit {
* and returns that value expressed in the unit's base.
* @throws NullPointerException if any argument is null
* @since 2019-05-22
+ * @since v0.3.0
*/
public FunctionalUnit(final ObjectProduct<BaseUnit> base,
final DoubleUnaryOperator converterFrom,
@@ -68,14 +72,16 @@ final class FunctionalUnit extends Unit {
/**
* Creates the {@code FunctionalUnit}.
- *
+ *
* @param base unit's base
* @param converterFrom function that accepts a value expressed in the unit's
* base and returns that value expressed in this unit.
* @param converterTo function that accepts a value expressed in the unit
* and returns that value expressed in the unit's base.
+ * @param ns name and symbol of resulting unit
* @throws NullPointerException if any argument is null
* @since 2019-05-22
+ * @since v0.3.0
*/
public FunctionalUnit(final ObjectProduct<BaseUnit> base,
final DoubleUnaryOperator converterFrom,
@@ -89,7 +95,7 @@ final class FunctionalUnit extends Unit {
/**
* {@inheritDoc}
- *
+ *
* Uses {@code converterFrom} to convert.
*/
@Override
@@ -99,7 +105,7 @@ final class FunctionalUnit extends Unit {
/**
* {@inheritDoc}
- *
+ *
* Uses {@code converterTo} to convert.
*/
@Override
diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
deleted file mode 100644
index e9b4d1f..0000000
--- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
+++ /dev/null
@@ -1,73 +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 <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.unit;
-
-import java.util.function.DoubleFunction;
-import java.util.function.ToDoubleFunction;
-
-import sevenUnits.utils.NameSymbol;
-import sevenUnits.utils.ObjectProduct;
-
-/**
- * A unitlike form that converts using two conversion functions.
- *
- * @since 2020-09-07
- */
-final class FunctionalUnitlike<V> extends Unitlike<V> {
- /**
- * A function that accepts a value in the unitlike form's base and returns a
- * value in the unitlike form.
- *
- * @since 2020-09-07
- */
- private final DoubleFunction<V> converterFrom;
-
- /**
- * A function that accepts a value in the unitlike form and returns a value
- * in the unitlike form's base.
- */
- private final ToDoubleFunction<V> converterTo;
-
- /**
- * Creates the {@code FunctionalUnitlike}.
- *
- * @param base unitlike form's base
- * @param converterFrom function that accepts a value in the unitlike form's
- * base and returns a value in the unitlike form.
- * @param converterTo function that accepts a value in the unitlike form
- * and returns a value in the unitlike form's base.
- * @throws NullPointerException if any argument is null
- * @since 2019-05-22
- */
- protected FunctionalUnitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns,
- DoubleFunction<V> converterFrom, ToDoubleFunction<V> converterTo) {
- super(unitBase, ns);
- this.converterFrom = converterFrom;
- this.converterTo = converterTo;
- }
-
- @Override
- protected V convertFromBase(double value) {
- return this.converterFrom.apply(value);
- }
-
- @Override
- protected double convertToBase(V value) {
- return this.converterTo.applyAsDouble(value);
- }
-
-}
diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java
index 6489229..85f6dd9 100644
--- a/src/main/java/sevenUnits/unit/LinearUnit.java
+++ b/src/main/java/sevenUnits/unit/LinearUnit.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -26,19 +26,21 @@ import sevenUnits.utils.UncertainDouble;
/**
* A unit that can be expressed as a product of its base and a number. For
* example, kilometres, inches and pounds.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
+ * @since v0.3.0
*/
public final class LinearUnit extends Unit {
/**
* Gets a {@code LinearUnit} from a unit and a value. For example, converts
* '59 °F' to a linear unit with the value of '288.15 K'
- *
+ *
* @param unit unit to convert
* @param value value to convert
* @return value expressed as a {@code LinearUnit}
* @since 2019-10-16
+ * @since v0.3.0
* @throws NullPointerException if unit is null
*/
public static LinearUnit fromUnitValue(final Unit unit, final double value) {
@@ -50,12 +52,13 @@ public final class LinearUnit extends Unit {
/**
* Gets a {@code LinearUnit} from a unit and a value. For example, converts
* '59 °F' to a linear unit with the value of '288.15 K'
- *
+ *
* @param unit unit to convert
* @param value value to convert
* @param ns name(s) and symbol of unit
* @return value expressed as a {@code LinearUnit}
* @since 2019-10-21
+ * @since v0.3.0
* @throws NullPointerException if unit or ns is null
*/
public static LinearUnit fromUnitValue(final Unit unit, final double value,
@@ -66,32 +69,26 @@ public final class LinearUnit extends Unit {
}
/**
+ * @param unit unit to get base version of
* @return the base unit associated with {@code unit}, as a
* {@code LinearUnit}.
* @since 2020-10-02
+ * @since v0.3.0
*/
public static LinearUnit getBase(final Unit unit) {
return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY);
}
/**
- * @return the base unit associated with {@code unitlike}, as a
- * {@code LinearUnit}.
- * @since 2020-10-02
- */
- public static LinearUnit getBase(final Unitlike<?> unit) {
- return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY);
- }
-
- /**
* Gets a {@code LinearUnit} from a unit base and a conversion factor. In
* other words, gets the product of {@code unitBase} and
* {@code conversionFactor}, expressed as a {@code LinearUnit}.
- *
+ *
* @param unitBase unit base to multiply by
* @param conversionFactor number to multiply base by
* @return product of base and conversion factor
* @since 2019-10-16
+ * @since v0.3.0
* @throws NullPointerException if unitBase is null
*/
public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase,
@@ -103,12 +100,13 @@ public final class LinearUnit extends Unit {
* Gets a {@code LinearUnit} from a unit base and a conversion factor. In
* other words, gets the product of {@code unitBase} and
* {@code conversionFactor}, expressed as a {@code LinearUnit}.
- *
+ *
* @param unitBase unit base to multiply by
* @param conversionFactor number to multiply base by
* @param ns name(s) and symbol of unit
* @return product of base and conversion factor
* @since 2019-10-21
+ * @since v0.3.0
* @throws NullPointerException if unitBase is null
*/
public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase,
@@ -118,21 +116,23 @@ public final class LinearUnit extends Unit {
/**
* The value of this unit as represented in its base form. Mathematically,
- *
+ *
* <pre>
* this = conversionFactor * getBase()
* </pre>
- *
+ *
* @since 2019-10-16
+ * @since v0.3.0
*/
private final double conversionFactor;
/**
* Creates the {@code LinearUnit}.
- *
+ *
* @param unitBase base of linear unit
* @param conversionFactor conversion factor between base and unit
* @since 2019-10-16
+ * @since v0.3.0
*/
private LinearUnit(final ObjectProduct<BaseUnit> unitBase,
final double conversionFactor, final NameSymbol ns) {
@@ -142,7 +142,7 @@ public final class LinearUnit extends Unit {
/**
* {@inheritDoc}
- *
+ *
* Converts by dividing by {@code conversionFactor}
*/
@Override
@@ -153,11 +153,12 @@ public final class LinearUnit extends Unit {
/**
* Converts an {@code UncertainDouble} value expressed in this unit to an
* {@code UncertainValue} value expressed in {@code other}.
- *
+ *
* @param other unit to convert to
* @param value value to convert
* @return converted value
* @since 2019-09-07
+ * @since v0.3.0
* @throws IllegalArgumentException if {@code other} is incompatible for
* conversion with this unit (as tested by
* {@link Unit#canConvertTo}).
@@ -169,15 +170,14 @@ public final class LinearUnit extends Unit {
if (this.canConvertTo(other))
return value.timesExact(
this.getConversionFactor() / other.getConversionFactor());
- else
- throw new IllegalArgumentException(
- String.format("Cannot convert from %s to %s.", this, other));
+ throw new IllegalArgumentException(
+ String.format("Cannot convert from %s to %s.", this, other));
}
/**
* {@inheritDoc}
- *
+ *
* Converts by multiplying by {@code conversionFactor}
*/
@Override
@@ -187,8 +187,9 @@ public final class LinearUnit extends Unit {
/**
* Converts an {@code UncertainDouble} to the base unit.
- *
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
UncertainDouble convertToBase(final UncertainDouble value) {
return value.timesExact(this.getConversionFactor());
@@ -196,7 +197,7 @@ public final class LinearUnit extends Unit {
/**
* Divides this unit by a scalar.
- *
+ *
* @param divisor scalar to divide by
* @return quotient
* @since 2018-12-23
@@ -208,7 +209,7 @@ public final class LinearUnit extends Unit {
/**
* Returns the quotient of this unit and another.
- *
+ *
* @param divisor unit to divide by
* @return quotient of two units
* @throws NullPointerException if {@code divisor} is null
@@ -219,22 +220,35 @@ public final class LinearUnit extends Unit {
Objects.requireNonNull(divisor, "other must not be null");
// divide the units
- final ObjectProduct<BaseUnit> base = this.getBase()
- .dividedBy(divisor.getBase());
+ final var base = this.getBase().dividedBy(divisor.getBase());
return valueOf(base,
this.getConversionFactor() / divisor.getConversionFactor());
}
/**
* {@inheritDoc}
- *
+ *
* Uses the base and conversion factor of units to test for equality.
*/
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof LinearUnit))
return false;
- final LinearUnit other = (LinearUnit) obj;
+ final var other = (LinearUnit) obj;
+ return Objects.equals(this.getBase(), other.getBase())
+ && Double.compare(this.getConversionFactor(),
+ other.getConversionFactor()) == 0;
+ }
+
+ /**
+ * @param other unit to test equality with
+ * @return true iff this unit and other are equal, ignoring small differences
+ * caused by floating-point error.
+ *
+ * @apiNote This method is not transitive, so it cannot be used as an equals
+ * method.
+ */
+ public boolean equalsApproximately(final LinearUnit other) {
return Objects.equals(this.getBase(), other.getBase())
&& DecimalComparison.equals(this.getConversionFactor(),
other.getConversionFactor());
@@ -243,6 +257,7 @@ public final class LinearUnit extends Unit {
/**
* @return conversion factor
* @since 2019-10-16
+ * @since v0.3.0
*/
public double getConversionFactor() {
return this.conversionFactor;
@@ -250,13 +265,13 @@ public final class LinearUnit extends Unit {
/**
* {@inheritDoc}
- *
+ *
* Uses the base and conversion factor to compute a hash code.
*/
@Override
public int hashCode() {
return 31 * this.getBase().hashCode()
- + DecimalComparison.hash(this.getConversionFactor());
+ + Double.hashCode(this.getConversionFactor());
}
/**
@@ -264,6 +279,7 @@ public final class LinearUnit extends Unit {
* is a {@code BaseUnit b} where
* {@code b.asLinearUnit().equals(this)} returns {@code true}.)
* @since 2019-10-16
+ * @since v0.3.0
*/
public boolean isBase() {
return this.isCoherent() && this.getBase().isSingleObject();
@@ -272,6 +288,7 @@ public final class LinearUnit extends Unit {
/**
* @return whether this unit is coherent (i.e. has conversion factor 1)
* @since 2019-10-16
+ * @since v0.3.0
*/
public boolean isCoherent() {
return this.getConversionFactor() == 1;
@@ -285,7 +302,7 @@ public final class LinearUnit extends Unit {
* does not meet this condition, an {@code IllegalArgumentException} will be
* thrown.
* </p>
- *
+ *
* @param subtrahend unit to subtract
* @return difference of units
* @throws IllegalArgumentException if {@code subtrahend} is not compatible
@@ -316,7 +333,7 @@ public final class LinearUnit extends Unit {
* does not meet this condition, an {@code IllegalArgumentException} will be
* thrown.
* </p>
- *
+ *
* @param addend unit to add
* @return sum of units
* @throws IllegalArgumentException if {@code addend} is not compatible for
@@ -341,7 +358,7 @@ public final class LinearUnit extends Unit {
/**
* Multiplies this unit by a scalar.
- *
+ *
* @param multiplier scalar to multiply by
* @return product
* @since 2018-12-23
@@ -353,7 +370,7 @@ public final class LinearUnit extends Unit {
/**
* Returns the product of this unit and another.
- *
+ *
* @param multiplier unit to multiply by
* @return product of two units
* @throws NullPointerException if {@code multiplier} is null
@@ -364,8 +381,7 @@ public final class LinearUnit extends Unit {
Objects.requireNonNull(multiplier, "other must not be null");
// multiply the units
- final ObjectProduct<BaseUnit> base = this.getBase()
- .times(multiplier.getBase());
+ final var base = this.getBase().times(multiplier.getBase());
return valueOf(base,
this.getConversionFactor() * multiplier.getConversionFactor());
}
@@ -379,7 +395,7 @@ public final class LinearUnit extends Unit {
/**
* Returns this unit but to an exponent.
- *
+ *
* @param exponent exponent to exponentiate unit to
* @return exponentiated unit
* @since 2019-01-15
@@ -390,6 +406,21 @@ public final class LinearUnit extends Unit {
Math.pow(this.conversionFactor, exponent));
}
+ /**
+ * Returns this unit to an exponent, rounding the resulting dimensions to the
+ * nearest integer.
+ *
+ * @param exponent exponent to raise unit to
+ * @return result of rounded exponentation
+ * @since 2024-08-22
+ * @since v1.0.0
+ * @see ObjectProduct#toExponentRounded
+ */
+ public LinearUnit toExponentRounded(final double exponent) {
+ return valueOf(this.getBase().toExponentRounded(exponent),
+ Math.pow(this.conversionFactor, exponent));
+ }
+
@Override
public LinearUnit withName(final NameSymbol ns) {
return valueOf(this.getBase(), this.getConversionFactor(), ns);
@@ -404,7 +435,7 @@ public final class LinearUnit extends Unit {
* have a symbol. <br>
* This method ignores alternate names of both this unit and the provided
* prefix.
- *
+ *
* @param prefix prefix to apply
* @return unit with prefix
* @since 2019-03-18
@@ -412,7 +443,7 @@ public final class LinearUnit extends Unit {
* @throws NullPointerException if prefix is null
*/
public LinearUnit withPrefix(final UnitPrefix prefix) {
- final LinearUnit unit = this.times(prefix.getMultiplier());
+ final var unit = this.times(prefix.getMultiplier());
// create new name and symbol, if possible
final String name;
diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java
index 3a9428b..ce60e3b 100644
--- a/src/main/java/sevenUnits/unit/LinearUnitValue.java
+++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -17,33 +17,38 @@
package sevenUnits.unit;
import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
-import java.util.Optional;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.ObjectProduct;
import sevenUnits.utils.UncertainDouble;
/**
* A possibly uncertain value expressed in a linear unit.
- *
+ *
* Unless otherwise indicated, all methods in this class throw a
* {@code NullPointerException} when an argument is null.
- *
+ *
* @author Adrien Hopkins
* @since 2020-07-26
+ * @since v0.3.0
*/
public final class LinearUnitValue {
+ /** The value 1 as a LinearUnitValue. */
public static final LinearUnitValue ONE = getExact(Metric.ONE, 1);
/**
* Gets an exact {@code LinearUnitValue}
- *
+ *
* @param unit unit to express with
* @param value value to express
* @return exact {@code LinearUnitValue} instance
* @since 2020-07-26
+ * @since v0.3.0
*/
- public static final LinearUnitValue getExact(final LinearUnit unit,
+ public static LinearUnitValue getExact(final LinearUnit unit,
final double value) {
return new LinearUnitValue(
Objects.requireNonNull(unit, "unit must not be null"),
@@ -52,14 +57,14 @@ public final class LinearUnitValue {
/**
* Gets an uncertain {@code LinearUnitValue}
- *
- * @param unit unit to express with
- * @param value value to express
- * @param uncertainty absolute uncertainty of value
+ *
+ * @param unit unit to express with
+ * @param value value to express
* @return uncertain {@code LinearUnitValue} instance
* @since 2020-07-26
+ * @since v0.3.0
*/
- public static final LinearUnitValue of(final LinearUnit unit,
+ public static LinearUnitValue of(final LinearUnit unit,
final UncertainDouble value) {
return new LinearUnitValue(
Objects.requireNonNull(unit, "unit must not be null"),
@@ -74,6 +79,7 @@ public final class LinearUnitValue {
* @param unit unit to express as
* @param value value to express
* @since 2020-07-26
+ * @since v0.3.0
*/
private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) {
this.unit = unit;
@@ -84,8 +90,9 @@ public final class LinearUnitValue {
* @return this value as a {@code UnitValue}. All uncertainty information is
* removed from the returned value.
* @since 2020-08-04
+ * @since v0.3.0
*/
- public final UnitValue asUnitValue() {
+ public UnitValue asUnitValue() {
return UnitValue.of(this.unit, this.value.value());
}
@@ -93,29 +100,68 @@ public final class LinearUnitValue {
* @param other a {@code LinearUnit}
* @return true iff this value can be represented with {@code other}.
* @since 2020-07-26
+ * @since v0.3.0
*/
- public final boolean canConvertTo(final LinearUnit other) {
+ public boolean canConvertTo(final LinearUnit other) {
return this.unit.canConvertTo(other);
}
/**
* Returns a LinearUnitValue that represents the same value expressed in a
* different unit
- *
+ *
* @param other new unit to express value in
* @return value expressed in {@code other}
* @since 2020-07-26
+ * @since v0.3.0
*/
- public final LinearUnitValue convertTo(final LinearUnit other) {
+ public LinearUnitValue convertTo(final LinearUnit other) {
return LinearUnitValue.of(other, this.unit.convertTo(other, this.value));
}
/**
+ * Convert a LinearUnitValue to a sum of multiple units, where all but the
+ * last have exact integer values.
+ *
+ * @param others units to convert to
+ * @return terms of the sum
+ * @throws IllegalArgumentException if no units are provided or units
+ * provided have incompatible bases
+ * @since 2024-08-15
+ * @since v1.0.0
+ */
+ public List<LinearUnitValue> convertToMultiple(
+ final List<LinearUnit> others) {
+ if (others.size() < 1)
+ throw new IllegalArgumentException("Must have at least one unit");
+ for (final LinearUnit unit : others) {
+ if (!Objects.equals(this.unit.getBase(), unit.getBase()))
+ throw new IllegalArgumentException(
+ "All provided units must have the same base as the value.");
+ }
+
+ var remaining = this;
+ final List<LinearUnitValue> values = new ArrayList<>(others.size());
+ for (final LinearUnit unit : others.subList(0, others.size() - 1)) {
+ final var remainingInUnit = remaining.convertTo(unit);
+ final var value = getExact(unit,
+ Math.floor(remainingInUnit.getValueExact() + 1e-12));
+ values.add(value);
+ remaining = remaining.minus(value);
+ }
+
+ final var lastValue = remaining.convertTo(others.get(others.size() - 1));
+ values.add(lastValue);
+ return values;
+ }
+
+ /**
* Divides this value by a scalar
- *
+ *
* @param divisor value to divide by
* @return multiplied value
* @since 2020-07-28
+ * @since v0.3.0
*/
public LinearUnitValue dividedBy(final double divisor) {
return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor));
@@ -123,10 +169,11 @@ public final class LinearUnitValue {
/**
* Divides this value by another value
- *
+ *
* @param divisor value to multiply by
* @return quotient
* @since 2020-07-28
+ * @since v0.3.0
*/
public LinearUnitValue dividedBy(final LinearUnitValue divisor) {
return LinearUnitValue.of(this.unit.dividedBy(divisor.unit),
@@ -137,15 +184,16 @@ public final class LinearUnitValue {
* Returns true if this and obj represent the same value, regardless of
* whether or not they are expressed in the same unit. So (1000 m).equals(1
* km) returns true.
- *
+ *
* @since 2020-07-26
+ * @since v0.3.0
* @see #equals(Object, boolean)
*/
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof LinearUnitValue))
return false;
- final LinearUnitValue other = (LinearUnitValue) obj;
+ final var other = (LinearUnitValue) obj;
return Objects.equals(this.unit.getBase(), other.unit.getBase())
&& this.unit.convertToBase(this.value)
.equals(other.unit.convertToBase(other.value));
@@ -155,18 +203,22 @@ public final class LinearUnitValue {
* Returns true if this and obj represent the same value, regardless of
* whether or not they are expressed in the same unit. So (1000 m).equals(1
* km) returns true.
- * <p>
- * If avoidFPErrors is true, this method will attempt to avoid floating-point
- * errors, at the cost of not always being transitive.
- *
+ *
+ * @param obj object to test equality with
+ * @param avoidFPErrors if true, this method will attempt to avoid
+ * floating-point errors, at the cost of not always
+ * being transitive.
+ * @return true iff this and obj are equal
+ *
* @since 2020-07-28
+ * @since v0.3.0
*/
public boolean equals(final Object obj, final boolean avoidFPErrors) {
if (!avoidFPErrors)
return this.equals(obj);
if (!(obj instanceof LinearUnitValue))
return false;
- final LinearUnitValue other = (LinearUnitValue) obj;
+ final var other = (LinearUnitValue) obj;
return Objects.equals(this.unit.getBase(), other.unit.getBase())
&& DecimalComparison.equals(this.unit.convertToBase(this.value),
other.unit.convertToBase(other.value));
@@ -175,16 +227,17 @@ public final class LinearUnitValue {
/**
* @param other another {@code LinearUnitValue}
* @return true iff this and other are within each other's uncertainty range
- *
+ *
* @since 2020-07-26
+ * @since v0.3.0
*/
public boolean equivalent(final LinearUnitValue other) {
if (other == null
|| !Objects.equals(this.unit.getBase(), other.unit.getBase()))
return false;
- final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1);
- final LinearUnitValue thisBase = this.convertTo(base);
- final LinearUnitValue otherBase = other.convertTo(base);
+ final var base = LinearUnit.valueOf(this.unit.getBase(), 1);
+ final var thisBase = this.convertTo(base);
+ final var otherBase = other.convertTo(base);
return thisBase.value.equivalent(otherBase.value);
}
@@ -192,24 +245,27 @@ public final class LinearUnitValue {
/**
* @return the unit
* @since 2020-09-29
+ * @since v0.3.0
*/
- public final LinearUnit getUnit() {
+ public LinearUnit getUnit() {
return this.unit;
}
/**
* @return the value
* @since 2020-09-29
+ * @since v0.3.0
*/
- public final UncertainDouble getValue() {
+ public UncertainDouble getValue() {
return this.value;
}
/**
* @return the exact value
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final double getValueExact() {
+ public double getValueExact() {
return this.value.value();
}
@@ -222,12 +278,13 @@ public final class LinearUnitValue {
/**
* Returns the difference of this value and another, expressed in this
* value's unit
- *
+ *
* @param subtrahend value to subtract
* @return difference of values
* @throws IllegalArgumentException if {@code subtrahend} has a unit that is
* not compatible for addition
* @since 2020-07-26
+ * @since v0.3.0
*/
public LinearUnitValue minus(final LinearUnitValue subtrahend) {
Objects.requireNonNull(subtrahend, "subtrahend may not be null");
@@ -237,19 +294,20 @@ public final class LinearUnitValue {
"Incompatible units for subtraction \"%s\" and \"%s\".",
this.unit, subtrahend.unit));
- final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit);
+ final var otherConverted = subtrahend.convertTo(this.unit);
return LinearUnitValue.of(this.unit,
this.value.minus(otherConverted.value));
}
/**
* Returns the sum of this value and another, expressed in this value's unit
- *
+ *
* @param addend value to add
* @return sum of values
* @throws IllegalArgumentException if {@code addend} has a unit that is not
* compatible for addition
* @since 2020-07-26
+ * @since v0.3.0
*/
public LinearUnitValue plus(final LinearUnitValue addend) {
Objects.requireNonNull(addend, "addend may not be null");
@@ -259,17 +317,18 @@ public final class LinearUnitValue {
"Incompatible units for addition \"%s\" and \"%s\".", this.unit,
addend.unit));
- final LinearUnitValue otherConverted = addend.convertTo(this.unit);
+ final var otherConverted = addend.convertTo(this.unit);
return LinearUnitValue.of(this.unit,
this.value.plus(otherConverted.value));
}
/**
* Multiplies this value by a scalar
- *
+ *
* @param multiplier value to multiply by
* @return multiplied value
* @since 2020-07-28
+ * @since v0.3.0
*/
public LinearUnitValue times(final double multiplier) {
return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier));
@@ -277,10 +336,11 @@ public final class LinearUnitValue {
/**
* Multiplies this value by another value
- *
+ *
* @param multiplier value to multiply by
* @return product
* @since 2020-07-28
+ * @since v0.3.0
*/
public LinearUnitValue times(final LinearUnitValue multiplier) {
return LinearUnitValue.of(this.unit.times(multiplier.unit),
@@ -289,16 +349,32 @@ public final class LinearUnitValue {
/**
* Raises a value to an exponent
- *
+ *
* @param exponent exponent to raise to
* @return result of exponentiation
* @since 2020-07-28
+ * @since v0.3.0
*/
public LinearUnitValue toExponent(final int exponent) {
return LinearUnitValue.of(this.unit.toExponent(exponent),
this.value.toExponentExact(exponent));
}
+ /**
+ * Raises this value to an exponent, rounding all dimensions to integers.
+ *
+ * @param exponent exponent to raise this value to
+ * @return result of exponentation
+ *
+ * @since 2024-08-22
+ * @since v1.0.0
+ * @see ObjectProduct#toExponentRounded
+ */
+ public LinearUnitValue toExponentRounded(final double exponent) {
+ return LinearUnitValue.of(this.unit.toExponentRounded(exponent),
+ this.value.toExponentExact(exponent));
+ }
+
@Override
public String toString() {
return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN);
@@ -313,23 +389,28 @@ public final class LinearUnitValue {
* single numbers.
* <p>
* Non-exact values are rounded intelligently based on their uncertainty.
- *
+ *
+ * @param showUncertainty whether to show the value's uncertainty
+ * @param roundingMode how to round numbers in this string
+ * @return string representing this value
+ *
* @since 2020-07-26
+ * @since v0.3.0
*/
public String toString(final boolean showUncertainty,
RoundingMode roundingMode) {
- final Optional<String> primaryName = this.unit.getPrimaryName();
- final Optional<String> symbol = this.unit.getSymbol();
- final String chosenName = symbol.orElse(primaryName.orElse(null));
+ final var primaryName = this.unit.getPrimaryName();
+ final var symbol = this.unit.getSymbol();
+ final var chosenName = symbol.orElse(primaryName.orElse(null));
- final UncertainDouble baseValue = this.unit.convertToBase(this.value);
+ final var baseValue = this.unit.convertToBase(this.value);
// get rounded strings
// if showUncertainty is true, add brackets around the string
- final String valueString = (showUncertainty ? "(" : "")
+ final var valueString = (showUncertainty ? "(" : "")
+ this.value.toString(showUncertainty, roundingMode)
+ (showUncertainty ? ")" : "");
- final String baseValueString = (showUncertainty ? "(" : "")
+ final var baseValueString = (showUncertainty ? "(" : "")
+ baseValue.toString(showUncertainty, roundingMode)
+ (showUncertainty ? ")" : "");
@@ -338,7 +419,6 @@ public final class LinearUnitValue {
return String.format("%s unnamed unit (= %s %s)", valueString,
baseValueString, this.unit.getBase()
.toString(unit -> unit.getSymbol().orElseThrow()));
- else
- return String.format("%s %s", valueString, chosenName);
+ return String.format("%s %s", valueString, chosenName);
}
}
diff --git a/src/main/java/sevenUnits/unit/LoadingException.java b/src/main/java/sevenUnits/unit/LoadingException.java
new file mode 100644
index 0000000..2a75c99
--- /dev/null
+++ b/src/main/java/sevenUnits/unit/LoadingException.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (C) 2024, 2025 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.unit;
+
+import java.nio.file.Path;
+import java.util.Optional;
+
+/**
+ * An exception that occurred when loading a file. This wrapper class adds more
+ * info about the error.
+ *
+ * @author Adrien Hopkins
+ * @since 2024-08-22
+ * @since v1.0.0
+ */
+public final class LoadingException extends RuntimeException {
+ /** The type of file that was being loaded. */
+ public enum FileType {
+ @SuppressWarnings("javadoc")
+ UNIT, @SuppressWarnings("javadoc")
+ DIMENSION
+ }
+
+ private static final long serialVersionUID = -8167971828216907607L;
+
+ private final long lineNumber;
+ private final String line;
+ private final Optional<Path> file;
+
+ private final FileType fileType;
+
+ private final RuntimeException problem;
+
+ /**
+ * Create a LoadingException from some information, without a file.
+ *
+ * @param lineNumber line number error happened on
+ * @param line text of invalid line
+ * @param fileType type of file
+ * @param problem problem, as an Exception
+ */
+ public LoadingException(long lineNumber, String line, FileType fileType,
+ RuntimeException problem) {
+ super(problem);
+ this.lineNumber = lineNumber;
+ this.line = line;
+ this.file = Optional.empty();
+ this.fileType = fileType;
+ this.problem = problem;
+ }
+
+ /**
+ * Create a LoadingException from some information, with a file.
+ *
+ * @param lineNumber line number error happened on
+ * @param line text of invalid line
+ * @param file file error happened on
+ * @param fileType type of file
+ * @param problem problem, as an Exception
+ */
+ public LoadingException(long lineNumber, String line, Path file,
+ FileType fileType, RuntimeException problem) {
+ super(problem);
+ this.lineNumber = lineNumber;
+ this.line = line;
+ this.file = Optional.of(file);
+ this.fileType = fileType;
+ this.problem = problem;
+ }
+
+ /** @return the file this error happened in, if there is one */
+ public Optional<Path> file() {
+ return this.file;
+ }
+
+ /** @return type of file that this error happened in */
+ public FileType fileType() {
+ return this.fileType;
+ }
+
+ @Override
+ public String getMessage() {
+ return this.file
+ .map(f -> String.format(
+ "Error parsing line %d of %s file '%s' (\"%s\"): %s",
+ this.lineNumber, this.fileType.toString().toLowerCase(), f,
+ this.line, this.problem))
+ .orElse(String.format(
+ "Error parsing line %d of %s stream (\"%s\"): %s",
+ this.lineNumber, this.fileType.toString().toLowerCase(),
+ this.line, this.problem));
+ }
+
+ /** @return text of line that caused this error */
+ public String line() {
+ return this.line;
+ }
+
+ /** @return number of line that caused this error */
+ public long lineNumber() {
+ return this.lineNumber;
+ }
+
+ /** @return the error, as an exception */
+ public RuntimeException problem() {
+ return this.problem;
+ }
+}
diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java
index 7841987..e712dc3 100644
--- a/src/main/java/sevenUnits/unit/Metric.java
+++ b/src/main/java/sevenUnits/unit/Metric.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * Copyright (C) 2018, 2021, 2022, 2024, 2025 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
@@ -24,19 +24,23 @@ import sevenUnits.utils.ObjectProduct;
/**
* All of the units, prefixes and dimensions that are used by the SI, as well as
* some outside the SI.
- *
+ *
* <p>
* This class does not include prefixed units. To obtain prefixed units, use
* {@link LinearUnit#withPrefix}:
- *
+ *
* <pre>
* LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
* </pre>
- *
- *
+ *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
+ * @since v0.3.0
*/
+// this class is just constants, most of which are obvious from the variable name
+// so no need to check for missing values
+@SuppressWarnings("javadoc")
public final class Metric {
/// dimensions used by SI units
// base dimensions, as BaseDimensions
@@ -67,8 +71,10 @@ public final class Metric {
}
/// base units of the SI
- // suppressing warnings since these are the same object, but in a different
- /// form (class)
+ // suppressing warnings since these are the same object,
+ // but in a different form (class)
+ // and because these objects are only used outside this class,
+ // where hiding is not a problem.
@SuppressWarnings("hiding")
public static final class BaseUnits {
public static final BaseUnit METRE = BaseUnit
@@ -101,9 +107,10 @@ public final class Metric {
/**
* Constants that relate to the SI or other systems.
- *
+ *
* @author Adrien Hopkins
* @since 2019-11-08
+ * @since v0.3.0
*/
public static final class Constants {
public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND)
@@ -343,7 +350,7 @@ public final class Metric {
pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
.withName(NameSymbol.of("neper", "Np"));
public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(),
- pr -> Math.log10(pr), dB -> Math.pow(10, dB))
+ Math::log10, dB -> Math.pow(10, dB))
.withName(NameSymbol.of("bel", "B"));
public static final Unit DECIBEL = Unit
.fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr),
diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java
deleted file mode 100644
index 950c547..0000000
--- a/src/main/java/sevenUnits/unit/MultiUnit.java
+++ /dev/null
@@ -1,161 +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 <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.unit;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import sevenUnits.utils.NameSymbol;
-import sevenUnits.utils.ObjectProduct;
-
-/**
- * A combination of units, like "5 foot + 7 inch". All but the last units should
- * have a whole number value associated with them.
- *
- * @since 2020-10-02
- */
-public final class MultiUnit extends Unitlike<List<Double>> {
- /**
- * Creates a {@code MultiUnit} from its units. It will not have a name or
- * symbol.
- *
- * @since 2020-10-03
- */
- public static final MultiUnit of(LinearUnit... units) {
- return of(Arrays.asList(units));
- }
-
- /**
- * Creates a {@code MultiUnit} from its units. It will not have a name or
- * symbol.
- *
- * @since 2020-10-03
- */
- public static final MultiUnit of(List<LinearUnit> units) {
- if (units.size() < 1)
- throw new IllegalArgumentException("Must have at least one unit");
- final ObjectProduct<BaseUnit> unitBase = units.get(0).getBase();
- for (final LinearUnit unit : units) {
- if (!unitBase.equals(unit.getBase()))
- throw new IllegalArgumentException(
- "All units must have the same base.");
- }
- return new MultiUnit(new ArrayList<>(units), unitBase, NameSymbol.EMPTY);
- }
-
- /**
- * The units that make up this value.
- */
- private final List<LinearUnit> units;
-
- /**
- * Creates a {@code MultiUnit}.
- *
- * @since 2020-10-03
- */
- private MultiUnit(List<LinearUnit> units, ObjectProduct<BaseUnit> unitBase,
- NameSymbol ns) {
- super(unitBase, ns);
- this.units = units;
- }
-
- @Override
- protected List<Double> convertFromBase(double value) {
- final List<Double> values = new ArrayList<>(this.units.size());
- double temp = value;
-
- for (final LinearUnit unit : this.units.subList(0,
- this.units.size() - 1)) {
- values.add(Math.floor(temp / unit.getConversionFactor()));
- temp %= unit.getConversionFactor();
- }
-
- values.add(this.units.size() - 1,
- this.units.get(this.units.size() - 1).convertFromBase(temp));
-
- return values;
- }
-
- /**
- * Converts a value expressed in this unitlike form to a value expressed in
- * {@code other}.
- *
- * @implSpec If conversion is possible, this implementation returns
- * {@code other.convertFromBase(this.convertToBase(value))}.
- * Therefore, overriding either of those methods will change the
- * output of this method.
- *
- * @param other unit to convert to
- * @param value value to convert
- * @return converted value
- * @since 2020-10-03
- * @throws IllegalArgumentException if {@code other} is incompatible for
- * conversion with this unitlike form (as
- * tested by {@link Unit#canConvertTo}).
- * @throws NullPointerException if other is null
- */
- public final <U extends Unitlike<V>, V> V convertTo(U other,
- double... values) {
- final List<Double> valueList = new ArrayList<>(values.length);
- for (final double d : values) {
- valueList.add(d);
- }
-
- return this.convertTo(other, valueList);
- }
-
- /**
- * Converts a value expressed in this unitlike form to a value expressed in
- * {@code other}.
- *
- * @implSpec If conversion is possible, this implementation returns
- * {@code other.convertFromBase(this.convertToBase(value))}.
- * Therefore, overriding either of those methods will change the
- * output of this method.
- *
- * @param other unit to convert to
- * @param value value to convert
- * @return converted value
- * @since 2020-10-03
- * @throws IllegalArgumentException if {@code other} is incompatible for
- * conversion with this unitlike form (as
- * tested by {@link Unit#canConvertTo}).
- * @throws NullPointerException if other is null
- */
- public final double convertTo(Unit other, double... values) {
- final List<Double> valueList = new ArrayList<>(values.length);
- for (final double d : values) {
- valueList.add(d);
- }
-
- return this.convertTo(other, valueList);
- }
-
- @Override
- protected double convertToBase(List<Double> value) {
- if (value.size() != this.units.size())
- throw new IllegalArgumentException("Wrong number of values for "
- + this.units.size() + "-unit MultiUnit.");
-
- double baseValue = 0;
- for (int i = 0; i < this.units.size(); i++) {
- baseValue += value.get(i) * this.units.get(i).getConversionFactor();
- }
- return baseValue;
- }
-}
diff --git a/src/main/java/sevenUnits/unit/USCustomary.java b/src/main/java/sevenUnits/unit/USCustomary.java
index fce829e..ef12043 100644
--- a/src/main/java/sevenUnits/unit/USCustomary.java
+++ b/src/main/java/sevenUnits/unit/USCustomary.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2024, 2025 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
@@ -18,16 +18,21 @@ package sevenUnits.unit;
/**
* A static utility class that contains units in the US Customary system.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-21
+ * @since v0.3.0
*/
+// this class is just constants, most of which are obvious from the variable name
+// so no need to check for missing values
+@SuppressWarnings("javadoc")
public final class USCustomary {
/**
* US Customary units that measure area
- *
+ *
* @author Adrien Hopkins
* @since 2019-11-08
+ * @since v0.3.0
*/
public static final class Area {
public static final LinearUnit SQUARE_SURVEY_FOOT = Length.SURVEY_FOOT
@@ -43,9 +48,10 @@ public final class USCustomary {
/**
* US Customary units that measure length
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-28
+ * @since v0.3.0
*/
public static final class Length {
public static final LinearUnit FOOT = BritishImperial.Length.FOOT;
@@ -73,9 +79,10 @@ public final class USCustomary {
/**
* mass units
- *
+ *
* @author Adrien Hopkins
* @since 2019-11-08
+ * @since v0.3.0
*/
public static final class Mass {
public static final LinearUnit GRAIN = BritishImperial.Mass.GRAIN;
@@ -93,9 +100,10 @@ public final class USCustomary {
/**
* Volume units
- *
+ *
* @author Adrien Hopkins
* @since 2019-11-08
+ * @since v0.3.0
*/
public static final class Volume {
public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3);
diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java
index 59e928a..40e6e0d 100644
--- a/src/main/java/sevenUnits/unit/Unit.java
+++ b/src/main/java/sevenUnits/unit/Unit.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -28,22 +28,23 @@ import sevenUnits.utils.ObjectProduct;
/**
* A unit that is composed of base units.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
+ * @since v0.3.0
*/
public abstract class Unit implements Nameable {
/**
* Returns a unit from its base and the functions it uses to convert to and
* from its base.
- *
+ *
* <p>
* For example, to get a unit representing the degree Celsius, the following
* code can be used:
- *
+ *
* {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);}
* </p>
- *
+ *
* @param base unit's base
* @param converterFrom function that accepts a value expressed in the unit's
* base and returns that value expressed in this unit.
@@ -51,6 +52,7 @@ public abstract class Unit implements Nameable {
* and returns that value expressed in the unit's base.
* @return a unit that uses the provided functions to convert.
* @since 2019-05-22
+ * @since v0.3.0
* @throws NullPointerException if any argument is null
*/
public static final Unit fromConversionFunctions(
@@ -63,14 +65,14 @@ public abstract class Unit implements Nameable {
/**
* Returns a unit from its base and the functions it uses to convert to and
* from its base.
- *
+ *
* <p>
* For example, to get a unit representing the degree Celsius, the following
* code can be used:
- *
+ *
* {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);}
* </p>
- *
+ *
* @param base unit's base
* @param converterFrom function that accepts a value expressed in the unit's
* base and returns that value expressed in this unit.
@@ -79,6 +81,7 @@ public abstract class Unit implements Nameable {
* @param ns names and symbol of unit
* @return a unit that uses the provided functions to convert.
* @since 2019-05-22
+ * @since v0.3.0
* @throws NullPointerException if any argument is null
*/
public static final Unit fromConversionFunctions(
@@ -90,15 +93,17 @@ public abstract class Unit implements Nameable {
/**
* The combination of units that this unit is based on.
- *
+ *
* @since 2019-10-16
+ * @since v0.3.0
*/
private final ObjectProduct<BaseUnit> unitBase;
/**
* This unit's name(s) and symbol
- *
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
private final NameSymbol nameSymbol;
@@ -106,28 +111,30 @@ public abstract class Unit implements Nameable {
* Cache storing the result of getDimension()
*
* @since 2019-10-16
+ * @since v0.3.0
*/
private transient ObjectProduct<BaseDimension> dimension = null;
/**
* A constructor that constructs {@code BaseUnit} instances.
- *
+ *
* @since 2019-10-16
+ * @since v0.3.0
*/
Unit(final NameSymbol nameSymbol) {
- if (this instanceof BaseUnit) {
- this.unitBase = ObjectProduct.oneOf((BaseUnit) this);
- } else
+ if (!(this instanceof BaseUnit))
throw new AssertionError();
+ this.unitBase = ObjectProduct.oneOf((BaseUnit) this);
this.nameSymbol = nameSymbol;
}
/**
* Creates the {@code Unit}.
- *
+ *
* @param unitBase base of unit
* @param ns names and symbol of unit
* @since 2019-10-16
+ * @since v0.3.0
* @throws NullPointerException if unitBase or ns is null
*/
protected Unit(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) {
@@ -137,18 +144,9 @@ public abstract class Unit implements Nameable {
}
/**
- * @return this unit as a {@link Unitlike}
- * @since 2020-09-07
- */
- public final Unitlike<Double> asUnitlike() {
- return Unitlike.fromConversionFunctions(this.getBase(),
- this::convertFromBase, this::convertToBase, this.getNameSymbol());
- }
-
- /**
* Checks if a value expressed in this unit can be converted to a value
* expressed in {@code other}
- *
+ *
* @param other unit or unitlike form to test with
* @return true if they are compatible
* @since 2019-01-13
@@ -161,21 +159,6 @@ public abstract class Unit implements Nameable {
}
/**
- * Checks if a value expressed in this unit can be converted to a value
- * expressed in {@code other}
- *
- * @param other unit or unitlike form to test with
- * @return true if they are compatible
- * @since 2019-01-13
- * @since v0.1.0
- * @throws NullPointerException if other is null
- */
- public final <W> boolean canConvertTo(final Unitlike<W> other) {
- Objects.requireNonNull(other, "other must not be null.");
- return Objects.equals(this.getBase(), other.getBase());
- }
-
- /**
* Converts from a value expressed in this unit's base unit to a value
* expressed in this unit.
* <p>
@@ -187,10 +170,10 @@ public abstract class Unit implements Nameable {
* If this unit <i>is</i> a base unit, this method should return
* {@code value}.
* </p>
- *
+ *
* @implSpec This method is used by {@link #convertTo}, and its behaviour
* affects the behaviour of {@code convertTo}.
- *
+ *
* @param value value expressed in <b>base</b> unit
* @return value expressed in <b>this</b> unit
* @since 2018-12-22
@@ -201,16 +184,17 @@ public abstract class Unit implements Nameable {
/**
* Converts a value expressed in this unit to a value expressed in
* {@code other}.
- *
+ *
* @implSpec If unit conversion is possible, this implementation returns
* {@code other.convertFromBase(this.convertToBase(value))}.
* Therefore, overriding either of those methods will change the
* output of this method.
- *
+ *
* @param other unit to convert to
* @param value value to convert
* @return converted value
* @since 2019-05-22
+ * @since v0.3.0
* @throws IllegalArgumentException if {@code other} is incompatible for
* conversion with this unit (as tested by
* {@link Unit#canConvertTo}).
@@ -220,37 +204,8 @@ public abstract class Unit implements Nameable {
Objects.requireNonNull(other, "other must not be null.");
if (this.canConvertTo(other))
return other.convertFromBase(this.convertToBase(value));
- else
- throw new IllegalArgumentException(
- String.format("Cannot convert from %s to %s.", this, other));
- }
-
- /**
- * Converts a value expressed in this unit to a value expressed in
- * {@code other}.
- *
- * @implSpec If conversion is possible, this implementation returns
- * {@code other.convertFromBase(this.convertToBase(value))}.
- * Therefore, overriding either of those methods will change the
- * output of this method.
- *
- * @param other unitlike form to convert to
- * @param value value to convert
- * @param <W> type of value to convert to
- * @return converted value
- * @since 2020-09-07
- * @throws IllegalArgumentException if {@code other} is incompatible for
- * conversion with this unit (as tested by
- * {@link Unit#canConvertTo}).
- * @throws NullPointerException if other is null
- */
- public final <W> W convertTo(final Unitlike<W> other, final double value) {
- Objects.requireNonNull(other, "other must not be null.");
- if (this.canConvertTo(other))
- return other.convertFromBase(this.convertToBase(value));
- else
- throw new IllegalArgumentException(
- String.format("Cannot convert from %s to %s.", this, other));
+ throw new IllegalArgumentException(
+ String.format("Cannot convert from %s to %s.", this, other));
}
/**
@@ -265,10 +220,10 @@ public abstract class Unit implements Nameable {
* If this unit <i>is</i> a base unit, this method should return
* {@code value}.
* </p>
- *
+ *
* @implSpec This method is used by {@link #convertTo}, and its behaviour
* affects the behaviour of {@code convertTo}.
- *
+ *
* @param value value expressed in <b>this</b> unit
* @return value expressed in <b>base</b> unit
* @since 2018-12-22
@@ -292,7 +247,7 @@ public abstract class Unit implements Nameable {
*/
public final ObjectProduct<BaseDimension> getDimension() {
if (this.dimension == null) {
- final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap();
+ final var mapping = this.unitBase.exponentMap();
final Map<BaseDimension, Integer> dimensionMap = new HashMap<>();
for (final BaseUnit key : mapping.keySet()) {
@@ -307,6 +262,7 @@ public abstract class Unit implements Nameable {
/**
* @return the nameSymbol
* @since 2020-09-07
+ * @since v0.3.0
*/
@Override
public final NameSymbol getNameSymbol() {
@@ -314,7 +270,7 @@ public abstract class Unit implements Nameable {
}
/**
- * Returns true iff this unit is metric.
+ * Determines whether this unit is metric.
* <p>
* "Metric" is defined by three conditions:
* <ul>
@@ -329,14 +285,17 @@ public abstract class Unit implements Nameable {
* <p>
* All SI units (as designated by the BIPM) except the degree Celsius are
* considered "metric" by this definition.
- *
+ *
+ * @return true iff this unit is metric.
+ *
* @since 2020-08-27
+ * @since v0.3.0
*/
public final boolean isMetric() {
// first condition - check that it is a linear unit
if (!(this instanceof LinearUnit))
return false;
- final LinearUnit linear = (LinearUnit) this;
+ final var linear = (LinearUnit) this;
// second condition - check that
for (final BaseUnit b : linear.getBase().getBaseSet()) {
@@ -352,18 +311,18 @@ public abstract class Unit implements Nameable {
/**
* @return a string representing this unit's definition
* @since 2022-03-10
+ * @since v0.3.0
*/
public String toDefinitionString() {
if (!this.unitBase.getNameSymbol().isEmpty())
return "derived from " + this.unitBase.getName();
- else
- return "derived from "
- + this.getBase().toString(BaseUnit::getShortName);
+ return "derived from " + this.getBase().toString(BaseUnit::getShortName);
}
/**
* @return a string containing both this unit's name and its definition
* @since 2022-03-10
+ * @since v0.3.0
*/
public final String toFullString() {
return this.toString() + " (" + this.toDefinitionString() + ")";
@@ -375,14 +334,14 @@ public abstract class Unit implements Nameable {
&& this.nameSymbol.getSymbol().isPresent())
return this.nameSymbol.getPrimaryName().orElseThrow() + " ("
+ this.nameSymbol.getSymbol().orElseThrow() + ")";
- else
- return this.getName();
+ return this.getName();
}
/**
* @param ns name(s) and symbol to use
* @return a copy of this unit with provided name(s) and symbol
* @since 2019-10-21
+ * @since v0.3.0
* @throws NullPointerException if ns is null
*/
public Unit withName(final NameSymbol ns) {
diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java
index 0120067..36c225f 100644
--- a/src/main/java/sevenUnits/unit/UnitDatabase.java
+++ b/src/main/java/sevenUnits/unit/UnitDatabase.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018-2024 Adrien Hopkins
+ * Copyright (C) 2018-2025 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
@@ -40,11 +40,9 @@ import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
import sevenUnits.utils.ConditionalExistenceCollections;
-import sevenUnits.utils.DecimalComparison;
import sevenUnits.utils.ExpressionParser;
import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
@@ -52,7 +50,7 @@ import sevenUnits.utils.UncertainDouble;
/**
* A database of units, prefixes and dimensions, and their names.
- *
+ *
* @author Adrien Hopkins
* @since 2019-01-07
* @since v0.1.0
@@ -89,7 +87,7 @@ public final class UnitDatabase {
* Because of ambiguities between prefixes (i.e. kilokilo = mega),
* {@link #containsValue} and {@link #values()} currently ignore prefixes.
* </p>
- *
+ *
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
@@ -97,7 +95,7 @@ public final class UnitDatabase {
private static final class PrefixedUnitMap implements Map<String, Unit> {
/**
* The class used for entry sets.
- *
+ *
* <p>
* If the map that created this set is infinite in size (has at least one
* unit and at least one prefix), this set is infinite as well. If this
@@ -105,7 +103,7 @@ public final class UnitDatabase {
* {@code IllegalStateException} instead of creating an infinite-sized
* array.
* </p>
- *
+ *
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
@@ -114,7 +112,7 @@ public final class UnitDatabase {
extends AbstractSet<Map.Entry<String, Unit>> {
/**
* The entry for this set.
- *
+ *
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
@@ -126,7 +124,7 @@ public final class UnitDatabase {
/**
* Creates the {@code PrefixedUnitEntry}.
- *
+ *
* @param key key
* @param value value
* @since 2019-04-14
@@ -139,6 +137,7 @@ public final class UnitDatabase {
/**
* @since 2019-05-03
+ * @since v0.3.0
*/
@Override
public boolean equals(final Object o) {
@@ -161,6 +160,7 @@ public final class UnitDatabase {
/**
* @since 2019-05-03
+ * @since v0.3.0
*/
@Override
public int hashCode() {
@@ -180,8 +180,9 @@ public final class UnitDatabase {
* string is the string representation of the key, then the equals
* ({@code =}) character, then the string representation of the
* value.
- *
+ *
* @since 2019-05-03
+ * @since v0.3.0
*/
@Override
public String toString() {
@@ -192,7 +193,7 @@ public final class UnitDatabase {
/**
* An iterator that iterates over the units of a
* {@code PrefixedUnitNameSet}.
- *
+ *
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
@@ -212,7 +213,9 @@ public final class UnitDatabase {
/**
* Creates the
* {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
- *
+ *
+ * @param map map to base iterator on
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@@ -228,7 +231,7 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private String getCurrentUnitName() {
- final StringBuilder unitName = new StringBuilder();
+ final var unitName = new StringBuilder();
for (final int i : this.prefixCoordinates) {
unitName.append(this.prefixNames.get(i));
}
@@ -241,18 +244,15 @@ public final class UnitDatabase {
public boolean hasNext() {
if (this.unitNames.isEmpty())
return false;
- else {
- if (this.prefixNames.isEmpty())
- return this.prefixCoordinates.isEmpty()
- && this.unitNamePosition < this.unitNames.size();
- else
- return true;
- }
+ if (this.prefixNames.isEmpty())
+ return this.prefixCoordinates.isEmpty()
+ && this.unitNamePosition < this.unitNames.size();
+ return true;
}
/**
* Changes this iterator's position to the next available one.
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@@ -268,7 +268,7 @@ public final class UnitDatabase {
this.prefixCoordinates.add(0, 0);
} else {
// get the prefix coordinate to increment, then increment
- int i = this.prefixCoordinates.size() - 1;
+ var i = this.prefixCoordinates.size() - 1;
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
@@ -294,7 +294,7 @@ public final class UnitDatabase {
@Override
public Entry<String, Unit> next() {
// get next element
- final Entry<String, Unit> nextEntry = this.peek();
+ final var nextEntry = this.peek();
// iterate to next position
this.incrementPosition();
@@ -306,6 +306,7 @@ public final class UnitDatabase {
* @return the next element in the iterator, without iterating over
* it
* @since 2019-05-03
+ * @since v0.3.0
*/
private Entry<String, Unit> peek() {
if (!this.hasNext())
@@ -322,7 +323,7 @@ public final class UnitDatabase {
}
}
- final String nextName = this.getCurrentUnitName();
+ final var nextName = this.getCurrentUnitName();
return new PrefixedUnitEntry(nextName, this.map.get(nextName));
}
@@ -330,8 +331,9 @@ public final class UnitDatabase {
/**
* Returns a string representation of the object. The exact details
* of the representation are unspecified and subject to change.
- *
+ *
* @since 2019-05-03
+ * @since v0.3.0
*/
@Override
public String toString() {
@@ -346,7 +348,7 @@ public final class UnitDatabase {
/**
* Creates the {@code PrefixedUnitNameSet}.
- *
+ *
* @param map map that created this set
* @since 2019-04-13
* @since v0.2.0
@@ -383,7 +385,7 @@ public final class UnitDatabase {
// This is OK because I'm in a try-catch block, catching the
// exact exception that would be thrown.
@SuppressWarnings("unchecked")
- final Entry<String, Unit> tempEntry = (Entry<String, Unit>) o;
+ final var tempEntry = (Entry<String, Unit>) o;
entry = tempEntry;
} catch (final ClassCastException e) {
throw new IllegalArgumentException(
@@ -441,55 +443,45 @@ public final class UnitDatabase {
public int size() {
if (this.map.units.isEmpty())
return 0;
- else {
- if (this.map.prefixes.isEmpty())
- return this.map.units.size();
- else
- // infinite set
- return Integer.MAX_VALUE;
- }
+ if (this.map.prefixes.isEmpty())
+ return this.map.units.size();
+ // infinite set
+ return Integer.MAX_VALUE;
}
- /**
- * @throws IllegalStateException if the set is infinite in size
- */
+ /** @throws IllegalStateException if the set is infinite in size */
@Override
public Object[] toArray() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray();
- else
- // infinite set
- throw new IllegalStateException(
- "Cannot make an infinite set into an array.");
+ // infinite set
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
}
- /**
- * @throws IllegalStateException if the set is infinite in size
- */
+ /** @throws IllegalStateException if the set is infinite in size */
@Override
public <T> T[] toArray(final T[] a) {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray(a);
- else
- // infinite set
- throw new IllegalStateException(
- "Cannot make an infinite set into an array.");
+ // infinite set
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
}
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toString();
- else
- return String.format(
- "Infinite set of name-unit entries created from units %s and prefixes %s",
- this.map.units, this.map.prefixes);
+ return String.format(
+ "Infinite set of name-unit entries created from units %s and prefixes %s",
+ this.map.units, this.map.prefixes);
}
}
/**
* The class used for unit name sets.
- *
+ *
* <p>
* If the map that created this set is infinite in size (has at least one
* unit and at least one prefix), this set is infinite as well. If this
@@ -497,7 +489,7 @@ public final class UnitDatabase {
* {@code IllegalStateException} instead of creating an infinite-sized
* array.
* </p>
- *
+ *
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
@@ -507,7 +499,7 @@ public final class UnitDatabase {
/**
* An iterator that iterates over the units of a
* {@code PrefixedUnitNameSet}.
- *
+ *
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
@@ -527,7 +519,9 @@ public final class UnitDatabase {
/**
* Creates the
* {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
- *
+ *
+ * @param map map to base itorator on
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@@ -543,7 +537,7 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private String getCurrentUnitName() {
- final StringBuilder unitName = new StringBuilder();
+ final var unitName = new StringBuilder();
for (final int i : this.prefixCoordinates) {
unitName.append(this.prefixNames.get(i));
}
@@ -556,18 +550,15 @@ public final class UnitDatabase {
public boolean hasNext() {
if (this.unitNames.isEmpty())
return false;
- else {
- if (this.prefixNames.isEmpty())
- return this.prefixCoordinates.isEmpty()
- && this.unitNamePosition < this.unitNames.size();
- else
- return true;
- }
+ if (this.prefixNames.isEmpty())
+ return this.prefixCoordinates.isEmpty()
+ && this.unitNamePosition < this.unitNames.size();
+ return true;
}
/**
* Changes this iterator's position to the next available one.
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@@ -583,7 +574,7 @@ public final class UnitDatabase {
this.prefixCoordinates.add(0, 0);
} else {
// get the prefix coordinate to increment, then increment
- int i = this.prefixCoordinates.size() - 1;
+ var i = this.prefixCoordinates.size() - 1;
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
@@ -608,7 +599,7 @@ public final class UnitDatabase {
@Override
public String next() {
- final String nextName = this.peek();
+ final var nextName = this.peek();
this.incrementPosition();
@@ -619,6 +610,7 @@ public final class UnitDatabase {
* @return the next element in the iterator, without iterating over
* it
* @since 2019-05-03
+ * @since v0.3.0
*/
private String peek() {
if (!this.hasNext())
@@ -640,8 +632,9 @@ public final class UnitDatabase {
/**
* Returns a string representation of the object. The exact details
* of the representation are unspecified and subject to change.
- *
+ *
* @since 2019-05-03
+ * @since v0.3.0
*/
@Override
public String toString() {
@@ -656,7 +649,7 @@ public final class UnitDatabase {
/**
* Creates the {@code PrefixedUnitNameSet}.
- *
+ *
* @param map map that created this set
* @since 2019-04-13
* @since v0.2.0
@@ -734,56 +727,46 @@ public final class UnitDatabase {
public int size() {
if (this.map.units.isEmpty())
return 0;
- else {
- if (this.map.prefixes.isEmpty())
- return this.map.units.size();
- else
- // infinite set
- return Integer.MAX_VALUE;
- }
+ if (this.map.prefixes.isEmpty())
+ return this.map.units.size();
+ // infinite set
+ return Integer.MAX_VALUE;
}
- /**
- * @throws IllegalStateException if the set is infinite in size
- */
+ /** @throws IllegalStateException if the set is infinite in size */
@Override
public Object[] toArray() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray();
- else
- // infinite set
- throw new IllegalStateException(
- "Cannot make an infinite set into an array.");
+ // infinite set
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
}
- /**
- * @throws IllegalStateException if the set is infinite in size
- */
+ /** @throws IllegalStateException if the set is infinite in size */
@Override
public <T> T[] toArray(final T[] a) {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray(a);
- else
- // infinite set
- throw new IllegalStateException(
- "Cannot make an infinite set into an array.");
+ // infinite set
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
}
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toString();
- else
- return String.format(
- "Infinite set of name-unit entries created from units %s and prefixes %s",
- this.map.units, this.map.prefixes);
+ return String.format(
+ "Infinite set of name-unit entries created from units %s and prefixes %s",
+ this.map.units, this.map.prefixes);
}
}
/**
* The units stored in this collection, without prefixes.
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
@@ -791,7 +774,7 @@ public final class UnitDatabase {
/**
* The available prefixes for use.
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
@@ -804,7 +787,7 @@ public final class UnitDatabase {
/**
* Creates the {@code PrefixedUnitMap}.
- *
+ *
* @param units map mapping unit names to units
* @param prefixes map mapping prefix names to prefixes
* @since 2019-04-13
@@ -855,11 +838,11 @@ public final class UnitDatabase {
if (!(key instanceof String))
throw new IllegalArgumentException(
"Attempted to test for a unit using a non-string name.");
- final String unitName = (String) key;
+ final var unitName = (String) key;
// Then, look for the longest prefix that is attached to a valid unit
String longestPrefix = null;
- int longestLength = 0;
+ var longestLength = 0;
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
@@ -871,7 +854,7 @@ public final class UnitDatabase {
// linear units can have prefixes)
if (unitName.startsWith(prefixName)
&& prefixName.length() > longestLength) {
- final String rest = unitName.substring(prefixName.length());
+ final var rest = unitName.substring(prefixName.length());
if (this.containsKey(rest)
&& this.get(rest) instanceof LinearUnit) {
longestPrefix = prefixName;
@@ -885,7 +868,7 @@ public final class UnitDatabase {
/**
* {@inheritDoc}
- *
+ *
* <p>
* Because of ambiguities between prefixes (i.e. kilokilo = mega), this
* method only tests for prefixless units.
@@ -914,11 +897,11 @@ public final class UnitDatabase {
if (!(key instanceof String))
throw new IllegalArgumentException(
"Attempted to obtain a unit using a non-string name.");
- final String unitName = (String) key;
+ final var unitName = (String) key;
// Then, look for the longest prefix that is attached to a valid unit
String longestPrefix = null;
- int longestLength = 0;
+ var longestLength = 0;
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
@@ -930,7 +913,7 @@ public final class UnitDatabase {
// linear units can have prefixes)
if (unitName.startsWith(prefixName)
&& prefixName.length() > longestLength) {
- final String rest = unitName.substring(prefixName.length());
+ final var rest = unitName.substring(prefixName.length());
if (this.containsKey(rest)
&& this.get(rest) instanceof LinearUnit) {
longestPrefix = prefixName;
@@ -942,16 +925,14 @@ public final class UnitDatabase {
// if none found, returns null
if (longestPrefix == null)
return null;
- else {
- // get necessary data
- final String rest = unitName.substring(longestLength);
- // this cast will not fail because I verified that it would work
- // before selecting this prefix
- final LinearUnit unit = (LinearUnit) this.get(rest);
- final UnitPrefix prefix = this.prefixes.get(longestPrefix);
-
- return unit.withPrefix(prefix);
- }
+ // get necessary data
+ final var rest = unitName.substring(longestLength);
+ // this cast will not fail because I verified that it would work
+ // before selecting this prefix
+ final var unit = (LinearUnit) this.get(rest);
+ final var prefix = this.prefixes.get(longestPrefix);
+
+ return unit.withPrefix(prefix);
}
@Override
@@ -1028,28 +1009,24 @@ public final class UnitDatabase {
public int size() {
if (this.units.isEmpty())
return 0;
- else {
- if (this.prefixes.isEmpty())
- return this.units.size();
- else
- // infinite set
- return Integer.MAX_VALUE;
- }
+ if (this.prefixes.isEmpty())
+ return this.units.size();
+ // infinite set
+ return Integer.MAX_VALUE;
}
@Override
public String toString() {
if (this.units.isEmpty() || this.prefixes.isEmpty())
return new HashMap<>(this).toString();
- else
- return String.format(
- "Infinite map of name-unit entries created from units %s and prefixes %s",
- this.units, this.prefixes);
+ return String.format(
+ "Infinite map of name-unit entries created from units %s and prefixes %s",
+ this.units, this.prefixes);
}
/**
* {@inheritDoc}
- *
+ *
* <p>
* Because of ambiguities between prefixes (i.e. kilokilo = mega), this
* method ignores prefixes.
@@ -1066,34 +1043,6 @@ public final class UnitDatabase {
}
/**
- * Replacements done to *all* expression types
- */
- private static final Map<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>();
-
- // add data to expression replacements
- static {
- // add spaces around operators
- for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) {
- EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator),
- " " + operator + " ");
- }
-
- // replace multiple spaces with a single space
- EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " ");
- // place brackets around any expression of the form "number unit", with or
- // without the space
- EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer
- + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers
- // after it
- + "\\s*" // optional space(s)
- + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters
- + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters
- + "(?!-?\\d)" // no number directly afterwards (avoids matching
- // "1e3")
- ), "\\($1 $2\\)");
- }
-
- /**
* A regular expression that separates names and expressions in unit files.
*/
private static final Pattern NAME_EXPRESSION = Pattern
@@ -1108,66 +1057,76 @@ public final class UnitDatabase {
/**
* The exponent operator
- *
+ *
* @param base base of exponentiation
* @param exponentUnit exponent
* @return result
* @since 2019-04-10
* @since v0.2.0
*/
- private static final LinearUnit exponentiateUnits(final LinearUnit base,
+ private static LinearUnit exponentiateUnits(final LinearUnit base,
final LinearUnit exponentUnit) {
- // exponent function - first check if o2 is a number,
- if (exponentUnit.getBase().equals(Metric.ONE.getBase())) {
- // then check if it is an integer,
- final double exponent = exponentUnit.getConversionFactor();
- if (DecimalComparison.equals(exponent % 1, 0))
- // then exponentiate
- return base.toExponent((int) (exponent + 0.5));
- else
- // not an integer
- throw new UnsupportedOperationException(
- "Decimal exponents are currently not supported.");
- } else
- // not a number
- throw new IllegalArgumentException("Exponents must be numbers.");
+ if (!exponentUnit.getBase().equals(Metric.ONE.getBase()))
+ throw new IllegalArgumentException(String.format(
+ "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.",
+ base, exponentUnit));
+
+ final var exponent = exponentUnit.getConversionFactor();
+ return base.toExponentRounded(exponent);
}
/**
* The exponent operator
- *
+ *
* @param base base of exponentiation
* @param exponentUnit exponent
* @return result
* @since 2020-08-04
+ * @since v0.3.0
*/
- private static final LinearUnitValue exponentiateUnitValues(
+ private static LinearUnitValue exponentiateUnitValues(
final LinearUnitValue base, final LinearUnitValue exponentValue) {
- // exponent function - first check if o2 is a number,
- if (exponentValue.canConvertTo(Metric.ONE)) {
- // then check if it is an integer,
- final double exponent = exponentValue.getValueExact();
- if (DecimalComparison.equals(exponent % 1, 0))
- // then exponentiate
- return base.toExponent((int) (exponent + 0.5));
- else
- // not an integer
- throw new UnsupportedOperationException(
- "Decimal exponents are currently not supported.");
- } else
- // not a number
- throw new IllegalArgumentException("Exponents must be numbers.");
+ if (!exponentValue.canConvertTo(Metric.ONE))
+ throw new IllegalArgumentException(String.format(
+ "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.",
+ base, exponentValue));
+
+ final var exponent = exponentValue.getValueExact();
+ return base.toExponentRounded(exponent);
+ }
+
+ /**
+ * Formats an expression so it can be parsed by the expression parser.
+ *
+ * Specifically, puts spaces around all operators so they can be parsed as
+ * words.
+ *
+ * @param expression expression to format
+ * @return formatted expression
+ * @since 2025-06-07
+ * @since v1.0.0
+ */
+ static String formatExpression(String expression) {
+ var modifiedExpression = expression;
+ for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) {
+ modifiedExpression = modifiedExpression.replaceAll(operator,
+ " " + operator + " ");
+ }
+
+ modifiedExpression = modifiedExpression.replaceAll("\\s+", " ");
+ return modifiedExpression;
}
/**
* @return true if entry represents a removable duplicate entry of map.
* @since 2021-05-22
+ * @since v0.3.0
*/
static <T> boolean isRemovableDuplicate(Map<String, T> map,
Entry<String, T> entry) {
for (final Entry<String, T> e : map.entrySet()) {
- final String name = e.getKey();
- final T value = e.getValue();
+ final var name = e.getKey();
+ final var value = e.getValue();
if (lengthFirstComparator.compare(entry.getKey(), name) < 0
&& Objects.equals(map.get(entry.getKey()), value))
return true;
@@ -1177,7 +1136,7 @@ public final class UnitDatabase {
/**
* The units in this system, excluding prefixes.
- *
+ *
* @since 2019-01-07
* @since v0.1.0
*/
@@ -1185,7 +1144,7 @@ public final class UnitDatabase {
/**
* The unit prefixes in this system.
- *
+ *
* @since 2019-01-14
* @since v0.1.0
*/
@@ -1193,7 +1152,7 @@ public final class UnitDatabase {
/**
* The dimensions in this system.
- *
+ *
* @since 2019-03-14
* @since v0.2.0
*/
@@ -1201,13 +1160,21 @@ public final class UnitDatabase {
/**
* A map mapping strings to units (including prefixes)
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
private final Map<String, Unit> units;
/**
+ * A map mapping strings to unit sets
+ *
+ * @since 2024-08-16
+ * @since v1.0.0
+ */
+ private final Map<String, List<LinearUnit>> unitSets;
+
+ /**
* The rule that specifies when prefix repetition is allowed. It takes in one
* argument: a list of the prefixes being applied to the unit
* <p>
@@ -1221,71 +1188,72 @@ public final class UnitDatabase {
/**
* A parser that can parse unit expressions.
- *
+ *
* @since 2019-03-22
* @since v0.2.0
*/
private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>(
- this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
- .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
- .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
- .addSpaceFunction("*")
- .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
- .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3)
- .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build();
+ this::getLinearUnit).addBinaryOperator("+", LinearUnit::plus, 0)
+ .addBinaryOperator("-", LinearUnit::minus, 0)
+ .addBinaryOperator("*", LinearUnit::times, 1)
+ .addBinaryOperator("space_times", LinearUnit::times, 2)
+ .addSpaceFunction("space_times")
+ .addBinaryOperator("/", LinearUnit::dividedBy, 1)
+ .addBinaryOperator("|", LinearUnit::dividedBy, 4)
+ .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 3).build();
/**
* A parser that can parse unit value expressions.
- *
+ *
* @since 2020-08-04
+ * @since v0.3.0
*/
private final ExpressionParser<LinearUnitValue> unitValueExpressionParser = new ExpressionParser.Builder<>(
this::getLinearUnitValue)
- .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
- .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
- .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
- .addSpaceFunction("*")
- .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
- .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3)
- .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2)
+ .addBinaryOperator("+", LinearUnitValue::plus, 0)
+ .addBinaryOperator("-", LinearUnitValue::minus, 0)
+ .addBinaryOperator("*", LinearUnitValue::times, 1)
+ .addBinaryOperator("space_times", LinearUnitValue::times, 2)
+ .addSpaceFunction("space_times")
+ .addBinaryOperator("/", LinearUnitValue::dividedBy, 1)
+ .addBinaryOperator("|", LinearUnitValue::dividedBy, 4)
+ .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 3)
.build();
/**
* A parser that can parse unit prefix expressions
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>(
- this::getPrefix).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
- .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
- .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
- .addSpaceFunction("*")
- .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
- .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3)
- .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()),
- 2)
+ this::getPrefix).addBinaryOperator("+", UnitPrefix::plus, 0)
+ .addBinaryOperator("-", UnitPrefix::minus, 0)
+ .addBinaryOperator("*", UnitPrefix::times, 1).addSpaceFunction("*")
+ .addBinaryOperator("/", UnitPrefix::dividedBy, 1)
+ .addBinaryOperator("|", UnitPrefix::dividedBy, 3).addBinaryOperator(
+ "^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 2)
.build();
/**
* A parser that can parse unit dimension expressions.
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>(
- this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0)
+ this::getDimension).addBinaryOperator("*", ObjectProduct::times, 0)
.addSpaceFunction("*")
- .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
- .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 2)
+ .addBinaryOperator("/", ObjectProduct::dividedBy, 0)
+ .addBinaryOperator("|", ObjectProduct::dividedBy, 2)
.addNumericOperator("^", (o1, o2) -> {
- int exponent = (int) Math.round(o2.value());
+ final var exponent = (int) Math.round(o2.value());
return o1.toExponent(exponent);
}, 1).build();
/**
* Creates the {@code UnitsDatabase}.
- *
+ *
* @since 2019-01-10
* @since v0.1.0
*/
@@ -1295,10 +1263,11 @@ public final class UnitDatabase {
/**
* Creates the {@code UnitsDatabase}
- *
+ *
* @param prefixRepetitionRule the rule that determines when prefix
* repetition is allowed
* @since 2020-08-26
+ * @since v0.3.0
*/
public UnitDatabase(Predicate<List<UnitPrefix>> prefixRepetitionRule) {
this.prefixlessUnits = new HashMap<>();
@@ -1309,11 +1278,12 @@ public final class UnitDatabase {
new PrefixedUnitMap(this.prefixlessUnits, this.prefixes),
entry -> this.prefixRepetitionRule
.test(this.getPrefixesFromName(entry.getKey())));
+ this.unitSets = new HashMap<>();
}
/**
* Adds a unit dimension to the database.
- *
+ *
* @param name dimension's name
* @param dimension dimension to add
* @throws NullPointerException if name or dimension is null
@@ -1324,14 +1294,14 @@ public final class UnitDatabase {
final ObjectProduct<BaseDimension> dimension) {
Objects.requireNonNull(name, "name may not be null");
Objects.requireNonNull(dimension, "dimension may not be null");
- final ObjectProduct<BaseDimension> namedDimension = dimension
+ final var namedDimension = dimension
.withName(dimension.getNameSymbol().withExtraName(name));
this.dimensions.put(name, namedDimension);
}
/**
* Adds to the list from a line in a unit dimension file.
- *
+ *
* @param line line to look at
* @param lineCounter number of line, for error messages
* @since 2019-04-10
@@ -1349,13 +1319,13 @@ public final class UnitDatabase {
}
// divide line into name and expression
- final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
+ final var lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
throw new IllegalArgumentException(String.format(
"Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.",
lineCounter));
- final String name = lineMatcher.group(1);
- final String expression = lineMatcher.group(2);
+ final var name = lineMatcher.group(1);
+ final var expression = lineMatcher.group(2);
// if (name.endsWith(" ")) {
// System.err.printf("Warning - line %d's dimension name ends in a space",
@@ -1369,22 +1339,13 @@ public final class UnitDatabase {
throw new IllegalArgumentException(String.format(
"! used but no dimension found (line %d).", lineCounter));
} else {
- // it's a unit, get the unit
- final ObjectProduct<BaseDimension> dimension;
- try {
- dimension = this.getDimensionFromExpression(expression);
- } catch (final IllegalArgumentException | NoSuchElementException e) {
- System.err.printf("Parsing error on line %d:%n", lineCounter);
- throw e;
- }
-
- this.addDimension(name, dimension);
+ this.addDimension(name, this.getDimensionFromExpression(expression));
}
}
/**
* Adds a unit prefix to the database.
- *
+ *
* @param name prefix's name
* @param prefix prefix to add
* @throws NullPointerException if name or prefix is null
@@ -1401,7 +1362,7 @@ public final class UnitDatabase {
/**
* Adds a unit to the database.
- *
+ *
* @param name unit's name
* @param unit unit to add
* @throws NullPointerException if unit is null
@@ -1418,7 +1379,7 @@ public final class UnitDatabase {
/**
* Adds to the list from a line in a unit file.
- *
+ *
* @param line line to look at
* @param lineCounter number of line, for error messages
* @since 2019-04-10
@@ -1436,14 +1397,14 @@ public final class UnitDatabase {
}
// divide line into name and expression
- final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
+ final var lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
throw new IllegalArgumentException(String.format(
"Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.",
lineCounter));
- final String name = lineMatcher.group(1);
+ final var name = lineMatcher.group(1);
- final String expression = lineMatcher.group(2);
+ final var expression = lineMatcher.group(2);
// this code should never occur
// if (name.endsWith(" ")) {
@@ -1457,47 +1418,55 @@ public final class UnitDatabase {
if (!this.containsUnitName(name))
throw new IllegalArgumentException(String
.format("! used but no unit found (line %d).", lineCounter));
+ } else if (name.endsWith("-")) {
+ final var prefixName = name.substring(0, name.length() - 1);
+ this.addPrefix(prefixName, this.getPrefixFromExpression(expression));
+ } else if (expression.contains(";")) {
+ // it's a multi-unit
+ this.addUnitSet(name, this.getUnitSetFromExpression(expression));
} else {
- if (name.endsWith("-")) {
- final UnitPrefix prefix;
- try {
- prefix = this.getPrefixFromExpression(expression);
- } catch (final IllegalArgumentException
- | NoSuchElementException e) {
- System.err.printf("Parsing error on line %d:%n", lineCounter);
- throw e;
- }
- final String prefixName = name.substring(0, name.length() - 1);
- this.addPrefix(prefixName, prefix);
- } else {
- // it's a unit, get the unit
- final Unit unit;
- try {
- unit = this.getUnitFromExpression(expression);
- } catch (final IllegalArgumentException
- | NoSuchElementException e) {
- System.err.printf("Parsing error on line %d:%n", lineCounter);
- throw e;
- }
- this.addUnit(name, unit);
- }
+ // it's a unit, get the unit
+ this.addUnit(name, this.getUnitFromExpression(expression));
}
}
/**
- * Removes all units, prefixes and dimensions from this database.
- *
+ * Add a unit set to the database.
+ *
+ * @param name name of unit set
+ * @param value unit set to add
+ * @since 2024-08-16
+ * @since v1.0.0
+ */
+ public void addUnitSet(String name, List<LinearUnit> value) {
+ if (value.isEmpty())
+ throw new IllegalArgumentException("Unit sets must not be empty.");
+ for (final LinearUnit unit : value.subList(1, value.size())) {
+ if (!Objects.equals(unit.getDimension(), value.get(0).getDimension()))
+ throw new IllegalArgumentException(
+ "Unit sets must be all the same dimension, " + value
+ + " is not.");
+ }
+
+ this.unitSets.put(name, value);
+ }
+
+ /**
+ * Removes all units, unit sets, prefixes and dimensions from this database.
+ *
* @since 2022-02-26
+ * @since v0.4.0
*/
public void clear() {
this.dimensions.clear();
this.prefixes.clear();
this.prefixlessUnits.clear();
+ this.unitSets.clear();
}
/**
* Tests if the database has a unit dimension with this name.
- *
+ *
* @param name name to test
* @return if database contains name
* @since 2019-03-14
@@ -1509,7 +1478,7 @@ public final class UnitDatabase {
/**
* Tests if the database has a unit prefix with this name.
- *
+ *
* @param name name to test
* @return if database contains name
* @since 2019-01-13
@@ -1522,7 +1491,7 @@ public final class UnitDatabase {
/**
* Tests if the database has a unit with this name, taking prefixes into
* consideration
- *
+ *
* @param name name to test
* @return if database contains name
* @since 2019-01-13
@@ -1533,6 +1502,19 @@ public final class UnitDatabase {
}
/**
+ * Returns true iff there is a unit set with this name.
+ *
+ * @param name name to check for
+ * @return true iff there is a unit set with this name
+ *
+ * @since 2024-08-16
+ * @since v1.0.0
+ */
+ public boolean containsUnitSetName(String name) {
+ return this.unitSets.containsKey(name);
+ }
+
+ /**
* @return a map mapping dimension names to dimensions
* @since 2019-04-13
* @since v0.2.0
@@ -1544,10 +1526,11 @@ public final class UnitDatabase {
/**
* Evaluates a unit expression, following the same rules as
* {@link #getUnitFromExpression}.
- *
+ *
* @param expression expression to parse
* @return {@code LinearUnitValue} representing value of expression
* @since 2020-08-04
+ * @since v0.3.0
*/
public LinearUnitValue evaluateUnitExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
@@ -1556,37 +1539,13 @@ public final class UnitDatabase {
if (this.containsUnitName(expression))
return this.getLinearUnitValue(expression);
- // force operators to have spaces
- String modifiedExpression = expression;
- modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
- modifiedExpression = modifiedExpression.replaceAll("-", " - ");
-
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
-
- // the previous operation breaks negative numbers, fix them!
- // (i.e. -2 becomes - 2)
- // FIXME the previous operaton also breaks stuff like "1e-5"
- for (int i = 0; i < modifiedExpression.length(); i++) {
- if (modifiedExpression.charAt(i) == '-'
- && (i < 2 || Arrays.asList('+', '-', '*', '/', '|', '^')
- .contains(modifiedExpression.charAt(i - 2)))) {
- // found a broken negative number
- modifiedExpression = modifiedExpression.substring(0, i + 1)
- + modifiedExpression.substring(i + 2);
- }
- }
-
- return this.unitValueExpressionParser.parseExpression(modifiedExpression);
+ return this.unitValueExpressionParser
+ .parseExpression(formatExpression(expression));
}
/**
* Gets a unit dimension from the database using its name.
- *
+ *
* @param name dimension's name
* @return dimension
* @since 2019-03-14
@@ -1594,12 +1553,11 @@ public final class UnitDatabase {
*/
public ObjectProduct<BaseDimension> getDimension(final String name) {
Objects.requireNonNull(name, "name must not be null.");
- final ObjectProduct<BaseDimension> dimension = this.dimensions.get(name);
+ final var dimension = this.dimensions.get(name);
if (dimension == null)
throw new NoSuchElementException(
"No dimension with name \"" + name + "\".");
- else
- return dimension;
+ return dimension;
}
/**
@@ -1614,8 +1572,9 @@ public final class UnitDatabase {
* multiplication)</li>
* <li>The operator '^' which exponentiates. Exponents must be integers.</li>
* </ul>
- *
+ *
* @param expression expression to parse
+ * @return parsed unit dimension
* @throws IllegalArgumentException if the expression cannot be parsed
* @throws NullPointerException if expression is null
* @since 2019-04-13
@@ -1629,23 +1588,14 @@ public final class UnitDatabase {
if (this.containsDimensionName(expression))
return this.getDimension(expression);
- // force operators to have spaces
- String modifiedExpression = expression;
-
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
-
- return this.unitDimensionParser.parseExpression(modifiedExpression);
+ return this.unitDimensionParser
+ .parseExpression(formatExpression(expression));
}
/**
* Gets a unit. If it is linear, cast it to a LinearUnit and return it.
* Otherwise, throw an {@code IllegalArgumentException}.
- *
+ *
* @param name unit's name
* @return unit
* @since 2019-03-22
@@ -1662,29 +1612,28 @@ public final class UnitDatabase {
"Format nonlinear units like: unit(value).");
// solve the function
- final Unit unit = this.getUnit(parts.get(0));
- final double value = Double.parseDouble(
+ final var unit = this.getUnit(parts.get(0));
+ final var value = Double.parseDouble(
parts.get(1).substring(0, parts.get(1).length() - 1));
return LinearUnit.fromUnitValue(unit, value);
- } else {
- // get a linear unit
- final Unit unit = this.getUnit(name);
-
- if (unit instanceof LinearUnit)
- return (LinearUnit) unit;
- else
- throw new IllegalArgumentException(
- String.format("%s is not a linear unit.", name));
}
+ // get a linear unit
+ final var unit = this.getUnit(name);
+
+ if (unit instanceof LinearUnit)
+ return (LinearUnit) unit;
+ throw new IllegalArgumentException(
+ String.format("%s is not a linear unit.", name));
}
/**
* Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be
* converted to their base units.
- *
+ *
* @param name name of unit
* @return {@code LinearUnitValue} instance
* @since 2020-08-04
+ * @since v0.3.0
*/
LinearUnitValue getLinearUnitValue(final String name) {
try {
@@ -1698,7 +1647,7 @@ public final class UnitDatabase {
/**
* Gets a unit prefix from the database from its name
- *
+ *
* @param name prefix's name
* @return prefix
* @since 2019-01-10
@@ -1708,30 +1657,30 @@ public final class UnitDatabase {
try {
return UnitPrefix.valueOf(Double.parseDouble(name));
} catch (final NumberFormatException e) {
- final UnitPrefix prefix = this.prefixes.get(name);
+ final var prefix = this.prefixes.get(name);
if (prefix == null)
throw new NoSuchElementException(
"No prefix with name \"" + name + "\".");
- else
- return prefix;
+ return prefix;
}
}
/**
* Gets all of the prefixes that are on a unit name, in application order.
- *
+ *
* @param unitName name of unit
* @return prefixes
* @since 2020-08-26
+ * @since v0.3.0
*/
List<UnitPrefix> getPrefixesFromName(final String unitName) {
final List<UnitPrefix> prefixes = new ArrayList<>();
- String name = unitName;
+ var name = unitName;
while (!this.prefixlessUnits.containsKey(name)) {
// find the longest prefix
String longestPrefixName = null;
- int longestLength = name.length();
+ var longestLength = name.length();
while (longestPrefixName == null) {
longestLength--;
@@ -1744,7 +1693,7 @@ public final class UnitDatabase {
}
// longest prefix found!
- final UnitPrefix prefix = this.getPrefix(longestPrefixName);
+ final var prefix = this.getPrefix(longestPrefixName);
prefixes.add(0, prefix);
name = name.substring(longestLength);
}
@@ -1757,7 +1706,7 @@ public final class UnitDatabase {
* Currently, prefix expressions are much simpler than unit expressions: They
* are either a number or the name of another prefix
* </p>
- *
+ *
* @param expression expression to input
* @return prefix
* @throws IllegalArgumentException if expression cannot be parsed
@@ -1772,30 +1721,22 @@ public final class UnitDatabase {
if (this.containsUnitName(expression))
return this.getPrefix(expression);
- // force operators to have spaces
- String modifiedExpression = expression;
-
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
-
- return this.prefixExpressionParser.parseExpression(modifiedExpression);
+ return this.prefixExpressionParser
+ .parseExpression(formatExpression(expression));
}
/**
* @return the prefixRepetitionRule
* @since 2020-08-26
+ * @since v0.3.0
*/
- public final Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
+ public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
return this.prefixRepetitionRule;
}
/**
* Gets a unit from the database from its name, looking for prefixes.
- *
+ *
* @param name unit's name
* @return unit
* @since 2019-01-10
@@ -1803,27 +1744,28 @@ public final class UnitDatabase {
*/
public Unit getUnit(final String name) {
try {
- final double value = Double.parseDouble(name);
+ final var value = Double.parseDouble(name);
return Metric.ONE.times(value);
} catch (final NumberFormatException e) {
- final Unit unit = this.units.get(name);
+ final var unit = this.units.get(name);
if (unit == null)
throw new NoSuchElementException("No unit " + name);
- else if (unit.getPrimaryName().isEmpty())
+ if (unit.getPrimaryName().isEmpty())
return unit.withName(NameSymbol.ofName(name));
- else if (!unit.getPrimaryName().get().equals(name)) {
+ if (!unit.getPrimaryName().get().equals(name)) {
final Set<String> otherNames = new HashSet<>(unit.getOtherNames());
otherNames.add(unit.getPrimaryName().get());
return unit.withName(NameSymbol.ofNullable(name,
unit.getSymbol().orElse(null), otherNames));
- } else if (!unit.getOtherNames().contains(name)) {
+ }
+ if (!unit.getOtherNames().contains(name)) {
final Set<String> otherNames = new HashSet<>(unit.getOtherNames());
otherNames.add(name);
return unit.withName(
NameSymbol.ofNullable(unit.getPrimaryName().orElse(null),
unit.getSymbol().orElse(null), otherNames));
- } else
- return unit;
+ }
+ return unit;
}
}
@@ -1842,8 +1784,9 @@ public final class UnitDatabase {
* <li>A number which is multiplied or divided</li>
* </ul>
* This method only works with linear units.
- *
+ *
* @param expression expression to parse
+ * @return parsed unit
* @throws IllegalArgumentException if the expression cannot be parsed
* @throws NullPointerException if expression is null
* @since 2019-01-07
@@ -1856,31 +1799,51 @@ public final class UnitDatabase {
if (this.containsUnitName(expression))
return this.getUnit(expression);
- // force operators to have spaces
- String modifiedExpression = expression;
- modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
- modifiedExpression = modifiedExpression.replaceAll("-", " - ");
+ return this.unitExpressionParser
+ .parseExpression(formatExpression(expression));
+ }
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
+ /**
+ * Get a unit set from its name, throwing a {@link NoSuchElementException} if
+ * there is none.
+ *
+ * @param name name of unit set
+ * @return unit set with that name
+ *
+ * @since 2024-08-16
+ * @since v1.0.0
+ */
+ public List<LinearUnit> getUnitSet(String name) {
+ final var unitSet = this.unitSets.get(name);
+ if (unitSet == null)
+ throw new NoSuchElementException("No unit set with name " + name);
+ return unitSet;
+ }
- // the previous operation breaks negative numbers, fix them!
- // (i.e. -2 becomes - 2)
- for (int i = 0; i < modifiedExpression.length(); i++) {
- if (modifiedExpression.charAt(i) == '-'
- && (i < 2 || Arrays.asList('+', '-', '*', '/', '|', '^')
- .contains(modifiedExpression.charAt(i - 2)))) {
- // found a broken negative number
- modifiedExpression = modifiedExpression.substring(0, i + 1)
- + modifiedExpression.substring(i + 2);
- }
- }
+ /**
+ * Parses a semicolon-separated expression to get the unit set being used.
+ *
+ * @since 2024-08-22
+ * @since v1.0.0
+ */
+ List<LinearUnit> getUnitSetFromExpression(String expression) {
+ final var parts = expression.split(";");
+ final List<LinearUnit> units = new ArrayList<>(parts.length);
+ for (final String unitName : parts) {
+ final var unit = this.getUnitFromExpression(unitName.trim());
- return this.unitExpressionParser.parseExpression(modifiedExpression);
+ if (!(unit instanceof LinearUnit))
+ throw new IllegalArgumentException(String.format(
+ "Unit '%s' is in a unit-set expression, but is not linear.",
+ unitName));
+ if (units.size() > 0 && !unit.canConvertTo(units.get(0)))
+ throw new IllegalArgumentException(String.format(
+ "Units in expression '%s' have different dimensions.",
+ expression));
+
+ units.add((LinearUnit) unit);
+ }
+ return units;
}
/**
@@ -1901,26 +1864,32 @@ public final class UnitDatabase {
* no unit is found, an IllegalArgumentException is thrown. This is used to
* define initial units and ensure that the database contains them.</li>
* </ul>
- *
+ *
* @param file file to read
- * @throws IllegalArgumentException if the file cannot be parsed, found or
- * read
- * @throws NullPointerException if file is null
+ * @throws NullPointerException if file is null
+ * @return list of errors that happened when loading file
* @since 2019-01-13
* @since v0.1.0
*/
- public void loadDimensionFile(final Path file) {
+ public List<LoadingException> loadDimensionFile(final Path file) {
Objects.requireNonNull(file, "file must not be null.");
+ final List<LoadingException> errors = new ArrayList<>();
try {
- long lineCounter = 0;
+ var lineCounter = 0L;
for (final String line : Files.readAllLines(file)) {
- this.addDimensionFromLine(line, ++lineCounter);
+ try {
+ this.addDimensionFromLine(line, ++lineCounter);
+ } catch (IllegalArgumentException | NoSuchElementException e) {
+ errors.add(new LoadingException(lineCounter, line, file,
+ LoadingException.FileType.DIMENSION, e));
+ }
}
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Could not find file " + file, e);
} catch (final IOException e) {
throw new IllegalArgumentException("Could not read file " + file, e);
}
+ return errors;
}
/**
@@ -1928,15 +1897,26 @@ public final class UnitDatabase {
* {@link #loadDimensionFile}.
*
* @param stream stream to load from
+ * @return list of all errors that happened loading the stream
* @since 2021-03-27
+ * @since v0.3.0
*/
- public void loadDimensionsFromStream(final InputStream stream) {
- try (final Scanner scanner = new Scanner(stream)) {
- long lineCounter = 0;
+ public List<LoadingException> loadDimensionsFromStream(
+ final InputStream stream) {
+ final List<LoadingException> errors = new ArrayList<>();
+ try (final var scanner = new Scanner(stream)) {
+ var lineCounter = 0L;
while (scanner.hasNextLine()) {
- this.addDimensionFromLine(scanner.nextLine(), ++lineCounter);
+ final var line = scanner.nextLine();
+ try {
+ this.addDimensionFromLine(line, ++lineCounter);
+ } catch (IllegalArgumentException | NoSuchElementException e) {
+ errors.add(new LoadingException(lineCounter, line,
+ LoadingException.FileType.DIMENSION, e));
+ }
}
}
+ return errors;
}
/**
@@ -1956,26 +1936,32 @@ public final class UnitDatabase {
* no unit is found, an IllegalArgumentException is thrown. This is used to
* define initial units and ensure that the database contains them.</li>
* </ul>
- *
+ *
* @param file file to read
- * @throws IllegalArgumentException if the file cannot be parsed, found or
- * read
- * @throws NullPointerException if file is null
+ * @throws NullPointerException if file is null
+ * @return list of errors that happened when loading file
* @since 2019-01-13
* @since v0.1.0
*/
- public void loadUnitsFile(final Path file) {
+ public List<LoadingException> loadUnitsFile(final Path file) {
Objects.requireNonNull(file, "file must not be null.");
+ final List<LoadingException> errors = new ArrayList<>();
try {
- long lineCounter = 0;
+ var lineCounter = 0L;
for (final String line : Files.readAllLines(file)) {
- this.addUnitOrPrefixFromLine(line, ++lineCounter);
+ try {
+ this.addUnitOrPrefixFromLine(line, ++lineCounter);
+ } catch (IllegalArgumentException | NoSuchElementException e) {
+ errors.add(new LoadingException(lineCounter, line, file,
+ LoadingException.FileType.UNIT, e));
+ }
}
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Could not find file " + file, e);
} catch (final IOException e) {
throw new IllegalArgumentException("Could not read file " + file, e);
}
+ return errors;
}
/**
@@ -1983,15 +1969,25 @@ public final class UnitDatabase {
* {@link #loadUnitsFile}.
*
* @param stream stream to load from
+ * @return list of all errors that happened loading the stream
* @since 2021-03-27
+ * @since v0.3.0
*/
- public void loadUnitsFromStream(InputStream stream) {
- try (final Scanner scanner = new Scanner(stream)) {
- long lineCounter = 0;
+ public List<LoadingException> loadUnitsFromStream(InputStream stream) {
+ final List<LoadingException> errors = new ArrayList<>();
+ try (final var scanner = new Scanner(stream)) {
+ var lineCounter = 0L;
while (scanner.hasNextLine()) {
- this.addUnitOrPrefixFromLine(scanner.nextLine(), ++lineCounter);
+ final var line = scanner.nextLine();
+ try {
+ this.addUnitOrPrefixFromLine(line, ++lineCounter);
+ } catch (IllegalArgumentException | NoSuchElementException e) {
+ errors.add(new LoadingException(lineCounter, line,
+ LoadingException.FileType.UNIT, e));
+ }
}
}
+ return errors;
}
/**
@@ -2003,17 +1999,17 @@ public final class UnitDatabase {
public Map<String, UnitPrefix> prefixMap(boolean includeDuplicates) {
if (includeDuplicates)
return Collections.unmodifiableMap(this.prefixes);
- else
- return Collections.unmodifiableMap(ConditionalExistenceCollections
- .conditionalExistenceMap(this.prefixes,
- entry -> !isRemovableDuplicate(this.prefixes, entry)));
+ return Collections.unmodifiableMap(ConditionalExistenceCollections
+ .conditionalExistenceMap(this.prefixes,
+ entry -> !isRemovableDuplicate(this.prefixes, entry)));
}
/**
* @param prefixRepetitionRule the prefixRepetitionRule to set
* @since 2020-08-26
+ * @since v0.3.0
*/
- public final void setPrefixRepetitionRule(
+ public void setPrefixRepetitionRule(
Predicate<List<UnitPrefix>> prefixRepetitionRule) {
this.prefixRepetitionRule = prefixRepetitionRule;
}
@@ -2041,18 +2037,18 @@ public final class UnitDatabase {
* <p>
* Specifically, the operations that will throw an IllegalStateException if
* the map is infinite in size are:
+ * </p>
* <ul>
* <li>{@code unitMap.entrySet().toArray()} (either overloading)</li>
* <li>{@code unitMap.keySet().toArray()} (either overloading)</li>
* </ul>
- * </p>
* <p>
* Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's
* {@link PrefixedUnitMap#containsValue containsValue} and
* {@link PrefixedUnitMap#values() values()} methods currently ignore
* prefixes.
* </p>
- *
+ *
* @return a map mapping unit names to units, including prefixed names
* @since 2019-04-13
* @since v0.2.0
@@ -2073,10 +2069,17 @@ public final class UnitDatabase {
public Map<String, Unit> unitMapPrefixless(boolean includeDuplicates) {
if (includeDuplicates)
return Collections.unmodifiableMap(this.prefixlessUnits);
- else
- return Collections.unmodifiableMap(ConditionalExistenceCollections
- .conditionalExistenceMap(this.prefixlessUnits,
- entry -> !isRemovableDuplicate(this.prefixlessUnits,
- entry)));
+ return Collections.unmodifiableMap(ConditionalExistenceCollections
+ .conditionalExistenceMap(this.prefixlessUnits,
+ entry -> !isRemovableDuplicate(this.prefixlessUnits, entry)));
+ }
+
+ /**
+ * @return an unmodifiable map mapping names to unit sets
+ * @since 2024-08-16
+ * @since v1.0.0
+ */
+ public Map<String, List<LinearUnit>> unitSetMap() {
+ return Collections.unmodifiableMap(this.unitSets);
}
}
diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java
index 9035969..af106b9 100644
--- a/src/main/java/sevenUnits/unit/UnitPrefix.java
+++ b/src/main/java/sevenUnits/unit/UnitPrefix.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -25,17 +25,19 @@ import sevenUnits.utils.Nameable;
/**
* A prefix that can be applied to a {@code LinearUnit} to multiply it by some
* value
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
+ * @since v0.3.0
*/
public final class UnitPrefix implements Nameable {
/**
* Gets a {@code UnitPrefix} from a multiplier
- *
+ *
* @param multiplier multiplier of prefix
* @return prefix
* @since 2019-10-16
+ * @since v0.3.0
*/
public static UnitPrefix valueOf(final double multiplier) {
return new UnitPrefix(multiplier, NameSymbol.EMPTY);
@@ -43,11 +45,12 @@ public final class UnitPrefix implements Nameable {
/**
* Gets a {@code UnitPrefix} from a multiplier and a name
- *
+ *
* @param multiplier multiplier of prefix
* @param ns name(s) and symbol of prefix
* @return prefix
* @since 2019-10-16
+ * @since v0.3.0
* @throws NullPointerException if ns is null
*/
public static UnitPrefix valueOf(final double multiplier,
@@ -58,21 +61,23 @@ public final class UnitPrefix implements Nameable {
/**
* This prefix's name(s) and symbol.
- *
+ *
* @since 2022-04-16
+ * @since v0.4.0
*/
private final NameSymbol nameSymbol;
/**
* The number that this prefix multiplies units by
- *
+ *
* @since 2019-10-16
+ * @since v0.3.0
*/
private final double multiplier;
/**
* Creates the {@code DefaultUnitPrefix}.
- *
+ *
* @param multiplier
* @since 2019-01-14
* @since v0.2.0
@@ -84,10 +89,11 @@ public final class UnitPrefix implements Nameable {
/**
* Divides this prefix by a scalar
- *
+ *
* @param divisor number to divide by
* @return quotient of prefix and scalar
* @since 2019-10-16
+ * @since v0.3.0
*/
public UnitPrefix dividedBy(final double divisor) {
return valueOf(this.getMultiplier() / divisor);
@@ -95,7 +101,7 @@ public final class UnitPrefix implements Nameable {
/**
* Divides this prefix by {@code other}.
- *
+ *
* @param other prefix to divide by
* @return quotient of prefixes
* @since 2019-04-13
@@ -107,18 +113,32 @@ public final class UnitPrefix implements Nameable {
/**
* {@inheritDoc}
- *
+ *
* Uses the prefix's multiplier to determine equality.
*/
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
- if (obj == null)
+ if ((obj == null) || !(obj instanceof UnitPrefix))
return false;
- if (!(obj instanceof UnitPrefix))
+ final var other = (UnitPrefix) obj;
+ return Double.compare(this.getMultiplier(), other.getMultiplier()) == 0;
+ }
+
+ /**
+ * @param other prefix to compare to
+ * @return true iff this prefix and other are equal, ignoring small
+ * differences caused by floating-point error.
+ *
+ * @apiNote This method is not transitive, so it cannot be used as an equals
+ * method.
+ */
+ public boolean equalsApproximately(final UnitPrefix other) {
+ if (this == other)
+ return true;
+ if (other == null)
return false;
- final UnitPrefix other = (UnitPrefix) obj;
return DecimalComparison.equals(this.getMultiplier(),
other.getMultiplier());
}
@@ -126,6 +146,7 @@ public final class UnitPrefix implements Nameable {
/**
* @return prefix's multiplier
* @since 2019-11-26
+ * @since v0.3.0
*/
public double getMultiplier() {
return this.multiplier;
@@ -138,46 +159,55 @@ public final class UnitPrefix implements Nameable {
/**
* {@inheritDoc}
- *
+ *
* Uses the prefix's multiplier to determine a hash code.
*/
@Override
public int hashCode() {
- return DecimalComparison.hash(this.getMultiplier());
+ return Double.hashCode(this.getMultiplier());
}
/**
- * Multiplies this prefix by a scalar
- *
- * @param multiplicand number to multiply by
- * @return product of prefix and scalar
- * @since 2019-10-16
+ * Subtracts {@code other} from this prefix and returns the result.
+ *
+ * @param other prefix to subtract
+ * @return difference of prefixes
+ *
+ * @since 2024-03-03
+ * @since v0.5.0
*/
- public UnitPrefix times(final double multiplicand) {
- return valueOf(this.getMultiplier() * multiplicand);
+ public UnitPrefix minus(final UnitPrefix other) {
+ return valueOf(this.getMultiplier() - other.getMultiplier());
}
/**
* Adds {@code other} to this prefix and returns the result.
- *
+ *
+ * @param other prefix to add
+ * @return sum of prefixes
+ *
* @since 2024-03-03
+ * @since v0.5.0
*/
public UnitPrefix plus(final UnitPrefix other) {
return valueOf(this.getMultiplier() + other.getMultiplier());
}
/**
- * Subtracts {@code other} from this prefix and returns the result.
- *
- * @since 2024-03-03
+ * Multiplies this prefix by a scalar
+ *
+ * @param multiplicand number to multiply by
+ * @return product of prefix and scalar
+ * @since 2019-10-16
+ * @since v0.3.0
*/
- public UnitPrefix minus(final UnitPrefix other) {
- return valueOf(this.getMultiplier() - other.getMultiplier());
+ public UnitPrefix times(final double multiplicand) {
+ return valueOf(this.getMultiplier() * multiplicand);
}
/**
* Multiplies this prefix by {@code other}.
- *
+ *
* @param other prefix to multiply by
* @return product of prefixes
* @since 2019-04-13
@@ -189,7 +219,7 @@ public final class UnitPrefix implements Nameable {
/**
* Raises this prefix to an exponent.
- *
+ *
* @param exponent exponent to raise to
* @return result of exponentiation.
* @since 2019-04-13
@@ -199,25 +229,23 @@ public final class UnitPrefix implements Nameable {
return valueOf(Math.pow(this.getMultiplier(), exponent));
}
- /**
- * @return a string describing the prefix and its multiplier
- */
+ /** @return a string describing the prefix and its multiplier */
@Override
public String toString() {
if (this.getPrimaryName().isPresent())
return String.format("%s (\u00D7 %s)", this.getPrimaryName().get(),
this.multiplier);
- else if (this.getSymbol().isPresent())
+ if (this.getSymbol().isPresent())
return String.format("%s (\u00D7 %s)", this.getSymbol().get(),
this.multiplier);
- else
- return String.format("Unit Prefix (\u00D7 %s)", this.multiplier);
+ return String.format("Unit Prefix (\u00D7 %s)", this.multiplier);
}
/**
* @param ns name(s) and symbol to use
* @return copy of this prefix with provided name(s) and symbol
* @since 2019-11-26
+ * @since v0.3.0
* @throws NullPointerException if ns is null
*/
public UnitPrefix withName(final NameSymbol ns) {
diff --git a/src/main/java/sevenUnits/unit/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java
index 9a87288..b195f13 100644
--- a/src/main/java/sevenUnits/unit/UnitType.java
+++ b/src/main/java/sevenUnits/unit/UnitType.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -30,9 +30,15 @@ import java.util.function.Predicate;
* </ul>
*
* @since 2022-04-10
+ * @since v0.4.0
*/
public enum UnitType {
- METRIC, SEMI_METRIC, NON_METRIC;
+ /** Units that pass {@link Unit#isMetric} */
+ METRIC,
+ /** certain exceptions like the degree Celsius */
+ SEMI_METRIC,
+ /** Non-metric, non-excepted units */
+ NON_METRIC;
/**
* Determines which type a unit is. The type will be:
@@ -46,13 +52,13 @@ public enum UnitType {
* @param isSemiMetric predicate to determine if a unit is semi-metric
* @return type of unit
* @since 2022-04-18
+ * @since v0.4.0
*/
public static final UnitType getType(Unit u, Predicate<Unit> isSemiMetric) {
if (isSemiMetric.test(u))
return SEMI_METRIC;
- else if (u.isMetric())
+ if (u.isMetric())
return METRIC;
- else
- return NON_METRIC;
+ return NON_METRIC;
}
}
diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java
index 2d01831..e24b6e2 100644
--- a/src/main/java/sevenUnits/unit/UnitValue.java
+++ b/src/main/java/sevenUnits/unit/UnitValue.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -17,23 +17,23 @@
package sevenUnits.unit;
import java.util.Objects;
-import java.util.Optional;
import sevenUnits.utils.NameSymbol;
/**
* A value expressed in a unit.
- *
+ *
* Unless otherwise indicated, all methods in this class throw a
* {@code NullPointerException} when an argument is null.
- *
+ *
* @author Adrien Hopkins
* @since 2020-07-26
+ * @since v0.3.0
*/
public final class UnitValue {
/**
* Creates a {@code UnitValue} from a unit and the associated value.
- *
+ *
* @param unit unit to use
* @param value value to use
* @return {@code UnitValue} instance
@@ -56,67 +56,52 @@ public final class UnitValue {
}
/**
+ * @param other unit to try to convert to
* @return true if this value can be converted to {@code other}.
* @since 2020-10-01
+ * @since v0.3.0
*/
- public final boolean canConvertTo(Unit other) {
- return this.unit.canConvertTo(other);
- }
-
- /**
- * @return true if this value can be converted to {@code other}.
- * @since 2020-10-01
- */
- public final <W> boolean canConvertTo(Unitlike<W> other) {
+ public boolean canConvertTo(Unit other) {
return this.unit.canConvertTo(other);
}
/**
- * Returns a UnitlikeValue that represents the same value expressed in a
- * different unitlike form.
- *
- * @param other new unit to express value in
- * @return value expressed in {@code other}
- */
- public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo(
- U other) {
- return UnitlikeValue.of(other,
- this.unit.convertTo(other, this.getValue()));
- }
-
- /**
* Returns a UnitValue that represents the same value expressed in a
* different unit
- *
+ *
* @param other new unit to express value in
* @return value expressed in {@code other}
*/
- public final UnitValue convertTo(Unit other) {
+ public UnitValue convertTo(Unit other) {
return UnitValue.of(other,
this.getUnit().convertTo(other, this.getValue()));
}
/**
- * Returns this unit value represented as a {@code LinearUnitValue} with this
+ * Returns this unit value represented as a {@link LinearUnitValue} with this
* unit's base unit as the base.
*
* @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not
* needed.
+ * @return this unit as a {@link LinearUnitValue}
* @since 2020-09-29
+ * @since v0.3.0
*/
- public final LinearUnitValue convertToBase(NameSymbol ns) {
- final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns);
+ public LinearUnitValue convertToBase(NameSymbol ns) {
+ final var base = LinearUnit.getBase(this.unit).withName(ns);
return this.convertToLinear(base);
}
/**
+ * @param newUnit unit to use for this value
* @return a {@code LinearUnitValue} that is equivalent to this value. It
* will have zero uncertainty.
* @since 2020-09-29
+ * @since v0.3.0
*/
- public final LinearUnitValue convertToLinear(LinearUnit other) {
- return LinearUnitValue.getExact(other,
- this.getUnit().convertTo(other, this.getValue()));
+ public LinearUnitValue convertToLinear(LinearUnit newUnit) {
+ return LinearUnitValue.getExact(newUnit,
+ this.getUnit().convertTo(newUnit, this.getValue()));
}
/**
@@ -128,7 +113,7 @@ public final class UnitValue {
public boolean equals(Object obj) {
if (!(obj instanceof UnitValue))
return false;
- final UnitValue other = (UnitValue) obj;
+ final var other = (UnitValue) obj;
return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase())
&& Double.doubleToLongBits(
this.getUnit().convertToBase(this.getValue())) == Double
@@ -139,16 +124,18 @@ public final class UnitValue {
/**
* @return the unit
* @since 2020-09-29
+ * @since v0.3.0
*/
- public final Unit getUnit() {
+ public Unit getUnit() {
return this.unit;
}
/**
* @return the value
* @since 2020-09-29
+ * @since v0.3.0
*/
- public final double getValue() {
+ public double getValue() {
return this.value;
}
@@ -160,16 +147,15 @@ public final class UnitValue {
@Override
public String toString() {
- final Optional<String> primaryName = this.getUnit().getPrimaryName();
- final Optional<String> symbol = this.getUnit().getSymbol();
+ final var primaryName = this.getUnit().getPrimaryName();
+ final var symbol = this.getUnit().getSymbol();
if (primaryName.isEmpty() && symbol.isEmpty()) {
- final double baseValue = this.getUnit().convertToBase(this.getValue());
+ final var baseValue = this.getUnit().convertToBase(this.getValue());
return String.format("%s unnamed unit (= %s %s)", this.getValue(),
baseValue, this.getUnit().getBase()
.toString(unit -> unit.getSymbol().orElseThrow()));
- } else {
- final String unitName = symbol.orElse(primaryName.get());
- return this.getValue() + " " + unitName;
}
+ final var unitName = symbol.orElse(primaryName.get());
+ return this.getValue() + " " + unitName;
}
}
diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java
deleted file mode 100644
index fef424e..0000000
--- a/src/main/java/sevenUnits/unit/Unitlike.java
+++ /dev/null
@@ -1,262 +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 <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.unit;
-
-import java.util.HashMap;
-import java.util.Map;
-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;
-
-/**
- * An object that can convert a value between multiple forms (instances of the
- * object); like a unit but the "converted value" can be any type.
- *
- * @since 2020-09-07
- */
-public abstract class Unitlike<V> implements Nameable {
- /**
- * Returns a unitlike form from its base and the functions it uses to convert
- * to and from its base.
- *
- * @param base unitlike form's base
- * @param converterFrom function that accepts a value expressed in the
- * unitlike form's base and returns that value expressed
- * in this unitlike form.
- * @param converterTo function that accepts a value expressed in the
- * unitlike form and returns that value expressed in the
- * unit's base.
- * @return a unitlike form that uses the provided functions to convert.
- * @since 2020-09-07
- * @throws NullPointerException if any argument is null
- */
- public static final <W> Unitlike<W> fromConversionFunctions(
- final ObjectProduct<BaseUnit> base,
- final DoubleFunction<W> converterFrom,
- final ToDoubleFunction<W> converterTo) {
- return new FunctionalUnitlike<>(base, NameSymbol.EMPTY, converterFrom,
- converterTo);
- }
-
- /**
- * Returns a unitlike form from its base and the functions it uses to convert
- * to and from its base.
- *
- * @param base unitlike form's base
- * @param converterFrom function that accepts a value expressed in the
- * unitlike form's base and returns that value expressed
- * in this unitlike form.
- * @param converterTo function that accepts a value expressed in the
- * unitlike form and returns that value expressed in the
- * unit's base.
- * @param ns names and symbol of unit
- * @return a unitlike form that uses the provided functions to convert.
- * @since 2020-09-07
- * @throws NullPointerException if any argument is null
- */
- public static final <W> Unitlike<W> fromConversionFunctions(
- final ObjectProduct<BaseUnit> base,
- final DoubleFunction<W> converterFrom,
- final ToDoubleFunction<W> converterTo, final NameSymbol ns) {
- return new FunctionalUnitlike<>(base, ns, converterFrom, converterTo);
- }
-
- /**
- * The combination of units that this unit is based on.
- *
- * @since 2019-10-16
- */
- private final ObjectProduct<BaseUnit> unitBase;
-
- /**
- * This unit's name(s) and symbol
- *
- * @since 2020-09-07
- */
- private final NameSymbol nameSymbol;
-
- /**
- * Cache storing the result of getDimension()
- *
- * @since 2019-10-16
- */
- private transient ObjectProduct<BaseDimension> dimension = null;
-
- /**
- * @param unitBase
- * @since 2020-09-07
- */
- protected Unitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) {
- this.unitBase = Objects.requireNonNull(unitBase,
- "unitBase may not be null");
- this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null");
- }
-
- /**
- * Checks if a value expressed in this unitlike form can be converted to a
- * value expressed in {@code other}
- *
- * @param other unit or unitlike form to test with
- * @return true if they are compatible
- * @since 2019-01-13
- * @since v0.1.0
- * @throws NullPointerException if other is null
- */
- public final boolean canConvertTo(final Unit other) {
- Objects.requireNonNull(other, "other must not be null.");
- return Objects.equals(this.getBase(), other.getBase());
- }
-
- /**
- * Checks if a value expressed in this unitlike form can be converted to a
- * value expressed in {@code other}
- *
- * @param other unit or unitlike form to test with
- * @return true if they are compatible
- * @since 2019-01-13
- * @since v0.1.0
- * @throws NullPointerException if other is null
- */
- public final <W> boolean canConvertTo(final Unitlike<W> other) {
- Objects.requireNonNull(other, "other must not be null.");
- return Objects.equals(this.getBase(), other.getBase());
- }
-
- protected abstract V convertFromBase(double value);
-
- /**
- * Converts a value expressed in this unitlike form to a value expressed in
- * {@code other}.
- *
- * @implSpec If conversion is possible, this implementation returns
- * {@code other.convertFromBase(this.convertToBase(value))}.
- * Therefore, overriding either of those methods will change the
- * output of this method.
- *
- * @param other unit to convert to
- * @param value value to convert
- * @return converted value
- * @since 2019-05-22
- * @throws IllegalArgumentException if {@code other} is incompatible for
- * conversion with this unitlike form (as
- * tested by {@link Unit#canConvertTo}).
- * @throws NullPointerException if other is null
- */
- public final double convertTo(final Unit other, final V value) {
- Objects.requireNonNull(other, "other must not be null.");
- if (this.canConvertTo(other))
- return other.convertFromBase(this.convertToBase(value));
- else
- throw new IllegalArgumentException(
- String.format("Cannot convert from %s to %s.", this, other));
- }
-
- /**
- * Converts a value expressed in this unitlike form to a value expressed in
- * {@code other}.
- *
- * @implSpec If conversion is possible, this implementation returns
- * {@code other.convertFromBase(this.convertToBase(value))}.
- * Therefore, overriding either of those methods will change the
- * output of this method.
- *
- * @param other unitlike form to convert to
- * @param value value to convert
- * @param <W> type of value to convert to
- * @return converted value
- * @since 2020-09-07
- * @throws IllegalArgumentException if {@code other} is incompatible for
- * conversion with this unitlike form (as
- * tested by {@link Unit#canConvertTo}).
- * @throws NullPointerException if other is null
- */
- public final <W> W convertTo(final Unitlike<W> other, final V value) {
- Objects.requireNonNull(other, "other must not be null.");
- if (this.canConvertTo(other))
- return other.convertFromBase(this.convertToBase(value));
- else
- throw new IllegalArgumentException(
- String.format("Cannot convert from %s to %s.", this, other));
- }
-
- protected abstract double convertToBase(V value);
-
- /**
- * @return combination of units that this unit is based on
- * @since 2018-12-22
- * @since v0.1.0
- */
- public final ObjectProduct<BaseUnit> getBase() {
- return this.unitBase;
- }
-
- /**
- * @return dimension measured by this unit
- * @since 2018-12-22
- * @since v0.1.0
- */
- public final ObjectProduct<BaseDimension> getDimension() {
- if (this.dimension == null) {
- final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap();
- final Map<BaseDimension, Integer> dimensionMap = new HashMap<>();
-
- for (final BaseUnit key : mapping.keySet()) {
- dimensionMap.put(key.getBaseDimension(), mapping.get(key));
- }
-
- this.dimension = ObjectProduct.fromExponentMapping(dimensionMap);
- }
- return this.dimension;
- }
-
- /**
- * @return the nameSymbol
- * @since 2020-09-07
- */
- @Override
- public final NameSymbol getNameSymbol() {
- return this.nameSymbol;
- }
-
- @Override
- public String toString() {
- return this.getPrimaryName().orElse("Unnamed unitlike form")
- + (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()));
- }
-
- /**
- * @param ns name(s) and symbol to use
- * @return a copy of this unitlike form with provided name(s) and symbol
- * @since 2020-09-07
- * @throws NullPointerException if ns is null
- */
- public Unitlike<V> withName(final NameSymbol ns) {
- return fromConversionFunctions(this.getBase(), this::convertFromBase,
- this::convertToBase,
- Objects.requireNonNull(ns, "ns must not be null."));
- }
-}
diff --git a/src/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java
deleted file mode 100644
index ad0d1ea..0000000
--- a/src/main/java/sevenUnits/unit/UnitlikeValue.java
+++ /dev/null
@@ -1,176 +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 <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.unit;
-
-import java.util.Optional;
-
-import sevenUnits.utils.NameSymbol;
-
-/**
- *
- * @since 2020-09-07
- */
-final class UnitlikeValue<T extends Unitlike<V>, V> {
- /**
- * Gets a {@code UnitlikeValue<V>}.
- *
- * @since 2020-10-02
- */
- public static <T extends Unitlike<V>, V> UnitlikeValue<T, V> of(T unitlike,
- V value) {
- return new UnitlikeValue<>(unitlike, value);
- }
-
- private final T unitlike;
- private final V value;
-
- /**
- * @param unitlike
- * @param value
- * @since 2020-09-07
- */
- private UnitlikeValue(T unitlike, V value) {
- this.unitlike = unitlike;
- this.value = value;
- }
-
- /**
- * @return true if this value can be converted to {@code other}.
- * @since 2020-10-01
- */
- public final boolean canConvertTo(Unit other) {
- return this.unitlike.canConvertTo(other);
- }
-
- /**
- * @return true if this value can be converted to {@code other}.
- * @since 2020-10-01
- */
- public final <W> boolean canConvertTo(Unitlike<W> other) {
- return this.unitlike.canConvertTo(other);
- }
-
- /**
- * Returns a UnitlikeValue that represents the same value expressed in a
- * different unitlike form.
- *
- * @param other new unit to express value in
- * @return value expressed in {@code other}
- */
- public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo(
- U other) {
- return UnitlikeValue.of(other,
- this.unitlike.convertTo(other, this.getValue()));
- }
-
- /**
- * Returns a UnitValue that represents the same value expressed in a
- * different unit
- *
- * @param other new unit to express value in
- * @return value expressed in {@code other}
- */
- public final UnitValue convertTo(Unit other) {
- return UnitValue.of(other,
- this.unitlike.convertTo(other, this.getValue()));
- }
-
- /**
- * Returns this unit value represented as a {@code LinearUnitValue} with this
- * unit's base unit as the base.
- *
- * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not
- * needed.
- * @since 2020-09-29
- */
- public final LinearUnitValue convertToBase(NameSymbol ns) {
- final LinearUnit base = LinearUnit.getBase(this.unitlike).withName(ns);
- return this.convertToLinear(base);
- }
-
- /**
- * @return a {@code LinearUnitValue} that is equivalent to this value. It
- * will have zero uncertainty.
- * @since 2020-09-29
- */
- public final LinearUnitValue convertToLinear(LinearUnit other) {
- return LinearUnitValue.getExact(other,
- this.getUnitlike().convertTo(other, this.getValue()));
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (!(obj instanceof UnitlikeValue))
- return false;
- final UnitlikeValue<?, ?> other = (UnitlikeValue<?, ?>) obj;
- if (this.getUnitlike() == null) {
- if (other.getUnitlike() != null)
- return false;
- } else if (!this.getUnitlike().equals(other.getUnitlike()))
- return false;
- if (this.getValue() == null) {
- if (other.getValue() != null)
- return false;
- } else if (!this.getValue().equals(other.getValue()))
- return false;
- return true;
- }
-
- /**
- * @return the unitlike
- * @since 2020-09-29
- */
- public final Unitlike<V> getUnitlike() {
- return this.unitlike;
- }
-
- /**
- * @return the value
- * @since 2020-09-29
- */
- public final V getValue() {
- return this.value;
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result
- + (this.getUnitlike() == null ? 0 : this.getUnitlike().hashCode());
- result = prime * result
- + (this.getValue() == null ? 0 : this.getValue().hashCode());
- return result;
- }
-
- @Override
- public String toString() {
- final Optional<String> primaryName = this.getUnitlike().getPrimaryName();
- final Optional<String> symbol = this.getUnitlike().getSymbol();
- if (primaryName.isEmpty() && symbol.isEmpty()) {
- final double baseValue = this.getUnitlike()
- .convertToBase(this.getValue());
- return String.format("%s unnamed unit (= %s %s)", this.getValue(),
- baseValue, this.getUnitlike().getBase());
- } else {
- final String unitName = symbol.orElse(primaryName.get());
- return this.getValue() + " " + unitName;
- }
- }
-}
diff --git a/src/main/java/sevenUnits/unit/package-info.java b/src/main/java/sevenUnits/unit/package-info.java
index 6aedb9d..c650b58 100644
--- a/src/main/java/sevenUnits/unit/package-info.java
+++ b/src/main/java/sevenUnits/unit/package-info.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019-2025 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,7 +16,7 @@
*/
/**
* Everything to do with the units that make up Unit Converter.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
* @since v0.1.0
diff --git a/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java b/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java
index b71a4e0..6244ee6 100644
--- a/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java
+++ b/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2024, 2025 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
@@ -49,18 +49,19 @@ import java.util.function.Predicate;
* Other than that, <i>the only difference between the provided collections and
* the returned collections are that elements don't exist if they don't pass the
* provided condition</i>.
- *
- *
+ *
+ *
* @author Adrien Hopkins
* @since 2019-10-17
+ * @since v0.3.0
*/
-// TODO add conditional existence Lists and Sorted/Navigable Sets/Maps
public final class ConditionalExistenceCollections {
/**
* Elements in this collection only exist if they meet a condition.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-17
+ * @since v0.3.0
* @param <E> type of element in collection
*/
static final class ConditionalExistenceCollection<E>
@@ -70,10 +71,11 @@ public final class ConditionalExistenceCollections {
/**
* Creates the {@code ConditionalExistenceCollection}.
- *
+ *
* @param collection
* @param existenceCondition
* @since 2019-10-17
+ * @since v0.3.0
*/
private ConditionalExistenceCollection(final Collection<E> collection,
final Predicate<E> existenceCondition) {
@@ -101,7 +103,7 @@ public final class ConditionalExistenceCollections {
// instance of E
// therefore this cast will always work
@SuppressWarnings("unchecked")
- final E e = (E) o;
+ final var e = (E) o;
return this.existenceCondition.test(e);
}
@@ -116,7 +118,7 @@ public final class ConditionalExistenceCollections {
public boolean remove(final Object o) {
// remove() must be first in the && statement, otherwise it may not
// execute
- final boolean containedObject = this.contains(o);
+ final var containedObject = this.contains(o);
return this.collection.remove(o) && containedObject;
}
@@ -147,9 +149,10 @@ public final class ConditionalExistenceCollections {
/**
* Elements in this wrapper iterator only exist if they pass a condition.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-17
+ * @since v0.3.0
* @param <E> type of elements in iterator
*/
static final class ConditionalExistenceIterator<E> implements Iterator<E> {
@@ -160,10 +163,11 @@ public final class ConditionalExistenceCollections {
/**
* Creates the {@code ConditionalExistenceIterator}.
- *
+ *
* @param iterator
* @param condition
* @since 2019-10-17
+ * @since v0.3.0
*/
private ConditionalExistenceIterator(final Iterator<E> iterator,
final Predicate<E> condition) {
@@ -174,8 +178,9 @@ public final class ConditionalExistenceCollections {
/**
* Gets the next element, and sets nextElement and hasNext accordingly.
- *
+ *
* @since 2019-10-17
+ * @since v0.3.0
*/
private void getAndSetNextElement() {
do {
@@ -197,11 +202,11 @@ public final class ConditionalExistenceCollections {
@Override
public E next() {
if (this.hasNext()) {
- final E next = this.nextElement;
+ final var next = this.nextElement;
this.getAndSetNextElement();
return next;
- } else
- throw new NoSuchElementException();
+ }
+ throw new NoSuchElementException();
}
@Override
@@ -212,9 +217,10 @@ public final class ConditionalExistenceCollections {
/**
* Mappings in this map only exist if the entry passes some condition.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-17
+ * @since v0.3.0
* @param <K> key type
* @param <V> value type
*/
@@ -224,10 +230,11 @@ public final class ConditionalExistenceCollections {
/**
* Creates the {@code ConditionalExistenceMap}.
- *
+ *
* @param map
* @param entryExistenceCondition
* @since 2019-10-17
+ * @since v0.3.0
*/
private ConditionalExistenceMap(final Map<K, V> map,
final Predicate<Entry<K, V>> entryExistenceCondition) {
@@ -243,10 +250,10 @@ public final class ConditionalExistenceCollections {
// only instances of K have mappings in the backing map
// since we know that key is a valid key, it must be an instance of K
@SuppressWarnings("unchecked")
- final K keyAsK = (K) key;
+ final var keyAsK = (K) key;
// get and test entry
- final V value = this.map.get(key);
+ final var value = this.map.get(key);
final Entry<K, V> entry = new SimpleEntry<>(keyAsK, value);
return this.entryExistenceCondition.test(entry);
}
@@ -262,7 +269,7 @@ public final class ConditionalExistenceCollections {
return this.containsKey(key) ? this.map.get(key) : null;
}
- private final Entry<K, V> getEntry(K key) {
+ private Entry<K, V> getEntry(K key) {
return new Entry<>() {
@Override
public K getKey() {
@@ -289,7 +296,7 @@ public final class ConditionalExistenceCollections {
@Override
public V put(final K key, final V value) {
- final V oldValue = this.map.put(key, value);
+ final var oldValue = this.map.put(key, value);
// get and test entry
final Entry<K, V> entry = new SimpleEntry<>(key, oldValue);
@@ -298,7 +305,7 @@ public final class ConditionalExistenceCollections {
@Override
public V remove(final Object key) {
- final V oldValue = this.map.remove(key);
+ final var oldValue = this.map.remove(key);
return this.containsKey(key) ? oldValue : null;
}
@@ -311,9 +318,10 @@ public final class ConditionalExistenceCollections {
/**
* Elements in this set only exist if a certain condition is true.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-17
+ * @since v0.3.0
* @param <E> type of element in set
*/
static final class ConditionalExistenceSet<E> extends AbstractSet<E> {
@@ -322,10 +330,11 @@ public final class ConditionalExistenceCollections {
/**
* Creates the {@code ConditionalNonexistenceSet}.
- *
+ *
* @param set set to use
* @param existenceCondition condition where element exists
* @since 2019-10-17
+ * @since v0.3.0
*/
private ConditionalExistenceSet(final Set<E> set,
final Predicate<E> existenceCondition) {
@@ -359,7 +368,7 @@ public final class ConditionalExistenceCollections {
// of E
// therefore this cast will always work
@SuppressWarnings("unchecked")
- final E e = (E) o;
+ final var e = (E) o;
return this.existenceCondition.test(e);
}
@@ -374,7 +383,7 @@ public final class ConditionalExistenceCollections {
public boolean remove(final Object o) {
// remove() must be first in the && statement, otherwise it may not
// execute
- final boolean containedObject = this.contains(o);
+ final var containedObject = this.contains(o);
return this.set.remove(o) && containedObject;
}
@@ -405,14 +414,15 @@ public final class ConditionalExistenceCollections {
/**
* Elements in the returned wrapper collection are ignored if they don't pass
* a condition.
- *
+ *
* @param <E> type of elements in collection
* @param collection collection to wrap
* @param existenceCondition elements only exist if this returns true
* @return wrapper collection
* @since 2019-10-17
+ * @since v0.3.0
*/
- public static final <E> Collection<E> conditionalExistenceCollection(
+ public static <E> Collection<E> conditionalExistenceCollection(
final Collection<E> collection,
final Predicate<E> existenceCondition) {
return new ConditionalExistenceCollection<>(collection,
@@ -422,14 +432,15 @@ public final class ConditionalExistenceCollections {
/**
* Elements in the returned wrapper iterator are ignored if they don't pass a
* condition.
- *
+ *
* @param <E> type of elements in iterator
* @param iterator iterator to wrap
* @param existenceCondition elements only exist if this returns true
* @return wrapper iterator
* @since 2019-10-17
+ * @since v0.3.0
*/
- public static final <E> Iterator<E> conditionalExistenceIterator(
+ public static <E> Iterator<E> conditionalExistenceIterator(
final Iterator<E> iterator, final Predicate<E> existenceCondition) {
return new ConditionalExistenceIterator<>(iterator, existenceCondition);
}
@@ -437,16 +448,16 @@ public final class ConditionalExistenceCollections {
/**
* Mappings in the returned wrapper map are ignored if the corresponding
* entry doesn't pass a condition
- *
+ *
* @param <K> type of key in map
* @param <V> type of value in map
* @param map map to wrap
* @param entryExistenceCondition mappings only exist if this returns true
* @return wrapper map
* @since 2019-10-17
+ * @since v0.3.0
*/
- public static final <K, V> Map<K, V> conditionalExistenceMap(
- final Map<K, V> map,
+ public static <K, V> Map<K, V> conditionalExistenceMap(final Map<K, V> map,
final Predicate<Entry<K, V>> entryExistenceCondition) {
return new ConditionalExistenceMap<>(map, entryExistenceCondition);
}
@@ -454,14 +465,15 @@ public final class ConditionalExistenceCollections {
/**
* Elements in the returned wrapper set are ignored if they don't pass a
* condition.
- *
+ *
* @param <E> type of elements in set
* @param set set to wrap
* @param existenceCondition elements only exist if this returns true
* @return wrapper set
* @since 2019-10-17
+ * @since v0.3.0
*/
- public static final <E> Set<E> conditionalExistenceSet(final Set<E> set,
+ public static <E> Set<E> conditionalExistenceSet(final Set<E> set,
final Predicate<E> existenceCondition) {
return new ConditionalExistenceSet<>(set, existenceCondition);
}
diff --git a/src/main/java/sevenUnits/utils/DecimalComparison.java b/src/main/java/sevenUnits/utils/DecimalComparison.java
index 0515b6b..1366fd3 100644
--- a/src/main/java/sevenUnits/utils/DecimalComparison.java
+++ b/src/main/java/sevenUnits/utils/DecimalComparison.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2024, 2025 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
@@ -20,7 +20,7 @@ import java.math.BigDecimal;
/**
* A class that contains methods to compare float and double values.
- *
+ *
* @author Adrien Hopkins
* @since 2019-03-18
* @since v0.2.0
@@ -29,7 +29,7 @@ public final class DecimalComparison {
/**
* The value used for double comparison. If two double values are within this
* value multiplied by the larger value, they are considered equal.
- *
+ *
* @since 2019-03-18
* @since v0.2.0
*/
@@ -38,7 +38,7 @@ public final class DecimalComparison {
/**
* The value used for float comparison. If two float values are within this
* value multiplied by the larger value, they are considered equal.
- *
+ *
* @since 2019-03-18
* @since v0.2.0
*/
@@ -63,21 +63,20 @@ public final class DecimalComparison {
* <li>Use {@link BigDecimal} instead of {@code double} (this will make a
* violation of transitivity 100% impossible)
* </ol>
- *
+ *
* @param a first value to test
* @param b second value to test
* @return whether they are equal
* @since 2019-03-18
* @since v0.2.0
- * @see #hashCode(double)
*/
- public static final boolean equals(final double a, final double b) {
+ public static boolean equals(final double a, final double b) {
return DecimalComparison.equals(a, b, DOUBLE_EPSILON);
}
/**
* Tests for double equality using a custom epsilon value.
- *
+ *
* <p>
* <strong>WARNING: </strong>this method is not technically transitive. If a
* and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
@@ -94,7 +93,7 @@ public final class DecimalComparison {
* <li>Use {@link BigDecimal} instead of {@code double} (this will make a
* violation of transitivity 100% impossible)
* </ol>
- *
+ *
* @param a first value to test
* @param b second value to test
* @param epsilon allowed difference
@@ -102,14 +101,14 @@ public final class DecimalComparison {
* @since 2019-03-18
* @since v0.2.0
*/
- public static final boolean equals(final double a, final double b,
+ public static boolean equals(final double a, final double b,
final double epsilon) {
return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
}
/**
* Tests for equality of float values using {@link #FLOAT_EPSILON}.
- *
+ *
* <p>
* <strong>WARNING: </strong>this method is not technically transitive. If a
* and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
@@ -126,20 +125,20 @@ public final class DecimalComparison {
* <li>Use {@link BigDecimal} instead of {@code float} (this will make a
* violation of transitivity 100% impossible)
* </ol>
- *
+ *
* @param a first value to test
* @param b second value to test
* @return whether they are equal
* @since 2019-03-18
* @since v0.2.0
*/
- public static final boolean equals(final float a, final float b) {
+ public static boolean equals(final float a, final float b) {
return DecimalComparison.equals(a, b, FLOAT_EPSILON);
}
/**
* Tests for float equality using a custom epsilon value.
- *
+ *
* <p>
* <strong>WARNING: </strong>this method is not technically transitive. If a
* and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
@@ -156,7 +155,7 @@ public final class DecimalComparison {
* <li>Use {@link BigDecimal} instead of {@code float} (this will make a
* violation of transitivity 100% impossible)
* </ol>
- *
+ *
* @param a first value to test
* @param b second value to test
* @param epsilon allowed difference
@@ -164,7 +163,7 @@ public final class DecimalComparison {
* @since 2019-03-18
* @since v0.2.0
*/
- public static final boolean equals(final float a, final float b,
+ public static boolean equals(final float a, final float b,
final float epsilon) {
return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
}
@@ -189,14 +188,14 @@ public final class DecimalComparison {
* <li>Use {@link BigDecimal} instead of {@code double} (this will make a
* violation of transitivity 100% impossible)
* </ol>
- *
+ *
* @param a first value to test
* @param b second value to test
* @return whether they are equal
* @since 2020-09-07
- * @see #hashCode(double)
+ * @since v0.3.0
*/
- public static final boolean equals(final UncertainDouble a,
+ public static boolean equals(final UncertainDouble a,
final UncertainDouble b) {
return DecimalComparison.equals(a.value(), b.value())
&& DecimalComparison.equals(a.uncertainty(), b.uncertainty());
@@ -204,7 +203,7 @@ public final class DecimalComparison {
/**
* Tests for {@code UncertainDouble} equality using a custom epsilon value.
- *
+ *
* <p>
* <strong>WARNING: </strong>this method is not technically transitive. If a
* and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
@@ -221,7 +220,7 @@ public final class DecimalComparison {
* <li>Use {@link BigDecimal} instead of {@code double} (this will make a
* violation of transitivity 100% impossible)
* </ol>
- *
+ *
* @param a first value to test
* @param b second value to test
* @param epsilon allowed difference
@@ -229,25 +228,13 @@ public final class DecimalComparison {
* @since 2019-03-18
* @since v0.2.0
*/
- public static final boolean equals(final UncertainDouble a,
+ public static boolean equals(final UncertainDouble a,
final UncertainDouble b, final double epsilon) {
return DecimalComparison.equals(a.value(), b.value(), epsilon)
&& DecimalComparison.equals(a.uncertainty(), b.uncertainty(),
epsilon);
}
- /**
- * Takes the hash code of doubles. Values that are equal according to
- * {@link #equals(double, double)} will have the same hash code.
- *
- * @param d double to hash
- * @return hash code of double
- * @since 2019-10-16
- */
- public static final int hash(final double d) {
- return Float.hashCode((float) d);
- }
-
// You may NOT get any DecimalComparison instances
private DecimalComparison() {
throw new AssertionError();
diff --git a/src/main/java/sevenUnits/utils/ExpressionParser.java b/src/main/java/sevenUnits/utils/ExpressionParser.java
index e248ff0..03c763c 100644
--- a/src/main/java/sevenUnits/utils/ExpressionParser.java
+++ b/src/main/java/sevenUnits/utils/ExpressionParser.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019, 2024 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2024, 2025 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
@@ -31,7 +31,7 @@ import java.util.function.UnaryOperator;
/**
* An object that can parse expressions with unary or binary operators.
- *
+ *
* @author Adrien Hopkins
* @param <T> type of object that exists in parsed expressions
* @since 2019-03-14
@@ -40,7 +40,7 @@ import java.util.function.UnaryOperator;
public final class ExpressionParser<T> {
/**
* A builder that can create {@code ExpressionParser<T>} instances.
- *
+ *
* @author Adrien Hopkins
* @param <T> type of object that exists in parsed expressions
* @since 2019-03-17
@@ -51,7 +51,7 @@ public final class ExpressionParser<T> {
* A function that obtains a parseable object from a string. For example,
* an integer {@code ExpressionParser} would use
* {@code Integer::parseInt}.
- *
+ *
* @since 2019-03-14
* @since v0.2.0
*/
@@ -59,7 +59,7 @@ public final class ExpressionParser<T> {
/**
* The function of the space as an operator (like 3 x y)
- *
+ *
* @since 2019-03-22
* @since v0.2.0
*/
@@ -68,7 +68,7 @@ public final class ExpressionParser<T> {
/**
* A map mapping operator strings to operator functions, for unary
* operators.
- *
+ *
* @since 2019-03-14
* @since v0.2.0
*/
@@ -77,7 +77,7 @@ public final class ExpressionParser<T> {
/**
* A map mapping operator strings to operator functions, for binary
* operators.
- *
+ *
* @since 2019-03-14
* @since v0.2.0
*/
@@ -85,14 +85,15 @@ public final class ExpressionParser<T> {
/**
* A map mapping operator strings to numeric functions.
- *
+ *
* @since 2024-03-23
+ * @since v0.5.0
*/
private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators;
/**
* Creates the {@code Builder}.
- *
+ *
* @param objectObtainer a function that can turn strings into objects of
* the type handled by the parser.
* @throws NullPointerException if {@code objectObtainer} is null
@@ -109,7 +110,7 @@ public final class ExpressionParser<T> {
/**
* Adds a binary operator to the builder.
- *
+ *
* @param text text used to reference the operator, like '+'
* @param operator operator to add
* @param priority operator's priority, which determines which operators
@@ -142,7 +143,7 @@ public final class ExpressionParser<T> {
/**
* Adds a two-argument operator where the second operator is a number.
* This is used for operations like vector scaling and exponentation.
- *
+ *
* @param text text used to reference the operator, like '^'
* @param operator operator to add
* @param priority operator's priority, which determines which operators
@@ -172,7 +173,7 @@ public final class ExpressionParser<T> {
/**
* Adds a function for spaces. You must use the text of an existing binary
* operator.
- *
+ *
* @param operator text of operator to use
* @return this builder
* @since 2019-03-22
@@ -191,7 +192,7 @@ public final class ExpressionParser<T> {
/**
* Adds a unary operator to the builder.
- *
+ *
* @param text text used to reference the operator, like '-'
* @param operator operator to add
* @param priority operator's priority, which determines which operators
@@ -235,18 +236,18 @@ public final class ExpressionParser<T> {
/**
* A binary operator with a priority field that determines which operators
* apply first.
- *
+ *
* @author Adrien Hopkins
* @param <T> type of operand and result
* @since 2019-03-17
* @since v0.2.0
*/
- private static abstract class PriorityBinaryOperator<T>
- implements BinaryOperator<T>, Comparable<PriorityBinaryOperator<T>> {
+ private static abstract class PriorityBiFunction<T, U, R> implements
+ BiFunction<T, U, R>, Comparable<PriorityBiFunction<T, U, R>> {
/**
* The operator's priority. Higher-priority operators are applied before
* lower-priority operators
- *
+ *
* @since 2019-03-17
* @since v0.2.0
*/
@@ -254,33 +255,32 @@ public final class ExpressionParser<T> {
/**
* Creates the {@code PriorityBinaryOperator}.
- *
+ *
* @param priority operator's priority
* @since 2019-03-17
* @since v0.2.0
*/
- public PriorityBinaryOperator(final int priority) {
+ public PriorityBiFunction(final int priority) {
this.priority = priority;
}
/**
* Compares this object to another by priority.
- *
+ *
* <p>
* {@inheritDoc}
* </p>
- *
+ *
* @since 2019-03-17
* @since v0.2.0
*/
@Override
- public int compareTo(final PriorityBinaryOperator<T> o) {
+ public int compareTo(final PriorityBiFunction<T, U, R> o) {
if (this.priority < o.priority)
return -1;
- else if (this.priority > o.priority)
+ if (this.priority > o.priority)
return 1;
- else
- return 0;
+ return 0;
}
/**
@@ -296,18 +296,18 @@ public final class ExpressionParser<T> {
/**
* A binary operator with a priority field that determines which operators
* apply first.
- *
+ *
* @author Adrien Hopkins
* @param <T> type of operand and result
* @since 2019-03-17
* @since v0.2.0
*/
- private static abstract class PriorityBiFunction<T, U, R> implements
- BiFunction<T, U, R>, Comparable<PriorityBiFunction<T, U, R>> {
+ private static abstract class PriorityBinaryOperator<T>
+ implements BinaryOperator<T>, Comparable<PriorityBinaryOperator<T>> {
/**
* The operator's priority. Higher-priority operators are applied before
* lower-priority operators
- *
+ *
* @since 2019-03-17
* @since v0.2.0
*/
@@ -315,33 +315,32 @@ public final class ExpressionParser<T> {
/**
* Creates the {@code PriorityBinaryOperator}.
- *
+ *
* @param priority operator's priority
* @since 2019-03-17
* @since v0.2.0
*/
- public PriorityBiFunction(final int priority) {
+ public PriorityBinaryOperator(final int priority) {
this.priority = priority;
}
/**
* Compares this object to another by priority.
- *
+ *
* <p>
* {@inheritDoc}
* </p>
- *
+ *
* @since 2019-03-17
* @since v0.2.0
*/
@Override
- public int compareTo(final PriorityBiFunction<T, U, R> o) {
+ public int compareTo(final PriorityBinaryOperator<T> o) {
if (this.priority < o.priority)
return -1;
- else if (this.priority > o.priority)
+ if (this.priority > o.priority)
return 1;
- else
- return 0;
+ return 0;
}
/**
@@ -357,7 +356,7 @@ public final class ExpressionParser<T> {
/**
* A unary operator with a priority field that determines which operators
* apply first.
- *
+ *
* @author Adrien Hopkins
* @param <T> type of operand and result
* @since 2019-03-17
@@ -368,7 +367,7 @@ public final class ExpressionParser<T> {
/**
* The operator's priority. Higher-priority operators are applied before
* lower-priority operators
- *
+ *
* @since 2019-03-17
* @since v0.2.0
*/
@@ -376,7 +375,7 @@ public final class ExpressionParser<T> {
/**
* Creates the {@code PriorityUnaryOperator}.
- *
+ *
* @param priority operator's priority
* @since 2019-03-17
* @since v0.2.0
@@ -387,11 +386,11 @@ public final class ExpressionParser<T> {
/**
* Compares this object to another by priority.
- *
+ *
* <p>
* {@inheritDoc}
* </p>
- *
+ *
* @since 2019-03-17
* @since v0.2.0
*/
@@ -399,10 +398,9 @@ public final class ExpressionParser<T> {
public int compareTo(final PriorityUnaryOperator<T> o) {
if (this.priority < o.priority)
return -1;
- else if (this.priority > o.priority)
+ if (this.priority > o.priority)
return 1;
- else
- return 0;
+ return 0;
}
/**
@@ -417,18 +415,18 @@ public final class ExpressionParser<T> {
/**
* The types of tokens that are available.
- *
+ *
* @author Adrien Hopkins
* @since 2019-03-14
* @since v0.2.0
*/
- private static enum TokenType {
+ private enum TokenType {
OBJECT, UNARY_OPERATOR, BINARY_OPERATOR, NUMERIC_OPERATOR;
}
/**
* The opening bracket.
- *
+ *
* @since 2019-03-22
* @since v0.2.0
*/
@@ -436,7 +434,7 @@ public final class ExpressionParser<T> {
/**
* The closing bracket.
- *
+ *
* @since 2019-03-22
* @since v0.2.0
*/
@@ -444,7 +442,7 @@ public final class ExpressionParser<T> {
/**
* Finds the other bracket in a pair of brackets, given the position of one.
- *
+ *
* @param string string that contains brackets
* @param bracketPosition position of first bracket
* @return position of matching bracket
@@ -456,7 +454,7 @@ public final class ExpressionParser<T> {
final int bracketPosition) {
Objects.requireNonNull(string, "string must not be null.");
- final char openingBracket = string.charAt(bracketPosition);
+ final var openingBracket = string.charAt(bracketPosition);
// figure out what closing bracket to look for
final char closingBracket;
@@ -477,12 +475,12 @@ public final class ExpressionParser<T> {
// level of brackets. every opening bracket increments this; every closing
// bracket decrements it
- int bracketLevel = 0;
+ var bracketLevel = 0;
// iterate over the string to find the closing bracket
- for (int currentPosition = bracketPosition; currentPosition < string
+ for (var currentPosition = bracketPosition; currentPosition < string
.length(); currentPosition++) {
- final char currentCharacter = string.charAt(currentPosition);
+ final var currentCharacter = string.charAt(currentPosition);
if (currentCharacter == openingBracket) {
bracketLevel++;
@@ -499,7 +497,7 @@ public final class ExpressionParser<T> {
/**
* A function that obtains a parseable object from a string. For example, an
* integer {@code ExpressionParser} would use {@code Integer::parseInt}.
- *
+ *
* @since 2019-03-14
* @since v0.2.0
*/
@@ -507,7 +505,7 @@ public final class ExpressionParser<T> {
/**
* A map mapping operator strings to operator functions, for unary operators.
- *
+ *
* @since 2019-03-14
* @since v0.2.0
*/
@@ -516,7 +514,7 @@ public final class ExpressionParser<T> {
/**
* A map mapping operator strings to operator functions, for binary
* operators.
- *
+ *
* @since 2019-03-14
* @since v0.2.0
*/
@@ -524,14 +522,15 @@ public final class ExpressionParser<T> {
/**
* A map mapping operator strings to numeric functions.
- *
+ *
* @since 2024-03-23
+ * @since v0.5.0
*/
private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators;
/**
* The operator for space, or null if spaces have no function.
- *
+ *
* @since 2019-03-22
* @since v0.2.0
*/
@@ -539,7 +538,7 @@ public final class ExpressionParser<T> {
/**
* Creates the {@code ExpressionParser}.
- *
+ *
* @param objectObtainer function to get objects from strings
* @param unaryOperators unary operators available to the parser
* @param binaryOperators binary operators available to the parser
@@ -568,7 +567,7 @@ public final class ExpressionParser<T> {
* {@code 2 * (3 + 4)}<br>
* becomes<br>
* {@code 2 3 4 + *}.
- *
+ *
* @param expression expression
* @return expression in RPN
* @throws IllegalArgumentException if expression is invalid (e.g.
@@ -582,13 +581,13 @@ public final class ExpressionParser<T> {
final List<String> components = new ArrayList<>();
// the part of the expression remaining to parse
- String partialExpression = expression;
+ var partialExpression = expression;
// find and deal with brackets
while (partialExpression.indexOf(OPENING_BRACKET) != -1) {
- final int openingBracketPosition = partialExpression
+ final var openingBracketPosition = partialExpression
.indexOf(OPENING_BRACKET);
- final int closingBracketPosition = findBracketPair(partialExpression,
+ final var closingBracketPosition = findBracketPair(partialExpression,
openingBracketPosition);
// check for function
@@ -596,7 +595,7 @@ public final class ExpressionParser<T> {
&& partialExpression.charAt(openingBracketPosition - 1) != ' ') {
// function like sin(2) or tempF(32)
// find the position of the last space
- int spacePosition = openingBracketPosition;
+ var spacePosition = openingBracketPosition;
while (spacePosition >= 0
&& partialExpression.charAt(spacePosition) != ' ') {
spacePosition--;
@@ -607,8 +606,6 @@ public final class ExpressionParser<T> {
.substring(0, spacePosition + 1).split(" ")));
components.add(partialExpression.substring(spacePosition + 1,
closingBracketPosition + 1));
- partialExpression = partialExpression
- .substring(closingBracketPosition + 1);
} else {
// normal brackets like (1 + 2) * (3 / 5)
components.addAll(Arrays.asList(partialExpression
@@ -616,9 +613,9 @@ public final class ExpressionParser<T> {
components.add(this.convertExpressionToReversePolish(
partialExpression.substring(openingBracketPosition + 1,
closingBracketPosition)));
- partialExpression = partialExpression
- .substring(closingBracketPosition + 1);
}
+ partialExpression = partialExpression
+ .substring(closingBracketPosition + 1);
}
// add everything else
@@ -631,7 +628,7 @@ public final class ExpressionParser<T> {
// deal with space multiplication (x y)
if (this.spaceOperator != null) {
- for (int i = 0; i < components.size() - 1; i++) {
+ for (var i = 0; i < components.size() - 1; i++) {
if (this.getTokenType(components.get(i)) == TokenType.OBJECT && this
.getTokenType(components.get(i + 1)) == TokenType.OBJECT) {
components.add(++i, this.spaceOperator);
@@ -641,7 +638,7 @@ public final class ExpressionParser<T> {
// turn the expression into reverse Polish
while (true) {
- final int highestPriorityOperatorPosition = this
+ final var highestPriorityOperatorPosition = this
.findHighestPriorityOperatorPosition(components);
if (highestPriorityOperatorPosition == -1) {
break;
@@ -656,9 +653,9 @@ public final class ExpressionParser<T> {
if (components.size() < 2)
throw new IllegalArgumentException(
"Invalid expression \"" + expression + "\"");
- final String unaryOperator = components
+ final var unaryOperator = components
.remove(highestPriorityOperatorPosition);
- final String operand = components
+ final var operand = components
.remove(highestPriorityOperatorPosition);
components.add(highestPriorityOperatorPosition,
operand + " " + unaryOperator);
@@ -668,14 +665,14 @@ public final class ExpressionParser<T> {
if (components.size() < 3)
throw new IllegalArgumentException(
"Invalid expression \"" + expression + "\"");
- final String binaryOperator = components
+ final var binaryOperator = components
.remove(highestPriorityOperatorPosition);
- final String operand1 = components
+ final var operand1 = components
.remove(highestPriorityOperatorPosition - 1);
- final String operand2 = components
+ final var operand2 = components
.remove(highestPriorityOperatorPosition - 1);
components.add(highestPriorityOperatorPosition - 1,
- operand2 + " " + operand1 + " " + binaryOperator);
+ operand1 + " " + operand2 + " " + binaryOperator);
break;
default:
throw new AssertionError("Expected operator, found non-operator.");
@@ -684,20 +681,15 @@ public final class ExpressionParser<T> {
// join all of the components together, then ensure there is only one
// space in a row
- String expressionRPN = String.join(" ", components).replaceAll(" +", " ");
-
- while (expressionRPN.charAt(0) == ' ') {
- expressionRPN = expressionRPN.substring(1);
- }
- while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') {
- expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1);
- }
- return expressionRPN;
+ if (components.size() != 1)
+ throw new IllegalArgumentException(
+ "Invalid expression \"" + expression + "\".");
+ return components.get(0).replaceAll(" +", " ").trim();
}
/**
* Finds the position of the highest-priority operator in a list
- *
+ *
* @param components components to test
* @param blacklist positions of operators that should be ignored
* @return position of highest priority, or -1 if the list contains no
@@ -710,19 +702,19 @@ public final class ExpressionParser<T> {
final List<String> components) {
Objects.requireNonNull(components, "components must not be null.");
// find highest priority
- int maxPriority = Integer.MIN_VALUE;
- int maxPriorityPosition = -1;
+ var maxPriority = Integer.MIN_VALUE;
+ var maxPriorityPosition = -1;
// go over components one by one
// if it is an operator, test its priority to see if it's max
// if it is, update maxPriority and maxPriorityPosition
- for (int i = 0; i < components.size(); i++) {
+ for (var i = 0; i < components.size(); i++) {
switch (this.getTokenType(components.get(i))) {
case UNARY_OPERATOR:
- final PriorityUnaryOperator<T> unaryOperator = this.unaryOperators
+ final var unaryOperator = this.unaryOperators
.get(components.get(i));
- final int unaryPriority = unaryOperator.getPriority();
+ final var unaryPriority = unaryOperator.getPriority();
if (unaryPriority > maxPriority) {
maxPriority = unaryPriority;
@@ -730,9 +722,9 @@ public final class ExpressionParser<T> {
}
break;
case BINARY_OPERATOR:
- final PriorityBinaryOperator<T> binaryOperator = this.binaryOperators
+ final var binaryOperator = this.binaryOperators
.get(components.get(i));
- final int binaryPriority = binaryOperator.getPriority();
+ final var binaryPriority = binaryOperator.getPriority();
if (binaryPriority > maxPriority) {
maxPriority = binaryPriority;
@@ -740,9 +732,9 @@ public final class ExpressionParser<T> {
}
break;
case NUMERIC_OPERATOR:
- final PriorityBiFunction<T, UncertainDouble, T> numericOperator = this.numericOperators
+ final var numericOperator = this.numericOperators
.get(components.get(i));
- final int numericPriority = numericOperator.getPriority();
+ final var numericPriority = numericOperator.getPriority();
if (numericPriority > maxPriority) {
maxPriority = numericPriority;
@@ -760,7 +752,7 @@ public final class ExpressionParser<T> {
/**
* Determines whether an inputted string is an object or an operator
- *
+ *
* @param token string to input
* @return type of token it is
* @throws NullPointerException if {@code expression} is null
@@ -772,17 +764,16 @@ public final class ExpressionParser<T> {
if (this.unaryOperators.containsKey(token))
return TokenType.UNARY_OPERATOR;
- else if (this.binaryOperators.containsKey(token))
+ if (this.binaryOperators.containsKey(token))
return TokenType.BINARY_OPERATOR;
- else if (this.numericOperators.containsKey(token))
+ if (this.numericOperators.containsKey(token))
return TokenType.NUMERIC_OPERATOR;
- else
- return TokenType.OBJECT;
+ return TokenType.OBJECT;
}
/**
* Parses an expression.
- *
+ *
* @param expression expression to parse
* @return result
* @throws NullPointerException if {@code expression} is null
@@ -796,7 +787,7 @@ public final class ExpressionParser<T> {
/**
* Parses an expression expressed in reverse Polish notation.
- *
+ *
* @param expression expression to parse
* @return result
* @throws NullPointerException if {@code expression} is null
@@ -821,12 +812,12 @@ public final class ExpressionParser<T> {
item, stack.size()));
// get two arguments and operator, then apply!
- final T o1 = stack.pop();
- final T o2 = stack.pop();
+ final var o1 = stack.pop();
+ final var o2 = stack.pop();
final BinaryOperator<T> binaryOperator = this.binaryOperators
.get(item);
- stack.push(binaryOperator.apply(o1, o2));
+ stack.push(binaryOperator.apply(o2, o1));
break;
case NUMERIC_OPERATOR:
@@ -835,8 +826,8 @@ public final class ExpressionParser<T> {
"Attempted to call binary operator %s with insufficient arguments.",
item));
- final T ot = stack.pop();
- final UncertainDouble on = doubleStack.pop();
+ final var ot = stack.pop();
+ final var on = doubleStack.pop();
final BiFunction<T, UncertainDouble, T> op = this.numericOperators
.get(item);
stack.push(op.apply(ot, on));
@@ -850,14 +841,14 @@ public final class ExpressionParser<T> {
// that's the only way to tell if an expression is a number or not.
try {
stack.push(this.objectObtainer.apply(item));
- } catch (Exception e) {
+ } catch (final Exception e) {
try {
doubleStack.push(UncertainDouble.fromString(item));
- } catch (IllegalArgumentException e2) {
+ } catch (final IllegalArgumentException e2) {
try {
doubleStack.push(
UncertainDouble.of(Double.parseDouble(item), 0));
- } catch (NumberFormatException e3) {
+ } catch (final NumberFormatException e3) {
throw e;
}
}
@@ -871,7 +862,7 @@ public final class ExpressionParser<T> {
item, stack.size()));
// get one argument and operator, then apply!
- final T o = stack.pop();
+ final var o = stack.pop();
final UnaryOperator<T> unaryOperator = this.unaryOperators
.get(item);
@@ -889,7 +880,7 @@ public final class ExpressionParser<T> {
if (stack.size() > 1)
throw new IllegalStateException(
"Computation ended up with more than one answer.");
- else if (stack.size() == 0)
+ if (stack.size() == 0)
throw new IllegalStateException(
"Computation ended up without an answer.");
return stack.pop();
diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java
index 49c44fa..089978b 100644
--- a/src/main/java/sevenUnits/utils/NameSymbol.java
+++ b/src/main/java/sevenUnits/utils/NameSymbol.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2022, 2024, 2025 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
@@ -19,34 +19,35 @@ 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
+ * @since v0.3.0
*/
public final class NameSymbol {
+ /** The {@code NameSymbol} with all fields empty. */
public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(),
Optional.empty(), new HashSet<>());
/**
* Creates a {@code NameSymbol}, ensuring that if primaryName is null and
* otherNames is not empty, one name is moved from otherNames to primaryName
- *
+ *
* Ensure that otherNames is not a copy of the inputted argument.
*/
- private static final NameSymbol create(final String name,
- final String symbol, final Set<String> otherNames) {
+ private static NameSymbol create(final String name, final String symbol,
+ final Set<String> otherNames) {
final Optional<String> primaryName;
if (name == null && !otherNames.isEmpty()) {
// get primary name and remove it from savedNames
- final Iterator<String> it = otherNames.iterator();
+ final var it = otherNames.iterator();
assert it.hasNext();
primaryName = Optional.of(it.next());
otherNames.remove(primaryName.get());
@@ -61,14 +62,15 @@ public final class NameSymbol {
/**
* 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
+ * @since v0.3.0
* @throws NullPointerException if name or symbol is null
*/
- public static final NameSymbol of(final String name, final String symbol) {
+ public static NameSymbol of(final String name, final String symbol) {
return new NameSymbol(Optional.of(name), Optional.of(symbol),
new HashSet<>());
}
@@ -76,15 +78,16 @@ public final class NameSymbol {
/**
* 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
+ * @since v0.3.0
* @throws NullPointerException if any argument is null
*/
- public static final NameSymbol of(final String name, final String symbol,
+ public static NameSymbol of(final String name, final String symbol,
final Set<String> otherNames) {
return new NameSymbol(Optional.of(name), Optional.of(symbol),
new HashSet<>(Objects.requireNonNull(otherNames,
@@ -94,12 +97,13 @@ public final class NameSymbol {
/**
* 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
+ * @since v0.3.0
* @throws NullPointerException if any argument is null
*/
public static final NameSymbol of(final String name, final String symbol,
@@ -112,13 +116,14 @@ public final class NameSymbol {
/**
* 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
+ * @since v0.3.0
* @throws NullPointerException if name is null
*/
- public static final NameSymbol ofName(final String name) {
+ public static NameSymbol ofName(final String name) {
return new NameSymbol(Optional.of(name), Optional.empty(),
new HashSet<>());
}
@@ -133,15 +138,16 @@ public final class NameSymbol {
* 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
+ * @since v0.3.0
*/
- public static final NameSymbol ofNullable(final String name,
- final String symbol, final Set<String> otherNames) {
+ public static NameSymbol ofNullable(final String name, final String symbol,
+ final Set<String> otherNames) {
return NameSymbol.create(name, symbol,
otherNames == null ? new HashSet<>() : new HashSet<>(otherNames));
}
@@ -156,12 +162,13 @@ public final class NameSymbol {
* 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
+ * @since v0.3.0
*/
public static final NameSymbol ofNullable(final String name,
final String symbol, final String... otherNames) {
@@ -171,13 +178,14 @@ public final class NameSymbol {
/**
* Gets a {@code NameSymbol} with a symbol and no names.
- *
+ *
* @param symbol symbol to use
* @return NameSymbol instance
* @since 2019-10-21
+ * @since v0.3.0
* @throws NullPointerException if symbol is null
*/
- public static final NameSymbol ofSymbol(final String symbol) {
+ public static NameSymbol ofSymbol(final String symbol) {
return new NameSymbol(Optional.empty(), Optional.of(symbol),
new HashSet<>());
}
@@ -189,21 +197,26 @@ public final class NameSymbol {
/**
* 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
+ * @since v0.3.0
*/
- private NameSymbol(final Optional<String> primaryName,
- final Optional<String> symbol, final Set<String> otherNames) {
+ NameSymbol(final Optional<String> primaryName, final Optional<String> symbol,
+ final Set<String> otherNames) {
this.primaryName = primaryName;
this.symbol = symbol;
- otherNames.remove(null);
- this.otherNames = Collections.unmodifiableSet(otherNames);
+ if (otherNames != null) {
+ otherNames.remove(null);
+ this.otherNames = Collections.unmodifiableSet(otherNames);
+ } else {
+ this.otherNames = Set.of();
+ }
- if (this.primaryName.isEmpty()) {
+ if (this.primaryName == null || this.primaryName.isEmpty()) {
assert this.otherNames.isEmpty();
}
}
@@ -214,7 +227,7 @@ public final class NameSymbol {
return true;
if (!(obj instanceof NameSymbol))
return false;
- final NameSymbol other = (NameSymbol) obj;
+ final var other = (NameSymbol) obj;
if (this.otherNames == null) {
if (other.otherNames != null)
return false;
@@ -236,31 +249,34 @@ public final class NameSymbol {
/**
* @return otherNames
* @since 2019-10-21
+ * @since v0.3.0
*/
- public final Set<String> getOtherNames() {
+ public Set<String> getOtherNames() {
return this.otherNames;
}
/**
* @return primaryName
* @since 2019-10-21
+ * @since v0.3.0
*/
- public final Optional<String> getPrimaryName() {
+ public Optional<String> getPrimaryName() {
return this.primaryName;
}
/**
* @return symbol
* @since 2019-10-21
+ * @since v0.3.0
*/
- public final Optional<String> getSymbol() {
+ public Optional<String> getSymbol() {
return this.symbol;
}
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
+ final var prime = 31;
+ var result = 1;
result = prime * result
+ (this.otherNames == null ? 0 : this.otherNames.hashCode());
result = prime * result
@@ -270,10 +286,8 @@ public final class NameSymbol {
return result;
}
- /**
- * @return true iff this {@code NameSymbol} contains no names or symbols.
- */
- public final boolean isEmpty() {
+ /** @return true iff this {@code NameSymbol} contains no names or symbols. */
+ public boolean isEmpty() {
// if primaryName is empty, otherNames must also be empty
return this.primaryName.isEmpty() && this.symbol.isEmpty();
}
@@ -282,11 +296,10 @@ public final class NameSymbol {
public String toString() {
if (this.isEmpty())
return "NameSymbol.EMPTY";
- else if (this.primaryName.isPresent() && this.symbol.isPresent())
+ if (this.primaryName.isPresent() && this.symbol.isPresent())
return this.primaryName.orElseThrow() + " ("
+ this.symbol.orElseThrow() + ")";
- else
- return this.primaryName.orElseGet(this.symbol::orElseThrow);
+ return this.primaryName.orElseGet(this.symbol::orElseThrow);
}
/**
@@ -294,16 +307,19 @@ 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
+ * @param name additional name to add
+ * @return copy of this NameSymbol with the additional name
+ *
* @since 2022-04-19
+ * @since v0.4.0
*/
- public final NameSymbol withExtraName(String name) {
+ public 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));
+ }
+ return NameSymbol.ofNullable(name, this.symbol.orElse(null));
}
} \ No newline at end of file
diff --git a/src/main/java/sevenUnits/utils/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java
index 3959a64..166de55 100644
--- a/src/main/java/sevenUnits/utils/Nameable.java
+++ b/src/main/java/sevenUnits/utils/Nameable.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2020 Adrien Hopkins
+ * Copyright (C) 2020, 2022, 2024, 2025 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
@@ -24,15 +24,17 @@ import java.util.Set;
* and symbol data should be immutable.
*
* @since 2020-09-07
+ * @since v0.3.0
*/
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
+ * @since v0.4.0
*/
default String getName() {
- final NameSymbol ns = this.getNameSymbol();
+ final var ns = this.getNameSymbol();
return ns.getPrimaryName().or(ns::getSymbol).orElse("Unnamed");
}
@@ -40,12 +42,14 @@ public interface Nameable {
* @return a {@code NameSymbol} that contains this object's primary name,
* symbol and other names
* @since 2020-09-07
+ * @since v0.3.0
*/
NameSymbol getNameSymbol();
/**
* @return set of alternate names
* @since 2020-09-07
+ * @since v0.3.0
*/
default Set<String> getOtherNames() {
return this.getNameSymbol().getOtherNames();
@@ -54,6 +58,7 @@ public interface Nameable {
/**
* @return preferred name of object
* @since 2020-09-07
+ * @since v0.3.0
*/
default Optional<String> getPrimaryName() {
return this.getNameSymbol().getPrimaryName();
@@ -63,15 +68,17 @@ public interface Nameable {
* @return a short name for the object - if there's a symbol, it's that,
* otherwise the symbol, otherwise "Unnamed"
* @since 2022-02-26
+ * @since v0.4.0
*/
default String getShortName() {
- final NameSymbol ns = this.getNameSymbol();
+ final var ns = this.getNameSymbol();
return ns.getSymbol().or(ns::getPrimaryName).orElse("Unnamed");
}
/**
* @return short symbol representing object
* @since 2020-09-07
+ * @since v0.3.0
*/
default Optional<String> getSymbol() {
return this.getNameSymbol().getSymbol();
diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java
index 5a29d79..23fe41c 100644
--- a/src/main/java/sevenUnits/utils/ObjectProduct.java
+++ b/src/main/java/sevenUnits/utils/ObjectProduct.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * Copyright (C) 2018, 2021, 2024, 2025 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
@@ -29,17 +29,26 @@ import java.util.function.Function;
/**
* An immutable product of multiple objects of a type, such as base units. The
* objects can be multiplied and exponentiated.
- *
+ *
* @author Adrien Hopkins
+ * @param <T> type of object that is being multiplied
* @since 2019-10-16
+ * @since v0.3.0
*/
public class ObjectProduct<T> implements Nameable {
/**
+ * If {@link #toExponentRounded}'s rounding changes exponents by more than
+ * this value, a warning will be printed to standard error.
+ */
+ private static final double ROUND_WARN_THRESHOLD = 1e-12;
+
+ /**
* Returns an empty ObjectProduct of a certain type
- *
+ *
* @param <T> type of objects that can be multiplied
* @return empty product
* @since 2019-10-16
+ * @since v0.3.0
*/
public static final <T> ObjectProduct<T> empty() {
return new ObjectProduct<>(new HashMap<>());
@@ -47,11 +56,12 @@ public class ObjectProduct<T> implements Nameable {
/**
* Gets an {@code ObjectProduct} from an object-to-integer mapping
- *
+ *
* @param <T> type of object in product
* @param map map mapping objects to exponents
* @return object product
* @since 2019-10-16
+ * @since v0.3.0
*/
public static final <T> ObjectProduct<T> fromExponentMapping(
final Map<T, Integer> map) {
@@ -61,10 +71,12 @@ public class ObjectProduct<T> implements Nameable {
/**
* Gets an ObjectProduct that has one of the inputted argument, and nothing
* else.
- *
+ *
* @param object object that will be in the product
+ * @param <T> type of object contained in returned ObjectProduct
* @return product
* @since 2019-10-16
+ * @since v0.3.0
* @throws NullPointerException if object is null
*/
public static final <T> ObjectProduct<T> oneOf(final T object) {
@@ -77,21 +89,21 @@ public class ObjectProduct<T> implements Nameable {
/**
* The objects that make up the product, mapped to their exponents. This map
* treats zero as null, and is immutable.
- *
+ *
* @since 2019-10-16
+ * @since v0.3.0
*/
final Map<T, Integer> exponents;
- /**
- * The object's name and symbol
- */
+ /** 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
+ * @since v0.3.0
*/
ObjectProduct(final Map<T, Integer> exponents) {
this(exponents, NameSymbol.EMPTY);
@@ -99,10 +111,11 @@ public class ObjectProduct<T> implements Nameable {
/**
* Creates the {@code ObjectProduct}.
- *
+ *
* @param exponents objects that make up this product
* @param nameSymbol name and symbol of object product
* @since 2019-10-16
+ * @since v0.3.0
*/
ObjectProduct(final Map<T, Integer> exponents, NameSymbol nameSymbol) {
this.exponents = Collections.unmodifiableMap(
@@ -117,6 +130,7 @@ public class ObjectProduct<T> implements Nameable {
* @param other other product
* @return quotient of two products
* @since 2019-10-16
+ * @since v0.3.0
* @throws NullPointerException if other is null
*/
public ObjectProduct<T> dividedBy(final ObjectProduct<T> other) {
@@ -150,6 +164,7 @@ public class ObjectProduct<T> implements Nameable {
/**
* @return immutable map mapping objects to exponents
* @since 2019-10-16
+ * @since v0.3.0
*/
public Map<T, Integer> exponentMap() {
return this.exponents;
@@ -177,7 +192,7 @@ public class ObjectProduct<T> implements Nameable {
/**
* Gets the exponent for a specific dimension.
- *
+ *
* @param dimension dimension to check
* @return exponent for that dimension
* @since 2018-12-12
@@ -201,10 +216,11 @@ public class ObjectProduct<T> implements Nameable {
* @return true if this product is a single object, i.e. it has one exponent
* of one and no other nonzero exponents
* @since 2019-10-16
+ * @since v0.3.0
*/
public boolean isSingleObject() {
- int oneCount = 0;
- boolean twoOrMore = false; // has exponents of 2 or more
+ var oneCount = 0;
+ var twoOrMore = false; // has exponents of 2 or more
for (final T b : this.getBaseSet()) {
if (this.getExponent(b) == 1) {
oneCount++;
@@ -221,6 +237,7 @@ public class ObjectProduct<T> implements Nameable {
* @param other other product
* @return product of two products
* @since 2019-10-16
+ * @since v0.3.0
* @throws NullPointerException if other is null
*/
public ObjectProduct<T> times(final ObjectProduct<T> other) {
@@ -242,10 +259,11 @@ public class ObjectProduct<T> implements Nameable {
/**
* Returns this product, but to an exponent
- *
+ *
* @param exponent exponent
* @return result of exponentiation
* @since 2019-10-16
+ * @since v0.3.0
*/
public ObjectProduct<T> toExponent(final int exponent) {
final Map<T, Integer> map = new HashMap<>(this.exponents);
@@ -256,12 +274,41 @@ public class ObjectProduct<T> implements Nameable {
}
/**
+ * Returns this product to an exponent, where every dimension is rounded to
+ * the nearest integer.
+ *
+ * This function will send a warning (via standard error) if the rounding
+ * significantly changes the value.
+ *
+ * @param exponent exponent to raise this product to
+ * @return result of exponentiation
+ *
+ * @since 2024-08-22
+ * @since v0.3.0
+ */
+ public ObjectProduct<T> toExponentRounded(final double exponent) {
+ final Map<T, Integer> map = new HashMap<>(this.exponents);
+ for (final T key : this.exponents.keySet()) {
+ final var newExponent = this.getExponent(key) * exponent;
+ if (Math.abs(
+ newExponent - Math.round(newExponent)) > ROUND_WARN_THRESHOLD) {
+ System.err.printf(
+ "Exponent Rounding Warning: Dimension exponents must be integers, so %d ^ %g = %g was rounded to %d.\n",
+ this.getExponent(key), exponent, newExponent,
+ Math.round(newExponent));
+ }
+ map.put(key, (int) Math.round(newExponent));
+ }
+ return new ObjectProduct<>(map);
+ }
+
+ /**
* Converts this product to a string using the objects'
* {@link Object#toString()} method (or {@link Nameable#getShortName} if
* available). If objects have a long toString representation, it is
* recommended to use {@link #toString(Function)} instead to shorten the
* returned string.
- *
+ *
* <p>
* {@inheritDoc}
*/
@@ -275,10 +322,11 @@ public class ObjectProduct<T> implements Nameable {
/**
* Converts this product to a string. The objects that make up this product
* are represented by {@code objectToString}
- *
+ *
* @param objectToString function to convert objects to strings
* @return string representation of product
* @since 2019-10-16
+ * @since v0.3.0
*/
public String toString(final Function<T, String> objectToString) {
final List<String> positiveStringComponents = new ArrayList<>();
@@ -298,18 +346,20 @@ public class ObjectProduct<T> implements Nameable {
}
}
- final String positiveString = positiveStringComponents.isEmpty() ? "1"
+ final var positiveString = positiveStringComponents.isEmpty() ? "1"
: String.join(" * ", positiveStringComponents);
- final String negativeString = negativeStringComponents.isEmpty() ? ""
+ final var negativeString = negativeStringComponents.isEmpty() ? ""
: " / " + String.join(" * ", negativeStringComponents);
return positiveString + negativeString;
}
/**
+ * @param nameSymbol name to add to this product
* @return named version of this {@code ObjectProduct}, using data from
* {@code nameSymbol}
* @since 2021-12-15
+ * @since v0.3.0
*/
public ObjectProduct<T> withName(NameSymbol nameSymbol) {
return new ObjectProduct<>(this.exponents, nameSymbol);
diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java
index fc47baa..4bb7ce5 100644
--- a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java
+++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -22,7 +22,6 @@ 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;
/**
@@ -39,8 +38,8 @@ import java.util.regex.Pattern;
* are made
* </ol>
*
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public final class SemanticVersionNumber
implements Comparable<SemanticVersionNumber> {
@@ -52,8 +51,8 @@ public final class SemanticVersionNumber
* throw NullPointerExceptions, everything else throws
* IllegalArgumentException.
*
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public static final class Builder {
private final int major;
@@ -69,8 +68,8 @@ 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
+ * @since v0.4.0
*/
private Builder(int major, int minor, int patch) {
this.major = major;
@@ -82,8 +81,8 @@ public final class SemanticVersionNumber
/**
* @return version number created by this builder
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public SemanticVersionNumber build() {
return new SemanticVersionNumber(this.major, this.minor, this.patch,
@@ -95,8 +94,8 @@ public final class SemanticVersionNumber
*
* @param identifiers build metadata
* @return this builder
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public Builder buildMetadata(List<String> identifiers) {
Objects.requireNonNull(identifiers, "identifiers may not be null");
@@ -115,8 +114,8 @@ public final class SemanticVersionNumber
*
* @param identifiers build metadata
* @return this builder
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public Builder buildMetadata(String... identifiers) {
Objects.requireNonNull(identifiers, "identifiers may not be null");
@@ -136,7 +135,7 @@ public final class SemanticVersionNumber
return true;
if (!(obj instanceof Builder))
return false;
- final Builder other = (Builder) obj;
+ final var other = (Builder) obj;
return Objects.equals(this.buildMetadata, other.buildMetadata)
&& this.major == other.major && this.minor == other.minor
&& this.patch == other.patch && Objects.equals(
@@ -154,8 +153,8 @@ public final class SemanticVersionNumber
*
* @param identifiers pre-release identifier(s) to add
* @return this builder
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public Builder preRelease(int... identifiers) {
Objects.requireNonNull(identifiers, "identifiers may not be null");
@@ -173,8 +172,8 @@ public final class SemanticVersionNumber
*
* @param identifiers pre-release identifier(s) to add
* @return this builder
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public Builder preRelease(List<String> identifiers) {
Objects.requireNonNull(identifiers, "identifiers may not be null");
@@ -193,8 +192,8 @@ public final class SemanticVersionNumber
*
* @param identifiers pre-release identifier(s) to add
* @return this builder
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public Builder preRelease(String... identifiers) {
Objects.requireNonNull(identifiers, "identifiers may not be null");
@@ -214,8 +213,8 @@ public final class SemanticVersionNumber
* @param identifier1 first identifier
* @param identifier2 second identifier
* @return this builder
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public Builder preRelease(String identifier1, int identifier2) {
Objects.requireNonNull(identifier1, "identifier1 may not be null");
@@ -249,12 +248,11 @@ public final class SemanticVersionNumber
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);
+ final var naturalComparison = o1.compareTo(o2);
if (naturalComparison == 0)
return SemanticVersionNumber.compareIdentifiers(o1.buildMetadata,
o2.buildMetadata);
- else
- return naturalComparison;
+ return naturalComparison;
};
};
@@ -280,11 +278,11 @@ 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
+ * @since v0.4.0
*/
- public static final SemanticVersionNumber.Builder builder(int major,
- int minor, int patch) {
+ public static SemanticVersionNumber.Builder builder(int major, int minor,
+ int patch) {
if (major < 0)
throw new IllegalArgumentException(
"Major version must be non-negative.");
@@ -304,60 +302,57 @@ 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
+ * @since v0.4.0
*/
- private static final int compareIdentifiers(List<String> a, List<String> b) {
+ private static int compareIdentifiers(List<String> a, List<String> b) {
// test pre-release size
- final int aSize = a.size();
- final int bSize = b.size();
+ final var aSize = a.size();
+ final var bSize = b.size();
// no identifiers is greater than any identifiers
if (aSize != 0 && bSize == 0)
return -1;
- else if (aSize == 0 && bSize != 0)
+ 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);
+ for (var i = 0; i < Math.min(aSize, bSize); i++) {
+ final var aElement = a.get(i);
+ final var 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
+ if (!NUMERIC_IDENTIFER.matcher(bElement).matches())
// 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
+ // both are numbers, compare them
+ final var aNumber = Integer.parseInt(aElement);
+ final var bNumber = Integer.parseInt(bElement);
+
+ if (aNumber < bNumber)
+ return -1;
+ if (aNumber > bNumber)
return 1;
- else {
- // both are not numbers, compare them
- final int comparison = aElement.compareTo(bElement);
- if (comparison != 0)
- return comparison;
- }
+ } 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 var 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)
+ if (aSize > bSize)
return 1;
- else
- return 0;
+ return 0;
+
+ // we just tested the stuff that's in common, maybe someone has more
+
}
/**
@@ -365,23 +360,23 @@ public final class SemanticVersionNumber
*
* @param versionString string to parse
* @return {@code SemanticVersionNumber} instance
- * @since v0.4.0
* @since 2022-02-19
- * @see {@link #toString}
+ * @since v0.4.0
+ * @see #toString
*/
- public static final SemanticVersionNumber fromString(String versionString) {
+ public static SemanticVersionNumber fromString(String versionString) {
// parse & validate version string
Objects.requireNonNull(versionString, "versionString may not be null");
- final Matcher m = VERSION_NUMBER.matcher(versionString);
+ final var 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));
+ final var major = Integer.parseInt(m.group(1));
+ final var minor = Integer.parseInt(m.group(2));
+ final var patch = Integer.parseInt(m.group(3));
// pre release
final List<String> preRelease;
@@ -406,13 +401,13 @@ public final class SemanticVersionNumber
/**
* Tests whether a string is a valid Semantic Version string
- *
+ *
* @param versionString string to test
* @return true iff string is valid
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
- public static final boolean isValidVersionString(String versionString) {
+ public static boolean isValidVersionString(String versionString) {
return VERSION_NUMBER.matcher(versionString).matches();
}
@@ -429,10 +424,10 @@ 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
+ * @since v0.4.0
*/
- public static final SemanticVersionNumber preRelease(int major, int minor,
+ public static SemanticVersionNumber preRelease(int major, int minor,
int patch, String preReleaseType, int preReleaseNumber) {
if (major < 0)
throw new IllegalArgumentException(
@@ -467,10 +462,10 @@ 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
+ * @since v0.4.0
*/
- public static final SemanticVersionNumber stableVersion(int major, int minor,
+ public static SemanticVersionNumber stableVersion(int major, int minor,
int patch) {
if (major < 0)
throw new IllegalArgumentException(
@@ -500,8 +495,8 @@ 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
+ * @since v0.4.0
*/
private SemanticVersionNumber(int major, int minor, int patch,
List<String> preReleaseIdentifiers, List<String> buildMetadata) {
@@ -514,8 +509,8 @@ public final class SemanticVersionNumber
/**
* @return build metadata (empty if there is none)
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public List<String> buildMetadata() {
return Collections.unmodifiableList(this.buildMetadata);
@@ -536,17 +531,17 @@ public final class SemanticVersionNumber
// test the three big numbers in order first
if (this.major < o.major)
return -1;
- else if (this.major > o.major)
+ if (this.major > o.major)
return 1;
if (this.minor < o.minor)
return -1;
- else if (this.minor > o.minor)
+ if (this.minor > o.minor)
return 1;
if (this.patch < o.patch)
return -1;
- else if (this.patch > o.patch)
+ if (this.patch > o.patch)
return 1;
// now we just compare pre-release identifiers
@@ -569,7 +564,7 @@ public final class SemanticVersionNumber
* </ul>
* If this function returns <b>false</b>, you may have to change your code to
* upgrade it to {@code other}
- *
+ *
* <p>
* Two version numbers that are identical (ignoring build metadata) are
* always compatible. Different version numbers are compatible as long as:
@@ -585,8 +580,8 @@ 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
+ * @since v0.4.0
*/
public boolean compatibleWith(SemanticVersionNumber other) {
Objects.requireNonNull(other, "other may not be null");
@@ -601,17 +596,14 @@ public final class SemanticVersionNumber
return true;
if (!(obj instanceof SemanticVersionNumber))
return false;
- final SemanticVersionNumber other = (SemanticVersionNumber) obj;
+ final var 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)
+ if ((this.major != other.major) || (this.minor != other.minor)
+ || (this.patch != other.patch))
return false;
if (this.preReleaseIdentifiers == null) {
if (other.preReleaseIdentifiers != null)
@@ -624,8 +616,8 @@ public final class SemanticVersionNumber
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
+ final var prime = 31;
+ var result = 1;
result = prime * result
+ (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode());
result = prime * result + this.major;
@@ -639,8 +631,8 @@ 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
+ * @since v0.4.0
*/
public boolean isStable() {
return this.major > 0 && this.preReleaseIdentifiers.isEmpty();
@@ -649,8 +641,8 @@ 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
+ * @since v0.4.0
*/
public int majorVersion() {
return this.major;
@@ -659,8 +651,8 @@ public final class SemanticVersionNumber
/**
* @return the MINOR version number, incremented when you add backwards
* compatible functionality
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public int minorVersion() {
return this.minor;
@@ -669,8 +661,8 @@ 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
+ * @since v0.4.0
*/
public int patchVersion() {
return this.patch;
@@ -679,8 +671,8 @@ public final class SemanticVersionNumber
/**
* @return identifiers describing this pre-release (empty if not a
* pre-release)
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public List<String> preReleaseIdentifiers() {
return Collections.unmodifiableList(this.preReleaseIdentifiers);
@@ -697,13 +689,13 @@ public final class SemanticVersionNumber
* 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".
- *
+ *
* @since v0.4.0
* @see <a href="https://semver.org">The official SemVer specification</a>
*/
@Override
public String toString() {
- String versionString = String.format("%d.%d.%d", this.major, this.minor,
+ var versionString = String.format("%d.%d.%d", this.major, this.minor,
this.patch);
if (!this.preReleaseIdentifiers.isEmpty()) {
versionString += "-" + String.join(".", this.preReleaseIdentifiers);
diff --git a/src/main/java/sevenUnits/utils/UncertainDouble.java b/src/main/java/sevenUnits/utils/UncertainDouble.java
index 66d8103..24ada20 100644
--- a/src/main/java/sevenUnits/utils/UncertainDouble.java
+++ b/src/main/java/sevenUnits/utils/UncertainDouble.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2020 Adrien Hopkins
+ * Copyright (C) 2020-2022, 2024, 2025 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
@@ -19,7 +19,6 @@ package sevenUnits.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
@@ -29,56 +28,59 @@ import java.util.regex.Pattern;
* arguments is null.
*
* @since 2020-09-07
+ * @since v0.3.0
*/
public final class UncertainDouble implements Comparable<UncertainDouble> {
- /**
- * The exact value 0
- */
+ /** The exact value 0 */
public static final UncertainDouble ZERO = UncertainDouble.of(0, 0);
static final String NUMBER_REGEX = "(\\d+(?:[\\.,]\\d+))";
- /**
- * A regular expression that can recognize toString forms
- */
+ /** A regular expression that can recognize toString forms */
static final Pattern TO_STRING = Pattern.compile(NUMBER_REGEX
- // optional "� [number]"
- + "(?:\\s*(?:�|\\+-)\\s*" + NUMBER_REGEX + ")?");
+ // 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.
- *
+ * "12345.678" will become 12345.678 ± 0.001.
+ *
+ * @param s string to parse
+ * @return parsed {@code UncertainDouble}
+ *
* @throws NumberFormatException if the argument is not a number
*
* @since 2022-04-18
+ * @since v0.4.0
*/
- public static final UncertainDouble fromRoundedString(String s) {
- final BigDecimal value = new BigDecimal(s);
- final double uncertainty = Math.pow(10, -value.scale());
+ public static UncertainDouble fromRoundedString(String s) {
+ final var value = new BigDecimal(s);
+ final var 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.
+ * Parses a string in the form of
+ * {@link UncertainDouble#toString(boolean, RoundingMode)} and returns the
+ * corresponding {@code UncertainDouble} instance.
* <p>
* This method allows some alternative forms of the string representation,
- * such as using "+-" instead of "�".
- *
+ * such as using "+-" instead of "±".
+ *
* @param s string to parse
* @return {@code UncertainDouble} instance
* @throws IllegalArgumentException if the string is invalid
* @since 2020-09-07
+ * @since v0.3.0
*/
- public static final UncertainDouble fromString(String s) {
+ public static UncertainDouble fromString(String s) {
Objects.requireNonNull(s, "s may not be null");
- final Matcher matcher = TO_STRING.matcher(s);
+ final var matcher = TO_STRING.matcher(s);
if (!matcher.matches())
throw new IllegalArgumentException(
- "Could not parse stirng \"" + s + "\".");
+ "Could not parse string \"" + s + "\".");
double value, uncertainty;
try {
@@ -88,7 +90,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
"String " + s + " not in correct format.");
}
- final String uncertaintyString = matcher.group(2);
+ final var uncertaintyString = matcher.group(2);
if (uncertaintyString == null) {
uncertainty = 0;
} else {
@@ -106,20 +108,32 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Gets an {@code UncertainDouble} from its value and <b>absolute</b>
* uncertainty.
- *
+ *
+ * @param value double's value
+ * @param uncertainty double's uncertainty (non-negative)
+ * @return {@code UncertainDouble} instance with these parameters
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public static final UncertainDouble of(double value, double uncertainty) {
+ public static UncertainDouble of(double value, double uncertainty) {
return new UncertainDouble(value, uncertainty);
}
/**
* Gets an {@code UncertainDouble} from its value and <b>relative</b>
* uncertainty.
- *
+ *
+ * @param value double's value
+ * @param relativeUncertainty double's uncertainty (non-negative); the
+ * absolute uncertainty is equal to this value
+ * multiplied by {@code relativeUncertainty}
+ * @return {@code UncertainDouble} instance with these parameters
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public static final UncertainDouble ofRelative(double value,
+ public static UncertainDouble ofRelative(double value,
double relativeUncertainty) {
return new UncertainDouble(value, value * relativeUncertainty);
}
@@ -132,6 +146,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
* @param value
* @param uncertainty
* @since 2020-09-07
+ * @since v0.3.0
*/
private UncertainDouble(double value, double uncertainty) {
this.value = value;
@@ -153,16 +168,20 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
* {@code false}.
*/
@Override
- public final int compareTo(UncertainDouble o) {
+ public int compareTo(UncertainDouble o) {
return Double.compare(this.value, o.value);
}
/**
* Returns the quotient of {@code this} and {@code other}.
- *
+ *
+ * @param other number to divide by
+ * @return quotient
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble dividedBy(UncertainDouble other) {
+ public UncertainDouble dividedBy(UncertainDouble other) {
Objects.requireNonNull(other, "other may not be null");
return UncertainDouble.ofRelative(this.value / other.value, Math
.hypot(this.relativeUncertainty(), other.relativeUncertainty()));
@@ -170,23 +189,26 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Returns the quotient of {@code this} and the exact value {@code other}.
- *
+ *
+ * @param other number to divide by
+ * @return quotient
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble dividedByExact(double other) {
+ public UncertainDouble dividedByExact(double other) {
return UncertainDouble.of(this.value / other, this.uncertainty / other);
}
@Override
- public final boolean equals(Object obj) {
+ public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof UncertainDouble))
return false;
- final UncertainDouble other = (UncertainDouble) obj;
- if (Double.compare(this.value, other.value) != 0)
- return false;
- if (Double.compare(this.uncertainty, other.uncertainty) != 0)
+ final var other = (UncertainDouble) obj;
+ if ((Double.compare(this.value, other.value) != 0)
+ || (Double.compare(this.uncertainty, other.uncertainty) != 0))
return false;
return true;
}
@@ -196,8 +218,9 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
* @return true iff this and {@code other} are within each other's
* uncertainty range.
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final boolean equivalent(UncertainDouble other) {
+ public boolean equivalent(UncertainDouble other) {
Objects.requireNonNull(other, "other may not be null");
return Math.abs(this.value - other.value) <= Math.min(this.uncertainty,
other.uncertainty);
@@ -205,10 +228,11 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Gets the preferred scale for rounding a value for toString.
- *
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- private final int getDisplayScale() {
+ private int getDisplayScale() {
// round based on uncertainty
// if uncertainty starts with 1 (ignoring zeroes and the decimal
// point), rounds
@@ -216,24 +240,23 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
// otherwise, rounds so that uncertainty has 1 significant digits.
// the value is rounded to the same number of decimal places as the
// uncertainty.
- final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty);
+ final var bigUncertainty = BigDecimal.valueOf(this.uncertainty);
// the scale that will give the uncertainty two decimal places
- final int twoDecimalPlacesScale = bigUncertainty.scale()
+ final var twoDecimalPlacesScale = bigUncertainty.scale()
- bigUncertainty.precision() + 2;
- final BigDecimal roundedUncertainty = bigUncertainty
+ final var roundedUncertainty = bigUncertainty
.setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN);
if (roundedUncertainty.unscaledValue().intValue() >= 20)
return twoDecimalPlacesScale - 1; // one decimal place
- else
- return twoDecimalPlacesScale;
+ return twoDecimalPlacesScale;
}
@Override
- public final int hashCode() {
- final int prime = 31;
- int result = 1;
+ public int hashCode() {
+ final var prime = 31;
+ var result = 1;
result = prime * result + Double.hashCode(this.value);
result = prime * result + Double.hashCode(this.uncertainty);
return result;
@@ -241,19 +264,24 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* @return true iff the value has no uncertainty
- *
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final boolean isExact() {
+ public boolean isExact() {
return this.uncertainty == 0;
}
/**
* Returns the difference of {@code this} and {@code other}.
- *
+ *
+ * @param other number to subtract
+ * @return result of subtraction
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble minus(UncertainDouble other) {
+ public UncertainDouble minus(UncertainDouble other) {
Objects.requireNonNull(other, "other may not be null");
return UncertainDouble.of(this.value - other.value,
Math.hypot(this.uncertainty, other.uncertainty));
@@ -261,19 +289,27 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Returns the difference of {@code this} and the exact value {@code other}.
- *
+ *
+ * @param other number to subtract
+ * @return result of subtraction
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble minusExact(double other) {
+ public UncertainDouble minusExact(double other) {
return UncertainDouble.of(this.value - other, this.uncertainty);
}
/**
* Returns the sum of {@code this} and {@code other}.
- *
+ *
+ * @param other number to add
+ * @return result of addition
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble plus(UncertainDouble other) {
+ public UncertainDouble plus(UncertainDouble other) {
Objects.requireNonNull(other, "other may not be null");
return UncertainDouble.of(this.value + other.value,
Math.hypot(this.uncertainty, other.uncertainty));
@@ -281,27 +317,36 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Returns the sum of {@code this} and the exact value {@code other}.
- *
+ *
+ * @param other number to add
+ * @return result of addition
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble plusExact(double other) {
+ public UncertainDouble plusExact(double other) {
return UncertainDouble.of(this.value + other, this.uncertainty);
}
/**
* @return relative uncertainty
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final double relativeUncertainty() {
+ public double relativeUncertainty() {
return this.uncertainty / this.value;
}
/**
* Returns the product of {@code this} and {@code other}.
- *
+ *
+ * @param other number to multiply
+ * @return product
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble times(UncertainDouble other) {
+ public UncertainDouble times(UncertainDouble other) {
Objects.requireNonNull(other, "other may not be null");
return UncertainDouble.ofRelative(this.value * other.value, Math
.hypot(this.relativeUncertainty(), other.relativeUncertainty()));
@@ -309,23 +354,31 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Returns the product of {@code this} and the exact value {@code other}.
- *
+ *
+ * @param other number to multiply
+ * @return product
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble timesExact(double other) {
+ public UncertainDouble timesExact(double other) {
return UncertainDouble.of(this.value * other, this.uncertainty * other);
}
/**
* Returns the result of {@code this} raised to the exponent {@code other}.
- *
+ *
+ * @param other exponent
+ * @return result of exponentation
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble toExponent(UncertainDouble other) {
+ public UncertainDouble toExponent(UncertainDouble other) {
Objects.requireNonNull(other, "other may not be null");
- final double result = Math.pow(this.value, other.value);
- final double relativeUncertainty = Math.hypot(
+ final var result = Math.pow(this.value, other.value);
+ final var relativeUncertainty = Math.hypot(
other.value * this.relativeUncertainty(),
Math.log(this.value) * other.uncertainty);
@@ -335,10 +388,14 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Returns the result of {@code this} raised the exact exponent
* {@code other}.
- *
+ *
+ * @param other exponent
+ * @return result of exponentation
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final UncertainDouble toExponentExact(double other) {
+ public UncertainDouble toExponentExact(double other) {
return UncertainDouble.ofRelative(Math.pow(this.value, other),
this.relativeUncertainty() * other);
}
@@ -346,23 +403,24 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
/**
* Returns a string representation of this {@code UncertainDouble}.
* <p>
- * This method returns the same value as {@link #toString(boolean)}, but
- * {@code showUncertainty} is true if and only if the uncertainty is
- * non-zero.
- *
+ * This method returns the same value as
+ * {@link #toString(boolean, RoundingMode)}, but {@code showUncertainty} is
+ * true if and only if the uncertainty is non-zero.
+ *
* <p>
* Examples:
- *
+ *
* <pre>
* UncertainDouble.of(3.27, 0.22).toString() = "3.3 � 0.2"
* UncertainDouble.of(3.27, 0.13).toString() = "3.27 � 0.13"
* UncertainDouble.of(-5.01, 0).toString() = "-5.01"
* </pre>
- *
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
@Override
- public final String toString() {
+ public String toString() {
return this.toString(!this.isExact(), RoundingMode.HALF_EVEN);
}
@@ -370,7 +428,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
* Returns a string representation of this {@code UncertainDouble}.
* <p>
* If {@code showUncertainty} is true, the string will be of the form "VALUE
- * � UNCERTAINTY", and if it is false the string will be of the form "VALUE"
+ * ± UNCERTAINTY", and if it is false the string will be of the form "VALUE"
* <p>
* VALUE represents a string representation of this {@code UncertainDouble}'s
* value. If the uncertainty is non-zero, the string will be rounded to the
@@ -382,20 +440,24 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
* digits otherwise it will be rounded to one significant digit.
* <p>
* Examples:
- *
+ *
* <pre>
* UncertainDouble.of(3.27, 0.22).toString(false) = "3.3"
- * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 � 0.2"
+ * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2"
* UncertainDouble.of(3.27, 0.13).toString(false) = "3.27"
- * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 � 0.13"
+ * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13"
* UncertainDouble.of(-5.01, 0).toString(false) = "-5.01"
- * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 � 0.0"
+ * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0"
* </pre>
- *
+ *
+ * @param showUncertainty uncertainty is only shown if this parameter is true
+ * @param roundingMode how to round values
+ * @return string representation of this {@code UncertainDouble}
+ *
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final String toString(boolean showUncertainty,
- RoundingMode roundingMode) {
+ public String toString(boolean showUncertainty, RoundingMode roundingMode) {
String valueString, uncertaintyString;
// generate the string representation of value and uncertainty
@@ -405,36 +467,37 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
} else {
// round the value and uncertainty according to getDisplayScale()
- final BigDecimal bigValue = BigDecimal.valueOf(this.value);
- final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty);
+ final var bigValue = BigDecimal.valueOf(this.value);
+ final var bigUncertainty = BigDecimal.valueOf(this.uncertainty);
- final int displayScale = this.getDisplayScale();
- final BigDecimal roundedUncertainty = bigUncertainty
- .setScale(displayScale, roundingMode);
- final BigDecimal roundedValue = bigValue.setScale(displayScale,
+ final var displayScale = this.getDisplayScale();
+ final var roundedUncertainty = bigUncertainty.setScale(displayScale,
roundingMode);
+ final var roundedValue = bigValue.setScale(displayScale, roundingMode);
valueString = roundedValue.toString();
uncertaintyString = roundedUncertainty.toString();
}
- // return "value" or "value � uncertainty" depending on showUncertainty
- return valueString + (showUncertainty ? " � " + uncertaintyString : "");
+ // return "value" or "value ± uncertainty" depending on showUncertainty
+ return valueString + (showUncertainty ? " ± " + uncertaintyString : "");
}
/**
* @return absolute uncertainty
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final double uncertainty() {
+ public double uncertainty() {
return this.uncertainty;
}
/**
* @return value without uncertainty
* @since 2020-09-07
+ * @since v0.3.0
*/
- public final double value() {
+ public double value() {
return this.value;
}
}
diff --git a/src/main/java/sevenUnits/utils/package-info.java b/src/main/java/sevenUnits/utils/package-info.java
index 350c62d..b600c17 100644
--- a/src/main/java/sevenUnits/utils/package-info.java
+++ b/src/main/java/sevenUnits/utils/package-info.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018-2020 Adrien Hopkins
+ * Copyright (C) 2018-2025 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
@@ -17,7 +17,7 @@
/**
* Supplementary classes that are not related to units, but are necessary for
* their function.
- *
+ *
* @author Adrien Hopkins
* @since 2019-03-14
* @since v0.2.0
diff --git a/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
index 1fb2709..0e38c67 100644
--- a/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
+++ b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
@@ -1,5 +1,18 @@
/**
- * @since 2020-08-26
+ * Copyright (C) 2020, 2022, 2024, 2025 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package sevenUnitsGUI;
@@ -13,14 +26,17 @@ import sevenUnits.unit.UnitPrefix;
* A rule that specifies whether prefix repetition is allowed
*
* @since 2020-08-26
+ * @since v0.3.0
*/
public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> {
+ /** Prefix repetition is never allowed; only one prefix may be used. */
NO_REPETITION {
@Override
public boolean test(List<UnitPrefix> prefixes) {
return prefixes.size() <= 1;
}
},
+ /** Prefix repetition is always allowed, without restrictions. */
NO_RESTRICTION {
@Override
public boolean test(List<UnitPrefix> prefixes) {
@@ -40,7 +56,7 @@ public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> {
final boolean magnifying;
if (prefixes.isEmpty())
return true;
- else if (prefixes.get(0).getMultiplier() > 1) {
+ if (prefixes.get(0).getMultiplier() > 1) {
magnifying = true;
} else {
magnifying = false;
@@ -52,15 +68,14 @@ public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> {
if (!Metric.DECIMAL_PREFIXES.contains(prefixes.get(0)))
return NO_REPETITION.test(prefixes);
- int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto,
+ var 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)
+ if (!Metric.DECIMAL_PREFIXES.contains(prefix)
+ || (magnifying != prefix.getMultiplier() > 1))
return false;
// check if the current prefix is correct
diff --git a/src/main/java/sevenUnitsGUI/DelegateListModel.java b/src/main/java/sevenUnitsGUI/DelegateListModel.java
index 798383b..da4f978 100644
--- a/src/main/java/sevenUnitsGUI/DelegateListModel.java
+++ b/src/main/java/sevenUnitsGUI/DelegateListModel.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * Copyright (C) 2018, 2022, 2024 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
@@ -31,7 +31,7 @@ import javax.swing.AbstractListModel;
* the delegated list's methods because the delegate methods handle updating the
* list.
* </p>
- *
+ *
* @author Adrien Hopkins
* @since 2019-01-14
* @since v0.1.0
@@ -46,7 +46,7 @@ final class DelegateListModel<E> extends AbstractListModel<E>
/**
* The list that this model is a delegate to.
- *
+ *
* @since 2019-01-14
* @since v0.1.0
*/
@@ -54,8 +54,9 @@ final class DelegateListModel<E> extends AbstractListModel<E>
/**
* Creates an empty {@code DelegateListModel}.
- *
+ *
* @since 2019-04-13
+ * @since v0.2.0
*/
public DelegateListModel() {
this(new ArrayList<>());
@@ -63,7 +64,7 @@ final class DelegateListModel<E> extends AbstractListModel<E>
/**
* Creates the {@code DelegateListModel}.
- *
+ *
* @param delegate list to delegate
* @since 2019-01-14
* @since v0.1.0
@@ -74,8 +75,8 @@ final class DelegateListModel<E> extends AbstractListModel<E>
@Override
public boolean add(final E element) {
- final int index = this.delegate.size();
- final boolean success = this.delegate.add(element);
+ final var index = this.delegate.size();
+ final var success = this.delegate.add(element);
this.fireIntervalAdded(this, index, index);
return success;
}
@@ -88,7 +89,7 @@ final class DelegateListModel<E> extends AbstractListModel<E>
@Override
public boolean addAll(final Collection<? extends E> c) {
- boolean changed = false;
+ var changed = false;
for (final E e : c) {
if (this.add(e)) {
changed = true;
@@ -108,7 +109,7 @@ final class DelegateListModel<E> extends AbstractListModel<E>
@Override
public void clear() {
- final int oldSize = this.delegate.size();
+ final var oldSize = this.delegate.size();
this.delegate.clear();
if (oldSize >= 1) {
this.fireIntervalRemoved(this, 0, oldSize - 1);
@@ -176,7 +177,7 @@ final class DelegateListModel<E> extends AbstractListModel<E>
@Override
public E remove(final int index) {
- final E returnValue = this.delegate.get(index);
+ final var returnValue = this.delegate.get(index);
this.delegate.remove(index);
this.fireIntervalRemoved(this, index, index);
return returnValue;
@@ -184,15 +185,15 @@ final class DelegateListModel<E> extends AbstractListModel<E>
@Override
public boolean remove(final Object o) {
- final int index = this.delegate.indexOf(o);
- final boolean returnValue = this.delegate.remove(o);
+ final var index = this.delegate.indexOf(o);
+ final var returnValue = this.delegate.remove(o);
this.fireIntervalRemoved(this, index, index);
return returnValue;
}
@Override
public boolean removeAll(final Collection<?> c) {
- boolean changed = false;
+ var changed = false;
for (final Object e : c) {
if (this.remove(e)) {
changed = true;
@@ -203,15 +204,15 @@ final class DelegateListModel<E> extends AbstractListModel<E>
@Override
public boolean retainAll(final Collection<?> c) {
- final int oldSize = this.size();
- final boolean returnValue = this.delegate.retainAll(c);
+ final var oldSize = this.size();
+ final var returnValue = this.delegate.retainAll(c);
this.fireIntervalRemoved(this, this.size(), oldSize - 1);
return returnValue;
}
@Override
public E set(final int index, final E element) {
- final E returnValue = this.delegate.get(index);
+ final var returnValue = this.delegate.get(index);
this.delegate.set(index, element);
this.fireContentsChanged(this, index, index);
return returnValue;
diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
index 882c995..ce69365 100644
--- a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
+++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2021 Adrien Hopkins
+ * Copyright (C) 2021, 2022, 2024, 2025 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
@@ -18,32 +18,32 @@ package sevenUnitsGUI;
/**
* A View that can convert unit expressions
- *
+ *
* @author Adrien Hopkins
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
public interface ExpressionConversionView extends View {
/**
* @return unit expression to convert <em>from</em>
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
String getFromExpression();
/**
* @return unit expression to convert <em>to</em>
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
String getToExpression();
/**
* Shows the output of an expression conversion to the user.
- *
+ *
* @param uc unit conversion to show
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
void showExpressionConversionOutput(UnitConversionRecord uc);
}
diff --git a/src/main/java/sevenUnitsGUI/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java
index 484a98f..ff942fb 100644
--- a/src/main/java/sevenUnitsGUI/FilterComparator.java
+++ b/src/main/java/sevenUnitsGUI/FilterComparator.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * Copyright (C) 2018, 2022, 2024, 2025 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
@@ -21,9 +21,9 @@ import java.util.Objects;
/**
* A comparator that compares strings using a filter.
- *
+ *
* @param <T> type of element being compared
- *
+ *
* @author Adrien Hopkins
* @since 2019-01-15
* @since v0.1.0
@@ -31,21 +31,21 @@ import java.util.Objects;
final class FilterComparator<T> implements Comparator<T> {
/**
* The filter that the comparator is filtered by.
- *
+ *
* @since 2019-01-15
* @since v0.1.0
*/
private final String filter;
/**
* The comparator to use if the arguments are otherwise equal.
- *
+ *
* @since 2019-01-15
* @since v0.1.0
*/
private final Comparator<T> comparator;
/**
* Whether or not the comparison is case-sensitive.
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@@ -53,7 +53,7 @@ final class FilterComparator<T> implements Comparator<T> {
/**
* Creates the {@code FilterComparator}.
- *
+ *
* @param filter
* @since 2019-01-15
* @since v0.1.0
@@ -64,7 +64,7 @@ final class FilterComparator<T> implements Comparator<T> {
/**
* Creates the {@code FilterComparator}.
- *
+ *
* @param filter string to filter by
* @param comparator comparator to fall back to if all else fails, null is
* compareTo.
@@ -79,7 +79,7 @@ final class FilterComparator<T> implements Comparator<T> {
/**
* Creates the {@code FilterComparator}.
- *
+ *
* @param filter string to filter by
* @param comparator comparator to fall back to if all else fails, null is
* compareTo.
@@ -118,19 +118,18 @@ final class FilterComparator<T> implements Comparator<T> {
// elements that start with the filter always go first
if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
return -1;
- else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
+ if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
return 1;
// elements that contain the filter but don't start with them go next
if (str0.contains(this.filter) && !str1.contains(this.filter))
return -1;
- else if (!str0.contains(this.filter) && !str1.contains(this.filter))
+ if (!str0.contains(this.filter) && !str1.contains(this.filter))
return 1;
// other elements go last
if (this.comparator == null)
return str0.compareTo(str1);
- else
- return this.comparator.compare(arg0, arg1);
+ return this.comparator.compare(arg0, arg1);
}
}
diff --git a/src/main/java/sevenUnitsGUI/GridBagBuilder.java b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
index fdbaee7..a9fede3 100644
--- a/src/main/java/sevenUnitsGUI/GridBagBuilder.java
+++ b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * Copyright (C) 2018, 2022, 2024, 2025 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
@@ -21,7 +21,7 @@ import java.awt.Insets;
/**
* A builder for Java's {@link java.awt.GridBagConstraints} class.
- *
+ *
* @author Adrien Hopkins
* @since 2018-11-30
* @since v0.1.0
@@ -40,7 +40,7 @@ final class GridBagBuilder {
* <p>
* The default value is <code>RELATIVE</code>. <code>gridx</code> should be a
* non-negative value.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#gridy
@@ -58,7 +58,7 @@ final class GridBagBuilder {
* <p>
* The default value is <code>RELATIVE</code>. <code>gridy</code> should be a
* non-negative value.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#gridx
@@ -76,7 +76,7 @@ final class GridBagBuilder {
* from <code>gridx</code> to the next to the last one in its row.
* <p>
* <code>gridwidth</code> should be non-negative and the default value is 1.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#gridheight
@@ -96,7 +96,7 @@ final class GridBagBuilder {
* <p>
* <code>gridheight</code> should be a non-negative value and the default
* value is 1.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#gridwidth
@@ -119,7 +119,7 @@ final class GridBagBuilder {
* <p>
* The default value of this field is <code>0</code>. <code>weightx</code>
* should be a non-negative value.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#weighty
@@ -142,7 +142,7 @@ final class GridBagBuilder {
* <p>
* The default value of this field is <code>0</code>. <code>weighty</code>
* should be a non-negative value.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#weightx
@@ -173,7 +173,7 @@ final class GridBagBuilder {
* <code>BELOW_BASELINE</code>, <code>BELOW_BASELINE_LEADING</code>, and
* <code>BELOW_BASELINE_TRAILING</code>. The default value is
* <code>CENTER</code>.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.ComponentOrientation
@@ -199,7 +199,7 @@ final class GridBagBuilder {
* </ul>
* <p>
* The default value is <code>NONE</code>.
- *
+ *
* @serial
* @see #clone()
*/
@@ -212,7 +212,7 @@ final class GridBagBuilder {
* amount of space between the component and the edges of its display area.
* <p>
* The default value is <code>new Insets(0, 0, 0, 0)</code>.
- *
+ *
* @serial
* @see #clone()
*/
@@ -226,7 +226,7 @@ final class GridBagBuilder {
* is at least its minimum width plus <code>ipadx</code> pixels.
* <p>
* The default value is <code>0</code>.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#ipady
@@ -241,7 +241,7 @@ final class GridBagBuilder {
* least its minimum height plus <code>ipady</code> pixels.
* <p>
* The default value is 0.
- *
+ *
* @serial
* @see #clone()
* @see java.awt.GridBagConstraints#ipadx
@@ -292,7 +292,6 @@ final class GridBagBuilder {
final int gridheight, final double weightx, final double weighty,
final int anchor, final int fill, final Insets insets, final int ipadx,
final int ipady) {
- super();
this.gridx = gridx;
this.gridy = gridy;
this.gridwidth = gridwidth;
@@ -418,6 +417,7 @@ final class GridBagBuilder {
/**
* @param anchor anchor to set
+ * @return this, so that you can chain methods
* @since 2018-11-30
* @since v0.1.0
*/
@@ -428,6 +428,7 @@ final class GridBagBuilder {
/**
* @param fill fill to set
+ * @return this, so that you can chain methods
* @since 2018-11-30
* @since v0.1.0
*/
@@ -438,6 +439,7 @@ final class GridBagBuilder {
/**
* @param insets insets to set
+ * @return this, so that you can chain methods
* @since 2018-11-30
* @since v0.1.0
*/
@@ -448,6 +450,7 @@ final class GridBagBuilder {
/**
* @param ipadx ipadx to set
+ * @return this, so that you can chain methods
* @since 2018-11-30
* @since v0.1.0
*/
@@ -458,6 +461,7 @@ final class GridBagBuilder {
/**
* @param ipady ipady to set
+ * @return this, so that you can chain methods
* @since 2018-11-30
* @since v0.1.0
*/
@@ -468,6 +472,7 @@ final class GridBagBuilder {
/**
* @param weightx weightx to set
+ * @return this, so that you can chain methods
* @since 2018-11-30
* @since v0.1.0
*/
@@ -478,6 +483,7 @@ final class GridBagBuilder {
/**
* @param weighty weighty to set
+ * @return this, so that you can chain methods
* @since 2018-11-30
* @since v0.1.0
*/
diff --git a/src/main/java/sevenUnitsGUI/Main.java b/src/main/java/sevenUnitsGUI/Main.java
index ff61b3b..3ff2fd9 100644
--- a/src/main/java/sevenUnitsGUI/Main.java
+++ b/src/main/java/sevenUnitsGUI/Main.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -19,8 +19,8 @@ package sevenUnitsGUI;
/**
* The main code for the 7Units GUI
*
- * @since v0.4.0
* @since 2022-04-19
+ * @since v0.4.0
*/
public final class Main {
@@ -28,8 +28,8 @@ public final class Main {
* The main method that starts 7Units
*
* @param args commandline arguments
- * @since v0.4.0
* @since 2022-04-19
+ * @since v0.4.0
*/
public static void main(String[] args) {
View.createTabbedView();
diff --git a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java
index 69f09e6..73d12bc 100644
--- a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java
+++ b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -34,8 +34,8 @@ 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
+ * @since v0.4.0
*/
public final class PrefixSearchRule implements
Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> {
@@ -70,10 +70,10 @@ public final class PrefixSearchRule implements
*
* @param prefixes prefixes to apply
* @return prefix rule
- * @since v0.4.0
* @since 2022-07-06
+ * @since v0.4.0
*/
- public static final PrefixSearchRule getCoherentOnlyRule(
+ public static PrefixSearchRule getCoherentOnlyRule(
Set<UnitPrefix> prefixes) {
return new PrefixSearchRule(prefixes,
u -> u.isCoherent() && !u.getName().equals("kilogram"));
@@ -84,30 +84,25 @@ public final class PrefixSearchRule implements
*
* @param prefixes prefixes to apply
* @return prefix rule
- * @since v0.4.0
* @since 2022-07-06
+ * @since v0.4.0
*/
- public static final PrefixSearchRule getUniversalRule(
- Set<UnitPrefix> prefixes) {
+ public static PrefixSearchRule getUniversalRule(Set<UnitPrefix> prefixes) {
return new PrefixSearchRule(prefixes, u -> true);
}
- /**
- * The set of prefixes that will be applied to the unit.
- */
+ /** The set of prefixes that will be applied to the unit. */
private final Set<UnitPrefix> prefixes;
- /**
- * Determines which units are given prefixes.
- */
+ /** Determines which units are given prefixes. */
private final Predicate<LinearUnit> prefixableUnitRule;
/**
* @param prefixes prefixes to add to units
* @param prefixableUnitRule function that determines which units get
* prefixes
- * @since v0.4.0
* @since 2022-07-06
+ * @since v0.4.0
*/
public PrefixSearchRule(Set<UnitPrefix> prefixes,
Predicate<LinearUnit> prefixableUnitRule) {
@@ -118,8 +113,8 @@ public final class PrefixSearchRule implements
@Override
public Map<String, LinearUnit> apply(Entry<String, LinearUnit> t) {
final Map<String, LinearUnit> outputUnits = new HashMap<>();
- final String originalName = t.getKey();
- final LinearUnit originalUnit = t.getValue();
+ final var originalName = t.getKey();
+ final var originalUnit = t.getValue();
outputUnits.put(originalName, originalUnit);
if (this.prefixableUnitRule.test(originalUnit)) {
for (final UnitPrefix prefix : this.prefixes) {
@@ -136,15 +131,15 @@ public final class PrefixSearchRule implements
return true;
if (!(obj instanceof PrefixSearchRule))
return false;
- final PrefixSearchRule other = (PrefixSearchRule) obj;
+ final var other = (PrefixSearchRule) obj;
return Objects.equals(this.prefixableUnitRule, other.prefixableUnitRule)
&& Objects.equals(this.prefixes, other.prefixes);
}
/**
* @return rule that determines which units get prefixes
- * @since v0.4.0
* @since 2022-07-09
+ * @since v0.4.0
*/
public Predicate<LinearUnit> getPrefixableUnitRule() {
return this.prefixableUnitRule;
@@ -152,8 +147,8 @@ public final class PrefixSearchRule implements
/**
* @return the prefixes that are applied by this rule
- * @since v0.4.0
* @since 2022-07-06
+ * @since v0.4.0
*/
public Set<UnitPrefix> getPrefixes() {
return this.prefixes;
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
index eba8438..d258e1f 100644
--- a/src/main/java/sevenUnitsGUI/Presenter.java
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2021-2022 Adrien Hopkins
+ * Copyright (C) 2021-2025 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,12 +16,12 @@
*/
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -40,12 +40,14 @@ import sevenUnits.unit.BaseUnit;
import sevenUnits.unit.BritishImperial;
import sevenUnits.unit.LinearUnit;
import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.LoadingException;
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.NameSymbol;
import sevenUnits.utils.Nameable;
import sevenUnits.utils.ObjectProduct;
import sevenUnits.utils.UncertainDouble;
@@ -55,9 +57,10 @@ import sevenUnitsGUI.StandardDisplayRules.UncertaintyBased;
/**
* An object that handles interactions between the view and the backend code
- *
+ *
* @author Adrien Hopkins
* @since 2021-12-15
+ * @since v0.4.0
*/
public final class Presenter {
/**
@@ -72,33 +75,25 @@ public final class Presenter {
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";
+ /** A Predicate that returns true iff the argument is a full base unit */
+ private static final Predicate<Unit> IS_FULL_BASE = unit -> unit instanceof LinearUnit
+ && ((LinearUnit) unit).isBase();
+ /**
+ * The default locale, used in two situations:
+ * <ul>
+ * <li>If no text is available in your locale, uses text from this locale.
+ * <li>Users are initialized with this locale.
+ * </ul>
+ */
+ static final String DEFAULT_LOCALE = "en";
- private static final Path userConfigDir() {
- if (System.getProperty("os.name").startsWith("Windows")) {
- final String envFolder = System.getenv("LOCALAPPDATA");
- if (envFolder == null || "".equals(envFolder)) {
- return Path.of(System.getenv("USERPROFILE"), "AppData", "Local");
- } else {
- return Path.of(envFolder);
- }
- } else {
- final String envFolder = System.getenv("XDG_CONFIG_HOME");
- if (envFolder == null || "".equals(envFolder)) {
- return Path.of(System.getenv("HOME"), ".config");
- } else {
- return Path.of(envFolder);
- }
- }
- }
-
- /** Gets a Path from a pathname in the config file. */
- private static Path pathFromConfig(String pathname) {
- return CONFIG_FILE.getParent().resolve(pathname);
- }
+ private static final List<String> LOCAL_LOCALES = List.of("en", "fr");
+ private static final Path USER_LOCALES_DIR = userConfigDir()
+ .resolve("SevenUnits").resolve("locales");
/**
* Adds default units and dimensions to a database.
- *
+ *
* @param database database to add to
* @since 2019-04-14
* @since v0.2.0
@@ -125,14 +120,32 @@ public final class Presenter {
database.addDimension("Temperature", Metric.Dimensions.TEMPERATURE);
}
+ private static String displayRuleToString(
+ Function<UncertainDouble, String> numberDisplayRule) {
+ if (numberDisplayRule instanceof FixedDecimals)
+ return String.format("FIXED_DECIMALS %d",
+ ((FixedDecimals) numberDisplayRule).decimalPlaces());
+ if (numberDisplayRule instanceof FixedPrecision)
+ return String.format("FIXED_PRECISION %d",
+ ((FixedPrecision) numberDisplayRule).significantFigures());
+ if (numberDisplayRule instanceof UncertaintyBased)
+ return "UNCERTAINTY_BASED";
+ return numberDisplayRule.toString();
+ }
+
/**
- * @return text in About file
- * @since 2022-02-19
+ * Determines where to wrap {@code toWrap} with a max line length of
+ * {@code maxLineLength}. If no good spot is found, returns -1.
+ *
+ * @since 2024-08-22
+ * @since v1.0.0
*/
- public static final String getAboutText() {
- return Presenter.getLinesFromResource("/about.txt").stream()
- .map(Presenter::withoutComments).collect(Collectors.joining("\n"))
- .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString());
+ private static int findLineSplit(String toWrap, int maxLineLength) {
+ for (var i = maxLineLength - 1; i >= 0; i--) {
+ if (Character.isWhitespace(toWrap.charAt(i)))
+ return i;
+ }
+ return -1;
}
/**
@@ -142,12 +155,13 @@ public final class Presenter {
* @param filename filename to get resource from
* @return contents of file
* @since 2021-03-27
+ * @since v0.3.0
*/
- private static final List<String> getLinesFromResource(String filename) {
+ private static List<String> getLinesFromResource(String filename) {
final List<String> lines = new ArrayList<>();
- try (InputStream stream = inputStream(filename);
- Scanner scanner = new Scanner(stream)) {
+ try (var stream = inputStream(filename);
+ var scanner = new Scanner(stream)) {
while (scanner.hasNextLine()) {
lines.add(scanner.nextLine());
}
@@ -165,16 +179,59 @@ public final class Presenter {
* @param filepath file to use as resource
* @return obtained Path
* @since 2021-03-27
+ * @since v0.3.0
*/
- private static final InputStream inputStream(String filepath) {
+ private static InputStream inputStream(String filepath) {
return Presenter.class.getResourceAsStream(filepath);
}
/**
+ * Convert a linear unit value to a string, where the number is rounded to
+ * the nearest integer.
+ *
+ * @since 2024-08-16
+ * @since v1.0.0
+ */
+ private static String linearUnitValueIntToString(LinearUnitValue uv) {
+ return Long.toString(Math.round(uv.getValueExact())) + " " + uv.getUnit();
+ }
+
+ private static Map.Entry<String, String> parseSettingLine(String line) {
+ final var equalsIndex = line.indexOf('=');
+ if (equalsIndex == -1)
+ throw new IllegalStateException(
+ "Settings file is malformed at line: " + line);
+
+ final var param = line.substring(0, equalsIndex);
+ final var value = line.substring(equalsIndex + 1);
+
+ return Map.entry(param, value);
+ }
+
+ /** Gets a Path from a pathname in the config file. */
+ private static Path pathFromConfig(String pathname) {
+ return CONFIG_FILE.getParent().resolve(pathname);
+ }
+
+ // ====== SETTINGS ======
+
+ private static String searchRuleToString(
+ Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
+ if (PrefixSearchRule.NO_PREFIXES.equals(searchRule))
+ return "NO_PREFIXES";
+ if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule))
+ return "COMMON_PREFIXES";
+ if (PrefixSearchRule.ALL_METRIC_PREFIXES.equals(searchRule))
+ return "ALL_METRIC_PREFIXES";
+ return searchRule.toString();
+ }
+
+ /**
* @return true iff a and b have any elements in common
* @since 2022-04-19
+ * @since v0.4.0
*/
- private static final boolean sharesAnyElements(Set<?> a, Set<?> b) {
+ private static boolean sharesAnyElements(Set<?> a, Set<?> b) {
for (final Object e : a) {
if (b.contains(e))
return true;
@@ -182,20 +239,55 @@ public final class Presenter {
return false;
}
+ private static Path userConfigDir() {
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ final var envFolder = System.getenv("LOCALAPPDATA");
+ if (envFolder == null || "".equals(envFolder))
+ return Path.of(System.getenv("USERPROFILE"), "AppData", "Local");
+ return Path.of(envFolder);
+ }
+ final var envFolder = System.getenv("XDG_CONFIG_HOME");
+ if (envFolder == null || "".equals(envFolder))
+ return Path.of(System.getenv("HOME"), ".config");
+ return Path.of(envFolder);
+ }
+
/**
* @return {@code line} with any comments removed.
* @since 2021-03-13
+ * @since v0.3.0
*/
- private static final String withoutComments(String line) {
- final int index = line.indexOf('#');
+ private static String withoutComments(String line) {
+ final var index = line.indexOf('#');
return index == -1 ? line : line.substring(0, index);
}
- // ====== SETTINGS ======
-
/**
- * The view that this presenter communicates with
+ * Wraps a string, ensuring no line is longer than {@code maxLineLength}.
+ *
+ * @since 2024-08-22
+ * @since v1.0.0
*/
+ private static String wrapString(String toWrap, int maxLineLength) {
+ final var wrapped = new StringBuilder(toWrap.length());
+ var remaining = toWrap;
+ while (remaining.length() > maxLineLength) {
+ final var spot = findLineSplit(toWrap, maxLineLength);
+ if (spot == -1) {
+ wrapped.append(remaining.substring(0, maxLineLength));
+ wrapped.append("-\n");
+ remaining = remaining.substring(maxLineLength).stripLeading();
+ } else {
+ wrapped.append(remaining.substring(0, spot));
+ wrapped.append("\n");
+ remaining = remaining.substring(spot + 1).stripLeading();
+ }
+ }
+ wrapped.append(remaining);
+ return wrapped.toString();
+ }
+
+ /** The view that this presenter communicates with */
private final View view;
/**
@@ -238,6 +330,12 @@ public final class Presenter {
*/
private final Set<String> metricExceptions;
+ /** maps locale names (e.g. 'en') to key-text maps */
+ final Map<String, Map<String, String>> locales;
+
+ /** name of locale in locales to use */
+ String userLocale;
+
/**
* 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
@@ -252,65 +350,61 @@ public final class Presenter {
private boolean showDuplicates = false;
/**
+ * The default unit, prefix, dimension and exception data will only be loaded
+ * if this variable is true.
+ */
+ private boolean useDefaultDatafiles = true;
+
+ /** Custom unit datafiles that will be loaded by {@link #reloadData} */
+ private final Set<Path> customUnitFiles = new HashSet<>();
+ /** Custom dimension datafiles that will be loaded by {@link #reloadData} */
+ private final Set<Path> customDimensionFiles = new HashSet<>();
+ /** Custom exception datafiles that will be loaded by {@link #reloadData} */
+ private final Set<Path> customExceptionFiles = new HashSet<>();
+
+ /**
* Creates a Presenter
- *
+ *
* @param view the view that this presenter communicates with
* @since 2021-12-15
+ * @since v0.4.0
*/
public Presenter(View view) {
this.view = view;
this.database = new UnitDatabase();
- addDefaults(this.database);
+ this.metricExceptions = new HashSet<>();
- // 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);
- }
+ this.locales = this.loadLocales();
+ this.userLocale = DEFAULT_LOCALE;
// set default settings temporarily
if (Files.exists(CONFIG_FILE)) {
this.loadSettings(CONFIG_FILE);
}
- // a Predicate that returns true iff the argument is a full base unit
- final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit
- && ((LinearUnit) unit).isBase();
+ this.reloadData();
// 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());
+ System.out.println(this.loadStatMsg());
+ }
+
+ private void addLocaleFile(Map<String, Map<String, String>> locales,
+ Path file) throws IOException {
+ final Map<String, String> locale = new HashMap<>();
+ final var fileName = file.getName(file.getNameCount() - 1).toString();
+ final var localeName = fileName.substring(0, fileName.length() - 4);
+ try (var lines = Files.lines(file)) {
+ lines.forEach(line -> this.addLocaleLine(locale, line));
+ }
+ locales.put(localeName, locale);
+ }
+
+ private void addLocaleLine(Map<String, String> locale, String line) {
+ final var parts = line.split("=", 2);
+ if (parts.length < 2)
+ return;
+
+ locale.put(parts[0], parts[1]);
}
/**
@@ -319,201 +413,387 @@ public final class Presenter {
* @param e entry
* @return stream of entries, ready for flat-mapping
* @since 2022-07-06
+ * @since v0.4.0
*/
- private final Stream<Map.Entry<String, Unit>> applySearchRule(
+ private Stream<Map.Entry<String, Unit>> applySearchRule(
Map.Entry<String, Unit> e) {
- final Unit u = e.getValue();
+ final var u = e.getValue();
if (u instanceof LinearUnit) {
- final String name = e.getKey();
+ final var name = e.getKey();
final Map.Entry<String, LinearUnit> linearEntry = Map.entry(name,
(LinearUnit) u);
return this.searchRule.apply(linearEntry).entrySet().stream().map(
entry -> Map.entry(entry.getKey(), (Unit) entry.getValue()));
- } else
- return Stream.of(e);
+ }
+ return Stream.of(e);
}
/**
* Converts from the view's input expression to its output expression.
* Displays an error message if any of the required fields are invalid.
- *
+ *
* @throws UnsupportedOperationException if the view does not support
* expression-based conversion (does
* not implement
* {@link ExpressionConversionView})
* @since 2021-12-15
+ * @since v0.4.0
*/
public void convertExpressions() {
- if (this.view instanceof ExpressionConversionView) {
- final ExpressionConversionView xcview = (ExpressionConversionView) this.view;
+ if (!(this.view instanceof ExpressionConversionView))
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is an ExpressionConversionView");
+ final var xcview = (ExpressionConversionView) this.view;
- final String fromExpression = xcview.getFromExpression();
- final String toExpression = xcview.getToExpression();
+ final var fromExpression = xcview.getFromExpression();
+ final var 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;
- }
+ // 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;
- }
+ final Optional<UnitConversionRecord> uc;
+ if (this.database.containsUnitSetName(toExpression)) {
+ uc = this.convertExpressionToNamedMultiUnit(fromExpression,
+ toExpression);
+ } else if (toExpression.contains(";")) {
+ final var toExpressions = toExpression.split(";");
+ uc = this.convertExpressionToMultiUnit(fromExpression, toExpressions);
+ } else {
+ uc = this.convertExpressionToExpression(fromExpression, toExpression);
+ }
+
+ uc.ifPresent(xcview::showExpressionConversionOutput);
+ }
+
+ /**
+ * Converts a unit expression to another expression.
+ *
+ * If an error happened, it is shown to the view and Optional.empty() is
+ * returned.
+ *
+ * @since 2024-08-15
+ * @since v1.0.0
+ */
+ private Optional<UnitConversionRecord> convertExpressionToExpression(
+ String fromExpression, String toExpression) {
+ // 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 Optional.empty();
+ }
+ 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 Optional.empty();
+ }
+
+ // convert and show output
+ if (!from.getUnit().canConvertTo(to)) {
+ this.view.showErrorMessage("Conversion Error",
+ "Cannot convert between \"" + fromExpression + "\" and \""
+ + toExpression + "\".");
+ return Optional.empty();
+ }
+ 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 var value = from.asUnitValue().convertTo(to).getValue();
+ uncertainValue = UncertainDouble.of(value, 0);
+ }
+
+ final var uc = UnitConversionRecord.valueOf(fromExpression, toExpression,
+ "", this.numberDisplayRule.apply(uncertainValue));
+ return Optional.of(uc);
+
+ }
+
+ /**
+ * Convert an expression to a MultiUnit. If an error happened, it is shown to
+ * the view and Optional.empty() is returned.
+ *
+ * @since 2024-08-15
+ * @since v1.0.0
+ */
+ private Optional<UnitConversionRecord> convertExpressionToMultiUnit(
+ String fromExpression, String[] toExpressions) {
+ final LinearUnitValue from;
+ 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 Optional.empty();
+ }
+
+ final List<LinearUnit> toUnits = new ArrayList<>(toExpressions.length);
+ for (final String toExpression : toExpressions) {
try {
- to = this.database.getUnitFromExpression(toExpression);
+ final var toI = this.database
+ .getUnitFromExpression(toExpression.trim());
+ if (!(toI instanceof LinearUnit)) {
+ this.view.showErrorMessage("Unit Type Error",
+ "Units separated by ';' must be linear; " + toI
+ + " is not.");
+ return Optional.empty();
+ }
+ toUnits.add((LinearUnit) toI);
} catch (final IllegalArgumentException | NoSuchElementException e) {
this.view.showErrorMessage("Parse Error",
"Could not recognize text in To entry: " + e.getMessage());
- return;
+ return Optional.empty();
}
+ }
+
+ final List<LinearUnitValue> toValues;
+ try {
+ toValues = from.convertToMultiple(toUnits);
+ } catch (final IllegalArgumentException e) {
+ this.view.showErrorMessage("Unit Error",
+ "Invalid units separated by ';': " + e.getMessage());
+ return Optional.empty();
+ }
- // convert and show output
- if (from.getUnit().canConvertTo(to)) {
- final UncertainDouble uncertainValue;
+ final var toExpression = this.linearUnitValueSumToString(toValues);
+ return Optional.of(
+ UnitConversionRecord.valueOf(fromExpression, toExpression, "", ""));
+ }
- // 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);
- }
+ /**
+ * Convert an expression to a MultiUnit with a name from the database. If an
+ * error happened, it is shown to the view and Optional.empty() is returned.
+ *
+ * @since 2024-08-15
+ * @since v1.0.0
+ */
+ private Optional<UnitConversionRecord> convertExpressionToNamedMultiUnit(
+ String fromExpression, String toName) {
+ final LinearUnitValue from;
+ 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 Optional.empty();
+ }
- final UnitConversionRecord uc = UnitConversionRecord.valueOf(
- fromExpression, toExpression, "",
- this.numberDisplayRule.apply(uncertainValue));
- xcview.showExpressionConversionOutput(uc);
- } else {
- this.view.showErrorMessage("Conversion Error",
- "Cannot convert between \"" + fromExpression + "\" and \""
- + toExpression + "\".");
- }
+ final var toUnits = this.database.getUnitSet(toName);
+ final List<LinearUnitValue> toValues;
+ try {
+ toValues = from.convertToMultiple(toUnits);
+ } catch (final IllegalArgumentException e) {
+ this.view.showErrorMessage("Unit Error",
+ "Invalid units separated by ';': " + e.getMessage());
+ return Optional.empty();
+ }
- } else
- throw new UnsupportedOperationException(
- "This function can only be called when the view is an ExpressionConversionView");
+ final var toExpression = this.linearUnitValueSumToString(toValues);
+ return Optional.of(
+ UnitConversionRecord.valueOf(fromExpression, toExpression, "", ""));
}
/**
* Converts from the view's input unit to its output unit. Displays an error
* message if any of the required fields are invalid.
- *
+ *
* @throws UnsupportedOperationException if the view does not support
* unit-based conversion (does not
* implement
* {@link UnitConversionView})
* @since 2021-12-15
+ * @since v0.4.0
*/
public void convertUnits() {
- if (this.view instanceof UnitConversionView) {
- final UnitConversionView ucview = (UnitConversionView) this.view;
+ if (!(this.view instanceof UnitConversionView))
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is a UnitConversionView.");
+ final var ucview = (UnitConversionView) this.view;
+
+ final var fromUnitOptional = ucview.getFromSelection();
+ final var toUnitOptional = ucview.getToSelection();
+ final var inputValueString = ucview.getInputValue();
+
+ // extract values from optionals
+ final String fromUnitString, toUnitString;
+ if (!fromUnitOptional.isPresent()) {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a From unit");
+ return;
+ }
+ fromUnitString = fromUnitOptional.orElseThrow();
+ if (!toUnitOptional.isPresent()) {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a To unit");
+ return;
+ }
+ toUnitString = toUnitOptional.orElseThrow();
- final Optional<String> fromUnitOptional = ucview.getFromSelection();
- final Optional<String> toUnitOptional = ucview.getToSelection();
- final String inputValueString = ucview.getInputValue();
+ // convert strings to data, checking if anything is invalid
+ final Unit fromUnit;
+ final UncertainDouble uncertainValue;
- // extract values from optionals
- final String fromUnitString, toUnitString;
- if (fromUnitOptional.isPresent()) {
- fromUnitString = fromUnitOptional.orElseThrow();
- } else {
- this.view.showErrorMessage("Unit Selection Error",
- "Please specify a From unit");
- return;
- }
- if (toUnitOptional.isPresent()) {
- toUnitString = toUnitOptional.orElseThrow();
- } else {
- this.view.showErrorMessage("Unit Selection Error",
- "Please specify a To unit");
- return;
- }
-
- // convert strings to data, checking if anything is invalid
- final Unit fromUnit, toUnit;
- final UncertainDouble uncertainValue;
-
- if (this.database.containsUnitName(fromUnitString)) {
- fromUnit = this.database.getUnit(fromUnitString);
- } else
- throw this.viewError("Nonexistent From unit: %s", fromUnitString);
- if (this.database.containsUnitName(toUnitString)) {
- toUnit = this.database.getUnit(toUnitString);
- } else
- throw this.viewError("Nonexistent To unit: %s", toUnitString);
- try {
- uncertainValue = UncertainDouble
- .fromRoundedString(inputValueString);
- } catch (final NumberFormatException e) {
- this.view.showErrorMessage("Value Error",
- "Invalid value " + inputValueString);
- return;
- }
+ if (!this.database.containsUnitName(fromUnitString))
+ throw this.viewError("Nonexistent From unit: %s", fromUnitString);
+ fromUnit = this.database.getUnit(fromUnitString);
+ try {
+ uncertainValue = UncertainDouble.fromRoundedString(inputValueString);
+ } catch (final NumberFormatException e) {
+ this.view.showErrorMessage("Value Error",
+ "Invalid value " + inputValueString);
+ return;
+ }
+ if (this.database.containsUnitName(toUnitString)) {
+ final var toUnit = this.database.getUnit(toUnitString);
+ ucview.showUnitConversionOutput(
+ this.convertUnitToUnit(fromUnitString, toUnitString,
+ inputValueString, fromUnit, toUnit, uncertainValue));
+ } else if (this.database.containsUnitSetName(toUnitString)) {
+ final var toMulti = this.database.getUnitSet(toUnitString);
+ ucview.showUnitConversionOutput(this.convertUnitToMulti(fromUnitString,
+ inputValueString, fromUnit, toMulti, uncertainValue));
+ } else
+ throw this.viewError("Nonexistent To unit: %s", toUnitString);
+ }
+ private UnitConversionRecord convertUnitToMulti(String fromUnitString,
+ String inputValueString, Unit fromUnit, List<LinearUnit> toMulti,
+ UncertainDouble uncertainValue) {
+ for (final LinearUnit toUnit : toMulti) {
if (!fromUnit.canConvertTo(toUnit))
throw this.viewError("Could not convert between %s and %s",
fromUnit, 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);
+ final LinearUnitValue initValue;
+ if (fromUnit instanceof LinearUnit) {
+ final var fromLinear = (LinearUnit) fromUnit;
+ initValue = LinearUnitValue.of(fromLinear, uncertainValue);
+ } else {
+ initValue = UnitValue.of(fromUnit, uncertainValue.value())
+ .convertToBase(NameSymbol.EMPTY);
+ }
- outputValueString = this.numberDisplayRule
- .apply(UncertainDouble.of(converted.getValue(), 0));
- }
+ final var converted = initValue.convertToMultiple(toMulti);
+ final var toExpression = this.linearUnitValueSumToString(converted);
+ return UnitConversionRecord.valueOf(fromUnitString, toExpression,
+ inputValueString, "");
- ucview.showUnitConversionOutput(
- UnitConversionRecord.valueOf(fromUnitString, toUnitString,
- inputValueString, outputValueString));
- } else
- throw new UnsupportedOperationException(
- "This function can only be called when the view is a UnitConversionView.");
+ }
+
+ private UnitConversionRecord convertUnitToUnit(String fromUnitString,
+ String toUnitString, String inputValueString, Unit fromUnit,
+ Unit toUnit, UncertainDouble uncertainValue) {
+ if (!fromUnit.canConvertTo(toUnit))
+ throw this.viewError("Could not convert between %s and %s", fromUnit,
+ 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 var fromLinear = (LinearUnit) fromUnit;
+ final var toLinear = (LinearUnit) toUnit;
+ final var initialValue = LinearUnitValue.of(fromLinear,
+ uncertainValue);
+ final var converted = initialValue.convertTo(toLinear);
+
+ outputValueString = this.numberDisplayRule.apply(converted.getValue());
+ } else {
+ final var initialValue = UnitValue.of(fromUnit,
+ uncertainValue.value());
+ final var converted = initialValue.convertTo(toUnit);
+
+ outputValueString = this.numberDisplayRule
+ .apply(UncertainDouble.of(converted.getValue(), 0));
+ }
+
+ return UnitConversionRecord.valueOf(fromUnitString, toUnitString,
+ inputValueString, outputValueString);
}
/**
* @return true iff duplicate units are shown in unit lists
* @since 2022-03-30
+ * @since v0.4.0
*/
public boolean duplicatesShown() {
return this.showDuplicates;
}
+ private String formatAboutText(Stream<String> rawLines) {
+ return rawLines.map(Presenter::withoutComments)
+ .collect(Collectors.joining("\n"))
+ .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString())
+ .replaceAll("\\[LOADSTATS\\]", wrapString(this.loadStatMsg(), 72));
+ }
+
+ /**
+ * @return text in About file
+ * @since 2022-02-19
+ * @since v0.4.0
+ */
+ public String getAboutText() {
+ final var customFilepath = Presenter
+ .pathFromConfig("about/" + this.userLocale + ".txt");
+ if (Files.exists(customFilepath)) {
+ try (var lines = Files.lines(customFilepath)) {
+ return this.formatAboutText(lines);
+ } catch (final IOException e) {
+ final var filename = String.format("/about/%s.txt",
+ this.userLocale);
+ return this.formatAboutText(
+ Presenter.getLinesFromResource(filename).stream());
+ }
+ }
+ if (LOCAL_LOCALES.contains(this.userLocale)) {
+ final var filename = String.format("/about/%s.txt", this.userLocale);
+ return this.formatAboutText(
+ Presenter.getLinesFromResource(filename).stream());
+ }
+ final var filename = String.format("/about/%s.txt", DEFAULT_LOCALE);
+ return this
+ .formatAboutText(Presenter.getLinesFromResource(filename).stream());
+
+ }
+
+ /**
+ * @return set of all locales available to select
+ * @since 2025-02-21
+ * @since v1.0.0
+ */
+ public Set<String> getAvailableLocales() {
+ return this.locales.keySet();
+ }
+
/**
* Gets a name for this dimension using the database
*
* @param dimension dimension to name
* @return name of dimension
* @since 2022-04-16
+ * @since v0.4.0
*/
- final String getDimensionName(ObjectProduct<BaseDimension> dimension) {
+ String getDimensionName(ObjectProduct<BaseDimension> 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()
@@ -522,9 +802,24 @@ public final class Presenter {
}
/**
+ * Gets the correct text for a provided ID. If this text is available in the
+ * user's locale, that text is provided. Otherwise, text is taken from the
+ * system default locale {@link #DEFAULT_LOCALE}.
+ *
+ * @param textID ID of text to get (used in locale files)
+ * @return text to be displayed
+ */
+ public String getLocalizedText(String textID) {
+ final var userLocale = this.locales.get(this.userLocale);
+ final var defaultLocale = this.locales.get(DEFAULT_LOCALE);
+ return userLocale.getOrDefault(textID, defaultLocale.get(textID));
+ }
+
+ /**
* @return the rule that is used by this presenter to convert numbers into
* strings
* @since 2022-04-10
+ * @since v0.4.0
*/
public Function<UncertainDouble, String> getNumberDisplayRule() {
return this.numberDisplayRule;
@@ -534,6 +829,7 @@ public final class Presenter {
* @return the rule that is used by this presenter to convert strings into
* numbers
* @since 2022-04-10
+ * @since v0.4.0
*/
@SuppressWarnings("unused") // not implemented yet
private Function<String, UncertainDouble> getNumberParsingRule() {
@@ -543,6 +839,7 @@ public final class Presenter {
/**
* @return the rule that determines whether a set of prefixes is valid
* @since 2022-04-19
+ * @since v0.4.0
*/
public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
return this.prefixRepetitionRule;
@@ -551,6 +848,7 @@ public final class Presenter {
/**
* @return the rule that determines which units are prefixed
* @since 2022-07-08
+ * @since v0.4.0
*/
public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getSearchRule() {
return this.searchRule;
@@ -559,6 +857,7 @@ public final class Presenter {
/**
* @return a search rule that shows all single prefixes
* @since 2022-07-08
+ * @since v0.4.0
*/
public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getUniversalSearchRule() {
return PrefixSearchRule.getCoherentOnlyRule(
@@ -566,19 +865,49 @@ public final class Presenter {
}
/**
+ * @return user's selected locale
+ * @since 2025-02-21
+ * @since v1.0.0
+ */
+ public String getUserLocale() {
+ return this.userLocale;
+ }
+
+ /**
* @return the view associated with this presenter
* @since 2022-04-19
+ * @since v0.4.0
*/
public View getView() {
return this.view;
}
/**
+ * Accepts a list of errors. If that list is non-empty, prints an error
+ * message and alerts the user.
+ *
+ * @since 2024-08-22
+ * @since v1.0.0
+ */
+ private void handleLoadErrors(List<LoadingException> errors) {
+ if (!errors.isEmpty()) {
+ final var errorMessage = String.format(
+ "%d error(s) happened while loading file:\n%s\n", errors.size(),
+ errors.stream().map(Throwable::getMessage)
+ .collect(Collectors.joining("\n")));
+ System.err.print(errorMessage);
+ this.view.showErrorMessage(errors.size() + "Loading Error(s)",
+ errorMessage);
+ }
+ }
+
+ /**
* @return whether or not the provided unit is semi-metric (i.e. an
* exception)
* @since 2022-04-16
+ * @since v0.4.0
*/
- final boolean isSemiMetric(Unit u) {
+ boolean isSemiMetric(Unit u) {
// determine if u is an exception
final var primaryName = u.getPrimaryName();
final var symbol = u.getSymbol();
@@ -590,27 +919,127 @@ public final class Presenter {
}
/**
+ * Convert a list of LinearUnitValues that you would get from a unit-set
+ * conversion to a string. All but the last have their numbers rendered as
+ * integers, since they are always integers. The last one follows the usual
+ * number display rule.
+ *
+ * @since 2024-08-16
+ * @since v1.0.0
+ */
+ private String linearUnitValueSumToString(List<LinearUnitValue> values) {
+ final var integerPart = values.subList(0, values.size() - 1).stream()
+ .map(Presenter::linearUnitValueIntToString)
+ .collect(Collectors.joining(" + "));
+ final var last = values.get(values.size() - 1);
+ return integerPart + " + " + this.numberDisplayRule.apply(last.getValue())
+ + " " + last.getUnit();
+ }
+
+ /** Load units, prefixes and dimensions from the default files. */
+ private void loadDefaultData() {
+ // load units and prefixes
+ try (final var units = inputStream(DEFAULT_UNITS_FILEPATH)) {
+ this.handleLoadErrors(this.database.loadUnitsFromStream(units));
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of unitsfile.txt failed.", e);
+ }
+
+ // load dimensions
+ try (final var dimensions = inputStream(DEFAULT_DIMENSIONS_FILEPATH)) {
+ this.handleLoadErrors(
+ this.database.loadDimensionsFromStream(dimensions));
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of dimensionfile.txt failed.", e);
+ }
+
+ // load metric exceptions
+ try {
+ try (var exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
+ var scanner = new Scanner(exceptions)) {
+ while (scanner.hasNextLine()) {
+ final var 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);
+ }
+ }
+
+ private void loadExceptionFile(Path exceptionFile) {
+ try (var lines = Files.lines(exceptionFile)) {
+ lines.map(Presenter::withoutComments)
+ .forEach(this.metricExceptions::add);
+ } catch (final IOException e) {
+ this.view.showErrorMessage("File Load Error",
+ "Error loading configured metric exception file \""
+ + exceptionFile + "\": " + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * Loads all available locales, including custom ones, into a map.
+ *
+ * @return map containing locales
+ */
+ private Map<String, Map<String, String>> loadLocales() {
+ final Map<String, Map<String, String>> locales = new HashMap<>();
+ for (final String localeName : LOCAL_LOCALES) {
+ final Map<String, String> locale = new HashMap<>();
+ final var filename = "/locales/" + localeName + ".txt";
+ getLinesFromResource(filename)
+ .forEach(line -> this.addLocaleLine(locale, line));
+ locales.put(localeName, locale);
+ }
+
+ if (Files.exists(USER_LOCALES_DIR)) {
+ try (var files = Files.list(USER_LOCALES_DIR)) {
+ files.forEach(localeFile -> {
+ try {
+ this.addLocaleFile(locales, localeFile);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ }
+ });
+ } catch (final IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return locales;
+ }
+
+ /**
* Loads settings from the user's settings file and applies them to the
* presenter.
*
* @param settingsFile file settings should be loaded from
* @since 2021-12-15
+ * @since v0.4.0
*/
void loadSettings(Path settingsFile) {
- for (Map.Entry<String, String> setting : settingsFromFile(settingsFile)) {
- final String value = setting.getValue();
+ this.customDimensionFiles.clear();
+ this.customExceptionFiles.clear();
+ this.customUnitFiles.clear();
+
+ for (final Map.Entry<String, String> setting : this
+ .settingsFromFile(settingsFile)) {
+ final var value = setting.getValue();
switch (setting.getKey()) {
// set manually to avoid the unnecessary saving of the non-manual
// methods
case "custom_dimension_file":
- this.database.loadDimensionFile(pathFromConfig(value));
+ this.customDimensionFiles.add(pathFromConfig(value));
break;
case "custom_exception_file":
- this.loadExceptionFile(pathFromConfig(value));
+ this.customExceptionFiles.add(pathFromConfig(value));
break;
case "custom_unit_file":
- this.database.loadUnitsFile(pathFromConfig(value));
+ this.customUnitFiles.add(pathFromConfig(value));
break;
case "number_display_rule":
this.setDisplayRuleFromString(value);
@@ -621,14 +1050,28 @@ public final class Presenter {
this.database.setPrefixRepetitionRule(this.prefixRepetitionRule);
break;
case "one_way":
- this.oneWayConversionEnabled = Boolean.valueOf(value);
+ this.oneWayConversionEnabled = Boolean.parseBoolean(value);
break;
case "include_duplicates":
- this.showDuplicates = Boolean.valueOf(value);
+ this.showDuplicates = Boolean.parseBoolean(value);
break;
case "search_prefix_rule":
this.setSearchRuleFromString(value);
break;
+ case "use_default_datafiles":
+ this.useDefaultDatafiles = Boolean.parseBoolean(value);
+ break;
+ case "locale":
+ if (this.locales.containsKey(value)) {
+ this.userLocale = value;
+ } else {
+ System.err.printf("Warning: unrecognized locale \"%s\".%n",
+ value);
+ this.view.showErrorMessage("Unrecognized Locale",
+ "Could not find locale \"" + value
+ + "\", resetting to default.");
+ }
+ break;
default:
System.err.printf("Warning: unrecognized setting \"%s\".%n",
setting.getKey());
@@ -641,86 +1084,38 @@ public final class Presenter {
}
}
- private List<Map.Entry<String, String>> settingsFromFile(Path settingsFile) {
- try (Stream<String> lines = Files.lines(settingsFile)) {
- return lines.map(Presenter::withoutComments)
- .filter(line -> !line.isBlank()).map(Presenter::parseSettingLine)
- .toList();
- } catch (final IOException e) {
- this.view.showErrorMessage("Settings Loading Error",
- "Error loading settings file. Using default settings.");
- return null;
- }
- }
-
- private static Map.Entry<String, String> parseSettingLine(String line) {
- final int equalsIndex = line.indexOf('=');
- if (equalsIndex == -1)
- throw new IllegalStateException(
- "Settings file is malformed at line: " + line);
-
- final String param = line.substring(0, equalsIndex);
- final String value = line.substring(equalsIndex + 1);
-
- return Map.entry(param, value);
- }
-
- private void setSearchRuleFromString(String ruleString) {
- switch (ruleString) {
- case "NO_PREFIXES":
- this.searchRule = PrefixSearchRule.NO_PREFIXES;
- break;
- case "COMMON_PREFIXES":
- this.searchRule = PrefixSearchRule.COMMON_PREFIXES;
- break;
- case "ALL_METRIC_PREFIXES":
- this.searchRule = PrefixSearchRule.ALL_METRIC_PREFIXES;
- break;
- default:
- System.err.printf(
- "Warning: unrecognized value for search_prefix_rule: %s\n",
- ruleString);
- }
- }
-
- private void setDisplayRuleFromString(String ruleString) {
- String[] tokens = ruleString.split(" ");
- switch (tokens[0]) {
- case "FIXED_DECIMALS":
- final int decimals = Integer.parseInt(tokens[1]);
- this.numberDisplayRule = StandardDisplayRules.fixedDecimals(decimals);
- break;
- case "FIXED_PRECISION":
- final int sigDigs = Integer.parseInt(tokens[1]);
- this.numberDisplayRule = StandardDisplayRules.fixedPrecision(sigDigs);
- break;
- case "UNCERTAINTY_BASED":
- this.numberDisplayRule = StandardDisplayRules.uncertaintyBased();
- break;
- default:
- this.numberDisplayRule = StandardDisplayRules
- .getStandardRule(ruleString);
- break;
- }
- }
-
- private void loadExceptionFile(Path exceptionFile) {
- try (Stream<String> lines = Files.lines(exceptionFile)) {
- lines.map(Presenter::withoutComments)
- .forEach(this.metricExceptions::add);
- } catch (IOException e) {
- this.view.showErrorMessage("File Load Error",
- "Error loading configured metric exception file \""
- + exceptionFile + "\": " + e.getLocalizedMessage());
- }
+ /**
+ * @return a message showing how much stuff has been loaded
+ * @since 2024-08-22
+ * @since v1.0.0
+ */
+ private String loadStatMsg() {
+ return this.getLocalizedText("load_stat_msg")
+ .replace("[u]",
+ Integer.toString(
+ this.database.unitMapPrefixless(false).size()))
+ .replace("[un]",
+ Integer
+ .toString(this.database.unitMapPrefixless(true).size()))
+ .replace("[b]",
+ Long.toString(this.database.unitMapPrefixless(false).values()
+ .stream().filter(IS_FULL_BASE).count()))
+ .replace("[p]",
+ Integer.toString(this.database.prefixMap(false).size()))
+ .replace("[pn]",
+ Integer.toString(this.database.prefixMap(true).size()))
+ .replace("[s]", Integer.toString(this.database.unitSetMap().size()))
+ .replace("[d]",
+ Integer.toString(this.database.dimensionMap().size()));
}
/**
* @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
+ * @since v0.4.0
*/
public boolean oneWayConversionEnabled() {
return this.oneWayConversionEnabled;
@@ -730,22 +1125,23 @@ public final class Presenter {
* 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
+ * @since v0.4.0
*/
public void postViewInitialize() {
// unit conversion specific stuff
if (this.view instanceof UnitConversionView) {
- final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var ucview = (UnitConversionView) this.view;
ucview.setDimensionNames(this.database.dimensionMap().keySet());
}
this.updateView();
+ this.view.updateText();
}
void prefixSelected() {
- final Optional<String> selectedPrefixName = this.view
- .getViewedPrefixName();
+ final var selectedPrefixName = this.view.getViewedPrefixName();
final Optional<UnitPrefix> selectedPrefix = selectedPrefixName
.map(name -> this.database.containsPrefixName(name)
? this.database.getPrefix(name)
@@ -755,19 +1151,37 @@ public final class Presenter {
String.valueOf(prefix.getMultiplier())));
}
+ /** Clears then reloads all unit, prefix, dimension and exception data. */
+ public void reloadData() {
+ this.database.clear();
+ this.metricExceptions.clear();
+ addDefaults(this.database);
+
+ if (this.useDefaultDatafiles) {
+ this.loadDefaultData();
+ }
+
+ this.customUnitFiles.forEach(
+ path -> this.handleLoadErrors(this.database.loadUnitsFile(path)));
+ this.customDimensionFiles.forEach(path -> this
+ .handleLoadErrors(this.database.loadDimensionFile(path)));
+ this.customExceptionFiles.forEach(this::loadExceptionFile);
+ }
+
/**
* Saves the presenter's current settings to the config file, creating it if
* it doesn't exist.
- *
+ *
* @return false iff the presenter could not write to the file
* @since 2022-04-19
+ * @since v0.4.0
*/
public boolean saveSettings() {
- final Path configDir = CONFIG_FILE.getParent();
+ final var configDir = CONFIG_FILE.getParent();
if (!Files.exists(configDir)) {
try {
Files.createDirectories(configDir);
- } catch (IOException e) {
+ } catch (final IOException e) {
return false;
}
}
@@ -775,64 +1189,32 @@ public final class Presenter {
return this.writeSettings(CONFIG_FILE);
}
- /**
- * Saves the presenter's settings to the user settings file.
- *
- * @param settingsFile file settings should be saved to
- * @since 2021-12-15
- */
- boolean writeSettings(Path settingsFile) {
- try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) {
- writer.write(String.format("number_display_rule=%s\n",
- displayRuleToString(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));
- writer.write(String.format("search_prefix_rule=%s\n",
- searchRuleToString(this.searchRule)));
- return true;
- } catch (final IOException e) {
- e.printStackTrace();
- this.view.showErrorMessage("I/O Error",
- "Error occurred while saving settings: "
- + e.getLocalizedMessage());
- return false;
+ private void setDisplayRuleFromString(String ruleString) {
+ final var tokens = ruleString.split(" ");
+ switch (tokens[0]) {
+ case "FIXED_DECIMALS":
+ final var decimals = Integer.parseInt(tokens[1]);
+ this.numberDisplayRule = StandardDisplayRules.fixedDecimals(decimals);
+ break;
+ case "FIXED_PRECISION":
+ final var sigDigs = Integer.parseInt(tokens[1]);
+ this.numberDisplayRule = StandardDisplayRules.fixedPrecision(sigDigs);
+ break;
+ case "UNCERTAINTY_BASED":
+ this.numberDisplayRule = StandardDisplayRules.uncertaintyBased();
+ break;
+ default:
+ this.numberDisplayRule = StandardDisplayRules
+ .getStandardRule(ruleString);
+ break;
}
}
- private static String searchRuleToString(
- Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
- if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) {
- return "NO_PREFIXES";
- } else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) {
- return "COMMON_PREFIXES";
- } else if (PrefixSearchRule.ALL_METRIC_PREFIXES.equals(searchRule)) {
- return "ALL_METRIC_PREFIXES";
- } else
- return searchRule.toString();
- }
-
- private static String displayRuleToString(
- Function<UncertainDouble, String> numberDisplayRule) {
- if (numberDisplayRule instanceof FixedDecimals) {
- return String.format("FIXED_DECIMALS %d",
- ((FixedDecimals) numberDisplayRule).decimalPlaces());
- } else if (numberDisplayRule instanceof FixedPrecision) {
- return String.format("FIXED_PRECISION %d",
- ((FixedPrecision) numberDisplayRule).significantFigures());
- } else if (numberDisplayRule instanceof UncertaintyBased) {
- return "UNCERTAINTY_BASED";
- } else
- return numberDisplayRule.toString();
- }
-
/**
* @param numberDisplayRule the new rule that will be used by this presenter
* to convert numbers into strings
* @since 2022-04-10
+ * @since v0.4.0
*/
public void setNumberDisplayRule(
Function<UncertainDouble, String> numberDisplayRule) {
@@ -843,6 +1225,7 @@ public final class Presenter {
* @param numberParsingRule the new rule that will be used by this presenter
* to convert strings into numbers
* @since 2022-04-10
+ * @since v0.4.0
*/
@SuppressWarnings("unused") // not implemented yet
private void setNumberParsingRule(
@@ -854,7 +1237,8 @@ public final class Presenter {
* @param oneWayConversionEnabled whether not one-way conversion should be
* enabled
* @since 2022-03-30
- * @see {@link #isOneWayConversionEnabled}
+ * @since v0.4.0
+ * @see #oneWayConversionEnabled
*/
public void setOneWayConversionEnabled(boolean oneWayConversionEnabled) {
this.oneWayConversionEnabled = oneWayConversionEnabled;
@@ -865,6 +1249,7 @@ public final class Presenter {
* @param prefixRepetitionRule the rule that determines whether a set of
* prefixes is valid
* @since 2022-04-19
+ * @since v0.4.0
*/
public void setPrefixRepetitionRule(
Predicate<List<UnitPrefix>> prefixRepetitionRule) {
@@ -878,30 +1263,86 @@ public final class Presenter {
* unit (including the unit itself) that should be
* searchable.
* @since 2022-07-08
+ * @since v0.4.0
*/
public void setSearchRule(
Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
this.searchRule = searchRule;
}
+ private void setSearchRuleFromString(String ruleString) {
+ switch (ruleString) {
+ case "NO_PREFIXES":
+ this.searchRule = PrefixSearchRule.NO_PREFIXES;
+ break;
+ case "COMMON_PREFIXES":
+ this.searchRule = PrefixSearchRule.COMMON_PREFIXES;
+ break;
+ case "ALL_METRIC_PREFIXES":
+ this.searchRule = PrefixSearchRule.ALL_METRIC_PREFIXES;
+ break;
+ default:
+ System.err.printf(
+ "Warning: unrecognized value for search_prefix_rule: %s\n",
+ ruleString);
+ }
+ }
+
/**
* @param showDuplicateUnits whether or not duplicate units should be shown
* @since 2022-03-30
+ * @since v0.4.0
*/
public void setShowDuplicates(boolean showDuplicateUnits) {
this.showDuplicates = showDuplicateUnits;
this.updateView();
}
+ private List<Map.Entry<String, String>> settingsFromFile(Path settingsFile) {
+ try (var lines = Files.lines(settingsFile)) {
+ return lines.map(Presenter::withoutComments)
+ .filter(line -> !line.isBlank()).map(Presenter::parseSettingLine)
+ .toList();
+ } catch (final IOException e) {
+ this.view.showErrorMessage("Settings Loading Error",
+ "Error loading settings file. Using default settings.");
+ return null;
+ }
+ }
+
+ /**
+ * Sets whether or not the default datafiles will be loaded. This method
+ * automatically updates the view's units.
+ *
+ * @param useDefaultDatafiles whether or not default datafiles should be
+ * loaded
+ */
+ public void setUseDefaultDatafiles(boolean useDefaultDatafiles) {
+ this.useDefaultDatafiles = useDefaultDatafiles;
+ this.reloadData();
+ this.updateView();
+ }
+
+ /**
+ * Sets the user's locale, updating the view.
+ *
+ * @param userLocale locale to use
+ */
+ public void setUserLocale(String userLocale) {
+ this.userLocale = userLocale;
+ this.view.updateText();
+ }
+
/**
* Shows a unit in the unit viewer
*
* @param u unit to show
* @since 2022-04-16
+ * @since v0.4.0
*/
- private final void showUnit(Unit u) {
+ private void showUnit(Unit u) {
final var nameSymbol = u.getNameSymbol();
- final boolean isBase = u instanceof BaseUnit
+ final var 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());
@@ -912,12 +1353,13 @@ public final class Presenter {
/**
* Runs whenever a unit name is selected in the unit viewer. Gets the
* description of a unit and displays it.
- *
+ *
* @since 2022-04-10
+ * @since v0.4.0
*/
void unitNameSelected() {
// get selected unit, if it's there and valid
- final Optional<String> selectedUnitName = this.view.getViewedUnitName();
+ final var selectedUnitName = this.view.getViewedUnitName();
final Optional<Unit> selectedUnit = selectedUnitName
.map(unitName -> this.database.containsUnitName(unitName)
? this.database.getUnit(unitName)
@@ -927,12 +1369,13 @@ public final class Presenter {
/**
* Updates the view's From and To units, if it has some
- *
+ *
* @since 2021-12-15
+ * @since v0.4.0
*/
public void updateView() {
if (this.view instanceof UnitConversionView) {
- final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var ucview = (UnitConversionView) this.view;
final var selectedDimensionName = ucview.getSelectedDimensionName();
// load units & prefixes into viewers
@@ -946,6 +1389,7 @@ public final class Presenter {
.entrySet().stream();
var toUnits = this.database.unitMapPrefixless(this.showDuplicates)
.entrySet().stream();
+ var unitSets = this.database.unitSetMap().entrySet().stream();
// filter by dimension, if one is selected
if (selectedDimensionName.isPresent()) {
@@ -955,6 +1399,8 @@ public final class Presenter {
u -> viewDimension.equals(u.getValue().getDimension()));
toUnits = toUnits.filter(
u -> viewDimension.equals(u.getValue().getDimension()));
+ unitSets = unitSets.filter(us -> viewDimension
+ .equals(us.getValue().get(0).getDimension()));
}
// filter by unit type, if desired
@@ -963,25 +1409,70 @@ public final class Presenter {
this::isSemiMetric) != UnitType.METRIC);
toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(),
this::isSemiMetric) != UnitType.NON_METRIC);
+ // unit sets are never considered metric
+ unitSets = unitSets
+ .filter(us -> this.metricExceptions.contains(us.getKey()));
}
// set unit names
ucview.setFromUnitNames(fromUnits.flatMap(this::applySearchRule)
.map(Map.Entry::getKey).collect(Collectors.toSet()));
- ucview.setToUnitNames(toUnits.flatMap(this::applySearchRule)
- .map(Map.Entry::getKey).collect(Collectors.toSet()));
+ final var toUnitNames = toUnits.flatMap(this::applySearchRule)
+ .map(Map.Entry::getKey);
+ final var unitSetNames = unitSets.map(Map.Entry::getKey);
+ final var toNames = Stream.concat(toUnitNames, unitSetNames)
+ .collect(Collectors.toSet());
+ ucview.setToUnitNames(toNames);
}
}
+ /** @return true iff the default datafiles are being used */
+ public boolean usingDefaultDatafiles() {
+ return this.useDefaultDatafiles;
+ }
+
/**
* @param message message to add
* @param args string formatting arguments for message
* @return AssertionError stating that an error has happened in the view's
* code
* @since 2022-04-09
+ * @since v0.4.0
*/
private AssertionError viewError(String message, Object... args) {
return new AssertionError("View Programming Error (from " + this.view
+ "): " + String.format(message, args));
}
+
+ /**
+ * Saves the presenter's settings to the user settings file.
+ *
+ * @param settingsFile file settings should be saved to
+ * @since 2021-12-15
+ * @since v0.4.0
+ */
+ boolean writeSettings(Path settingsFile) {
+ try (var writer = Files.newBufferedWriter(settingsFile)) {
+ writer.write(String.format("number_display_rule=%s\n",
+ displayRuleToString(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));
+ writer.write(String.format("search_prefix_rule=%s\n",
+ searchRuleToString(this.searchRule)));
+ writer.write(String.format("use_default_datafiles=%s\n",
+ this.useDefaultDatafiles));
+ writer.write(String.format("locale=%s\n", this.userLocale));
+ return true;
+ } catch (final IOException e) {
+ e.printStackTrace();
+ this.view.showErrorMessage("I/O Error",
+ "Error occurred while saving settings: "
+ + e.getLocalizedMessage());
+ return false;
+ }
+ }
}
diff --git a/src/main/java/sevenUnitsGUI/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java
index 8fba459..96f71de 100644
--- a/src/main/java/sevenUnitsGUI/SearchBoxList.java
+++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2022, 2024, 2025 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
@@ -49,7 +49,7 @@ final class SearchBoxList<E> extends JPanel {
/**
* The text to place in an empty search box.
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
@@ -57,7 +57,7 @@ final class SearchBoxList<E> extends JPanel {
/**
* The color to use for an empty foreground.
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
@@ -82,8 +82,9 @@ final class SearchBoxList<E> extends JPanel {
/**
* Creates an empty SearchBoxList
- *
+ *
* @since 2022-02-19
+ * @since v0.4.0
*/
public SearchBoxList() {
this(List.of(), null, false);
@@ -91,9 +92,10 @@ final class SearchBoxList<E> extends JPanel {
/**
* Creates the {@code SearchBoxList}.
- *
+ *
* @param itemsToFilter items to put in the list
* @since 2019-04-14
+ * @since v0.2.0
*/
public SearchBoxList(final Collection<E> itemsToFilter) {
this(itemsToFilter, null, false);
@@ -101,12 +103,12 @@ final class SearchBoxList<E> extends JPanel {
/**
* Creates the {@code SearchBoxList}.
- *
+ *
* @param itemsToFilter items to put in the list
* @param defaultOrdering default ordering of items after filtration
* (null=Comparable)
* @param caseSensitive whether or not the filtration is case-sensitive
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
@@ -147,7 +149,7 @@ final class SearchBoxList<E> extends JPanel {
/**
* Adds an additional filter for searching.
- *
+ *
* @param filter filter to add.
* @since 2019-04-13
* @since v0.2.0
@@ -158,7 +160,7 @@ final class SearchBoxList<E> extends JPanel {
/**
* Resets the search filter.
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
@@ -170,6 +172,7 @@ final class SearchBoxList<E> extends JPanel {
* @return items available in search list, including items that are hidden by
* the search filter
* @since 2022-03-30
+ * @since v0.4.0
*/
public Collection<E> getItems() {
return Collections.unmodifiableCollection(this.itemsToFilter);
@@ -180,7 +183,7 @@ final class SearchBoxList<E> extends JPanel {
* @since 2019-04-14
* @since v0.2.0
*/
- public final JTextField getSearchBox() {
+ public JTextField getSearchBox() {
return this.searchBox;
}
@@ -194,9 +197,8 @@ final class SearchBoxList<E> extends JPanel {
private Predicate<E> getSearchFilter(final String searchText) {
if (this.caseSensitive)
return item -> item.toString().contains(searchText);
- else
- return item -> item.toString().toLowerCase()
- .contains(searchText.toLowerCase());
+ return item -> item.toString().toLowerCase()
+ .contains(searchText.toLowerCase());
}
/**
@@ -204,7 +206,7 @@ final class SearchBoxList<E> extends JPanel {
* @since 2019-04-14
* @since v0.2.0
*/
- public final JList<E> getSearchList() {
+ public JList<E> getSearchList() {
return this.searchItems;
}
@@ -228,16 +230,16 @@ final class SearchBoxList<E> extends JPanel {
/**
* Re-applies the filters.
- *
+ *
* @since 2019-04-13
* @since v0.2.0
*/
public void reapplyFilter() {
- final String searchText = this.searchBoxEmpty ? ""
+ final var searchText = this.searchBoxEmpty ? ""
: this.searchBox.getText();
- final FilterComparator<E> comparator = new FilterComparator<>(searchText,
+ final var comparator = new FilterComparator<>(searchText,
this.defaultOrdering, this.caseSensitive);
- final Predicate<E> searchFilter = this.getSearchFilter(searchText);
+ final var searchFilter = this.getSearchFilter(searchText);
this.listModel.clear();
this.itemsToFilter.forEach(item -> {
@@ -255,7 +257,7 @@ final class SearchBoxList<E> extends JPanel {
/**
* Runs whenever the search box gains focus.
- *
+ *
* @param e focus event
* @since 2019-04-13
* @since v0.2.0
@@ -270,7 +272,7 @@ final class SearchBoxList<E> extends JPanel {
/**
* Runs whenever the search box loses focus.
- *
+ *
* @param e focus event
* @since 2019-04-13
* @since v0.2.0
@@ -288,7 +290,7 @@ final class SearchBoxList<E> extends JPanel {
* <p>
* Reapplies the search filter, and custom filters.
* </p>
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@@ -296,11 +298,11 @@ final class SearchBoxList<E> extends JPanel {
if (this.searchBoxFocused) {
this.searchBoxEmpty = this.searchBox.getText().equals("");
}
- final String searchText = this.searchBoxEmpty ? ""
+ final var searchText = this.searchBoxEmpty ? ""
: this.searchBox.getText();
- final FilterComparator<E> comparator = new FilterComparator<>(searchText,
+ final var comparator = new FilterComparator<>(searchText,
this.defaultOrdering, this.caseSensitive);
- final Predicate<E> searchFilter = this.getSearchFilter(searchText);
+ final var searchFilter = this.getSearchFilter(searchText);
// initialize list with items that match the filter then sort
this.listModel.clear();
@@ -323,6 +325,7 @@ final class SearchBoxList<E> extends JPanel {
*
* @param newItems new items to put in list
* @since 2021-05-22
+ * @since v0.3.0
*/
public void setItems(Collection<? extends E> newItems) {
this.itemsToFilter.clear();
@@ -332,8 +335,9 @@ final class SearchBoxList<E> extends JPanel {
/**
* Manually updates the search box's item list.
- *
+ *
* @since 2020-08-27
+ * @since v0.3.0
*/
public void updateList() {
this.searchBoxTextChanged();
diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
index d00263b..16d31ae 100644
--- a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
+++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -28,28 +28,28 @@ 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
+ * @since v0.4.0
*/
public final class StandardDisplayRules {
/**
* A rule that rounds to a fixed number of decimal places.
*
- * @since v0.4.0
* @since 2022-04-18
+ * @since v0.4.0
*/
public static final class FixedDecimals
implements Function<UncertainDouble, String> {
+ /** Regular expression used for converting this to a string. */
public static final Pattern TO_STRING_PATTERN = Pattern
.compile("Round to (\\d+) decimal places");
- /**
- * The number of places to round to.
- */
+ /** The number of places to round to. */
private final int decimalPlaces;
/**
* @param decimalPlaces
* @since 2022-04-18
+ * @since v0.4.0
*/
private FixedDecimals(int decimalPlaces) {
this.decimalPlaces = decimalPlaces;
@@ -65,6 +65,7 @@ public final class StandardDisplayRules {
/**
* @return the number of decimal places this rule rounds to
* @since 2022-04-18
+ * @since v0.4.0
*/
public int decimalPlaces() {
return this.decimalPlaces;
@@ -76,7 +77,7 @@ public final class StandardDisplayRules {
return true;
if (!(obj instanceof FixedDecimals))
return false;
- final FixedDecimals other = (FixedDecimals) obj;
+ final var other = (FixedDecimals) obj;
if (this.decimalPlaces != other.decimalPlaces)
return false;
return true;
@@ -96,22 +97,22 @@ public final class StandardDisplayRules {
/**
* A rule that rounds to a fixed number of significant digits.
*
- * @since v0.4.0
* @since 2022-04-18
+ * @since v0.4.0
*/
public static final class FixedPrecision
implements Function<UncertainDouble, String> {
+ /** Regular expression used for converting this to a string. */
public static final Pattern TO_STRING_PATTERN = Pattern
.compile("Round to (\\d+) significant figures");
- /**
- * The number of significant figures to round to.
- */
+ /** The number of significant figures to round to. */
private final MathContext mathContext;
/**
* @param significantFigures
* @since 2022-04-18
+ * @since v0.4.0
*/
private FixedPrecision(int significantFigures) {
this.mathContext = new MathContext(significantFigures,
@@ -130,7 +131,7 @@ public final class StandardDisplayRules {
return true;
if (!(obj instanceof FixedPrecision))
return false;
- final FixedPrecision other = (FixedPrecision) obj;
+ final var other = (FixedPrecision) obj;
if (this.mathContext == null) {
if (other.mathContext != null)
return false;
@@ -148,6 +149,7 @@ public final class StandardDisplayRules {
/**
* @return the number of significant figures this rule rounds to
* @since 2022-04-18
+ * @since v0.4.0
*/
public int significantFigures() {
return this.mathContext.getPrecision();
@@ -165,8 +167,8 @@ 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
+ * @since v0.4.0
*/
public static final class UncertaintyBased
implements Function<UncertainDouble, String> {
@@ -193,10 +195,10 @@ 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
+ * @since v0.4.0
*/
- public static final FixedDecimals fixedDecimals(int decimalPlaces) {
+ public static FixedDecimals fixedDecimals(int decimalPlaces) {
return new FixedDecimals(decimalPlaces);
}
@@ -204,10 +206,10 @@ 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
+ * @since v0.4.0
*/
- public static final FixedPrecision fixedPrecision(int significantFigures) {
+ public static FixedPrecision fixedPrecision(int significantFigures) {
return new FixedPrecision(significantFigures);
}
@@ -218,10 +220,10 @@ 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
+ * @since v0.4.0
*/
- public static final Function<UncertainDouble, String> getStandardRule(
+ public static Function<UncertainDouble, String> getStandardRule(
String ruleToString) {
if (UNCERTAINTY_BASED_ROUNDING_RULE.toString().equals(ruleToString))
return UNCERTAINTY_BASED_ROUNDING_RULE;
@@ -230,13 +232,13 @@ public final class StandardDisplayRules {
final var placesMatch = FixedDecimals.TO_STRING_PATTERN
.matcher(ruleToString);
if (placesMatch.matches())
- return new FixedDecimals(Integer.valueOf(placesMatch.group(1)));
+ return new FixedDecimals(Integer.parseInt(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)));
+ return new FixedPrecision(Integer.parseInt(sigFigMatch.group(1)));
throw new IllegalArgumentException(
"Provided string does not match any given rules.");
@@ -244,10 +246,10 @@ public final class StandardDisplayRules {
/**
* @return an UncertainDouble-based rounding rule
- * @since v0.4.0
* @since 2022-04-18
+ * @since v0.4.0
*/
- public static final UncertaintyBased uncertaintyBased() {
+ public static UncertaintyBased uncertaintyBased() {
return UNCERTAINTY_BASED_ROUNDING_RULE;
}
diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java
index 997acc3..8be58f5 100644
--- a/src/main/java/sevenUnitsGUI/TabbedView.java
+++ b/src/main/java/sevenUnitsGUI/TabbedView.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -24,13 +24,16 @@ import java.awt.event.ItemEvent;
import java.awt.event.KeyEvent;
import java.util.AbstractSet;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.BorderFactory;
@@ -64,8 +67,8 @@ import sevenUnits.utils.UncertainDouble;
/**
* A View that separates its functions into multiple tabs
*
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
final class TabbedView implements ExpressionConversionView, UnitConversionView {
/**
@@ -73,8 +76,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
*
* @param <E> type of item in list
*
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
private static final class JComboBoxItemSet<E> extends AbstractSet<E> {
private final JComboBox<E> comboBox;
@@ -82,6 +85,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
/**
* @param comboBox combo box to get items from
* @since 2022-02-19
+ * @since v0.4.0
*/
public JComboBoxItemSet(JComboBox<E> comboBox) {
this.comboBox = comboBox;
@@ -101,9 +105,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
public E next() {
if (this.hasNext())
return JComboBoxItemSet.this.comboBox.getItemAt(this.index++);
- else
- throw new NoSuchElementException(
- "Iterator has finished iteration");
+ throw new NoSuchElementException(
+ "Iterator has finished iteration");
}
};
}
@@ -119,10 +122,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
* The standard types of rounding, corresponding to the options on the
* TabbedView's settings panel.
*
- * @since v0.4.0
* @since 2022-04-18
+ * @since v0.4.0
*/
- private static enum StandardRoundingType {
+ private enum StandardRoundingType {
/**
* Rounds to a fixed number of significant digits. Precision is used,
* representing the number of significant digits to round to.
@@ -144,8 +147,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
* Creates a TabbedView.
*
* @param args command line arguments
- * @since v0.4.0
* @since 2022-02-19
+ * @since v0.4.0
*/
public static void main(String[] args) {
// This view doesn't need to do anything, the side effects of creating it
@@ -195,15 +198,19 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
/** The text box for prefix data in the prefix viewer */
private final JTextArea prefixTextBox;
- // SETTINGS STUFF
+ // INFO & SETTINGS STUFF
+ final JTextArea infoTextArea;
+ private final JComboBox<String> localeSelector;
private StandardRoundingType roundingType;
private int precision;
+ private final Map<String, Consumer<String>> localizedTextSetters;
+
/**
* Creates the view and makes it visible to the user
- *
- * @since v0.4.0
+ *
* @since 2022-02-19
+ * @since v0.4.0
*/
public TabbedView() {
// enable system look and feel
@@ -218,21 +225,25 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// initialize important components
this.presenter = new Presenter(this);
- this.frame = new JFrame("7Units " + ProgramInfo.VERSION);
+ this.frame = new JFrame("7Units (Unlocalized)");
this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// master components (those that contain everything else within them)
this.masterPane = new JTabbedPane();
this.frame.add(this.masterPane);
+ this.localizedTextSetters = new HashMap<>();
+
// ============ UNIT CONVERSION TAB ============
- final JPanel convertUnitPanel = new JPanel();
+ final var convertUnitPanel = new JPanel();
this.masterPane.addTab("Convert Units", convertUnitPanel);
+ this.localizedTextSetters.put("tv.convert_units.title",
+ txt -> this.masterPane.setTitleAt(0, txt));
this.masterPane.setMnemonicAt(0, KeyEvent.VK_U);
convertUnitPanel.setLayout(new BorderLayout());
{ // panel for input part
- final JPanel inputPanel = new JPanel();
+ final var inputPanel = new JPanel();
convertUnitPanel.add(inputPanel, BorderLayout.CENTER);
inputPanel.setLayout(new GridLayout(1, 3));
inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6));
@@ -240,7 +251,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
this.fromSearch = new SearchBoxList<>();
inputPanel.add(this.fromSearch);
- final JPanel inBetweenPanel = new JPanel();
+ final var inBetweenPanel = new JPanel();
inputPanel.add(inBetweenPanel);
inBetweenPanel.setLayout(new BorderLayout());
@@ -249,7 +260,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
this.dimensionSelector
.addItemListener(e -> this.presenter.updateView());
- final JLabel arrowLabel = new JLabel("-->");
+ final var arrowLabel = new JLabel("-->");
inBetweenPanel.add(arrowLabel, BorderLayout.CENTER);
arrowLabel.setHorizontalAlignment(SwingConstants.CENTER);
@@ -258,12 +269,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
}
{ // panel for submit and output, and also value entry
- final JPanel outputPanel = new JPanel();
+ final var outputPanel = new JPanel();
convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END);
outputPanel.setLayout(new BorderLayout());
outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6));
- final JLabel valuePrompt = new JLabel("Value to convert: ");
+ final var valuePrompt = new JLabel();
+ this.localizedTextSetters.put("tv.convert_units.value_prompt",
+ valuePrompt::setText);
outputPanel.add(valuePrompt, BorderLayout.LINE_START);
this.valueInput = new JTextField();
@@ -271,6 +284,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// conversion button
this.convertUnitButton = new JButton("Convert");
+ this.localizedTextSetters.put("tv.convert_units.convert_btn",
+ this.convertUnitButton::setText);
outputPanel.add(this.convertUnitButton, BorderLayout.LINE_END);
this.convertUnitButton
.addActionListener(e -> this.presenter.convertUnits());
@@ -283,23 +298,31 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
}
// ============ EXPRESSION CONVERSION TAB ============
- final JPanel convertExpressionPanel = new JPanel();
+ final var convertExpressionPanel = new JPanel();
this.masterPane.addTab("Convert Unit Expressions",
convertExpressionPanel);
+ this.localizedTextSetters.put("tv.convert_expressions.title",
+ txt -> this.masterPane.setTitleAt(1, txt));
this.masterPane.setMnemonicAt(1, KeyEvent.VK_E);
convertExpressionPanel.setLayout(new GridLayout(4, 1));
// from and to expressions
this.fromEntry = new JTextField();
convertExpressionPanel.add(this.fromEntry);
- this.fromEntry.setBorder(BorderFactory.createTitledBorder("From"));
+ this.localizedTextSetters.put("tv.convert_expressions.from",
+ txt -> this.fromEntry
+ .setBorder(BorderFactory.createTitledBorder(txt)));
this.toEntry = new JTextField();
convertExpressionPanel.add(this.toEntry);
- this.toEntry.setBorder(BorderFactory.createTitledBorder("To"));
+ this.localizedTextSetters.put("tv.convert_expressions.to",
+ txt -> this.toEntry
+ .setBorder(BorderFactory.createTitledBorder(txt)));
// button to convert
- this.convertExpressionButton = new JButton("Convert");
+ this.convertExpressionButton = new JButton();
+ this.localizedTextSetters.put("tv.convert_expressions.convert_btn",
+ this.convertExpressionButton::setText);
convertExpressionPanel.add(this.convertExpressionButton);
this.convertExpressionButton
@@ -309,13 +332,16 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// output of conversion
this.expressionOutput = new JTextArea(2, 32);
convertExpressionPanel.add(this.expressionOutput);
- this.expressionOutput
- .setBorder(BorderFactory.createTitledBorder("Output"));
+ this.localizedTextSetters.put("tv.convert_expressions.output",
+ txt -> this.expressionOutput
+ .setBorder(BorderFactory.createTitledBorder(txt)));
this.expressionOutput.setEditable(false);
// =========== UNIT VIEWER ===========
- final JPanel unitLookupPanel = new JPanel();
+ final var unitLookupPanel = new JPanel();
this.masterPane.addTab("Unit Viewer", unitLookupPanel);
+ this.localizedTextSetters.put("tv.unit_viewer.title",
+ txt -> this.masterPane.setTitleAt(2, txt));
this.masterPane.setMnemonicAt(2, KeyEvent.VK_V);
unitLookupPanel.setLayout(new GridLayout());
@@ -331,8 +357,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
this.unitTextBox.setLineWrap(true);
// ============ PREFIX VIEWER =============
- final JPanel prefixLookupPanel = new JPanel();
+ final var prefixLookupPanel = new JPanel();
this.masterPane.addTab("Prefix Viewer", prefixLookupPanel);
+ this.localizedTextSetters.put("tv.prefix_viewer.title",
+ txt -> this.masterPane.setTitleAt(3, txt));
this.masterPane.setMnemonicAt(3, KeyEvent.VK_P);
prefixLookupPanel.setLayout(new GridLayout(1, 2));
@@ -349,23 +377,24 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// ============ INFO PANEL ============
- final JPanel infoPanel = new JPanel();
+ final var infoPanel = new JPanel();
this.masterPane.addTab("\uD83D\uDEC8", // info (i) character
new JScrollPane(infoPanel));
- final JTextArea infoTextArea = new JTextArea();
- infoTextArea.setEditable(false);
- infoTextArea.setOpaque(false);
- infoPanel.add(infoTextArea);
- infoTextArea.setText(Presenter.getAboutText());
+ this.infoTextArea = new JTextArea();
+ this.infoTextArea.setEditable(false);
+ this.infoTextArea.setOpaque(false);
+ infoPanel.add(this.infoTextArea);
// ============ SETTINGS PANEL ============
+ this.localeSelector = new JComboBox<>();
this.masterPane.addTab("\u2699",
new JScrollPane(this.createSettingsPanel()));
this.masterPane.setMnemonicAt(5, KeyEvent.VK_S);
// ============ FINALIZE CREATION OF VIEW ============
this.presenter.postViewInitialize();
+ this.updateText();
this.frame.pack();
this.frame.setVisible(true);
}
@@ -375,40 +404,46 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
* code more organized, as this function is massive!)
*
* @since 2022-02-19
+ * @since v0.4.0
*/
private JPanel createSettingsPanel() {
- final JPanel settingsPanel = new JPanel();
+ final var settingsPanel = new JPanel();
settingsPanel
.setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS));
// ============ ROUNDING SETTINGS ============
{
- final JPanel roundingPanel = new JPanel();
+ final var roundingPanel = new JPanel();
settingsPanel.add(roundingPanel);
- roundingPanel.setBorder(new TitledBorder("Rounding Settings"));
+ this.localizedTextSetters.put("tv.settings.rounding.title",
+ txt -> roundingPanel.setBorder(new TitledBorder(txt)));
roundingPanel.setLayout(new GridBagLayout());
// rounding rule selection
- final ButtonGroup roundingRuleButtons = new ButtonGroup();
+ final var 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:");
+ final var roundingRuleLabel = new JLabel();
+ this.localizedTextSetters.put("tv.settings.rounding.rule",
+ roundingRuleLabel::setText);
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:");
+ final var sliderLabel = new JLabel();
+ this.localizedTextSetters.put("tv.settings.rounding.precision",
+ sliderLabel::setText);
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);
+ final var sigDigSlider = new JSlider(0, 12);
roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5)
.setAnchor(GridBagConstraints.LINE_START).build());
@@ -428,8 +463,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
});
// significant digit rounding
- final JRadioButton fixedPrecision = new JRadioButton(
- "Fixed Precision");
+ final var fixedPrecision = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.rounding.fixed_sigfig",
+ fixedPrecision::setText);
if (this.roundingType == StandardRoundingType.SIGNIFICANT_DIGITS) {
fixedPrecision.setSelected(true);
}
@@ -444,8 +480,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
.setAnchor(GridBagConstraints.LINE_START).build());
// decimal place rounding
- final JRadioButton fixedDecimals = new JRadioButton(
- "Fixed Decimal Places");
+ final var fixedDecimals = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.rounding.fixed_places",
+ fixedDecimals::setText);
if (this.roundingType == StandardRoundingType.DECIMAL_PLACES) {
fixedDecimals.setSelected(true);
}
@@ -460,8 +497,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
.setAnchor(GridBagConstraints.LINE_START).build());
// scientific rounding
- final JRadioButton relativePrecision = new JRadioButton(
- "Uncertainty-Based Rounding");
+ final var relativePrecision = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.rounding.uncertainty",
+ relativePrecision::setText);
if (this.roundingType == StandardRoundingType.UNCERTAINTY) {
relativePrecision.setSelected(true);
}
@@ -478,10 +516,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// ============ PREFIX REPETITION SETTINGS ============
{
- final JPanel prefixRepetitionPanel = new JPanel();
+ final var prefixRepetitionPanel = new JPanel();
settingsPanel.add(prefixRepetitionPanel);
- prefixRepetitionPanel
- .setBorder(new TitledBorder("Prefix Repetition Settings"));
+ this.localizedTextSetters.put("tv.settings.repetition.title",
+ txt -> prefixRepetitionPanel.setBorder(new TitledBorder(txt)));
prefixRepetitionPanel.setLayout(new GridBagLayout());
final var prefixRule = this.getPresenterPrefixRule()
@@ -489,9 +527,11 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
"Presenter loaded non-standard prefix rule"));
// prefix rules
- final ButtonGroup prefixRuleButtons = new ButtonGroup();
+ final var prefixRuleButtons = new ButtonGroup();
- final JRadioButton noRepetition = new JRadioButton("No Repetition");
+ final var noRepetition = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.repetition.no",
+ noRepetition::setText);
if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
noRepetition.setSelected(true);
}
@@ -504,7 +544,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton noRestriction = new JRadioButton("No Restriction");
+ final var noRestriction = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.repetition.any",
+ noRestriction::setText);
if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
noRestriction.setSelected(true);
}
@@ -517,8 +559,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton customRepetition = new JRadioButton(
- "Complex Repetition");
+ final var customRepetition = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.repetition.complex",
+ customRepetition::setText);
if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
customRepetition.setSelected(true);
}
@@ -534,18 +577,20 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// ============ SEARCH SETTINGS ============
{
- final JPanel searchingPanel = new JPanel();
+ final var searchingPanel = new JPanel();
settingsPanel.add(searchingPanel);
- searchingPanel.setBorder(new TitledBorder("Search Settings"));
+ this.localizedTextSetters.put("tv.settings.search.title",
+ txt -> searchingPanel.setBorder(new TitledBorder(txt)));
searchingPanel.setLayout(new GridBagLayout());
// searching rules
- final ButtonGroup searchRuleButtons = new ButtonGroup();
+ final var searchRuleButtons = new ButtonGroup();
final var searchRule = this.presenter.getSearchRule();
- final JRadioButton noPrefixes = new JRadioButton(
- "Never Include Prefixed Units");
+ final var noPrefixes = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.search.no_prefixes",
+ noPrefixes::setText);
noPrefixes.addActionListener(e -> {
this.presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES);
this.presenter.updateView();
@@ -555,8 +600,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton commonPrefixes = new JRadioButton(
- "Include Common Prefixes");
+ final var commonPrefixes = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.search.common_prefixes",
+ commonPrefixes::setText);
commonPrefixes.addActionListener(e -> {
this.presenter.setSearchRule(PrefixSearchRule.COMMON_PREFIXES);
this.presenter.updateView();
@@ -566,8 +612,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
searchingPanel.add(commonPrefixes, new GridBagBuilder(0, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton alwaysInclude = new JRadioButton(
- "Include All Single Prefixes");
+ final var alwaysInclude = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.search.all_prefixes",
+ alwaysInclude::setText);
alwaysInclude.addActionListener(e -> {
this.presenter
.setSearchRule(this.presenter.getUniversalSearchRule());
@@ -592,34 +639,66 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// ============ OTHER SETTINGS ============
{
- final JPanel miscPanel = new JPanel();
+ final var miscPanel = new JPanel();
settingsPanel.add(miscPanel);
miscPanel.setLayout(new GridBagLayout());
- final JCheckBox oneWay = new JCheckBox("Convert One Way Only");
+ final var oneWay = new JCheckBox();
+ this.localizedTextSetters.put("tv.settings.oneway", oneWay::setText);
oneWay.setSelected(this.presenter.oneWayConversionEnabled());
oneWay.addItemListener(e -> {
this.presenter.setOneWayConversionEnabled(
e.getStateChange() == ItemEvent.SELECTED);
this.presenter.saveSettings();
});
- miscPanel.add(oneWay, new GridBagBuilder(0, 0)
+ miscPanel.add(oneWay, new GridBagBuilder(0, 0, 2, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JCheckBox showAllVariations = new JCheckBox(
- "Show Duplicate Units & Prefixes");
+ final var showAllVariations = new JCheckBox();
+ this.localizedTextSetters.put("tv.settings.show_duplicate",
+ showAllVariations::setText);
showAllVariations.setSelected(this.presenter.duplicatesShown());
showAllVariations.addItemListener(e -> {
this.presenter
.setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED);
this.presenter.saveSettings();
});
- miscPanel.add(showAllVariations, new GridBagBuilder(0, 1)
+ miscPanel.add(showAllVariations, new GridBagBuilder(0, 1, 2, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final var useDefaultFiles = new JCheckBox();
+ this.localizedTextSetters.put("tv.settings.use_default_files",
+ useDefaultFiles::setText);
+ useDefaultFiles.setSelected(this.presenter.usingDefaultDatafiles());
+ useDefaultFiles.addItemListener(e -> {
+ this.presenter.setUseDefaultDatafiles(
+ e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ miscPanel.add(useDefaultFiles, new GridBagBuilder(0, 2, 2, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JButton unitFileButton = new JButton("Manage Unit Data Files");
+ final var localeLabel = new JLabel();
+ this.localizedTextSetters.put("tv.settings.locale",
+ localeLabel::setText);
+ miscPanel.add(localeLabel, new GridBagBuilder(0, 3, 1, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ this.presenter.getAvailableLocales().stream().sorted()
+ .forEachOrdered(this.localeSelector::addItem);
+ this.localeSelector.setSelectedItem(this.presenter.getUserLocale());
+ this.localeSelector.addItemListener(e -> {
+ this.presenter.setUserLocale((String) e.getItem());
+ this.presenter.saveSettings();
+ });
+ miscPanel.add(localeSelector, new GridBagBuilder(1, 3, 1, 1)
+ .setAnchor(GridBagConstraints.LINE_END).build());
+
+ final var unitFileButton = new JButton();
+ this.localizedTextSetters.put("tv.settings.unitfiles.button",
+ unitFileButton::setText);
unitFileButton.setEnabled(false);
- miscPanel.add(unitFileButton, new GridBagBuilder(0, 2)
+ miscPanel.add(unitFileButton, new GridBagBuilder(0, 4, 2, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
}
@@ -662,8 +741,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
/**
* @return the precision of the presenter's rounding rule, if that is
* meaningful
- * @since v0.4.0
* @since 2022-04-18
+ * @since v0.4.0
*/
private OptionalInt getPresenterPrecision() {
final var presenterRule = this.presenter.getNumberDisplayRule();
@@ -671,18 +750,17 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
return OptionalInt
.of(((StandardDisplayRules.FixedDecimals) presenterRule)
.decimalPlaces());
- else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
return OptionalInt
.of(((StandardDisplayRules.FixedPrecision) presenterRule)
.significantFigures());
- else
- return OptionalInt.empty();
+ return OptionalInt.empty();
}
/**
* @return presenter's prefix repetition rule
- * @since v0.4.0
* @since 2022-04-19
+ * @since v0.4.0
*/
private Optional<DefaultPrefixRepetitionRule> getPresenterPrefixRule() {
final var prefixRule = this.presenter.getPrefixRepetitionRule();
@@ -694,25 +772,24 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
/**
* Determines which rounding type the presenter is currently using, if any.
*
- * @since v0.4.0
* @since 2022-04-18
+ * @since v0.4.0
*/
private Optional<StandardRoundingType> getPresenterRoundingType() {
final var presenterRule = this.presenter.getNumberDisplayRule();
if (Objects.equals(presenterRule,
StandardDisplayRules.uncertaintyBased()))
return Optional.of(StandardRoundingType.UNCERTAINTY);
- else if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
+ if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
return Optional.of(StandardRoundingType.DECIMAL_PLACES);
- else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
return Optional.of(StandardRoundingType.SIGNIFICANT_DIGITS);
- else
- return Optional.empty();
+ return Optional.empty();
}
@Override
public Optional<String> getSelectedDimensionName() {
- final String selectedItem = (String) this.dimensionSelector
+ final var selectedItem = (String) this.dimensionSelector
.getSelectedItem();
return Optional.ofNullable(selectedItem);
}
@@ -780,8 +857,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
@Override
public void showExpressionConversionOutput(UnitConversionRecord uc) {
- this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(),
- uc.outputValueString(), uc.toName()));
+ this.expressionOutput.setText(uc.toString());
}
@Override
@@ -806,9 +882,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
/**
* Sets the presenter's rounding rule to the one specified by the current
* settings
- *
- * @since v0.4.0
+ *
* @since 2022-04-18
+ * @since v0.4.0
*/
private void updatePresenterRoundingRule() {
final Function<UncertainDouble, String> roundingRule;
@@ -828,4 +904,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
this.presenter.setNumberDisplayRule(roundingRule);
this.presenter.saveSettings();
}
+
+ @Override
+ public void updateText() {
+ this.frame.setTitle(this.presenter.getLocalizedText("tv.title")
+ .replace("[v]", ProgramInfo.VERSION.toString()));
+ this.infoTextArea.setText(this.presenter.getAboutText());
+ this.localizedTextSetters.forEach(
+ (id, action) -> action.accept(this.presenter.getLocalizedText(id)));
+ }
}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
index 43a62e6..3c2bb6c 100644
--- a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
+++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -24,18 +24,18 @@ import sevenUnits.unit.UnitValue;
/**
* A record of a conversion between units or expressions
*
- * @since v0.4.0
* @since 2022-04-09
+ * @since v0.4.0
*/
public final class UnitConversionRecord {
/**
* Gets a {@code UnitConversionRecord} from two linear unit values
*
- * @param input input unit & value
- * @param output output unit & value
+ * @param input input unit &amp; value
+ * @param output output unit &amp; value
* @return unit conversion record
- * @since v0.4.0
* @since 2022-04-09
+ * @since v0.4.0
*/
public static UnitConversionRecord fromLinearValues(LinearUnitValue input,
LinearUnitValue output) {
@@ -48,11 +48,11 @@ public final class UnitConversionRecord {
/**
* Gets a {@code UnitConversionRecord} from two unit values
*
- * @param input input unit & value
- * @param output output unit & value
+ * @param input input unit &amp; value
+ * @param output output unit &amp; value
* @return unit conversion record
- * @since v0.4.0
* @since 2022-04-09
+ * @since v0.4.0
*/
public static UnitConversionRecord fromValues(UnitValue input,
UnitValue output) {
@@ -70,8 +70,8 @@ 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
+ * @since v0.4.0
*/
public static UnitConversionRecord valueOf(String fromName, String toName,
String inputValueString, String outputValueString) {
@@ -79,13 +79,9 @@ public final class UnitConversionRecord {
outputValueString);
}
- /**
- * The name of the unit or expression that was converted from
- */
+ /** The name of the unit or expression that was converted from */
private final String fromName;
- /**
- * The name of the unit or expression that was converted to
- */
+ /** The name of the unit or expression that was converted to */
private final String toName;
/**
@@ -106,6 +102,7 @@ public final class UnitConversionRecord {
* @param inputValueString string representing input value
* @param outputValueString string representing output value
* @since 2022-04-09
+ * @since v0.4.0
*/
private UnitConversionRecord(String fromName, String toName,
String inputValueString, String outputValueString) {
@@ -121,7 +118,7 @@ public final class UnitConversionRecord {
return true;
if (!(obj instanceof UnitConversionRecord))
return false;
- final UnitConversionRecord other = (UnitConversionRecord) obj;
+ final var other = (UnitConversionRecord) obj;
if (this.fromName == null) {
if (other.fromName != null)
return false;
@@ -147,8 +144,8 @@ public final class UnitConversionRecord {
/**
* @return name of unit or expression that was converted from
- * @since v0.4.0
* @since 2022-04-09
+ * @since v0.4.0
*/
public String fromName() {
return this.fromName;
@@ -156,8 +153,8 @@ public final class UnitConversionRecord {
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
+ final var prime = 31;
+ var result = 1;
result = prime * result
+ (this.fromName == null ? 0 : this.fromName.hashCode());
result = prime * result + (this.inputValueString == null ? 0
@@ -171,8 +168,8 @@ public final class UnitConversionRecord {
/**
* @return string representing input value
- * @since v0.4.0
* @since 2022-04-09
+ * @since v0.4.0
*/
public String inputValueString() {
return this.inputValueString;
@@ -180,8 +177,8 @@ public final class UnitConversionRecord {
/**
* @return string representing output value
- * @since v0.4.0
* @since 2022-04-09
+ * @since v0.4.0
*/
public String outputValueString() {
return this.outputValueString;
@@ -189,8 +186,8 @@ public final class UnitConversionRecord {
/**
* @return name of unit or expression that was converted to
- * @since v0.4.0
* @since 2022-04-09
+ * @since v0.4.0
*/
public String toName() {
return this.toName;
@@ -198,9 +195,9 @@ public final class UnitConversionRecord {
@Override
public String toString() {
- final String inputString = this.inputValueString.isBlank() ? this.fromName
+ final var inputString = this.inputValueString.isBlank() ? this.fromName
: this.inputValueString + " " + this.fromName;
- final String outputString = this.outputValueString.isBlank() ? this.toName
+ final var outputString = this.outputValueString.isBlank() ? this.toName
: this.outputValueString + " " + this.toName;
return inputString + " = " + outputString;
}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java
index b9077f7..fa3a388 100644
--- a/src/main/java/sevenUnitsGUI/UnitConversionView.java
+++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2021-2022 Adrien Hopkins
+ * Copyright (C) 2021, 2022, 2024, 2025 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
@@ -21,59 +21,59 @@ import java.util.Set;
/**
* A View that supports single unit-based conversion
- *
+ *
* @author Adrien Hopkins
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
public interface UnitConversionView extends View {
/**
* @return dimensions available for filtering
- * @since v0.4.0
* @since 2022-01-29
+ * @since v0.4.0
*/
Set<String> getDimensionNames();
/**
* @return name of unit to convert <em>from</em>
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
Optional<String> getFromSelection();
/**
* @return list of names of units available to convert from
- * @since v0.4.0
* @since 2022-03-30
+ * @since v0.4.0
*/
Set<String> getFromUnitNames();
/**
* @return value to convert between the units (specifically, the numeric
* string provided by the user)
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
String getInputValue();
/**
* @return selected dimension
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
Optional<String> getSelectedDimensionName();
/**
* @return name of unit to convert <em>to</em>
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
Optional<String> getToSelection();
/**
* @return list of names of units available to convert to
- * @since v0.4.0
* @since 2022-03-30
+ * @since v0.4.0
*/
Set<String> getToUnitNames();
@@ -81,8 +81,8 @@ 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
+ * @since v0.4.0
*/
void setDimensionNames(Set<String> dimensionNames);
@@ -92,8 +92,8 @@ 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
+ * @since v0.4.0
*/
void setFromUnitNames(Set<String> unitNames);
@@ -103,18 +103,17 @@ 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
+ * @since v0.4.0
*/
void setToUnitNames(Set<String> unitNames);
/**
* Shows the output of a unit conversion.
- *
- * @param input input unit & value (obtained from this view)
- * @param output output unit & value
- * @since v0.4.0
+ *
+ * @param uc record of unit conversion
* @since 2021-12-24
+ * @since v0.4.0
*/
void showUnitConversionOutput(UnitConversionRecord uc);
}
diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java
index 7dd0c44..0adeb3a 100644
--- a/src/main/java/sevenUnitsGUI/View.java
+++ b/src/main/java/sevenUnitsGUI/View.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2021-2022 Adrien Hopkins
+ * Copyright (C) 2021, 2022, 2024, 2025 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
@@ -24,16 +24,16 @@ import sevenUnits.utils.NameSymbol;
/**
* An object that controls user interaction with 7Units
- *
+ *
* @author Adrien Hopkins
- * @since v0.4.0
* @since 2021-12-15
+ * @since v0.4.0
*/
public interface View {
/**
* @return a new tabbed view
- * @since v0.4.0
* @since 2022-04-19
+ * @since v0.4.0
*/
static View createTabbedView() {
return new TabbedView();
@@ -41,22 +41,22 @@ public interface View {
/**
* @return the presenter associated with this view
- * @since v0.4.0
* @since 2022-04-19
+ * @since v0.4.0
*/
Presenter getPresenter();
/**
* @return name of prefix currently being viewed
- * @since v0.4.0
* @since 2022-04-10
+ * @since v0.4.0
*/
Optional<String> getViewedPrefixName();
/**
* @return name of unit currently being viewed
- * @since v0.4.0
* @since 2022-04-10
+ * @since v0.4.0
*/
Optional<String> getViewedUnitName();
@@ -65,8 +65,8 @@ public interface View {
* viewer
*
* @param prefixNames prefix names to view
- * @since v0.4.0
* @since 2022-04-10
+ * @since v0.4.0
*/
void setViewablePrefixNames(Set<String> prefixNames);
@@ -74,8 +74,8 @@ 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
+ * @since v0.4.0
*/
void setViewableUnitNames(Set<String> unitNames);
@@ -85,8 +85,8 @@ 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
+ * @since v0.4.0
*/
void showErrorMessage(String title, String message);
@@ -95,8 +95,8 @@ 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
+ * @since v0.4.0
*/
void showPrefix(NameSymbol name, String multiplierString);
@@ -107,9 +107,16 @@ 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
+ * @since v0.4.0
*/
void showUnit(NameSymbol name, String definition, String dimensionName,
UnitType type);
+
+ /**
+ * Updates the view's text to reflect the presenter's locale.
+ *
+ * This method <b>must not</b> call {@link Presenter#setUserLocale(String)}.
+ */
+ void updateText();
}
diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java
index e6593fb..750e2d9 100644
--- a/src/main/java/sevenUnitsGUI/ViewBot.java
+++ b/src/main/java/sevenUnitsGUI/ViewBot.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -30,10 +30,10 @@ import sevenUnits.utils.Nameable;
/**
* A class that simulates a View (supports both unit and expression conversion)
* for testing. Getters and setters work as expected.
- *
+ *
* @author Adrien Hopkins
- * @since v0.4.0
* @since 2022-01-29
+ * @since v0.4.0
*/
public final class ViewBot
implements UnitConversionView, ExpressionConversionView {
@@ -42,6 +42,7 @@ public final class ViewBot
* {@link View#showPrefix(NameSymbol, String)}, for testing.
*
* @since 2022-04-16
+ * @since v0.4.0
*/
public static final class PrefixViewingRecord implements Nameable {
private final NameSymbol nameSymbol;
@@ -51,6 +52,7 @@ public final class ViewBot
* @param nameSymbol
* @param multiplierString
* @since 2022-04-16
+ * @since v0.4.0
*/
public PrefixViewingRecord(NameSymbol nameSymbol,
String multiplierString) {
@@ -64,7 +66,7 @@ public final class ViewBot
return true;
if (!(obj instanceof PrefixViewingRecord))
return false;
- final PrefixViewingRecord other = (PrefixViewingRecord) obj;
+ final var other = (PrefixViewingRecord) obj;
return Objects.equals(this.multiplierString, other.multiplierString)
&& Objects.equals(this.nameSymbol, other.nameSymbol);
}
@@ -79,17 +81,19 @@ public final class ViewBot
return Objects.hash(this.multiplierString, this.nameSymbol);
}
+ /** @return A string representation of the prefix multiplier. */
public String multiplierString() {
return this.multiplierString;
}
+ /** @return A {@code NameSymbol} describing the prefix. */
public NameSymbol nameSymbol() {
return this.nameSymbol;
}
@Override
public String toString() {
- final StringBuilder builder = new StringBuilder();
+ final var builder = new StringBuilder();
builder.append("PrefixViewingRecord [nameSymbol=");
builder.append(this.nameSymbol);
builder.append(", multiplierString=");
@@ -104,6 +108,7 @@ public final class ViewBot
* {@link View#showUnit(NameSymbol, String, String, UnitType)}, for testing.
*
* @since 2022-04-16
+ * @since v0.4.0
*/
public static final class UnitViewingRecord implements Nameable {
private final NameSymbol nameSymbol;
@@ -112,7 +117,12 @@ public final class ViewBot
private final UnitType unitType;
/**
+ * @param nameSymbol name(s) and symbol of unit
+ * @param definition unit's definition string
+ * @param dimensionName name of unit's dimension
+ * @param unitType type of unit (metric/semi-metric/non-metric)
* @since 2022-04-16
+ * @since v0.4.0
*/
public UnitViewingRecord(NameSymbol nameSymbol, String definition,
String dimensionName, UnitType unitType) {
@@ -125,6 +135,7 @@ public final class ViewBot
/**
* @return the definition
* @since 2022-04-16
+ * @since v0.4.0
*/
public String definition() {
return this.definition;
@@ -133,6 +144,7 @@ public final class ViewBot
/**
* @return the dimensionName
* @since 2022-04-16
+ * @since v0.4.0
*/
public String dimensionName() {
return this.dimensionName;
@@ -144,7 +156,7 @@ public final class ViewBot
return true;
if (!(obj instanceof UnitViewingRecord))
return false;
- final UnitViewingRecord other = (UnitViewingRecord) obj;
+ final var other = (UnitViewingRecord) obj;
return Objects.equals(this.definition, other.definition)
&& Objects.equals(this.dimensionName, other.dimensionName)
&& Objects.equals(this.nameSymbol, other.nameSymbol)
@@ -154,6 +166,7 @@ public final class ViewBot
/**
* @return the nameSymbol
* @since 2022-04-16
+ * @since v0.4.0
*/
@Override
public NameSymbol getNameSymbol() {
@@ -166,13 +179,14 @@ public final class ViewBot
this.nameSymbol, this.unitType);
}
+ /** @return name(s) and symbol of unit */
public NameSymbol nameSymbol() {
return this.nameSymbol;
}
@Override
public String toString() {
- final StringBuilder builder = new StringBuilder();
+ final var builder = new StringBuilder();
builder.append("UnitViewingRecord [nameSymbol=");
builder.append(this.nameSymbol);
builder.append(", definition=");
@@ -188,6 +202,7 @@ public final class ViewBot
/**
* @return the unitType
* @since 2022-04-16
+ * @since v0.4.0
*/
public UnitType unitType() {
return this.unitType;
@@ -236,6 +251,7 @@ public final class ViewBot
* Creates a new {@code ViewBot} with a new presenter.
*
* @since 2022-01-29
+ * @since v0.4.0
*/
public ViewBot() {
this.presenter = new Presenter(this);
@@ -249,6 +265,7 @@ public final class ViewBot
/**
* @return list of records of expression conversions done by this bot
* @since 2022-04-09
+ * @since v0.4.0
*/
public List<UnitConversionRecord> expressionConversionList() {
return Collections.unmodifiableList(this.expressionConversions);
@@ -257,6 +274,7 @@ public final class ViewBot
/**
* @return the available dimensions
* @since 2022-01-29
+ * @since v0.4.0
*/
@Override
public Set<String> getDimensionNames() {
@@ -276,6 +294,7 @@ public final class ViewBot
/**
* @return the units available for selection in From
* @since 2022-01-29
+ * @since v0.4.0
*/
@Override
public Set<String> getFromUnitNames() {
@@ -290,6 +309,7 @@ public final class ViewBot
/**
* @return the presenter associated with tihs view
* @since 2022-01-29
+ * @since v0.4.0
*/
@Override
public Presenter getPresenter() {
@@ -314,6 +334,7 @@ public final class ViewBot
/**
* @return the units available for selection in To
* @since 2022-01-29
+ * @since v0.4.0
*/
@Override
public Set<String> getToUnitNames() {
@@ -333,6 +354,7 @@ public final class ViewBot
/**
* @return list of records of this viewBot's prefix views
* @since 2022-04-16
+ * @since v0.4.0
*/
public List<PrefixViewingRecord> prefixViewList() {
return Collections.unmodifiableList(this.prefixViewingRecords);
@@ -350,6 +372,7 @@ public final class ViewBot
* @param fromExpression the expression to convert from
* @throws NullPointerException if {@code fromExpression} is null
* @since 2022-01-29
+ * @since v0.4.0
*/
public void setFromExpression(String fromExpression) {
this.fromExpression = Objects.requireNonNull(fromExpression,
@@ -359,6 +382,7 @@ public final class ViewBot
/**
* @param fromSelection the fromSelection to set
* @since 2022-01-29
+ * @since v0.4.0
*/
public void setFromSelection(Optional<String> fromSelection) {
this.fromSelection = Objects.requireNonNull(fromSelection,
@@ -368,6 +392,7 @@ public final class ViewBot
/**
* @param fromSelection the fromSelection to set
* @since 2022-02-10
+ * @since v0.4.0
*/
public void setFromSelection(String fromSelection) {
this.setFromSelection(Optional.of(fromSelection));
@@ -381,20 +406,27 @@ public final class ViewBot
/**
* @param inputValue the inputValue to set
* @since 2022-01-29
+ * @since v0.4.0
*/
public void setInputValue(String inputValue) {
this.inputValue = inputValue;
}
/**
- * @param selectedDimension the selectedDimension to set
+ * @param selectedDimensionName the selectedDimensionName to set
* @since 2022-01-29
+ * @since v0.4.0
*/
public void setSelectedDimensionName(
Optional<String> selectedDimensionName) {
this.selectedDimensionName = selectedDimensionName;
}
+ /**
+ * Sets the view's selected dimension
+ *
+ * @param selectedDimensionName name of dimension to select (string)
+ */
public void setSelectedDimensionName(String selectedDimensionName) {
this.setSelectedDimensionName(Optional.of(selectedDimensionName));
}
@@ -405,6 +437,7 @@ public final class ViewBot
* @param toExpression the expression to convert to
* @throws NullPointerException if {@code toExpression} is null
* @since 2022-01-29
+ * @since v0.4.0
*/
public void setToExpression(String toExpression) {
this.toExpression = Objects.requireNonNull(toExpression,
@@ -412,14 +445,16 @@ public final class ViewBot
}
/**
- * @param toSelection the toSelection to set
+ * @param toSelection unit set in the 'To' selection
* @since 2022-01-29
+ * @since v0.4.0
*/
public void setToSelection(Optional<String> toSelection) {
this.toSelection = Objects.requireNonNull(toSelection,
"toSelection cannot be null.");
}
+ /** @param toSelection unit set in the 'To' selection */
public void setToSelection(String toSelection) {
this.setToSelection(Optional.of(toSelection));
}
@@ -439,18 +474,28 @@ public final class ViewBot
// do nothing, ViewBot supports selecting any unit
}
+ /** @param viewedPrefixName name of prefix being used */
public void setViewedPrefixName(Optional<String> viewedPrefixName) {
this.prefixViewerSelection = viewedPrefixName;
}
+ /**
+ * @param viewedPrefixName name of prefix being used (may not be null)
+ * @throws NullPointerException if {@code viewedPrefixName} is null
+ */
public void setViewedPrefixName(String viewedPrefixName) {
this.setViewedPrefixName(Optional.of(viewedPrefixName));
}
+ /** @param viewedUnitName name of unit being used */
public void setViewedUnitName(Optional<String> viewedUnitName) {
this.unitViewerSelection = viewedUnitName;
}
+ /**
+ * @param viewedUnitName name of unit being used (may not be null)
+ * @throws NullPointerException if {@code viewedUnitName} is null
+ */
public void setViewedUnitName(String viewedUnitName) {
this.setViewedUnitName(Optional.of(viewedUnitName));
}
@@ -493,6 +538,7 @@ public final class ViewBot
/**
* @return list of records of every unit conversion made by this bot
* @since 2022-04-09
+ * @since v0.4.0
*/
public List<UnitConversionRecord> unitConversionList() {
return Collections.unmodifiableList(this.unitConversions);
@@ -501,8 +547,14 @@ public final class ViewBot
/**
* @return list of records of unit viewings made by this bot
* @since 2022-04-16
+ * @since v0.4.0
*/
public List<UnitViewingRecord> unitViewList() {
return Collections.unmodifiableList(this.unitViewingRecords);
}
+
+ @Override
+ public void updateText() {
+ // do nothing, since ViewBot is not localized
+ }
}
diff --git a/src/main/java/sevenUnitsGUI/package-info.java b/src/main/java/sevenUnitsGUI/package-info.java
index cff1ded..9432960 100644
--- a/src/main/java/sevenUnitsGUI/package-info.java
+++ b/src/main/java/sevenUnitsGUI/package-info.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2021 Adrien Hopkins
+ * Copyright (C) 2021-2025 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,8 +16,9 @@
*/
/**
* The MVP GUI of SevenUnits
- *
+ *
* @author Adrien Hopkins
* @since 2021-12-15
+ * @since v0.4.0
*/
package sevenUnitsGUI; \ No newline at end of file
diff --git a/src/main/resources/about.txt b/src/main/resources/about/en.txt
index 782b422..37986e5 100644
--- a/src/main/resources/about.txt
+++ b/src/main/resources/about/en.txt
@@ -4,14 +4,18 @@ About 7Units Version [VERSION]
inspired by GNU Units (https://www.gnu.org/software/units/).
You can use it to simply convert units, but you can also
use it like a calculator, computing and converting expressions
-like "10 m/s + (25^2 - 5^2) mi/hr".
+like "10 m/s + (25^2 - 5^2) mi/h".
This software was written by Adrien Hopkins
<adrien.p.hopkins@gmail.com>.
+Unit/Prefix/Dimension Statistics:
+
+[LOADSTATS]
+
Copyright Notice:
-Unit Converter Copyright (C) 2018-2024 Adrien Hopkins
+Unit Converter Copyright (C) 2018-2025 Adrien Hopkins
This program comes with ABSOLUTELY NO WARRANTY;
for details read the LICENSE file, section 15
diff --git a/src/main/resources/about/fr.txt b/src/main/resources/about/fr.txt
new file mode 100644
index 0000000..fc7ed83
--- /dev/null
+++ b/src/main/resources/about/fr.txt
@@ -0,0 +1,24 @@
+À propos de 7Unités version [VERSION]
+
+7Unités est une programme pour convertir les unités avec plusieurs fonctions,
+inspiré par GNU Units (https://www.gnu.org/software/units/).
+Vous pouvez l’utiliser pour convertir des unités, mais vous pouvez aussi
+l’utiliser comme calculatrice, computer et convertir des expressions
+comme "10 m/s + (25^2 - 5^2) mi/h".
+
+Ce logiciel est par Adrien Hopkins <adrien.p.hopkins@gmail.com>.
+
+Statistiques d’unités, préfixes et dimensions:
+
+[LOADSTATS]
+
+Copyright Notice:
+
+Unit Converter Copyright (C) 2018-2025 Adrien Hopkins
+This program comes with ABSOLUTELY NO WARRANTY;
+for details read the LICENSE file, section 15
+
+This is free software, and you are welcome to redistribute
+it under certain conditions; for details go to
+<https://www.gnu.org/licenses/quick-guide-gplv3.html>
+or read the LICENSE file.
diff --git a/src/main/resources/locales/en.txt b/src/main/resources/locales/en.txt
new file mode 100644
index 0000000..666e363
--- /dev/null
+++ b/src/main/resources/locales/en.txt
@@ -0,0 +1,31 @@
+tv.title=7Units [v]
+tv.convert_units.title=Convert Units
+tv.convert_units.value_prompt=Value to convert:
+tv.convert_units.convert_btn=Convert
+tv.convert_expressions.title=Convert Unit Expressions
+tv.convert_expressions.from=From
+tv.convert_expressions.to=To
+tv.convert_expressions.convert_btn=Convert
+tv.convert_expressions.output=Output
+tv.unit_viewer.title=Unit Viewer
+tv.prefix_viewer.title=Prefix Viewer
+tv.settings.rounding.title=Rounding Settings
+tv.settings.rounding.rule=Rounding Rule:
+tv.settings.rounding.precision=Precision:
+tv.settings.rounding.fixed_sigfig=Fixed Precision
+tv.settings.rounding.fixed_places=Fixed Decimal Places
+tv.settings.rounding.uncertainty=Uncertainty-based Rounding
+tv.settings.repetition.title=Prefix Repetition Settings
+tv.settings.repetition.no=No Repetition
+tv.settings.repetition.any=No Restriction
+tv.settings.repetition.complex=Complex Repetition
+tv.settings.search.title=Search Settings
+tv.settings.search.no_prefixes=Never Include Prefixed Units
+tv.settings.search.common_prefixes=Include Common Prefixes
+tv.settings.search.all_prefixes=Include All Single Prefixes
+tv.settings.oneway=Convert One Way Only
+tv.settings.show_duplicate=Show Duplicate Units & Prefixes
+tv.settings.use_default_files=Use Default Datafiles
+tv.settings.locale=🌐 Locale:
+tv.settings.unitfiles.button=Manage Unit Data Files
+load_stat_msg=Successfully loaded [u] unique units with [un] names ([b] base units), [p] unique prefixes with [pn] names, [s] unit sets, and [d] named dimensions.
diff --git a/src/main/resources/locales/fr.txt b/src/main/resources/locales/fr.txt
new file mode 100644
index 0000000..3fef030
--- /dev/null
+++ b/src/main/resources/locales/fr.txt
@@ -0,0 +1,31 @@
+tv.title=7Unités [v]
+tv.convert_units.title=Convertir Unités
+tv.convert_units.value_prompt=Valueur à convertir:
+tv.convert_units.convert_btn=Convertir
+tv.convert_expressions.title=Convertir Expressions
+tv.convert_expressions.from=De
+tv.convert_expressions.to=À
+tv.convert_expressions.convert_btn=Convertir
+tv.convert_expressions.output=Résultat
+tv.unit_viewer.title=Unités
+tv.prefix_viewer.title=Préfixes
+tv.settings.rounding.title=Préférences d’Arrondissement
+tv.settings.rounding.rule=Règle d’Arrondissement
+tv.settings.rounding.precision=Précision:
+tv.settings.rounding.fixed_sigfig=Precision fixe
+tv.settings.rounding.fixed_places=Chiffres fixe
+tv.settings.rounding.uncertainty=Arrondissement d’Incertitude
+tv.settings.repetition.title=Préférences de répetition de préfixes
+tv.settings.repetition.no=Non répetition
+tv.settings.repetition.any=Tout répetition
+tv.settings.repetition.complex=Répetition Complexe
+tv.settings.search.title=Préférences de Recherche
+tv.settings.search.no_prefixes=Jamais inclure unités préfixés
+tv.settings.search.common_prefixes=Inclure préfixes fréquents
+tv.settings.search.all_prefixes=Inclure tous préfixes seuls
+tv.settings.oneway=Convertir Seulement en un Direction
+tv.settings.show_duplicate=Montrer unités et préfixes doubles
+tv.settings.use_default_files=Utilise donées par défaut
+tv.settings.locale=🌐 Locale:
+tv.settings.unitfiles.button=Gérer donées d’unités
+load_stat_msg=Chargé [u] unités uniques avec [un] noms ([b] unités bases), [p] préfixes uniques avec [pn] noms, [s] collections d’unités, et [d] dimensions nomées.
diff --git a/src/main/resources/metric_exceptions.txt b/src/main/resources/metric_exceptions.txt
index 73748c0..433dbb5 100644
--- a/src/main/resources/metric_exceptions.txt
+++ b/src/main/resources/metric_exceptions.txt
@@ -10,6 +10,7 @@ min
minute
h
hour
+hms
d
day
wk
diff --git a/src/main/resources/unitsfile.txt b/src/main/resources/unitsfile.txt
index 543c821..17fe98a 100644
--- a/src/main/resources/unitsfile.txt
+++ b/src/main/resources/unitsfile.txt
@@ -155,6 +155,7 @@ minute 60 second
min minute
hour 3600 second
h hour
+hms hour; minute; second
day 86400 second
d day
week 7 day
@@ -164,6 +165,7 @@ gregorianyear 365.2425 day
gregorianmonth gregorianyear / 12
# Other non-SI "metric" units
+are 100 m^2
litre 0.001 m^3
liter litre
l litre
@@ -187,8 +189,16 @@ inch foot / 12
in inch
yard 3 foot
yd yard
+chain 66 ft
+ch chain
+furlong 10 chain
mile 1760 yard
mi mile
+ftin ft; in
+ydftin yd; ft; in
+
+# Imperial area units
+acre chain * furlong
# Compressed notation
kph km / hour
diff --git a/src/test/java/sevenUnits/unit/MultiUnitTest.java b/src/test/java/sevenUnits/unit/MultiUnitTest.java
deleted file mode 100644
index 949a1f1..0000000
--- a/src/test/java/sevenUnits/unit/MultiUnitTest.java
+++ /dev/null
@@ -1,110 +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 <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.unit;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Random;
-import java.util.concurrent.ThreadLocalRandom;
-
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests related to the {@code MultiUnit}.
- *
- * @since 2020-10-03
- */
-class MultiUnitTest {
-
- /**
- * Ensures that the {@code MultiUnit} can convert properly.
- */
- @Test
- final void testConvert() {
- final Random rng = ThreadLocalRandom.current();
- final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT,
- BritishImperial.Length.INCH);
-
- assertEquals(1702.0,
- footInch.convertTo(Metric.METRE.withPrefix(Metric.MILLI),
- Arrays.asList(5.0, 7.0)),
- 1.0);
-
- for (int i = 0; i < 1000; i++) {
- final double feet = rng.nextInt(1000);
- final double inches = rng.nextDouble() * 12;
- final double millimetres = feet * 304.8 + inches * 25.4;
-
- final List<Double> feetAndInches = Metric.METRE
- .withPrefix(Metric.MILLI).convertTo(footInch, millimetres);
- assertEquals(feet, feetAndInches.get(0), 1e-10);
- assertEquals(inches, feetAndInches.get(1), 1e-10);
- }
- }
-
- /**
- * Test method for {@link sevenUnits.unit.MultiUnit#convertFromBase(double)}.
- */
- @Test
- final void testConvertFromBase() {
- final Random rng = ThreadLocalRandom.current();
- final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT,
- BritishImperial.Length.INCH);
-
- // 1.7 m =~ 5' + 7"
- final List<Double> values = footInch.convertFromBase(1.7018);
-
- assertEquals(5, values.get(0));
- assertEquals(7, values.get(1), 1e-12);
-
- for (int i = 0; i < 1000; i++) {
- final double feet = rng.nextInt(1000);
- final double inches = rng.nextDouble() * 12;
- final double metres = feet * 0.3048 + inches * 0.0254;
-
- final List<Double> feetAndInches = footInch.convertFromBase(metres);
- assertEquals(feet, feetAndInches.get(0), 1e-10);
- assertEquals(inches, feetAndInches.get(1), 1e-10);
- }
- }
-
- /**
- * Test method for
- * {@link sevenUnits.unit.MultiUnit#convertToBase(java.util.List)}.
- */
- @Test
- final void testConvertToBase() {
- final Random rng = ThreadLocalRandom.current();
- final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT,
- BritishImperial.Length.INCH);
-
- // 1.7 m =~ 5' + 7"
- assertEquals(1.7018, footInch.convertToBase(Arrays.asList(5.0, 7.0)),
- 1e-12);
-
- for (int i = 0; i < 1000; i++) {
- final double feet = rng.nextInt(1000);
- final double inches = rng.nextDouble() * 12;
- final double metres = feet * 0.3048 + inches * 0.0254;
-
- assertEquals(metres,
- footInch.convertToBase(Arrays.asList(feet, inches)), 1e-12);
- }
- }
-}
diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
index 9d650f0..3d6d663 100644
--- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
+++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -23,20 +23,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
-import java.io.InputStream;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import sevenUnits.utils.NameSymbol;
@@ -45,7 +46,7 @@ import sevenUnits.utils.UncertainDouble;
/**
* A test for the {@link UnitDatabase} class. This is NOT part of this program's
* public API.
- *
+ *
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
@@ -57,8 +58,8 @@ class UnitDatabaseTest {
private V value;
/**
- *
* @since 2021-10-07
+ * @since v0.3.2
*/
public SimpleEntry(K key, V value) {
this.key = key;
@@ -94,7 +95,7 @@ class UnitDatabaseTest {
@Override
public V setValue(V value) {
- final V oldValue = this.value;
+ final var oldValue = this.value;
this.value = value;
return oldValue;
}
@@ -135,6 +136,7 @@ class UnitDatabaseTest {
* @param value value in entry
* @return entry
* @since 2021-10-07
+ * @since v0.3.2
*/
private static <K, V> Map.Entry<K, V> entry(K key, V value) {
return new SimpleEntry<>(key, value);
@@ -146,14 +148,18 @@ class UnitDatabaseTest {
*
* @param loadTo database to load to
* @param path path of file to load
+ * @return exceptions returned by file loading
* @since 2021-10-04
+ * @since v0.3.2
*/
- private static void loadDimensionFile(UnitDatabase loadTo, String path) {
- try (final InputStream testFile = UnitDatabaseTest.class
+ private static List<LoadingException> loadDimensionFile(UnitDatabase loadTo,
+ String path) {
+ try (final var testFile = UnitDatabaseTest.class
.getResourceAsStream(path)) {
- loadTo.loadDimensionsFromStream(testFile);
+ return loadTo.loadDimensionsFromStream(testFile);
} catch (final IOException e) {
fail(e.getClass() + " occurred upon loading file \"" + path + "\".");
+ return Collections.emptyList();
}
}
@@ -163,26 +169,89 @@ class UnitDatabaseTest {
*
* @param loadTo database to load to
* @param path path of file to load
+ * @return exceptions returned by file loading
* @since 2021-09-22
+ * @since v0.3.2
*/
- private static void loadUnitsFile(UnitDatabase loadTo, String path) {
- try (final InputStream testFile = UnitDatabaseTest.class
+ private static List<LoadingException> loadUnitsFile(UnitDatabase loadTo,
+ String path) {
+ try (final var testFile = UnitDatabaseTest.class
.getResourceAsStream(path)) {
- loadTo.loadUnitsFromStream(testFile);
+ return loadTo.loadUnitsFromStream(testFile);
} catch (final IOException e) {
fail(e.getClass() + " occurred upon loading file \"" + path + "\".");
+ return Collections.emptyList();
}
}
+ private static final Stream<Arguments> testEvaluateExpressionInvalid() {
+ return Stream.of(Arguments.of("K^K"), Arguments.of("1 + K"));
+ }
+
+ private static final Stream<Arguments> testEvaluateExpressionValid() {
+ final var uncertainTwoThirds = UncertainDouble.of(2.0, 1.0)
+ .dividedBy(UncertainDouble.of(3.0, 1.0));
+ return Stream.of(
+ Arguments.of("J + (2 * 3) J + (20 / 4) J",
+ LinearUnitValue.of(J,
+ UncertainDouble.of(12, Math.sqrt(14.625)))),
+ Arguments.of("J + 2 * 3 * J + 20 / 4 * J",
+ LinearUnitValue.of(J,
+ UncertainDouble.of(12, Math.sqrt(14.625)))),
+ Arguments.of("J - -1 * J",
+ LinearUnitValue.of(J, UncertainDouble.of(2, 1))),
+ Arguments.of("K^2",
+ LinearUnitValue.of(K.times(K), UncertainDouble.of(1, 0))),
+ Arguments.of("2 J / 3 J",
+ LinearUnitValue.of(J.dividedBy(J), uncertainTwoThirds)));
+ }
+
+ private static final Stream<Arguments> testFormatExpression() {
+ return Stream.of(Arguments.of("1*2", "1 * 2"),
+ Arguments.of("1/2", "1 / 2"), Arguments.of("1|2", "1 | 2"),
+ Arguments.of("1^2", "1 ^ 2"), Arguments.of("1 * 2", "1 * 2"),
+ Arguments.of("+1", "+1"), Arguments.of("-1", "-1"),
+ Arguments.of("1.1e+5", "1.1e+5"),
+ Arguments.of("1.25e-5", "1.25e-5"));
+ }
+
+ /**
+ * Tests expressions that are valid to the parser, but semantically invalid
+ * (e.g. adding different dimensions)
+ *
+ * @param expression expression to test - should throw
+ * {@link IllegalArgumentException}
+ */
+ @ParameterizedTest
+ @MethodSource
+ public void testEvaluateExpressionInvalid(String expression) {
+ final var database = new UnitDatabase();
+
+ database.addUnit("J", J);
+ database.addUnit("K", K);
+
+ database.addPrefix("A", A);
+ database.addPrefix("B", B);
+ database.addPrefix("C", C);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> database.getUnitFromExpression(expression));
+ assertThrows(IllegalArgumentException.class,
+ () -> database.evaluateUnitExpression(expression));
+ }
+
/**
* A test for the {@link UnitDatabase#evaluateUnitExpression(String)}
* function. Simple because the expression parser has its own test.
- *
+ *
* @since 2021-09-27
+ * @since v0.3.2
*/
- @Test
- public void testEvaluateExpression() {
- final UnitDatabase database = new UnitDatabase();
+ @ParameterizedTest
+ @MethodSource
+ public void testEvaluateExpressionValid(String expression,
+ LinearUnitValue expected) {
+ final var database = new UnitDatabase();
database.addUnit("J", J);
database.addUnit("K", K);
@@ -191,27 +260,30 @@ class UnitDatabaseTest {
database.addPrefix("B", B);
database.addPrefix("C", C);
- final LinearUnitValue expected = LinearUnitValue.of(J,
- UncertainDouble.of(12, Math.sqrt(14.625)));
- // note: units are exact, each number has an uncertainty of 1
- final LinearUnitValue actual = database
- .evaluateUnitExpression("J + (2 * 3) J + (20 / 4) J");
+ final var actual = database.evaluateUnitExpression(expression);
assertEquals(expected, actual);
- // check that negation works properly
- assertEquals(2,
- database.evaluateUnitExpression("J - -1 * J").getValueExact());
+ final var expectedU = expected.getUnit().times(expected.getValueExact());
+ final var actualU = database.getUnitFromExpression(expression);
+ assertEquals(expectedU, actualU);
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ public void testFormatExpression(String expression, String expected) {
+ assertEquals(expected, UnitDatabase.formatExpression(expression));
}
/**
* Test for {@link UnitDatabase#getUnit}, {@link UnitDatabase#getLinearUnit}
* and {@link UnitDatabase#getLinearUnitValue}.
- *
+ *
* @since 2021-10-07
+ * @since v0.3.2
*/
@Test
public void testGetUnit() {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("m", Metric.METRE);
database.addUnit("meter", Metric.METRE);
@@ -239,14 +311,15 @@ class UnitDatabaseTest {
/**
* Confirms that operations that shouldn't function for infinite databases
* throw an {@code IllegalStateException}.
- *
+ *
* @since 2019-05-03
+ * @since v0.3.0
*/
// @Test
// @Timeout(value = 1, unit = TimeUnit.SECONDS)
public void testInfiniteSetExceptions() {
// load units
- final UnitDatabase infiniteDatabase = new UnitDatabase();
+ final var infiniteDatabase = new UnitDatabase();
infiniteDatabase.addUnit("J", J);
infiniteDatabase.addUnit("K", K);
@@ -255,9 +328,8 @@ class UnitDatabaseTest {
infiniteDatabase.addPrefix("B", B);
infiniteDatabase.addPrefix("C", C);
- final Set<Entry<String, Unit>> entrySet = infiniteDatabase.unitMap()
- .entrySet();
- final Set<String> keySet = infiniteDatabase.unitMap().keySet();
+ final var entrySet = infiniteDatabase.unitMap().entrySet();
+ final var keySet = infiniteDatabase.unitMap().keySet();
assertThrows(IllegalStateException.class, () -> entrySet.toArray());
assertThrows(IllegalStateException.class, () -> keySet.toArray());
}
@@ -267,18 +339,20 @@ class UnitDatabaseTest {
*
* @param num which file to test
* @since 2021-10-04
+ * @since v0.3.2
*/
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
public void testLoadingInvalidDimensionFile(int num) {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addDimension("LENGTH", Metric.Dimensions.LENGTH);
database.addDimension("MASS", Metric.Dimensions.MASS);
database.addDimension("TIME", Metric.Dimensions.TIME);
- final String filename = String.format("/test-dimensionfile-invalid%d.txt",
+ final var filename = String.format("/test-dimensionfile-invalid%d.txt",
num);
- final RuntimeException e = assertThrows(RuntimeException.class,
- () -> loadDimensionFile(database, filename));
+ final var errs = loadDimensionFile(database, filename);
+ assertFalse(errs.isEmpty(), "no error from invalid file " + filename);
+ final var e = errs.get(0).problem();
assertTrue(e instanceof IllegalArgumentException
|| e instanceof NoSuchElementException);
}
@@ -288,27 +362,29 @@ class UnitDatabaseTest {
*
* @param num which file to test
* @since 2021-09-27
+ * @since v0.3.2
*/
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3, 4, 5 })
public void testLoadingInvalidUnitFile(int num) {
- final UnitDatabase database = new UnitDatabase();
- final String filename = String.format("/test-unitsfile-invalid%d.txt",
- num);
- final RuntimeException e = assertThrows(RuntimeException.class,
- () -> loadUnitsFile(database, filename));
+ final var database = new UnitDatabase();
+ final var filename = String.format("/test-unitsfile-invalid%d.txt", num);
+ final var errs = loadUnitsFile(database, filename);
+ assertFalse(errs.isEmpty(), "no error from invalid file " + filename);
+ final var e = errs.get(0).problem();
assertTrue(e instanceof IllegalArgumentException
|| e instanceof NoSuchElementException);
}
/**
* Tests loading a valid dimension-file with some derived dimensions.
- *
+ *
* @since 2021-10-04
+ * @since v0.3.2
*/
@Test
public void testLoadingValidDimensions() {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addDimension("LENGTH", Metric.Dimensions.LENGTH);
database.addDimension("MASS", Metric.Dimensions.MASS);
database.addDimension("TIME", Metric.Dimensions.TIME);
@@ -321,27 +397,33 @@ class UnitDatabaseTest {
/**
* Tests loading a valid unitfile with some prefixes and no units.
- *
+ *
* @since 2021-09-22
+ * @since v0.3.2
*/
@Test
public void testLoadingValidPrefixes() {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
loadUnitsFile(database, "/test-unitsfile-valid2.txt");
assertEquals(7, database.getPrefix("A").getMultiplier());
assertEquals(11, database.getPrefix("B").getMultiplier());
assertEquals(13, database.getPrefix("C").getMultiplier());
+
+ // test invalid prefixes
+ assertThrows(NoSuchElementException.class,
+ () -> database.getPrefix("N/A"));
}
/**
* Tests loading a valid unitfile with some units and preloaded prefixes
- *
+ *
* @since 2021-09-22
+ * @since v0.3.2
*/
@Test
public void testLoadingValidUnits() {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("U", U);
database.addUnit("V", V);
@@ -357,21 +439,21 @@ class UnitDatabaseTest {
final Unit expected1 = ((LinearUnit) U).withPrefix(A).withPrefix(B)
.withPrefix(C);
- final Unit actual1 = database.getUnit("test1");
+ final var actual1 = database.getUnit("test1");
assertEquals(expected1, actual1);
final Unit expected2 = ((LinearUnit) W).withPrefix(B)
.times(((LinearUnit) V).withPrefix(C));
- final Unit actual2 = database.getUnit("test2");
+ final var actual2 = database.getUnit("test2");
assertEquals(expected2, actual2);
final Unit expected3 = ((LinearUnit) U)
.times(A.getMultiplier() + C.getMultiplier() - B.getMultiplier());
- final Unit actual3 = database.getUnit("test3");
+ final var actual3 = database.getUnit("test3");
assertEquals(expected3, actual3);
- final UnitValue expected4 = UnitValue.of(U, 1);
- final UnitValue actual4 = database
+ final var expected4 = UnitValue.of(U, 1);
+ final var actual4 = database
.evaluateUnitExpression("-5 * U + -3 * U + 12 * U - 3 * U")
.asUnitValue();
assertEquals(expected4, actual4);
@@ -382,21 +464,21 @@ class UnitDatabaseTest {
/**
* Tests the iterator of the prefixless unit map. These tests are simple, as
* the unit map iterator is simple.
- *
+ *
* @since 2021-10-07
+ * @since v0.3.2
*/
@Test
public void testPrefixedUnitMapIterator() {
- final UnitDatabase database1 = new UnitDatabase();
+ final var database1 = new UnitDatabase();
database1.addUnit("U", U);
database1.addUnit("V", V);
database1.addUnit("W", W);
- final Map<String, Unit> map1 = database1.unitMap();
- final Iterator<String> keyIterator1 = map1.keySet().iterator();
- final Iterator<Map.Entry<String, Unit>> entryIterator1 = map1.entrySet()
- .iterator();
+ final var map1 = database1.unitMap();
+ final var keyIterator1 = map1.keySet().iterator();
+ final var entryIterator1 = map1.entrySet().iterator();
final Set<String> expectedKeys = Set.of("U", "V", "W");
final Set<String> actualKeys = new HashSet<>();
@@ -418,13 +500,13 @@ class UnitDatabaseTest {
/**
* Test that prefixes correctly apply to units.
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@Test
public void testPrefixes() {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("U", U);
database.addUnit("V", V);
@@ -439,29 +521,28 @@ class UnitDatabaseTest {
assertEquals(expected, database.getPrefixesFromName("ABCU"));
// get the product
- final Unit abcuNonlinear = database.getUnit("ABCU");
+ final var abcuNonlinear = database.getUnit("ABCU");
assert abcuNonlinear instanceof LinearUnit;
- final LinearUnit abcu = (LinearUnit) abcuNonlinear;
+ final var abcu = (LinearUnit) abcuNonlinear;
assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(),
abcu.getConversionFactor(), 1e-15);
}
/**
* Tests the functionnalites of the prefixless unit map.
- *
+ *
* <p>
* The map should be an auto-updating view of the units in the database.
* </p>
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@Test
public void testPrefixlessUnitMap() {
- final UnitDatabase database = new UnitDatabase();
- final Map<String, Unit> prefixlessUnits = database
- .unitMapPrefixless(true);
+ final var database = new UnitDatabase();
+ final var prefixlessUnits = database.unitMapPrefixless(true);
database.addUnit("U", U);
database.addUnit("V", V);
@@ -478,13 +559,13 @@ class UnitDatabaseTest {
/**
* Tests that the database correctly stores and retrieves units, ignoring
* prefixes.
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@Test
public void testPrefixlessUnits() {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("U", U);
database.addUnit("V", V);
@@ -517,7 +598,7 @@ class UnitDatabaseTest {
@Test
public void testToString() {
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("J", J);
database.addUnit("K", J);
@@ -538,14 +619,14 @@ class UnitDatabaseTest {
/**
* Test that unit expressions return the correct value.
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@Test
public void testUnitExpressions() {
// load units
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("U", U);
database.addUnit("V", V);
@@ -560,13 +641,13 @@ class UnitDatabaseTest {
// first test - test prefixes and operations
final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C)
.withPrefix(C);
- final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W");
+ final var actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W");
assertEquals(expected1, actual1);
// second test - test addition and subtraction
final Unit expected2 = J.times(58);
- final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej");
+ final var actual2 = database.getUnitFromExpression("2 fj + 6 ej");
assertEquals(expected2, actual2);
@@ -579,14 +660,14 @@ class UnitDatabaseTest {
/**
* Tests both the unit name iterator and the name-unit entry iterator
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@Test
public void testUnitIterator() {
// load units
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("J", J);
database.addUnit("K", K);
@@ -595,19 +676,17 @@ class UnitDatabaseTest {
database.addPrefix("B", B);
database.addPrefix("C", C);
- final int NUM_UNITS = database.unitMapPrefixless(true).size();
- final int NUM_PREFIXES = database.prefixMap(true).size();
+ final var NUM_UNITS = database.unitMapPrefixless(true).size();
+ final var NUM_PREFIXES = database.prefixMap(true).size();
- final Iterator<String> nameIterator = database.unitMap().keySet()
- .iterator();
- final Iterator<Entry<String, Unit>> entryIterator = database.unitMap()
- .entrySet().iterator();
+ final var nameIterator = database.unitMap().keySet().iterator();
+ final var entryIterator = database.unitMap().entrySet().iterator();
- int expectedLength = 1;
- int unitsWithThisLengthSoFar = 0;
+ var expectedLength = 1;
+ var unitsWithThisLengthSoFar = 0;
// loop 1000 times
- for (int i = 0; i < 1000; i++) {
+ for (var i = 0; i < 1000; i++) {
// expected length of next
if (unitsWithThisLengthSoFar >= NUM_UNITS
* (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) {
@@ -616,9 +695,9 @@ class UnitDatabaseTest {
}
// test that stuff is valid
- final String nextName = nameIterator.next();
- final Unit nextUnit = database.getUnit(nextName);
- final Entry<String, Unit> nextEntry = entryIterator.next();
+ final var nextName = nameIterator.next();
+ final var nextUnit = database.getUnit(nextName);
+ final var nextEntry = entryIterator.next();
assertEquals(expectedLength, nextName.length());
assertEquals(nextName, nextEntry.getKey());
@@ -628,13 +707,13 @@ class UnitDatabaseTest {
}
// test toString for consistency
- final String entryIteratorString = entryIterator.toString();
- for (int i = 0; i < 3; i++) {
+ final var entryIteratorString = entryIterator.toString();
+ for (var i = 0; i < 3; i++) {
assertEquals(entryIteratorString, entryIterator.toString());
}
- final String nameIteratorString = nameIterator.toString();
- for (int i = 0; i < 3; i++) {
+ final var nameIteratorString = nameIterator.toString();
+ for (var i = 0; i < 3; i++) {
assertEquals(nameIteratorString, nameIterator.toString());
}
}
@@ -646,14 +725,14 @@ class UnitDatabaseTest {
* For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this
* case, "AB-C-U" is the correct choice.
* </p>
- *
+ *
* @since 2019-04-14
* @since v0.2.0
*/
@Test
public void testUnitPrefixCombinations() {
// load units
- final UnitDatabase database = new UnitDatabase();
+ final var database = new UnitDatabase();
database.addUnit("J", J);
@@ -665,7 +744,7 @@ class UnitDatabaseTest {
// test 1 - AB-C-J vs A-BC-J vs A-B-C-J
final Unit expected1 = J.withPrefix(AB).withPrefix(C);
- final Unit actual1 = database.getUnit("ABCJ");
+ final var actual1 = database.getUnit("ABCJ");
assertEquals(expected1, actual1);
@@ -674,8 +753,61 @@ class UnitDatabaseTest {
database.addPrefix("ABC", UnitPrefix.valueOf(17));
final Unit expected2 = J.times(17);
- final Unit actual2 = database.getUnit("ABCJ");
+ final var actual2 = database.getUnit("ABCJ");
assertEquals(expected2, actual2);
}
+
+ /**
+ * Tests the ability to create, read, and delete unit sets.
+ *
+ * @since 2025-04-30
+ * @since v1.0.0
+ */
+ @Test
+ void testUnitSetsInvalid() {
+ final List<LinearUnit> units = List.of(Metric.SECOND,
+ BritishImperial.Length.INCH);
+
+ final var database = new UnitDatabase();
+
+ assertThrows(IllegalArgumentException.class,
+ () -> database.addUnitSet("badtest", units));
+ assertThrows(NoSuchElementException.class,
+ () -> database.getUnitSet("badtest"));
+
+ database.addUnit("ft", BritishImperial.Length.FOOT);
+ database.addUnit("s", Metric.SECOND);
+ database.addUnit("dC", Metric.CELSIUS);
+ database.addUnit("K", Metric.KELVIN);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> database.getUnitSetFromExpression("K; dC"),
+ "getUnitSetFromExpression allowed nonlinear unit.");
+ assertThrows(IllegalArgumentException.class,
+ () -> database.getUnitSetFromExpression("ft; s"),
+ "getUnitSetFromExpression allowed units of different dimension.");
+ }
+
+ /**
+ * Tests the ability to create, read, and delete unit sets.
+ *
+ * @since 2025-04-30
+ * @since v1.0.0
+ */
+ @Test
+ void testUnitSetsValid() {
+ final List<LinearUnit> units = List.of(BritishImperial.Length.FOOT,
+ BritishImperial.Length.INCH);
+
+ final var database = new UnitDatabase();
+
+ database.addUnitSet("ftintest", units);
+ assertEquals(units, database.getUnitSet("ftintest"));
+
+ database.addUnit("ft", BritishImperial.Length.FOOT);
+ database.addUnit("in", BritishImperial.Length.INCH);
+ assertEquals(units, database.getUnitSetFromExpression("ft; in"));
+ }
+
}
diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java
index c93043b..fb21723 100644
--- a/src/test/java/sevenUnits/unit/UnitTest.java
+++ b/src/test/java/sevenUnits/unit/UnitTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * Copyright (C) 2018, 2021, 2022, 2024, 2025 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
@@ -21,7 +21,6 @@ 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;
@@ -29,12 +28,11 @@ import org.junit.jupiter.api.Test;
import sevenUnits.utils.DecimalComparison;
import sevenUnits.utils.NameSymbol;
-import sevenUnits.utils.UncertainDouble;
/**
* Testing the various Unit classes. This is NOT part of this program's public
* API.
- *
+ *
* @author Adrien Hopkins
* @since 2018-12-22
* @since v0.1.0
@@ -45,21 +43,25 @@ class UnitTest {
@Test
public void testAdditionAndSubtraction() {
- final LinearUnit inch = Metric.METRE.times(0.0254)
+ final var inch = Metric.METRE.times(0.0254)
.withName(NameSymbol.of("inch", "in"));
- final LinearUnit foot = Metric.METRE.times(0.3048)
+ final var foot = Metric.METRE.times(0.3048)
.withName(NameSymbol.of("foot", "ft"));
- assertEquals(inch.plus(foot), Metric.METRE.times(0.3302));
- assertEquals(foot.minus(inch), Metric.METRE.times(0.2794));
+ assertTrue(
+ inch.plus(foot).equalsApproximately(Metric.METRE.times(0.3302)),
+ String.format("Expected: %s, Actual: %s", inch.plus(foot),
+ Metric.METRE.times(0.3302)));
+ assertTrue(
+ foot.minus(inch).equalsApproximately(Metric.METRE.times(0.2794)),
+ String.format("Expected: %s, Actual: %s", foot.minus(inch),
+ Metric.METRE.times(0.2794)));
// test with LinearUnitValue
- final LinearUnitValue value1 = LinearUnitValue.getExact(Metric.METRE, 15);
- final LinearUnitValue value2 = LinearUnitValue.getExact(foot, 120);
- final LinearUnitValue value3 = LinearUnitValue.getExact(Metric.METRE,
- 0.5);
- final LinearUnitValue value4 = LinearUnitValue.getExact(Metric.KILOGRAM,
- 60);
+ final var value1 = LinearUnitValue.getExact(Metric.METRE, 15);
+ final var value2 = LinearUnitValue.getExact(foot, 120);
+ final var value3 = LinearUnitValue.getExact(Metric.METRE, 0.5);
+ final var value4 = LinearUnitValue.getExact(Metric.KILOGRAM, 60);
// make sure addition is done correctly
assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001);
@@ -81,24 +83,24 @@ class UnitTest {
@Test
public void testConversion() {
- final LinearUnit metre = Metric.METRE;
+ final var metre = Metric.METRE;
final Unit inch = metre.times(0.0254);
- final UnitValue value = UnitValue.of(inch, 75);
+ final var value = UnitValue.of(inch, 75);
assertEquals(1.9, inch.convertTo(metre, 75), 0.01);
assertEquals(1.9, value.convertTo(metre).getValue(), 0.01);
// try random stuff
- for (int i = 0; i < 1000; i++) {
+ for (var i = 0; i < 1000; i++) {
// initiate random values
- final double conversionFactor = UnitTest.rng.nextDouble() * 1000000;
- final double testValue = UnitTest.rng.nextDouble() * 1000000;
- final double expected = testValue * conversionFactor;
+ final var conversionFactor = UnitTest.rng.nextDouble() * 1000000;
+ final var testValue = UnitTest.rng.nextDouble() * 1000000;
+ final var expected = testValue * conversionFactor;
// test
final Unit unit = Metric.METRE.times(conversionFactor);
- final double actual = unit.convertToBase(testValue);
+ final var actual = unit.convertToBase(testValue);
assertEquals(actual, expected,
expected * DecimalComparison.DOUBLE_EPSILON);
@@ -107,7 +109,7 @@ class UnitTest {
@Test
public void testEquals() {
- final LinearUnit metre = Metric.METRE;
+ final var metre = Metric.METRE;
final Unit meter = Metric.BaseUnits.METRE.asLinearUnit();
assertEquals(metre, meter);
@@ -127,19 +129,19 @@ class UnitTest {
@Test
public void testMultiplicationAndDivision() {
// test unit-times-unit multiplication
- final LinearUnit generatedJoule = Metric.KILOGRAM
+ final var generatedJoule = Metric.KILOGRAM
.times(Metric.METRE.toExponent(2))
.dividedBy(Metric.SECOND.toExponent(2));
- final LinearUnit actualJoule = Metric.JOULE;
+ final var actualJoule = Metric.JOULE;
assertEquals(generatedJoule, actualJoule);
// test multiplication by conversion factors
- final LinearUnit kilometre = Metric.METRE.times(1000);
- final LinearUnit hour = Metric.SECOND.times(3600);
- final LinearUnit generatedKPH = kilometre.dividedBy(hour);
+ final var kilometre = Metric.METRE.times(1000);
+ final var hour = Metric.SECOND.times(3600);
+ final var generatedKPH = kilometre.dividedBy(hour);
- final LinearUnit actualKPH = Metric.METRE.dividedBy(Metric.SECOND)
+ final var actualKPH = Metric.METRE.dividedBy(Metric.SECOND)
.dividedBy(3.6);
assertEquals(generatedKPH, actualKPH);
@@ -147,82 +149,9 @@ class UnitTest {
@Test
public void testPrefixes() {
- final LinearUnit generatedKilometre = Metric.METRE
- .withPrefix(Metric.KILO);
- final LinearUnit actualKilometre = Metric.METRE.times(1000);
+ final var generatedKilometre = Metric.METRE.withPrefix(Metric.KILO);
+ final var actualKilometre = Metric.METRE.times(1000);
assertEquals(generatedKilometre, actualKilometre);
}
-
- /**
- * Tests converting an uncertain LinearUnitValue to a string.
- *
- * @since 2021-11-04
- */
- @Test
- public void testValueToString1() {
- final LinearUnitValue value = LinearUnitValue.of(Metric.METRE,
- UncertainDouble.of(10, 0.24));
-
- assertEquals("(10.0 � 0.2) m", value.toString());
- assertEquals("(10.0 � 0.2) m",
- value.toString(true, RoundingMode.HALF_EVEN));
- assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
- }
-
- /**
- * Tests converting a certain LinearUnitValue to a string.
- *
- * @since 2021-11-04
- */
- @Test
- public void testValueToString2() {
- final LinearUnitValue value = LinearUnitValue.of(Metric.METRE,
- UncertainDouble.of(10, 0));
-
- assertEquals("10.0 m", value.toString());
- assertEquals("(10.0 � 0.0) m",
- value.toString(true, RoundingMode.HALF_EVEN));
- assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
- }
-
- /**
- * Tests converting an unnamed LinearUnitValue to a string.
- *
- * @since 2021-11-04
- */
- @Test
- public void testValueToString3() {
- final LinearUnitValue value = LinearUnitValue.of(
- Metric.METRE.withName(NameSymbol.EMPTY),
- UncertainDouble.of(10, 0.24));
-
- assertEquals("10.0 unnamed unit (= 10.0 m)",
- value.toString(false, RoundingMode.HALF_EVEN));
- }
-
- /**
- * Tests converting a named UnitValue to a string.
- *
- * @since 2021-11-04
- */
- @Test
- public void testValueToString4() {
- final UnitValue value = UnitValue.of(BritishImperial.FAHRENHEIT, 80);
-
- assertEquals("80.0 \u00B0F", value.toString());
- }
-
- /**
- * Tests converting an unnamed UnitValue to a string.
- *
- * @since 2021-11-04
- */
- @Test
- public void testValueToString5() {
- final UnitValue value = UnitValue
- .of(USCustomary.FAHRENHEIT.withName(NameSymbol.EMPTY), 50);
-
- assertEquals("50.0 unnamed unit (= 283.15 K)", value.toString());
- }
}
diff --git a/src/test/java/sevenUnits/unit/UnitValueTest.java b/src/test/java/sevenUnits/unit/UnitValueTest.java
new file mode 100644
index 0000000..3679703
--- /dev/null
+++ b/src/test/java/sevenUnits/unit/UnitValueTest.java
@@ -0,0 +1,240 @@
+/**
+ * Copyright (C) 2021, 2025 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.unit;
+
+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 java.math.RoundingMode;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * Tests for the UnitValue and LinearUnitValue classes
+ *
+ * @since v1.0.0
+ */
+public final class UnitValueTest {
+ private static Stream<Arguments> testConvertToMultiple() {
+ return Stream.of(
+ Arguments.of(
+ LinearUnitValue.getExact(BritishImperial.Length.INCH, 64),
+ List.of(BritishImperial.Length.FOOT,
+ BritishImperial.Length.INCH),
+ List.of(5.0, 4.0)),
+ Arguments.of(LinearUnitValue.getExact(Metric.SECOND, 44102.5),
+ List.of(Metric.HOUR, Metric.MINUTE, Metric.SECOND),
+ List.of(12.0, 15.0, 2.5)));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ public void testConvertToMultiple(LinearUnitValue valFrom,
+ List<LinearUnit> others, List<Double> expectedValues) {
+ final var actual = valFrom.convertToMultiple(others);
+ assertEquals(others.size(), expectedValues.size(), String.format(
+ "%s converted to %s had the wrong number of resulting units (expected: %d, actual: %d).",
+ valFrom, others, others.size(), actual.size()));
+
+ for (var i = 0; i < others.size(); i++) {
+ final var expectedValue = LinearUnitValue.getExact(others.get(i),
+ expectedValues.get(i));
+ assertTrue(expectedValue.equivalent(actual.get(i)),
+ String.format("Value %d: expected %s, actual %s.", i,
+ expectedValue, actual.get(i)));
+ }
+ }
+
+ @Test
+ public void testConvertToMultipleErrors() {
+ assertThrows(IllegalArgumentException.class,
+ () -> LinearUnitValue.ONE.convertToMultiple(List.of()));
+ assertThrows(IllegalArgumentException.class,
+ () -> LinearUnitValue.ONE.convertToMultiple(List.of(Metric.METRE)));
+ }
+
+ // I am intentionally testing whether or not equals gracefully returns false
+ // when it gets an argument of the wrong type
+ @SuppressWarnings("unlikely-arg-type")
+ @Test
+ public void testEqualsHashCode() {
+ final var v1 = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(1.0, 0.01));
+ final var v2 = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(1.0, 0.01));
+ final var v3 = LinearUnitValue.of(Metric.MILLIMETRE,
+ UncertainDouble.of(1000.0, 10.0));
+ final var v4 = LinearUnitValue.of(Metric.MILLIMETRE,
+ UncertainDouble.of(1000.0, 20.0));
+
+ assertTrue(Objects.equals(v1, v2),
+ "Two identical values are not equal()");
+ assertTrue(Objects.equals(v1.hashCode(), v2.hashCode()),
+ "Two identical values have different hash codes.");
+ assertTrue(Objects.equals(v1, v3),
+ "Two values with the same value but different units are not equal()");
+ assertTrue(Objects.equals(v1.hashCode(), v3.hashCode()),
+ "Two values with the same value but different units have different hash codes.");
+ assertFalse(Objects.equals(v1, v4),
+ "Two values with the same value but different uncertainties are equal()");
+ assertFalse(Objects.equals(v1, "Hello"));
+ assertFalse(v1.equals("Hello", true));
+ }
+
+ @Test
+ public void testEqualsNoFP() {
+ final var v1 = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(1.0, 0.01));
+ final var v2 = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(1.0, 0.01));
+ final var v3 = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(1.0 + 5e-16, 0.01));
+ assertTrue(v1.equals(v2, false));
+ assertFalse(v1.equals(v3, false));
+ assertTrue(v1.equals(v3, true));
+ }
+
+ @Test
+ public void testOperations() {
+ final var v1 = LinearUnitValue.getExact(Metric.METRE, 2.0);
+ final var v2 = LinearUnitValue.getExact(Metric.METRE, 4.0);
+
+ assertEquals(v2, v1.times(2));
+ assertEquals(v1, v2.dividedBy(2));
+ assertEquals(LinearUnitValue.getExact(Metric.SQUARE_METRE, 8.0),
+ v1.times(v2));
+ assertEquals(LinearUnitValue.getExact(Metric.ONE, 0.5), v1.dividedBy(v2));
+ assertEquals(LinearUnitValue.getExact(Metric.SQUARE_METRE, 4.0),
+ v1.toExponent(2));
+ assertEquals(LinearUnitValue.getExact(Metric.SQUARE_METRE, 4.0),
+ v1.toExponentRounded(2.0));
+ }
+
+ @Test
+ public void testValueEquivalent() {
+ final var v1 = LinearUnitValue.of(BritishImperial.Length.INCH,
+ UncertainDouble.of(1.0, 0.0));
+ final var v2 = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(0.0255, 0.0));
+ assertFalse(v1.equivalent(v2));
+
+ final var v3 = LinearUnitValue.of(BritishImperial.Length.INCH,
+ UncertainDouble.of(1.0, 1.0));
+ final var v4 = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(0.0255, 0.025));
+ assertTrue(v3.equivalent(v4));
+ }
+
+ @Test
+ public void testValueEquivalentErrors() {
+ final var v1 = LinearUnitValue.of(BritishImperial.Length.INCH,
+ UncertainDouble.of(1.0, 0.0));
+ final var v2 = LinearUnitValue.of(BritishImperial.CALORIE,
+ UncertainDouble.of(1.0, 0.0));
+ assertFalse(v1.equivalent(v2));
+
+ assertFalse(v1.equivalent(null));
+ }
+
+ /**
+ * Tests converting an uncertain LinearUnitValue to a string.
+ *
+ * @since 2021-11-04
+ * @since v0.3.2
+ */
+ @Test
+ public void testValueToString1() {
+ final var value = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(10, 0.24));
+
+ assertEquals("(10.0 ± 0.2) m", value.toString());
+ assertEquals("(10.0 ± 0.2) m",
+ value.toString(true, RoundingMode.HALF_EVEN));
+ assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
+ }
+
+ /**
+ * Tests converting a certain LinearUnitValue to a string.
+ *
+ * @since 2021-11-04
+ * @since v0.3.2
+ */
+ @Test
+ public void testValueToString2() {
+ final var value = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.of(10, 0));
+
+ assertEquals("10.0 m", value.toString());
+ assertEquals("(10.0 ± 0.0) m",
+ value.toString(true, RoundingMode.HALF_EVEN));
+ assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
+ }
+
+ /**
+ * Tests converting an unnamed LinearUnitValue to a string.
+ *
+ * @since 2021-11-04
+ * @since v0.3.2
+ */
+ @Test
+ public void testValueToString3() {
+ final var value = LinearUnitValue.of(
+ Metric.METRE.withName(NameSymbol.EMPTY),
+ UncertainDouble.of(10, 0.24));
+
+ assertEquals("10.0 unnamed unit (= 10.0 m)",
+ value.toString(false, RoundingMode.HALF_EVEN));
+ }
+
+ /**
+ * Tests converting a named UnitValue to a string.
+ *
+ * @since 2021-11-04
+ * @since v0.3.2
+ */
+ @Test
+ public void testValueToString4() {
+ final var value = UnitValue.of(BritishImperial.FAHRENHEIT, 80);
+
+ assertEquals("80.0 \u00B0F", value.toString());
+ }
+
+ /**
+ * Tests converting an unnamed UnitValue to a string.
+ *
+ * @since 2021-11-04
+ * @since v0.3.2
+ */
+ @Test
+ public void testValueToString5() {
+ final var value = UnitValue
+ .of(USCustomary.FAHRENHEIT.withName(NameSymbol.EMPTY), 50);
+
+ assertEquals("50.0 unnamed unit (= 283.15 K)", value.toString());
+ }
+}
diff --git a/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java b/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java
index 868385b..8711847 100644
--- a/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java
+++ b/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2022, 2024, 2025 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
@@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -38,31 +37,34 @@ import sevenUnits.utils.ConditionalExistenceCollections.ConditionalExistenceIter
* normal operations on conditional existence collections and ensures that
* elements that do not pass the existence condition are not included in the
* results.
- *
+ *
* @author Adrien Hopkins
* @since 2019-10-16
+ * @since v0.3.0
*/
class ConditionalExistenceCollectionsTest {
/**
* The returned iterator ignores elements that don't start with "a".
- *
+ *
* @return test iterator
* @since 2019-10-17
+ * @since v0.3.0
*/
ConditionalExistenceIterator<String> getTestIterator() {
final List<String> items = Arrays.asList("aa", "ab", "ba");
- final Iterator<String> it = items.iterator();
- final ConditionalExistenceIterator<String> cit = (ConditionalExistenceIterator<String>) ConditionalExistenceCollections
+ final var it = items.iterator();
+ final var cit = (ConditionalExistenceIterator<String>) ConditionalExistenceCollections
.conditionalExistenceIterator(it, s -> s.startsWith("a"));
return cit;
}
/**
* The returned map ignores mappings where the value is zero.
- *
+ *
* @return map to be used for test data
* @since 2019-10-16
+ * @since v0.3.0
*/
Map<String, Integer> getTestMap() {
final Map<String, Integer> map = new HashMap<>();
@@ -76,60 +78,49 @@ class ConditionalExistenceCollectionsTest {
return conditionalMap;
}
- /**
- * Test method for the ConditionalExistenceMap's containsKey method.
- */
+ /** Test method for the ConditionalExistenceMap's containsKey method. */
@Test
void testContainsKeyObject() {
- final Map<String, Integer> map = this.getTestMap();
+ final var map = this.getTestMap();
assertTrue(map.containsKey("one"));
assertTrue(map.containsKey("ten"));
assertFalse(map.containsKey("five"));
assertFalse(map.containsKey("zero"));
}
- /**
- * Test method for the ConditionalExistenceMap's containsValue method.
- */
+ /** Test method for the ConditionalExistenceMap's containsValue method. */
@Test
void testContainsValueObject() {
- final Map<String, Integer> map = this.getTestMap();
+ final var map = this.getTestMap();
assertTrue(map.containsValue(1));
assertTrue(map.containsValue(10));
assertFalse(map.containsValue(5));
assertFalse(map.containsValue(0));
}
- /**
- * Test method for the ConditionalExistenceMap's entrySet method.
- */
+ /** Test method for the ConditionalExistenceMap's entrySet method. */
@Test
void testEntrySet() {
- final Map<String, Integer> map = this.getTestMap();
+ final var map = this.getTestMap();
for (final Entry<String, Integer> e : map.entrySet()) {
assertTrue(e.getValue() != 0);
}
}
- /**
- * Test method for the ConditionalExistenceMap's get method.
- */
+ /** Test method for the ConditionalExistenceMap's get method. */
@Test
void testGetObject() {
- final Map<String, Integer> map = this.getTestMap();
+ final var map = this.getTestMap();
assertEquals(1, map.get("one"));
assertEquals(10, map.get("ten"));
assertEquals(null, map.get("five"));
assertEquals(null, map.get("zero"));
}
- /**
- * Test method for the ConditionalExistenceCollection's iterator.
- */
+ /** Test method for the ConditionalExistenceCollection's iterator. */
@Test
void testIterator() {
- final ConditionalExistenceIterator<String> testIterator = this
- .getTestIterator();
+ final var testIterator = this.getTestIterator();
assertTrue(testIterator.hasNext);
assertTrue(testIterator.hasNext());
@@ -147,22 +138,18 @@ class ConditionalExistenceCollectionsTest {
assertThrows(NoSuchElementException.class, testIterator::next);
}
- /**
- * Test method for the ConditionalExistenceMap's keySet operation.
- */
+ /** Test method for the ConditionalExistenceMap's keySet operation. */
@Test
void testKeySet() {
- final Map<String, Integer> map = this.getTestMap();
- assertFalse(map.keySet().contains("zero"));
+ final var map = this.getTestMap();
+ assertFalse(map.containsKey("zero"));
}
- /**
- * Test method for the ConditionalExistenceMap's values operation.
- */
+ /** Test method for the ConditionalExistenceMap's values operation. */
@Test
void testValues() {
- final Map<String, Integer> map = this.getTestMap();
- assertFalse(map.values().contains(0));
+ final var map = this.getTestMap();
+ assertFalse(map.containsValue(0));
}
}
diff --git a/src/test/java/sevenUnits/utils/ExpressionParserTest.java b/src/test/java/sevenUnits/utils/ExpressionParserTest.java
index 3a95285..2e0b4b0 100644
--- a/src/test/java/sevenUnits/utils/ExpressionParserTest.java
+++ b/src/test/java/sevenUnits/utils/ExpressionParserTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2021, 2024, 2025 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
@@ -17,6 +17,7 @@
package sevenUnits.utils;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import java.util.stream.IntStream;
@@ -26,11 +27,10 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
-// TODO add tests for expression-to-RPN and RPN-to-result
/**
* A test for the {@code ExpressionParser} class. This is NOT part of this
* program's public API.
- *
+ *
* @author Adrien Hopkins
* @since 2019-03-22
* @since v0.2.0
@@ -39,13 +39,13 @@ class ExpressionParserTest {
private static final ExpressionParser<Integer> numberParser = new ExpressionParser.Builder<>(
Integer::parseInt).addBinaryOperator("+", (o1, o2) -> o1 + o2, 0)
.addBinaryOperator("-", (o1, o2) -> o1 - o2, 0)
- .addBinaryOperator("*", (o1, o2) -> o1 * o2, 1)
- .addBinaryOperator("/", (o1, o2) -> o1 / o2, 1)
- .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build();
+ .addUnaryOperator("neg", o1 -> -o1, 1)
+ .addBinaryOperator("*", (o1, o2) -> o1 * o2, 2)
+ .addBinaryOperator("/", (o1, o2) -> o1 / o2, 2)
+ .addUnaryOperator("recip", o1 -> 1 / o1, 3)
+ .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 4).build();
- /**
- * The expressions used in the expression parsing tests
- */
+ /** The expressions used in the expression parsing tests */
private static final List<String> TEST_EXPRESSIONS = List.of(
// test parsing of expressions
"1 + 2 ^ 5 * 3", "(1 + 2) ^ 5 * 3",
@@ -54,21 +54,81 @@ class ExpressionParserTest {
// ensure it normally goes from left to right
"1 + 2 + 3 + 4", "12 - 4 - 3", "12 - (4 - 3)", "1 / 2 + 3");
- /**
- * The expected results for evaluating these expressions
- */
+ /** The expected results for evaluating these expressions */
private static final int[] RESULTS = { 97, 729, 133, 10, 5, 11, 3 };
+ private static final Stream<Arguments> testConvertExpressionToRPN() {
+ return Stream.of(Arguments.of("1 + 2 ^ 5 * 3", "1 2 5 ^ 3 * +"),
+ Arguments.of("(1 + 2) ^ 5 * 3", "1 2 + 5 ^ 3 *"),
+ Arguments.of("12 * 5 + (3 ^ (2 * 3) - 72) / (3 + 3 * 2)",
+ "12 5 * 3 2 3 * ^ 72 - 3 3 2 * + / +"),
+ Arguments.of("1 + 2 + 3 + 4", "1 2 + 3 + 4 +"),
+ Arguments.of("12 - 4 - 3", "12 4 - 3 -"),
+ Arguments.of("12 - (4 - 3)", "12 4 3 - -"),
+ Arguments.of("1 / 2 + 3", "1 2 / 3 +"), Arguments.of("12", "12"),
+ Arguments.of("2 * 3 + 4", "2 3 * 4 +"),
+ Arguments.of("(2 * 3) + 4", "2 3 * 4 +"),
+ Arguments.of("2 * 3 - 4", "2 3 * 4 -"),
+ Arguments.of("(2 * 3) - 4", "2 3 * 4 -"),
+ Arguments.of("2 * (3 + 4)", "2 3 4 + *"),
+ Arguments.of("2 * (3 - 4)", "2 3 4 - *"),
+ Arguments.of("neg 2", "2 neg"),
+ Arguments.of("1 + neg 2", "1 2 neg +"));
+ }
+
+ private static final Stream<String> testInvalidExpression() {
+ return Stream.of("+", "1 +", "1 + * 2", "1 (+ 1)", "neg");
+ }
+
+ private static final Stream<String> testInvalidRPN() {
+ return Stream.of("+", "1 +", "1 + * 2", "1 * 2", "1 2", "neg");
+ }
+
/**
* @return A stream of objects, where each one is an expression and the
* expected result
* @since 2021-09-27
+ * @since v0.3.2
*/
private static final Stream<Arguments> testParseExpressionData() {
return IntStream.range(0, TEST_EXPRESSIONS.size())
.mapToObj(i -> Arguments.of(TEST_EXPRESSIONS.get(i), RESULTS[i]));
}
+ private static final Stream<Arguments> testParseRPN() {
+ return Stream.of(Arguments.of("1 2 5 ^ 3 * +", 97),
+ Arguments.of("1 2 + 5 ^ 3 *", 729),
+ Arguments.of("12 5 * 3 2 3 * ^ 72 - 3 3 2 * + / +", 133),
+ Arguments.of("1 2 + 3 + 4 +", 10), Arguments.of("12 4 - 3 -", 5),
+ Arguments.of("12 4 3 - -", 11), Arguments.of("1 2 / 3 +", 3),
+ Arguments.of("12", 12), Arguments.of("2 3 * 4 +", 10),
+ Arguments.of("2 3 * 4 -", 2), Arguments.of("2 3 4 + *", 14),
+ Arguments.of("2 3 4 - *", -2), Arguments.of("2 neg", -2),
+ Arguments.of("1 2 neg +", -1));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ public void testConvertExpressionToRPN(String expression,
+ String expectedRPN) {
+ assertEquals(expectedRPN,
+ numberParser.convertExpressionToReversePolish(expression));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ public void testInvalidExpression(String expression) {
+ assertThrows(RuntimeException.class,
+ () -> numberParser.convertExpressionToReversePolish(expression));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ public void testInvalidRPN(String expressionRPN) {
+ assertThrows(RuntimeException.class,
+ () -> numberParser.parseReversePolishExpression(expressionRPN));
+ }
+
/**
* Test method for
* {@link sevenUnits.utils.ExpressionParser#parseExpression(java.lang.String)}.
@@ -78,4 +138,11 @@ class ExpressionParserTest {
public void testParseExpression(String expression, int value) {
assertEquals(value, numberParser.parseExpression(expression));
}
+
+ @ParameterizedTest
+ @MethodSource
+ public void testParseRPN(String expressionRPN, int value) {
+ assertEquals(value,
+ numberParser.parseReversePolishExpression(expressionRPN));
+ }
}
diff --git a/src/test/java/sevenUnits/utils/NameSymbolTest.java b/src/test/java/sevenUnits/utils/NameSymbolTest.java
new file mode 100644
index 0000000..f8843e0
--- /dev/null
+++ b/src/test/java/sevenUnits/utils/NameSymbolTest.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (C) 2025 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for the {@link NameSymbol} class.
+ *
+ * @since v1.0.0
+ */
+class NameSymbolTest {
+ private static Stream<Arguments> testEqualsHashCode() {
+ return Stream.of(
+ Arguments.of(NameSymbol.ofName("test"), NameSymbol.ofName("test"),
+ true),
+ Arguments.of(NameSymbol.ofName("a"), NameSymbol.ofName("b"), false),
+ Arguments.of(NameSymbol.ofSymbol("test"),
+ NameSymbol.ofSymbol("test"), true),
+ Arguments.of(NameSymbol.ofSymbol("a"), NameSymbol.ofSymbol("b"),
+ false),
+ Arguments.of(NameSymbol.ofName("test"), NameSymbol.ofSymbol("test"),
+ false),
+ Arguments.of(NameSymbol.of("main", "s"), NameSymbol.of("main", "s"),
+ true),
+ Arguments.of(NameSymbol.of("main", "s"),
+ NameSymbol.of("main", "s", "m"), false),
+ Arguments.of(new NameSymbol(null, null, new HashSet<>()),
+ new NameSymbol(null, null, new HashSet<>()), true),
+ Arguments.of(
+ new NameSymbol(Optional.of("main"), Optional.of("s"),
+ new HashSet<>()),
+ new NameSymbol(null, null, new HashSet<>()), false),
+ Arguments.of(new NameSymbol(null, null, new HashSet<>()),
+ new NameSymbol(Optional.of("main"), Optional.of("s"),
+ new HashSet<>()),
+ false),
+ Arguments.of(
+ new NameSymbol(Optional.of("main"), null, new HashSet<>()),
+ new NameSymbol(Optional.of("main"), Optional.of("s"),
+ new HashSet<>()),
+ false));
+ }
+
+ @Test
+ public void testCreate() {
+ final Set<String> names = Set.of("a", "b", "c");
+ final var ns = NameSymbol.ofNullable(null, null, names);
+ assertTrue(ns.getPrimaryName().isPresent(),
+ "NameSymbol created without primary name.");
+ assertTrue(names.contains(ns.getPrimaryName().orElseThrow()),
+ String.format("Primary name (%s) was not obtained from names set.",
+ ns.getPrimaryName()));
+ assertFalse(
+ ns.getOtherNames().contains(ns.getPrimaryName().orElseThrow()),
+ String.format("Primary name (%s) was included in other names set.",
+ ns.getPrimaryName()));
+ assertEquals(Set.of("a", "b", "c"), names,
+ "names input was changed by ofNullable()");
+ }
+
+ /**
+ * Tests that two NameSymbols are or are not equal. If they are equal, also
+ * ensures they have the same hash code.
+ *
+ * @param a first NameSymbol to test
+ * @param b second NameSymbol to test
+ * @param equal true iff a should be equal to be, otherwise false
+ */
+ @ParameterizedTest
+ @MethodSource
+ public void testEqualsHashCode(NameSymbol a, NameSymbol b, boolean equal) {
+ if (equal) {
+ assertTrue(Objects.equals(a, b));
+ assertEquals(a.hashCode(), b.hashCode(),
+ "Equal NameSymbol instances have different hash codes.");
+ } else {
+ assertFalse(Objects.equals(a, b));
+ }
+ }
+}
diff --git a/src/test/java/sevenUnits/utils/ObjectProductTest.java b/src/test/java/sevenUnits/utils/ObjectProductTest.java
index 8c6b353..7c5df88 100644
--- a/src/test/java/sevenUnits/utils/ObjectProductTest.java
+++ b/src/test/java/sevenUnits/utils/ObjectProductTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * Copyright (C) 2018, 2021, 2024, 2025 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
@@ -34,7 +34,7 @@ import sevenUnits.unit.Metric;
/**
* Tests for {@link ObjectProduct} using BaseDimension as a test object. This is
* NOT part of this program's public API.
- *
+ *
* @author Adrien Hopkins
* @since 2018-12-12
* @since v0.1.0
@@ -42,7 +42,7 @@ import sevenUnits.unit.Metric;
class ObjectProductTest {
/**
* Tests {@link UnitDimension#equals}
- *
+ *
* @since 2018-12-12
* @since v0.1.0
*/
@@ -54,7 +54,7 @@ class ObjectProductTest {
/**
* Tests {@code UnitDimension}'s exponentiation
- *
+ *
* @since 2019-01-15
* @since v0.1.0
*/
@@ -66,7 +66,7 @@ class ObjectProductTest {
/**
* Tests {@code UnitDimension}'s multiplication and division.
- *
+ *
* @since 2018-12-12
* @since v0.1.0
*/
diff --git a/src/test/java/sevenUnits/utils/SemanticVersionTest.java b/src/test/java/sevenUnits/utils/SemanticVersionTest.java
index 1e59ae3..5b74812 100644
--- a/src/test/java/sevenUnits/utils/SemanticVersionTest.java
+++ b/src/test/java/sevenUnits/utils/SemanticVersionTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -35,12 +35,14 @@ import org.junit.jupiter.api.Test;
* Tests for {@link SemanticVersionNumber}
*
* @since 2022-02-19
+ * @since v0.4.0
*/
public final class SemanticVersionTest {
/**
* Test for {@link SemanticVersionNumber#compatible}
- *
+ *
* @since 2022-02-20
+ * @since v0.4.0
*/
@Test
public void testCompatibility() {
@@ -64,38 +66,37 @@ public final class SemanticVersionTest {
/**
* Tests {@link SemanticVersionNumber#toString} for complex version numbers
- *
+ *
* @since 2022-02-19
+ * @since v0.4.0
*/
@Test
public void testComplexToString() {
- final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
- .build();
+ final var 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)
+ final var 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();
+ final var 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
+ * @since v0.4.0
*/
@Test
public void testComplexVersions() {
- final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
- .build();
+ final var 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)
+ final var v2 = builder(4, 5, 6).preRelease("abc", 123)
.buildMetadata("2022-02-19").build();
assertEquals(4, v2.majorVersion());
assertEquals(5, v2.minorVersion());
@@ -103,8 +104,7 @@ public final class SemanticVersionTest {
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();
+ final var v3 = builder(1, 0, 0).preRelease("x-y-z", "--").build();
assertEquals(1, v3.majorVersion());
assertEquals(0, v3.minorVersion());
assertEquals(0, v3.patchVersion());
@@ -114,8 +114,9 @@ public final class SemanticVersionTest {
/**
* Test that semantic version strings can be parsed correctly
- *
+ *
* @since 2022-02-19
+ * @since v0.4.0
* @see SemanticVersionNumber#fromString
* @see SemanticVersionNumber#isValidVersionString
*/
@@ -153,9 +154,7 @@ public final class SemanticVersionTest {
"Could not parse 1.2.3-abc.56.def+2022abc99");
}
- /**
- * Ensures it is impossible to create invalid version numbers
- */
+ /** Ensures it is impossible to create invalid version numbers */
@Test
public void testInvalidVersionNumbers() {
// stableVersion()
@@ -199,7 +198,7 @@ public final class SemanticVersionTest {
assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7),
"Negative major version number tolerated by builder");
- final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3);
+ final var testBuilder = builder(1, 2, 3);
// note: builder.buildMetadata(null) doesn't even compile lol
// builder.buildMetadata
assertThrows(NullPointerException.class,
@@ -265,8 +264,9 @@ public final class SemanticVersionTest {
/**
* Test for {@link SemanticVersionNumber#isStable}
- *
+ *
* @since 2022-02-19
+ * @since v0.4.0
*/
@Test
public void testIsStable() {
@@ -288,29 +288,27 @@ public final class SemanticVersionTest {
* {@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
+ * @since v0.4.0
*/
@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)
+ final var v100a = builder(1, 0, 0).preRelease("alpha").build(); // 1.0.0-alpha
+ final var v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1
+ final var v100ab = builder(1, 0, 0).preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta
+ final var v100b = builder(1, 0, 0).preRelease("beta").build(); // 1.0.0-alpha
+ final var v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2
+ final var v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11
+ final var v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1
+ final var v100 = stableVersion(1, 0, 0);
+ final var 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);
+ final var v200 = stableVersion(2, 0, 0);
+ final var v201 = stableVersion(2, 0, 1);
+ final var v210 = stableVersion(2, 1, 0);
+ final var v211 = stableVersion(2, 1, 1);
+ final var v300 = stableVersion(3, 0, 0);
// test order of version numbers
assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1");
@@ -348,17 +346,18 @@ public final class SemanticVersionTest {
/**
* Tests that simple stable versions can be created and their parts read
- *
+ *
* @since 2022-02-19
+ * @since v0.4.0
*/
@Test
public void testSimpleStableVersions() {
- final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ final var v100 = stableVersion(1, 0, 0);
assertEquals(1, v100.majorVersion());
assertEquals(0, v100.minorVersion());
assertEquals(0, v100.patchVersion());
- final SemanticVersionNumber v925 = stableVersion(9, 2, 5);
+ final var v925 = stableVersion(9, 2, 5);
assertEquals(9, v925.majorVersion());
assertEquals(2, v925.minorVersion());
assertEquals(5, v925.patchVersion());
@@ -367,26 +366,28 @@ public final class SemanticVersionTest {
/**
* Tests that {@link SemanticVersionNumber#toString} works for simple version
* numbers
- *
+ *
* @since 2022-02-19
+ * @since v0.4.0
*/
@Test
public void testSimpleToString() {
- final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ final var v100 = stableVersion(1, 0, 0);
assertEquals("1.0.0", v100.toString());
- final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1);
+ final var 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
+ * @since v0.4.0
*/
@Test
public void testSimpleUnstableVersions() {
- final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1);
+ final var v350a1 = preRelease(3, 5, 0, "alpha", 1);
assertEquals(3, v350a1.majorVersion(),
"Incorrect major version for v3.5.0a1");
assertEquals(5, v350a1.minorVersion(),
diff --git a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
index 36b373b..733a308 100644
--- a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
+++ b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2021 Adrien Hopkins
+ * Copyright (C) 2021, 2022, 2024, 2025 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
@@ -32,11 +32,10 @@ import org.junit.jupiter.api.Test;
*
* @author Adrien Hopkins
* @since 2021-11-29
+ * @since v0.3.2
*/
class UncertainDoubleTest {
- /**
- * Ensures that the compareTo function behaves correctly.
- */
+ /** Ensures that the compareTo function behaves correctly. */
@Test
final void testCompareTo() {
assertTrue(of(2.0, 0.5).compareTo(of(2.0, 0.1)) == 0);
@@ -44,23 +43,18 @@ class UncertainDoubleTest {
assertTrue(of(2.0, 0.5).compareTo(of(3.0, 0.1)) < 0);
}
- /**
- * Tests the ___exact operations
- */
+ /** Tests the ___exact operations */
@Test
final void testExactOperations() {
- final UncertainDouble x = UncertainDouble.of(Math.PI, 0.1);
+ final var x = UncertainDouble.of(Math.PI, 0.1);
// slightly different because roundoff errors
- final UncertainDouble x1 = UncertainDouble.of(Math.PI + Math.E - Math.E,
- 0.1);
- final UncertainDouble x2 = UncertainDouble.of(Math.PI * Math.E / Math.E,
- 0.1);
+ final var x1 = UncertainDouble.of(Math.PI + Math.E - Math.E, 0.1);
+ final var x2 = UncertainDouble.of(Math.PI * Math.E / Math.E, 0.1);
// get results
- final UncertainDouble result1 = x.plusExact(Math.E).minusExact(Math.E);
- final UncertainDouble result2 = x.timesExact(Math.E)
- .dividedByExact(Math.E);
+ final var result1 = x.plusExact(Math.E).minusExact(Math.E);
+ final var result2 = x.timesExact(Math.E).dividedByExact(Math.E);
// test that these operations work & don't change uncertainty
assertEquals(x1, result1);
@@ -75,36 +69,35 @@ class UncertainDoubleTest {
/**
* Test for {@link UncertainDouble#fromRoundedString}
- *
+ *
* @since 2022-04-18
+ * @since v0.4.0
*/
@Test
final void testFromRoundedString() {
assertEquals(of(12345.678, 0.001), fromRoundedString("12345.678"));
}
- /**
- * Test for {@link UncertainDouble#fromString}
- */
+ /** Test for {@link UncertainDouble#fromString} */
@Test
final void testFromString() {
// valid strings
- assertEquals(of(2.0, 0.5), fromString("2.0 � 0.5"));
+ assertEquals(of(2.0, 0.5), fromString("2.0 ± 0.5"));
assertEquals(of(2.0, 0.5), fromString("2.0 +- 0.5"));
assertEquals(of(2.0, 0.0), fromString("2.0"));
// invalid strings
- for (final String s : List.of("2.A", "A", "2.0 � .", "� 3.5")) {
+ for (final String s : List.of("2.A", "A", "2.0 ± ", " ± 3.5")) {
assertThrows(IllegalArgumentException.class, () -> fromString(s));
}
// back and forth
- assertEquals("2.0 � 0.5", of(2.0, 0.5).toString());
+ assertEquals("2.0 ± 0.5", of(2.0, 0.5).toString());
assertEquals("2.0", of(2.0, 0).toString());
}
@Test
final void testHashCode() {
- assertEquals(of(2.0, 0.5).hashCode(), fromString("2.0 � 0.5").hashCode());
+ assertEquals(of(2.0, 0.5).hashCode(), fromString("2.0 ± 0.5").hashCode());
}
}
diff --git a/src/test/java/sevenUnitsGUI/I18nTest.java b/src/test/java/sevenUnitsGUI/I18nTest.java
new file mode 100644
index 0000000..2f90d76
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/I18nTest.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (C) 2025 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for the internationalization system.
+ *
+ * @since v1.0.0
+ */
+class I18nTest {
+ private static final Stream<String> testLocaleSupported() {
+ return Stream.of("en", "fr");
+ }
+
+ private static final Stream<Arguments> testLocalization() {
+ return Stream.of(Arguments.of("tv.title", "en", "7Units [v]"),
+ Arguments.of("tv.title", "fr", "7Unités [v]"),
+ Arguments.of("tv.convert_units.title", "en", "Convert Units"),
+ Arguments.of("tv.convert_units.title", "fr", "Convertir Unités"));
+ }
+
+ /**
+ * Tests that the default locale is supported.
+ *
+ * @since v1.0.0
+ * @see Presenter#DEFAULT_LOCALE
+ */
+ @Test
+ void testDefaultLocaleSupported() {
+ final var p = new Presenter(new ViewBot());
+ assertNotNull(p.locales.get(Presenter.DEFAULT_LOCALE),
+ "Default locale is not supported.");
+ }
+
+ /**
+ * Ensures that the system supports the provided locale.
+ *
+ * @param localeName locale to test for support
+ *
+ * @since 2025-06-04
+ * @since v1.0.0
+ */
+ @ParameterizedTest
+ @MethodSource
+ void testLocaleSupported(String localeName) {
+ final var p = new Presenter(new ViewBot());
+ assertNotNull(p.locales.get(localeName),
+ "Locale \"" + localeName + "\" is not supported.");
+ }
+
+ /**
+ * Tests that the system can correctly localize text, using the default
+ * locales.
+ *
+ * @param key key of text to localize
+ * @param locale locale to use
+ * @param expected expected value of output text
+ *
+ * @since 2025-06-04
+ * @since v1.0.0
+ */
+ @ParameterizedTest
+ @MethodSource
+ void testLocalization(String key, String locale, String expected) {
+ final var p = new Presenter(new ViewBot());
+ p.setUserLocale(locale);
+ final var actual = p.getLocalizedText(key);
+ assertEquals(expected, actual);
+ }
+
+}
diff --git a/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java b/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java
index 476e407..ead5f4a 100644
--- a/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java
+++ b/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -31,15 +31,15 @@ import sevenUnits.unit.Metric;
/**
* Tests for the default prefix repetition rules.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
class PrefixRepetitionTest {
/**
* Ensures that the complex repetition rule disallows invalid prefix lists.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testInvalidComplexRepetition() {
@@ -57,9 +57,9 @@ class PrefixRepetitionTest {
/**
* Tests the {@code NO_REPETITION} rule.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testNoRepetition() {
@@ -71,9 +71,9 @@ class PrefixRepetitionTest {
/**
* Tests the {@code NO_RESTRICTION} rule.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testNoRestriction() {
@@ -85,9 +85,9 @@ class PrefixRepetitionTest {
/**
* Ensures that the complex repetition rule allows valid prefix lists.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testValidComplexRepetition() {
diff --git a/src/test/java/sevenUnitsGUI/PrefixSearchTest.java b/src/test/java/sevenUnitsGUI/PrefixSearchTest.java
index 305d0d7..00dd960 100644
--- a/src/test/java/sevenUnitsGUI/PrefixSearchTest.java
+++ b/src/test/java/sevenUnitsGUI/PrefixSearchTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -18,6 +18,7 @@ package sevenUnitsGUI;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static sevenUnitsGUI.PrefixSearchRule.COMMON_PREFIXES;
import static sevenUnitsGUI.PrefixSearchRule.NO_PREFIXES;
import static sevenUnitsGUI.PrefixSearchRule.getCoherentOnlyRule;
@@ -35,13 +36,11 @@ import sevenUnits.unit.Metric;
/**
* Tests for {@link PrefixSearchRule}
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
class PrefixSearchTest {
- /**
- * A method that creates duplicate copies of the common prefix rule.
- */
+ /** A method that creates duplicate copies of the common prefix rule. */
private static final PrefixSearchRule getCommonRuleCopy() {
return getCoherentOnlyRule(Set.of(Metric.KILO, Metric.MILLI));
}
@@ -51,8 +50,8 @@ class PrefixSearchTest {
* {@link sevenUnitsGUI.PrefixSearchRule#apply(java.util.Map.Entry)}, for a
* coherent unit and {@link PrefixSearchRule#COMMON_PREFIXES}.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
final void testCoherentPrefixSearch() {
@@ -69,8 +68,8 @@ class PrefixSearchTest {
* Test method for
* {@link sevenUnitsGUI.PrefixSearchRule#equals(java.lang.Object)}.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
final void testEquals() {
@@ -84,8 +83,8 @@ class PrefixSearchTest {
/**
* Test method for {@link sevenUnitsGUI.PrefixSearchRule#getPrefixes()}.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
final void testGetPrefixes() {
@@ -97,8 +96,8 @@ class PrefixSearchTest {
/**
* Test method for {@link sevenUnitsGUI.PrefixSearchRule#hashCode()}.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
final void testHashCode() {
@@ -109,9 +108,9 @@ class PrefixSearchTest {
/**
* Tests prefix searching for a non-coherent unit and
* {@link PrefixSearchRule#COMMON_PREFIXES}.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
final void testNonCoherentPrefixSearch() {
@@ -124,9 +123,9 @@ class PrefixSearchTest {
/**
* Tests that {@link PrefixSearchRule#NO_PREFIXES} returns the original unit.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testNoPrefixes() {
@@ -145,14 +144,17 @@ class PrefixSearchTest {
/**
* Test method for {@link sevenUnitsGUI.PrefixSearchRule#toString()}.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
final void testToString() {
- assertEquals(
- "Apply the following prefixes: [kilo (\u00D7 1000.0), milli (\u00D7 0.001)]",
- COMMON_PREFIXES.toString());
+ final var toString = COMMON_PREFIXES.toString();
+ final var valid1 = "Apply the following prefixes: [kilo (\u00D7 1000.0), milli (\u00D7 0.001)]";
+ final var valid2 = "Apply the following prefixes: [milli (\u00D7 0.001), kilo (\u00D7 1000.0)]";
+
+ assertTrue(valid1.equals(toString) || valid2.equals(toString),
+ "COMMON_PREFIXES.toString invalid (was \"" + toString + "\").");
}
}
diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java
index 9e25a08..9ac5b84 100644
--- a/src/test/java/sevenUnitsGUI/PresenterTest.java
+++ b/src/test/java/sevenUnitsGUI/PresenterTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022-2025 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
@@ -17,13 +17,12 @@
package sevenUnitsGUI;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
-import java.math.RoundingMode;
import java.nio.file.Path;
import java.util.HashSet;
-import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -32,12 +31,12 @@ import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import sevenUnits.unit.BaseDimension;
import sevenUnits.unit.BritishImperial;
import sevenUnits.unit.LinearUnit;
-import sevenUnits.unit.LinearUnitValue;
import sevenUnits.unit.Metric;
import sevenUnits.unit.Unit;
import sevenUnits.unit.UnitDatabase;
@@ -53,11 +52,11 @@ import sevenUnits.utils.UncertainDouble;
* <em>Note: this test outputs a lot to the standard output, because creating a
* {@link UnitDatabase} and converting with a {@link ViewBot} both trigger
* println statements.</em>
- *
+ *
* @author Adrien Hopkins
*
- * @since v0.4.0
* @since 2022-02-10
+ * @since v0.4.0
*/
public final class PresenterTest {
private static final Path TEST_SETTINGS = Path.of("src", "test", "resources",
@@ -74,10 +73,10 @@ public final class PresenterTest {
/**
* @return rounding rules used by {@link #testRoundingRules}
- * @since v0.4.0
* @since 2022-04-16
+ * @since v0.4.0
*/
- private static final Stream<Function<UncertainDouble, String>> getRoundingRules() {
+ private static Stream<Function<UncertainDouble, String>> getRoundingRules() {
final var SCIENTIFIC_ROUNDING = StandardDisplayRules.uncertaintyBased();
final var INTEGER_ROUNDING = StandardDisplayRules.fixedDecimals(0);
final var SIG_FIG_ROUNDING = StandardDisplayRules.fixedPrecision(4);
@@ -85,81 +84,99 @@ public final class PresenterTest {
return Stream.of(SCIENTIFIC_ROUNDING, INTEGER_ROUNDING, SIG_FIG_ROUNDING);
}
- private static final Stream<Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>>> getSearchRules() {
+ private static Stream<Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>>> getSearchRules() {
return SEARCH_RULES;
}
- private static final Set<String> names(Set<? extends Nameable> units) {
+ private static Set<String> names(Set<? extends Nameable> units) {
return units.stream().map(Nameable::getName).collect(Collectors.toSet());
}
+ private static Stream<Arguments> testConvertExpressions() {
+ return Stream.of(
+ Arguments.of("10000.0 m", "km", "10000.0 m = 10.00000 km"),
+ Arguments.of("1.0 m", "ft; in", "1.0 m = 3 ft + 3 in"),
+ Arguments.of("1.0 m", "ftin", "1.0 m = 3 ft + 3 in"));
+ }
+
+ private static Stream<Arguments> testConvertUnits() {
+ return Stream.of(
+ Arguments.of("m", "km", 10000.0, "10000.0 m = 10.00000 km"),
+ Arguments.of("m", "ftin", 1.0, "1.0 m = 3 ft + 3 in"));
+ }
+
/**
* Test method for {@link Presenter#convertExpressions}
- *
- * @since v0.4.0
+ *
* @since 2022-02-12
+ * @since v0.4.0
*/
- @Test
- void testConvertExpressions() {
+ @ParameterizedTest
+ @MethodSource
+ void testConvertExpressions(String from, String to, String expectedOutput) {
// setup
- final ViewBot viewBot = new ViewBot();
- final Presenter presenter = new Presenter(viewBot);
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
- viewBot.setFromExpression("10000.0 m");
- viewBot.setToExpression("km");
+ viewBot.setFromExpression(from);
+ viewBot.setToExpression(to);
// convert expression
presenter.convertExpressions();
// test result
- final List<UnitConversionRecord> outputs = viewBot
- .expressionConversionList();
- assertEquals("10000.0 m = 10.00000 km",
- outputs.get(outputs.size() - 1).toString());
+ final var outputs = viewBot.expressionConversionList();
+ assertEquals(expectedOutput, outputs.get(outputs.size() - 1).toString());
}
/**
- * Tests that unit-conversion Views can correctly convert units
- *
- * @since v0.4.0
+ * Test method for {@link Presenter#convertUnits}
+ *
* @since 2022-02-12
+ * @since v0.4.0
*/
- @Test
- void testConvertUnits() {
+ @ParameterizedTest
+ @MethodSource
+ void testConvertUnits(String from, String to, double value,
+ String expectedOutput) {
// setup
- final ViewBot viewBot = new ViewBot();
- final Presenter presenter = new Presenter(viewBot);
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
- viewBot.setFromUnitNames(names(testUnits));
- viewBot.setToUnitNames(names(testUnits));
- viewBot.setFromSelection("metre");
- viewBot.setToSelection("kilometre");
- viewBot.setInputValue("10000.0");
+ viewBot.setFromSelection(from);
+ viewBot.setToSelection(to);
+ viewBot.setInputValue(Double.toString(value));
- // convert units
+ // convert expression
presenter.convertUnits();
- /*
- * use result from system as expected - I'm not testing unit conversion
- * here (that's for the backend tests), I'm just testing that it correctly
- * calls the unit conversion system
- */
- final LinearUnitValue expectedInput = LinearUnitValue.of(Metric.METRE,
- UncertainDouble.fromRoundedString("10000.0"));
- final LinearUnitValue expectedOutput = expectedInput
- .convertTo(Metric.KILOMETRE);
- 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());
+ // test result
+ final var outputs = viewBot.unitConversionList();
+ assertEquals(expectedOutput, outputs.get(outputs.size() - 1).toString());
+ }
+
+ /**
+ * Ensures that the default unitfile can be disabled.
+ *
+ * @since 2025-02-23
+ * @since v1.0.0
+ */
+ @Test
+ void testDisableDefault() {
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ assumeTrue(presenter.database.containsUnitName("joule"),
+ "Attempted to test disabling default on unit not in default file.");
+ presenter.setUseDefaultDatafiles(false);
+ assertFalse(presenter.database.containsUnitName("joule"),
+ "Presenter disabled default datafiles, but still contains the joule.");
}
/**
* Tests that duplicate units are successfully removed, if that is asked for
- *
- * @since v0.4.0
+ *
* @since 2022-04-16
+ * @since v0.4.0
*/
@Test
void testDuplicateUnits() {
@@ -190,9 +207,9 @@ public final class PresenterTest {
/**
* Tests that one-way conversion correctly filters From and To units
- *
- * @since v0.4.0
+ *
* @since 2022-04-16
+ * @since v0.4.0
*/
@Test
void testOneWayConversion() {
@@ -224,9 +241,9 @@ public final class PresenterTest {
/**
* Tests the prefix-viewing functionality.
- *
- * @since v0.4.0
+ *
* @since 2022-04-16
+ * @since v0.4.0
*/
@Test
void testPrefixViewing() {
@@ -254,9 +271,9 @@ public final class PresenterTest {
/**
* Tests that rounding rules are used correctly.
- *
- * @since v0.4.0
+ *
* @since 2022-04-16
+ * @since v0.4.0
*/
@ParameterizedTest
@MethodSource("getRoundingRules")
@@ -273,9 +290,9 @@ public final class PresenterTest {
presenter.convertUnits();
// test the result of the rounding
- final String expectedOutputString = roundingRule
+ final var expectedOutputString = roundingRule
.apply(UncertainDouble.fromRoundedString("12.3456789"));
- final String actualOutputString = viewBot.unitConversionList().get(0)
+ final var actualOutputString = viewBot.unitConversionList().get(0)
.outputValueString();
assertEquals(expectedOutputString, actualOutputString);
}
@@ -284,8 +301,8 @@ public final class PresenterTest {
* Tests that the Presenter correctly applies search rules.
*
* @param searchRule search rule to test
- * @since v0.4.0
* @since 2022-07-08
+ * @since v0.4.0
*/
@ParameterizedTest
@MethodSource("getSearchRules")
@@ -309,7 +326,7 @@ public final class PresenterTest {
.apply(Map.entry("inch", BritishImperial.Length.INCH)).keySet());
expectedOutput.addAll(
searchRule.apply(Map.entry("metre", Metric.METRE)).keySet());
- final Set<String> actualOutput = viewBot.getFromUnitNames();
+ final var actualOutput = viewBot.getFromUnitNames();
// test output
assertEquals(expectedOutput, actualOutput);
@@ -317,9 +334,9 @@ public final class PresenterTest {
/**
* Tests that settings can be saved to and loaded from a file.
- *
- * @since v0.4.0
+ *
* @since 2022-04-16
+ * @since v0.4.0
*/
@Test
void testSettingsSaving() {
@@ -351,9 +368,9 @@ public final class PresenterTest {
/**
* Ensures the Presenter generates the correct data upon a unit-viewing.
- *
- * @since v0.4.0
+ *
* @since 2022-04-16
+ * @since v0.4.0
*/
@Test
void testUnitViewing() {
@@ -385,15 +402,15 @@ public final class PresenterTest {
/**
* Test for {@link Presenter#updateView()}
- *
- * @since v0.4.0
+ *
* @since 2022-02-12
+ * @since v0.4.0
*/
@Test
void testUpdateView() {
// setup
- final ViewBot viewBot = new ViewBot();
- final Presenter presenter = new Presenter(viewBot);
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
presenter.setOneWayConversionEnabled(false);
presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES);
@@ -415,8 +432,8 @@ public final class PresenterTest {
// filter to length units only, then get the filtered sets of units
presenter.updateView();
- final Set<String> fromUnits = viewBot.getFromUnitNames();
- final Set<String> toUnits = viewBot.getToUnitNames();
+ final var fromUnits = viewBot.getFromUnitNames();
+ final var toUnits = viewBot.getToUnitNames();
// test that fromUnits/toUnits is [METRE, KILOMETRE]
assertEquals(Set.of("metre", "kilometre"), fromUnits);
diff --git a/src/test/java/sevenUnitsGUI/RoundingTest.java b/src/test/java/sevenUnitsGUI/RoundingTest.java
index f749f85..589b8d0 100644
--- a/src/test/java/sevenUnitsGUI/RoundingTest.java
+++ b/src/test/java/sevenUnitsGUI/RoundingTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -41,8 +41,8 @@ import sevenUnitsGUI.StandardDisplayRules.UncertaintyBased;
/**
* Tests that ensure the rounding rules work as intended.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
class RoundingTest {
// rounding rules to test
@@ -69,8 +69,8 @@ class RoundingTest {
/**
* @return arguments for
* {@link #testFixedDecimalRounding(UncertainDouble, String, String, String)}
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
private static final Stream<Arguments> fixedDecimalRoundingExamples() {
// input, zero decimal string, two decimal string, six decimal string
@@ -83,8 +83,8 @@ class RoundingTest {
/**
* @return arguments for
* {@link #testFixedPrecisionRounding(UncertainDouble, String, String, String)}
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
private static final Stream<Arguments> fixedPrecisionRoundingExamples() {
// input, one sig fig string, three s.f. string, six s.f. string
@@ -97,8 +97,8 @@ class RoundingTest {
/**
* @return arguments for
* {@link #testUncertaintyRounding(UncertainDouble, String)}
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
private static final Stream<Arguments> uncertaintyRoundingExamples() {
// input, uncertainty rounding string
@@ -112,8 +112,8 @@ class RoundingTest {
* Test for {@link FixedDecimals#decimalPlaces()} and
* {@link FixedPrecision#significantFigures()}.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testDataMethods() {
@@ -137,9 +137,9 @@ class RoundingTest {
/**
* Tests that the rounding methods' equals() methods work.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testEquals() {
@@ -161,7 +161,7 @@ class RoundingTest {
// test that FixedDecimals is never equal to FixedPrecision
// this unlikely argument is the test - the equals should return false!
@SuppressWarnings("unlikely-arg-type")
- final boolean differentRulesEqual = Objects.equals(fixedDecimals(4),
+ final var differentRulesEqual = Objects.equals(fixedDecimals(4),
fixedPrecision(4));
assertFalse(differentRulesEqual, "fixedDecimals(4) == fixedPrecision(4)");
}
@@ -174,6 +174,7 @@ class RoundingTest {
* @param twoDecimalString expected string for two decimal places
* @param sixDecimalString expected string for six decimal places
* @since 2022-07-17
+ * @since v0.4.0
*/
@ParameterizedTest
@MethodSource("fixedDecimalRoundingExamples")
@@ -200,8 +201,8 @@ class RoundingTest {
* @param oneSigFigString expected string for one significant figure
* @param threeSigFigString expected string for three significant figures
* @param twelveSigFigString expected string for twelve significant figures
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@ParameterizedTest
@MethodSource("fixedPrecisionRoundingExamples")
@@ -225,9 +226,9 @@ class RoundingTest {
/**
* Tests that {@link StandardDisplayRules#getStandardRule} gets rounding
* rules as intended.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testGetStandardRule() {
@@ -243,9 +244,9 @@ class RoundingTest {
/**
* Tests that the rounding methods' equals() methods work.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testHashCode() {
@@ -257,9 +258,9 @@ class RoundingTest {
/**
* Tests that the {@code toString()} methods of the three rounding rule
* classes work correctly.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testToString() {
@@ -273,8 +274,8 @@ class RoundingTest {
*
* @param input input number
* @param output expected output string
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
@ParameterizedTest
@MethodSource("uncertaintyRoundingExamples")
diff --git a/src/test/java/sevenUnitsGUI/TabbedViewTest.java b/src/test/java/sevenUnitsGUI/TabbedViewTest.java
index 165718f..b32579c 100644
--- a/src/test/java/sevenUnitsGUI/TabbedViewTest.java
+++ b/src/test/java/sevenUnitsGUI/TabbedViewTest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2022 Adrien Hopkins
+ * Copyright (C) 2022, 2024, 2025 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
@@ -18,20 +18,24 @@ package sevenUnitsGUI;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.util.concurrent.TimeUnit;
+
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
/**
- * Test for the TabbedView
+ * Test for the TabbedView.
*
- * @since v0.4.0
* @since 2022-07-17
+ * @since v0.4.0
*/
+@Timeout(value = 10, unit = TimeUnit.SECONDS)
class TabbedViewTest {
/**
* @return a view with all settings set to standard values
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
private static final TabbedView setupView() {
final var view = new TabbedView();
@@ -50,9 +54,9 @@ class TabbedViewTest {
/**
* Simulates an expression conversion operation, and ensures it works
* properly.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testExpressionConversion() {
@@ -71,9 +75,9 @@ class TabbedViewTest {
/**
* Simulates a unit conversion operation, and ensures it works properly.
- *
- * @since v0.4.0
+ *
* @since 2022-07-17
+ * @since v0.4.0
*/
@Test
void testUnitConversion() {