summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2020-08-27 08:07:21 -0500
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2020-08-27 08:07:21 -0500
commite2f141427e441daa9d6be0ba8a30b844ca4391e0 (patch)
treec6cf591c9db5bc8e753c271396f226f2eb42e222
parent0245594222bfa0bd9a47d8326ed323c7356ac27c (diff)
Added the ability to restrict conversion to customary->metric.
-rw-r--r--src/org/unitConverter/converterGUI/SearchBoxList.java122
-rw-r--r--src/org/unitConverter/converterGUI/UnitConverterGUI.java89
-rw-r--r--src/org/unitConverter/unit/SI.java59
-rw-r--r--src/org/unitConverter/unit/Unit.java250
-rw-r--r--src/org/unitConverter/unit/UnitTest.java12
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