diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2020-08-27 08:07:21 -0500 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2020-08-27 08:07:21 -0500 |
commit | e2f141427e441daa9d6be0ba8a30b844ca4391e0 (patch) | |
tree | c6cf591c9db5bc8e753c271396f226f2eb42e222 | |
parent | 0245594222bfa0bd9a47d8326ed323c7356ac27c (diff) |
Added the ability to restrict conversion to customary->metric.
-rw-r--r-- | src/org/unitConverter/converterGUI/SearchBoxList.java | 122 | ||||
-rw-r--r-- | src/org/unitConverter/converterGUI/UnitConverterGUI.java | 89 | ||||
-rw-r--r-- | src/org/unitConverter/unit/SI.java | 59 | ||||
-rw-r--r-- | src/org/unitConverter/unit/Unit.java | 250 | ||||
-rw-r--r-- | src/org/unitConverter/unit/UnitTest.java | 12 |
5 files changed, 333 insertions, 199 deletions
diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java index 1995466..10ef589 100644 --- a/src/org/unitConverter/converterGUI/SearchBoxList.java +++ b/src/org/unitConverter/converterGUI/SearchBoxList.java @@ -36,13 +36,13 @@ import javax.swing.JTextField; * @since v0.2.0 */ final class SearchBoxList extends JPanel { - + /** * @since 2019-04-13 * @since v0.2.0 */ private static final long serialVersionUID = 6226930279415983433L; - + /** * The text to place in an empty search box. * @@ -50,7 +50,7 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ private static final String EMPTY_TEXT = "Search..."; - + /** * The color to use for an empty foreground. * @@ -58,94 +58,92 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); - + // the components private final Collection<String> itemsToFilter; private final DelegateListModel<String> listModel; private final JTextField searchBox; private final JList<String> searchItems; - + private boolean searchBoxEmpty = true; - - // I need to do this because, for some reason, Swing is auto-focusing my search box without triggering a focus + + // I need to do this because, for some reason, Swing is auto-focusing my + // search box without triggering a focus // event. private boolean searchBoxFocused = false; - + private Predicate<String> customSearchFilter = o -> true; private final Comparator<String> defaultOrdering; private final boolean caseSensitive; - + /** * Creates the {@code SearchBoxList}. * - * @param itemsToFilter - * items to put in the list + * @param itemsToFilter items to put in the list * @since 2019-04-14 */ public SearchBoxList(final Collection<String> itemsToFilter) { this(itemsToFilter, null, false); } - + /** * 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 + * @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 */ - public SearchBoxList(final Collection<String> itemsToFilter, final Comparator<String> defaultOrdering, + public SearchBoxList(final Collection<String> itemsToFilter, + final Comparator<String> defaultOrdering, final boolean caseSensitive) { super(new BorderLayout(), true); this.itemsToFilter = itemsToFilter; this.defaultOrdering = defaultOrdering; this.caseSensitive = caseSensitive; - + // create the components this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); this.searchItems = new JList<>(this.listModel); - + this.searchBox = new JTextField(EMPTY_TEXT); this.searchBox.setForeground(EMPTY_FOREGROUND); - + // add them to the panel this.add(this.searchBox, BorderLayout.PAGE_START); this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); - + // set up the search box this.searchBox.addFocusListener(new FocusListener() { @Override public void focusGained(final FocusEvent e) { SearchBoxList.this.searchBoxFocusGained(e); } - + @Override public void focusLost(final FocusEvent e) { SearchBoxList.this.searchBoxFocusLost(e); } }); - + this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); this.searchBoxEmpty = true; } - + /** * Adds an additional filter for searching. * - * @param filter - * filter to add. + * @param filter filter to add. * @since 2019-04-13 * @since v0.2.0 */ public void addSearchFilter(final Predicate<String> filter) { this.customSearchFilter = this.customSearchFilter.and(filter); } - + /** * Resets the search filter. * @@ -155,7 +153,7 @@ final class SearchBoxList extends JPanel { public void clearSearchFilters() { this.customSearchFilter = o -> true; } - + /** * @return this component's search box component * @since 2019-04-14 @@ -164,11 +162,11 @@ final class SearchBoxList extends JPanel { public final JTextField getSearchBox() { return this.searchBox; } - + /** - * @param searchText - * text to search for - * @return a filter that filters out that text, based on this list's case sensitive setting + * @param searchText text to search for + * @return a filter that filters out that text, based on this list's case + * sensitive setting * @since 2019-04-14 * @since v0.2.0 */ @@ -176,9 +174,10 @@ final class SearchBoxList extends JPanel { if (this.caseSensitive) return string -> string.contains(searchText); else - return string -> string.toLowerCase().contains(searchText.toLowerCase()); + return string -> string.toLowerCase() + .contains(searchText.toLowerCase()); } - + /** * @return this component's list component * @since 2019-04-14 @@ -187,7 +186,7 @@ final class SearchBoxList extends JPanel { public final JList<String> getSearchList() { return this.searchItems; } - + /** * @return index selected in item list * @since 2019-04-14 @@ -196,7 +195,7 @@ final class SearchBoxList extends JPanel { public int getSelectedIndex() { return this.searchItems.getSelectedIndex(); } - + /** * @return value selected in item list * @since 2019-04-13 @@ -205,7 +204,7 @@ final class SearchBoxList extends JPanel { public String getSelectedValue() { return this.searchItems.getSelectedValue(); } - + /** * Re-applies the filters. * @@ -213,29 +212,30 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ public void reapplyFilter() { - final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); final Predicate<String> searchFilter = this.getSearchFilter(searchText); - + this.listModel.clear(); this.itemsToFilter.forEach(string -> { if (searchFilter.test(string)) { this.listModel.add(string); } }); - + // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); - + // sorts the remaining items this.listModel.sort(comparator); } - + /** * Runs whenever the search box gains focus. * - * @param e - * focus event + * @param e focus event * @since 2019-04-13 * @since v0.2.0 */ @@ -246,12 +246,11 @@ final class SearchBoxList extends JPanel { this.searchBox.setForeground(Color.BLACK); } } - + /** * Runs whenever the search box loses focus. * - * @param e - * focus event + * @param e focus event * @since 2019-04-13 * @since v0.2.0 */ @@ -262,7 +261,7 @@ final class SearchBoxList extends JPanel { this.searchBox.setForeground(EMPTY_FOREGROUND); } } - + /** * Runs whenever the text in the search box is changed. * <p> @@ -276,10 +275,12 @@ final class SearchBoxList extends JPanel { if (this.searchBoxFocused) { this.searchBoxEmpty = this.searchBox.getText().equals(""); } - final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); final Predicate<String> searchFilter = this.getSearchFilter(searchText); - + // initialize list with items that match the filter then sort this.listModel.clear(); this.itemsToFilter.forEach(string -> { @@ -287,11 +288,20 @@ final class SearchBoxList extends JPanel { this.listModel.add(string); } }); - + // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); - + // sorts the remaining items this.listModel.sort(comparator); } + + /** + * Manually updates the search box's item list. + * + * @since 2020-08-27 + */ + public void updateList() { + this.searchBoxTextChanged(); + } } diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 5fe4ee5..75ab16d 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -56,6 +56,7 @@ import javax.swing.JTextField; import javax.swing.WindowConstants; import javax.swing.border.TitledBorder; +import org.unitConverter.math.ConditionalExistenceCollections; import org.unitConverter.math.ObjectProduct; import org.unitConverter.unit.BaseDimension; import org.unitConverter.unit.BritishImperial; @@ -129,6 +130,13 @@ final class UnitConverterGUI { private final Comparator<String> prefixNameComparator; + // conditions for existence of From and To entries + // used for one-way conversion + private final MutablePredicate<String> fromExistenceCondition = new MutablePredicate<>( + s -> true); + private final MutablePredicate<String> toExistenceCondition = new MutablePredicate<>( + s -> true); + /* * Rounding-related settings. I am using my own system, and not * MathContext, because MathContext does not support decimal place based @@ -338,6 +346,16 @@ final class UnitConverterGUI { } /** + * @return a list of all the entries in the dimension-based converter's + * From box + * @since 2020-08-27 + */ + public final Set<String> fromEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.fromExistenceCondition); + } + + /** * @return a comparator to compare prefix names * @since 2019-04-14 * @since v0.2.0 @@ -437,6 +455,25 @@ final class UnitConverterGUI { } /** + * Enables or disables one-way conversion. + * + * @param oneWay whether one-way conversion should be on (true) or off + * (false) + * @since 2020-08-27 + */ + public final void setOneWay(boolean oneWay) { + if (oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> !this.database.getUnit(unitName).isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + } + + /** * @param precision new value of precision * @since 2019-01-15 * @since v0.1.0 @@ -463,6 +500,16 @@ final class UnitConverterGUI { } /** + * @return a list of all the entries in the dimension-based converter's To + * box + * @since 2020-08-27 + */ + public final Set<String> toEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.toExistenceCondition); + } + + /** * Returns true if and only if the unit represented by {@code unitName} * has the dimension represented by {@code dimensionName}. * @@ -505,7 +552,7 @@ final class UnitConverterGUI { * @since 2019-04-14 * @since v0.2.0 */ - public final Set<String> unitNameSet() { + private final Set<String> unitNameSet() { return this.database.unitMapPrefixless().keySet(); } } @@ -579,8 +626,8 @@ final class UnitConverterGUI { this.presenter.getPrefixNameComparator(), true); this.unitTextBox = new JTextArea(); this.prefixTextBox = new JTextArea(); - this.fromSearch = new SearchBoxList(this.presenter.unitNameSet()); - this.toSearch = new SearchBoxList(this.presenter.unitNameSet()); + this.fromSearch = new SearchBoxList(this.presenter.fromEntries()); + this.toSearch = new SearchBoxList(this.presenter.toEntries()); this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); this.dimensionBasedOutput = new JTextArea(2, 32); this.fromEntry = new JTextField(); @@ -630,15 +677,6 @@ final class UnitConverterGUI { } /** - * @return text inputted into dimension-based converter - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getDimensionConverterText() { - return this.valueInput.getText(); - } - - /** * @return selection in "From" selector in dimension-based converter * @since 2019-04-13 * @since v0.2.0 @@ -717,6 +755,9 @@ final class UnitConverterGUI { { // pane with all of the tabs masterPanel.add(this.masterPane, BorderLayout.CENTER); + // update stuff + this.masterPane.addChangeListener(e -> this.update()); + { // a panel for unit conversion using a selector final JPanel convertUnitPanel = new JPanel(); this.masterPane.addTab("Convert Units", convertUnitPanel); @@ -1080,17 +1121,25 @@ final class UnitConverterGUI { .setBorder(new TitledBorder("Miscellaneous Settings")); miscPanel.setLayout(new GridBagLayout()); + final JCheckBox oneWay = new JCheckBox( + "Convert One Way Only"); + oneWay.setSelected(false); + oneWay.addItemListener( + e -> this.presenter.setOneWay(e.getStateChange() == 1)); + miscPanel.add(oneWay, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + final JCheckBox showAllVariations = new JCheckBox( "Show Symbols in \"Convert Units\""); showAllVariations.setSelected(true); showAllVariations.setEnabled(false); - miscPanel.add(showAllVariations, new GridBagBuilder(0, 0) + miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); final JButton unitFileButton = new JButton( "Manage Unit Data Files"); unitFileButton.setEnabled(false); - miscPanel.add(unitFileButton, new GridBagBuilder(0, 1) + miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); } } @@ -1153,6 +1202,18 @@ final class UnitConverterGUI { JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); } + + public void update() { + switch (this.getActivePane()) { + case UNIT_CONVERTER: + this.fromSearch.updateList(); + this.toSearch.updateList(); + break; + default: + // do nothing, for now + break; + } + } } public static void main(final String[] args) { diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java index a4fbd5f..f36cf28 100644 --- a/src/org/unitConverter/unit/SI.java +++ b/src/org/unitConverter/unit/SI.java @@ -17,6 +17,7 @@ package org.unitConverter.unit;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -91,6 +92,9 @@ public final class SI { public static final BaseUnit DOLLAR = BaseUnit
.valueOf(BaseDimensions.CURRENCY, "dollar", "$");
+ public static final Set<BaseUnit> BASE_UNITS = setOf(METRE, KILOGRAM,
+ SECOND, AMPERE, KELVIN, MOLE, CANDELA, BIT);
+
// You may NOT get SI.BaseUnits instances!
private BaseUnits() {
throw new AssertionError();
@@ -210,6 +214,7 @@ public final class SI { /// The units of the SI
public static final LinearUnit ONE = LinearUnit
.valueOf(ObjectProduct.empty(), 1);
+
public static final LinearUnit METRE = BaseUnits.METRE.asLinearUnit()
.withName(NameSymbol.of("metre", "m", "meter"));
public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit()
@@ -228,10 +233,10 @@ public final class SI { .withName(NameSymbol.of("bit", "b"));
public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit()
.withName(NameSymbol.of("dollar", "$"));
-
// Non-base units
public static final LinearUnit RADIAN = METRE.dividedBy(METRE)
.withName(NameSymbol.of("radian", "rad"));
+
public static final LinearUnit STERADIAN = RADIAN.times(RADIAN)
.withName(NameSymbol.of("steradian", "sr"));
public static final LinearUnit HERTZ = ONE.dividedBy(SECOND)
@@ -277,10 +282,10 @@ public final class SI { // for dose equivalent
public static final LinearUnit KATAL = MOLE.dividedBy(SECOND)
.withName(NameSymbol.of("katal", "kat"));
-
// common derived units included for convenience
public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000)
.withName(NameSymbol.of("gram", "g"));
+
public static final LinearUnit SQUARE_METRE = METRE.toExponent(2)
.withName(NameSymbol.of("square metre", "m^2", "square meter",
"metre squared", "meter squared"));
@@ -290,12 +295,12 @@ public final class SI { public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND)
.withName(
NameSymbol.of("metre per second", "m/s", "meter per second"));
-
// Non-SI units included for convenience
public static final Unit CELSIUS = Unit
.fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15,
tempC -> tempC + 273.15)
.withName(NameSymbol.of("degree Celsius", "\u00B0C"));
+
public static final LinearUnit MINUTE = SECOND.times(60)
.withName(NameSymbol.of("minute", "min"));
public static final LinearUnit HOUR = MINUTE.times(60)
@@ -324,7 +329,7 @@ public final class SI { .withName(NameSymbol.of("tonne", "t", "metric ton"));
public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27)
.withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate
- // value
+ // value
public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19)
.withName(NameSymbol.of("electron volt", "eV"));
public static final LinearUnit BYTE = BIT.times(8)
@@ -339,11 +344,11 @@ public final class SI { .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr),
dB -> Math.pow(10, dB / 10))
.withName(NameSymbol.of("decibel", "dB"));
-
/// The prefixes of the SI
// expanding decimal prefixes
public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3)
.withName(NameSymbol.of("kilo", "k", "K"));
+
public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6)
.withName(NameSymbol.of("mega", "M"));
public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9)
@@ -358,10 +363,10 @@ public final class SI { .withName(NameSymbol.of("zetta", "Z"));
public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24)
.withName(NameSymbol.of("yotta", "Y"));
-
// contracting decimal prefixes
public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3)
.withName(NameSymbol.of("milli", "m"));
+
public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6)
.withName(NameSymbol.of("micro", "\u03BC", "u")); // mu
public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9)
@@ -376,10 +381,10 @@ public final class SI { .withName(NameSymbol.of("zepto", "z"));
public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24)
.withName(NameSymbol.of("yocto", "y"));
-
// prefixes that don't match the pattern of thousands
public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1)
.withName(NameSymbol.of("deka", "da", "deca", "D"));
+
public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2)
.withName(NameSymbol.of("hecto", "h", "H", "hekto"));
public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1)
@@ -398,25 +403,29 @@ public final class SI { .withName(NameSymbol.of("pebi", "Pi"));
public static final UnitPrefix EXBI = PEBI.times(1024)
.withName(NameSymbol.of("exbi", "Ei"));
-
// sets of prefixes
- public static final Set<UnitPrefix> ALL_PREFIXES = new HashSet<>(
- Arrays.asList(DEKA, HECTO, KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA,
- YOTTA, DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO,
- YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI, EXBI));
- public static final Set<UnitPrefix> DECIMAL_PREFIXES = new HashSet<>(
- Arrays.asList(DEKA, HECTO, KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA,
- YOTTA, DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO,
- YOCTO));
- public static final Set<UnitPrefix> THOUSAND_PREFIXES = new HashSet<>(
- Arrays.asList(KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI,
- MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO));
- public static final Set<UnitPrefix> MAGNIFYING_PREFIXES = new HashSet<>(
- Arrays.asList(DEKA, HECTO, KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA,
- YOTTA, KIBI, MEBI, GIBI, TEBI, PEBI, EXBI));
- public static final Set<UnitPrefix> REDUCING_PREFIXES = new HashSet<>(
- Arrays.asList(DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO,
- ZEPTO, YOCTO));
+ public static final Set<UnitPrefix> ALL_PREFIXES = setOf(DEKA, HECTO, KILO,
+ MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, MICRO,
+ NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI,
+ EXBI);
+
+ public static final Set<UnitPrefix> DECIMAL_PREFIXES = setOf(DEKA, HECTO,
+ KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI,
+ MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO);
+ public static final Set<UnitPrefix> THOUSAND_PREFIXES = setOf(KILO, MEGA,
+ GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI, MICRO, NANO, PICO, FEMTO,
+ ATTO, ZEPTO, YOCTO);
+ public static final Set<UnitPrefix> MAGNIFYING_PREFIXES = setOf(DEKA, HECTO,
+ KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, KIBI, MEBI, GIBI,
+ TEBI, PEBI, EXBI);
+ public static final Set<UnitPrefix> REDUCING_PREFIXES = setOf(DECI, CENTI,
+ MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO);
+
+ // this method just calls Arrays.asList, which is itself safe.
+ @SafeVarargs
+ private static final <T> Set<T> setOf(T... args) {
+ return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(args)));
+ }
// You may NOT get SI instances!
private SI() {
diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java index 35b32fc..eb9b000 100644 --- a/src/org/unitConverter/unit/Unit.java +++ b/src/org/unitConverter/unit/Unit.java @@ -25,6 +25,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.DoubleUnaryOperator; +import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ObjectProduct; /** @@ -35,209 +36,211 @@ import org.unitConverter.math.ObjectProduct; */ public abstract class Unit { /** - * Returns a unit from its base and the functions it uses to convert to and from its base. + * 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: + * 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. - * @param converterTo - * function that accepts a value expressed in the unit and returns that value expressed in the unit's - * base. + * @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. * @return a unit that uses the provided functions to convert. * @since 2019-05-22 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ - public static final Unit fromConversionFunctions(final ObjectProduct<BaseUnit> base, - final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { return new FunctionalUnit(base, converterFrom, converterTo); } - + /** - * Returns a unit from its base and the functions it uses to convert to and from its base. + * 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: + * 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. - * @param converterTo - * function that accepts a value expressed in the unit and returns that value expressed in the unit's - * base. - * @param ns - * names and symbol of unit + * @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 names and symbol of unit * @return a unit that uses the provided functions to convert. * @since 2019-05-22 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ - public static final Unit fromConversionFunctions(final ObjectProduct<BaseUnit> base, - final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo, final NameSymbol ns) { + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { return new FunctionalUnit(base, converterFrom, converterTo, ns); } - + /** * The combination of units that this unit is based on. * * @since 2019-10-16 */ private final ObjectProduct<BaseUnit> unitBase; - + /** * The primary name used by this unit. */ private final Optional<String> primaryName; - + /** * A short symbol used to represent this unit. */ private final Optional<String> symbol; - + /** * A set of any additional names and/or spellings that the unit uses. */ private final Set<String> otherNames; - + /** * Cache storing the result of getDimension() * * @since 2019-10-16 */ private transient ObjectProduct<BaseDimension> dimension = null; - + /** * Creates the {@code AbstractUnit}. * - * @param unitBase - * base of unit - * @param ns - * names and symbol of unit + * @param unitBase base of unit + * @param ns names and symbol of unit * @since 2019-10-16 - * @throws NullPointerException - * if unitBase or ns is null + * @throws NullPointerException if unitBase or ns is null */ protected Unit(final ObjectProduct<BaseUnit> unitBase, final NameSymbol ns) { - this.unitBase = Objects.requireNonNull(unitBase, "unitBase must not be null."); - this.primaryName = Objects.requireNonNull(ns, "ns must not be null.").getPrimaryName(); + this.unitBase = Objects.requireNonNull(unitBase, + "unitBase must not be null."); + this.primaryName = Objects.requireNonNull(ns, "ns must not be null.") + .getPrimaryName(); this.symbol = ns.getSymbol(); this.otherNames = ns.getOtherNames(); } - + /** * A constructor that constructs {@code BaseUnit} instances. * * @since 2019-10-16 */ - Unit(final String primaryName, final String symbol, final Set<String> otherNames) { + Unit(final String primaryName, final String symbol, + final Set<String> otherNames) { if (this instanceof BaseUnit) { this.unitBase = ObjectProduct.oneOf((BaseUnit) this); } else throw new AssertionError(); this.primaryName = Optional.of(primaryName); this.symbol = Optional.of(symbol); - this.otherNames = Collections.unmodifiableSet( - new HashSet<>(Objects.requireNonNull(otherNames, "additionalNames must not be null."))); + this.otherNames = Collections.unmodifiableSet(new HashSet<>(Objects + .requireNonNull(otherNames, "additionalNames must not be null."))); } - + /** - * Checks if a value expressed in this unit can be converted to a value expressed in {@code other} + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} * - * @param other - * unit to test with + * @param other unit to test with * @return true if the units are compatible * @since 2019-01-13 * @since v0.1.0 - * @throws NullPointerException - * if other is null + * @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()); } - + /** - * Converts from a value expressed in this unit's base unit to a value expressed in this unit. + * Converts from a value expressed in this unit's base unit to a value + * expressed in this unit. * <p> - * This must be the inverse of {@code convertToBase}, so {@code convertFromBase(convertToBase(value))} must be equal - * to {@code value} for any value, ignoring precision loss by roundoff error. + * This must be the inverse of {@code convertToBase}, so + * {@code convertFromBase(convertToBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. * </p> * <p> - * If this unit <i>is</i> a base unit, this method should return {@code value}. + * 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}. + * @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 + * @param value value expressed in <b>base</b> unit * @return value expressed in <b>this</b> unit * @since 2018-12-22 * @since v0.1.0 */ protected abstract double convertFromBase(double value); - + /** - * Converts a value expressed in this unit to a value expressed in {@code other}. + * 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. + * {@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 + * @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 unit (as tested by - * {@link Unit#canConvertTo}). - * @throws NullPointerException - * if other is null + * @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 double convertTo(final Unit 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)); } - + /** - * Converts from a value expressed in this unit to a value expressed in this unit's base unit. + * Converts from a value expressed in this unit to a value expressed in this + * unit's base unit. * <p> - * This must be the inverse of {@code convertFromBase}, so {@code convertToBase(convertFromBase(value))} must be - * equal to {@code value} for any value, ignoring precision loss by roundoff error. + * This must be the inverse of {@code convertFromBase}, so + * {@code convertToBase(convertFromBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. * </p> * <p> - * If this unit <i>is</i> a base unit, this method should return {@code value}. + * 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}. + * @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 + * @param value value expressed in <b>this</b> unit * @return value expressed in <b>base</b> unit * @since 2018-12-22 * @since v0.1.0 */ protected abstract double convertToBase(double value); - + /** * @return combination of units that this unit is based on * @since 2018-12-22 @@ -246,7 +249,7 @@ public abstract class Unit { public final ObjectProduct<BaseUnit> getBase() { return this.unitBase; } - + /** * @return dimension measured by this unit * @since 2018-12-22 @@ -256,16 +259,16 @@ public abstract class Unit { 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 additionalNames * @since 2019-10-21 @@ -273,7 +276,7 @@ public abstract class Unit { public final Set<String> getOtherNames() { return this.otherNames; } - + /** * @return primaryName * @since 2019-10-21 @@ -281,7 +284,7 @@ public abstract class Unit { public final Optional<String> getPrimaryName() { return this.primaryName; } - + /** * @return symbol * @since 2019-10-21 @@ -289,25 +292,64 @@ public abstract class Unit { public final Optional<String> getSymbol() { return this.symbol; } - + + /** + * Returns true iff this unit is metric. + * <p> + * "Metric" is defined by three conditions: + * <ul> + * <li>Must be an instance of {@link LinearUnit}.</li> + * <li>Must be based on the SI base units (as determined by getBase())</li> + * <li>The conversion factor must be a power of 10.</li> + * </ul> + * <p> + * Note that this definition excludes some units that many would consider + * "metric", such as the degree Celsius (fails the first condition), + * calories, minutes and hours (fail the third condition). + * <p> + * All SI units (as designated by the BIPM) except the degree Celsius are + * considered "metric" by this definition. + * + * @since 2020-08-27 + */ + public final boolean isMetric() { + // first condition - check that it is a linear unit + if (!(this instanceof LinearUnit)) + return false; + final LinearUnit linear = (LinearUnit) this; + + // second condition - check that + for (final BaseUnit b : linear.getBase().getBaseSet()) { + if (!SI.BaseUnits.BASE_UNITS.contains(b)) + return false; + } + + // third condition - check that conversion factor is a power of 10 + return DecimalComparison + .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); + } + @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "") - + ", derived from " + this.getBase().toString(u -> u.getSymbol().get()) - + (this.getOtherNames().isEmpty() ? "" : ", also called " + String.join(", ", this.getOtherNames())); + + (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 + * @param ns name(s) and symbol to use * @return a copy of this unit with provided name(s) and symbol * @since 2019-10-21 - * @throws NullPointerException - * if ns is null + * @throws NullPointerException if ns is null */ public Unit withName(final NameSymbol ns) { - return fromConversionFunctions(this.getBase(), this::convertFromBase, this::convertToBase, + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, Objects.requireNonNull(ns, "ns must not be null.")); } } diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index 2cf3126..ff83805 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -16,6 +16,7 @@ */ package org.unitConverter.unit; +import static org.junit.Assert.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -105,6 +106,17 @@ class UnitTest { } @Test + public void testIsMetric() { + final Unit metre = SI.METRE; + final Unit megasecond = SI.SECOND.withPrefix(SI.MEGA); + final Unit hour = SI.HOUR; + + assertTrue(metre.isMetric()); + assertTrue(megasecond.isMetric()); + assertFalse(hour.isMetric()); + } + + @Test public void testMultiplicationAndDivision() { // test unit-times-unit multiplication final LinearUnit generatedJoule = SI.KILOGRAM |