From 5d8cde96027f6d68d8e003b0b07b0bfab239d7cf Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 2 Jul 2020 16:47:31 -0500 Subject: Added a settings tab with settings --- .../converterGUI/UnitConverterGUI.java | 144 +++++++++++++++++---- 1 file changed, 122 insertions(+), 22 deletions(-) diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 0230728..e0f7c8e 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -17,6 +17,8 @@ package org.unitConverter.converterGUI; import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.io.File; @@ -31,18 +33,24 @@ import java.util.Set; import java.util.function.Predicate; import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.WindowConstants; +import javax.swing.border.TitledBorder; import org.unitConverter.math.ObjectProduct; import org.unitConverter.unit.BaseDimension; @@ -684,7 +692,7 @@ final class UnitConverterGUI { this.masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); - convertExpressionPanel.setLayout(new GridLayout(5, 1)); + convertExpressionPanel.setLayout(new GridLayout(4, 1)); { // panel for units to convert from final JPanel fromPanel = new JPanel(); @@ -730,27 +738,6 @@ final class UnitConverterGUI { this.output.setEditable(false); } } - - { // panel for specifying precision - final JPanel sigDigPanel = new JPanel(); - convertExpressionPanel.add(sigDigPanel); - - sigDigPanel.setBorder(BorderFactory.createTitledBorder("Significant Digits")); - - { // slider - final JSlider sigDigSlider = new JSlider(0, 12); - sigDigPanel.add(sigDigSlider); - - sigDigSlider.setMajorTickSpacing(4); - sigDigSlider.setMinorTickSpacing(1); - sigDigSlider.setSnapToTicks(true); - sigDigSlider.setPaintTicks(true); - sigDigSlider.setPaintLabels(true); - - sigDigSlider.addChangeListener( - e -> this.presenter.setSignificantFigures(sigDigSlider.getValue())); - } - } } { // panel to look up units @@ -794,6 +781,119 @@ final class UnitConverterGUI { this.prefixTextBox.setLineWrap(true); } } + + { // Settings panel + final JPanel settingsPanel = new JPanel(); + this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); + this.masterPane.setMnemonicAt(4, KeyEvent.VK_S); + + settingsPanel.setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); + + { // rounding settings + final JPanel roundingPanel = new JPanel(); + settingsPanel.add(roundingPanel); + roundingPanel.setBorder(new TitledBorder("Rounding Settings")); + roundingPanel.setLayout(new GridBagLayout()); + + // rounding rule selection + final ButtonGroup roundingRuleButtons = new ButtonGroup(); + + final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); + roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrecision = new JRadioButton("Fixed Precision"); + fixedPrecision.setSelected(true); + roundingRuleButtons.add(fixedPrecision); + roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedDecimals = new JRadioButton("Fixed Decimal Places"); + roundingRuleButtons.add(fixedDecimals); + roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton relativePrecision = new JRadioButton("Scientific Precision"); + roundingRuleButtons.add(relativePrecision); + roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); + + final JLabel sliderLabel = new JLabel("Precision:"); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4).setAnchor(GridBagConstraints.LINE_START).build()); + + final JSlider sigDigSlider = new JSlider(0, 12); + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5).setAnchor(GridBagConstraints.LINE_START).build()); + + sigDigSlider.setMajorTickSpacing(4); + sigDigSlider.setMinorTickSpacing(1); + sigDigSlider.setSnapToTicks(true); + sigDigSlider.setPaintTicks(true); + sigDigSlider.setPaintLabels(true); + + sigDigSlider.addChangeListener( + e -> this.presenter.setSignificantFigures(sigDigSlider.getValue())); + } + + { // prefix repetition settings + final JPanel prefixRepetitionPanel = new JPanel(); + settingsPanel.add(prefixRepetitionPanel); + prefixRepetitionPanel.setBorder(new TitledBorder("Prefix Repetition Settings")); + prefixRepetitionPanel.setLayout(new GridBagLayout()); + + // prefix rules + final ButtonGroup prefixRuleButtons = new ButtonGroup(); + + final JRadioButton noRepetition = new JRadioButton("No Repetition"); + noRepetition.setSelected(true); + prefixRuleButtons.add(noRepetition); + prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton noRestriction = new JRadioButton("No Restriction"); + prefixRuleButtons.add(noRestriction); + prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton customRepetition = new JRadioButton("Custom Repetition Rule"); + prefixRuleButtons.add(customRepetition); + prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + } + + { // search settings + final JPanel searchingPanel = new JPanel(); + settingsPanel.add(searchingPanel); + searchingPanel.setBorder(new TitledBorder("Search Settings")); + searchingPanel.setLayout(new GridBagLayout()); + + // searching rules + final ButtonGroup searchRuleButtons = new ButtonGroup(); + + final JRadioButton noPrefixes = new JRadioButton("Never Include Prefixed Units"); + noPrefixes.setSelected(true); + searchRuleButtons.add(noPrefixes); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrefixes = new JRadioButton("Include Some Prefixes"); + searchRuleButtons.add(fixedPrefixes); + searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton explicitPrefixes = new JRadioButton("Include Explicit Prefixes"); + searchRuleButtons.add(explicitPrefixes); + searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton alwaysInclude = new JRadioButton("Include All Single Prefixes"); + searchRuleButtons.add(alwaysInclude); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); + } + + { // miscellaneous settings + final JPanel miscPanel = new JPanel(); + settingsPanel.add(miscPanel); + miscPanel.setBorder(new TitledBorder("Miscellaneous Settings")); + miscPanel.setLayout(new GridBagLayout()); + + final JCheckBox showAllVariations = new JCheckBox("Show Symbols in \"Convert Units\""); + showAllVariations.setSelected(true); + miscPanel.add(showAllVariations, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + + final JButton unitFileButton = new JButton("Manage Unit Data Files"); + miscPanel.add(unitFileButton, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + } + } } } -- cgit v1.2.3 From 573a3d9ba3392697233f75ea3a9c02839e07fde0 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 7 Jul 2020 16:17:15 -0500 Subject: Fixed select-converter's inability to convert tiny numbers --- .../converterGUI/UnitConverterGUI.java | 35 +++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index e0f7c8e..aa81a74 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -25,6 +25,10 @@ import java.io.File; import java.math.BigDecimal; import java.math.MathContext; import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -200,18 +204,19 @@ final class UnitConverterGUI { final Unit from = this.database.getUnit(fromSelection); final Unit to = this.database.getUnit(toSelection); - final String input = this.view.getDimensionConverterInput(); - if (input.equals("")) { - this.view.showErrorDialog("Error", "No value to convert entered."); + final double beforeValue; + try { + beforeValue = this.view.getDimensionConverterInput(); + } catch (ParseException e) { + this.view.showErrorDialog("Error", "Error in parsing: " + e.getMessage()); return; } - final double beforeValue = Double.parseDouble(input); final double value = from.convertTo(to, beforeValue); final String output = this.getRoundedString(value); this.view.setDimensionConverterOutputText( - String.format("%s %s = %s %s", input, fromSelection, output, toSelection)); + String.format("%s %s = %s %s", this.view.getDimensionConverterText(), fromSelection, output, toSelection)); } /** @@ -414,6 +419,8 @@ final class UnitConverterGUI { } private static class View { + private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + /** The view's frame. */ private final JFrame frame; /** The view's associated presenter. */ @@ -469,7 +476,7 @@ final class UnitConverterGUI { this.prefixTextBox = new JTextArea(); this.fromSearch = new SearchBoxList(this.presenter.unitNameSet()); this.toSearch = new SearchBoxList(this.presenter.unitNameSet()); - this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################")); + this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); this.dimensionBasedOutput = new JTextArea(2, 32); this.fromEntry = new JTextField(); this.toEntry = new JTextField(); @@ -502,10 +509,24 @@ final class UnitConverterGUI { /** * @return value in dimension-based converter + * @throws ParseException + * @since 2020-07-07 + */ + public double getDimensionConverterInput() throws ParseException { + Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); + if (value instanceof Double) { + return (double) value; + } else if (value instanceof Long) { + return (double) (((Long) value).longValue()); + } else throw new AssertionError(); + } + + /** + * @return text inputted into dimension-based converter * @since 2019-04-13 * @since v0.2.0 */ - public String getDimensionConverterInput() { + public String getDimensionConverterText() { return this.valueInput.getText(); } -- cgit v1.2.3 From 7c3c6c21ce20eb48cc5c1caa150f3bb950c6a93b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 16 Jul 2020 16:49:00 -0500 Subject: Added basic rounding settings. --- .../converterGUI/UnitConverterGUI.java | 88 +++++++++++++++++----- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index aa81a74..daae2c6 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -24,6 +24,7 @@ import java.awt.event.KeyEvent; import java.io.File; import java.math.BigDecimal; import java.math.MathContext; +import java.math.RoundingMode; import java.text.DecimalFormat; import java.text.FieldPosition; import java.text.NumberFormat; @@ -75,7 +76,18 @@ final class UnitConverterGUI { * A tab in the View. */ private enum Pane { - UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER; + UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, SETTINGS; + } + + /** + * Different types of rounding. + * + * Significant digits: Rounds to a number of digits. i.e. with precision 5, 12345.6789 rounds to 12346. + * Decimal places: Rounds to a number of digits after the decimal point, i.e. with precision 5, 12345.6789 rounds to 12345.67890. + * Scientific: Rounds based on the number of digits and operations, following standard scientific rounding. + */ + private static enum RoundingType { + SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; } private static class Presenter { @@ -125,7 +137,14 @@ final class UnitConverterGUI { private final Comparator prefixNameComparator; - private int significantFigures = 6; + /* + * Rounding-related settings. I am using my own system, and not MathContext, + * because MathContext does not support decimal place based or scientific + * rounding, only significant digit based rounding. + */ + private int precision = 6; + + private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; /** * Creates the presenter. @@ -213,10 +232,10 @@ final class UnitConverterGUI { } final double value = from.convertTo(to, beforeValue); - final String output = this.getRoundedString(value); + final String output = this.getRoundedString(new BigDecimal(value)); - this.view.setDimensionConverterOutputText( - String.format("%s %s = %s %s", this.view.getDimensionConverterText(), fromSelection, output, toSelection)); + this.view.setDimensionConverterOutputText(String.format("%s %s = %s %s", + this.view.getDimensionConverterText(), fromSelection, output, toSelection)); } /** @@ -271,7 +290,7 @@ final class UnitConverterGUI { value = from.convertTo(to, 1); // round value - final String output = this.getRoundedString(value); + final String output = this.getRoundedString(new BigDecimal(value)); this.view.setExpressionConverterOutputText( String.format("%s = %s %s", fromUnitString, output, toUnitString)); @@ -280,7 +299,7 @@ final class UnitConverterGUI { value = SI.ONE.dividedBy((LinearUnit) from).convertTo(to, 1); // round value - final String output = this.getRoundedString(value); + final String output = this.getRoundedString(new BigDecimal(value)); this.view.setExpressionConverterOutputText( String.format("1 / %s = %s %s", fromUnitString, output, toUnitString)); @@ -316,10 +335,23 @@ final class UnitConverterGUI { * @since 2019-04-14 * @since v0.2.0 */ - private final String getRoundedString(final double value) { - // round value - final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures)); - String output = bigValue.toString(); + private final String getRoundedString(final BigDecimal value) { + // round value based on rounding type + final BigDecimal roundedValue; + switch (roundingType) { + case DECIMAL_PLACES: + roundedValue = value.setScale(precision, RoundingMode.HALF_EVEN); + break; + case SCIENTIFIC: + throw new UnsupportedOperationException("Not yet implemented."); + case SIGNIFICANT_DIGITS: + roundedValue = value.round(new MathContext(this.precision)); + break; + default: + throw new AssertionError("Invalid switch condition."); + } + + String output = roundedValue.toString(); // remove trailing zeroes if (output.contains(".")) { @@ -364,12 +396,12 @@ final class UnitConverterGUI { } /** - * @param significantFigures new value of significantFigures + * @param precision new value of precision * @since 2019-01-15 * @since v0.1.0 */ - public final void setSignificantFigures(final int significantFigures) { - this.significantFigures = significantFigures; + public final void setPrecision(final int precision) { + this.precision = precision; } /** @@ -416,6 +448,14 @@ final class UnitConverterGUI { public final Set unitNameSet() { return this.database.unitMapPrefixless().keySet(); } + + /** + * @param roundingType the roundingType to set + * @since 2020-07-16 + */ + public final void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + } } private static class View { @@ -502,6 +542,8 @@ final class UnitConverterGUI { return Pane.UNIT_VIEWER; case 3: return Pane.PREFIX_VIEWER; + case 4: + return Pane.SETTINGS; default: throw new AssertionError("No selected pane, or invalid pane."); } @@ -824,14 +866,18 @@ final class UnitConverterGUI { final JRadioButton fixedPrecision = new JRadioButton("Fixed Precision"); fixedPrecision.setSelected(true); + fixedPrecision.addActionListener(e -> this.presenter.setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); roundingRuleButtons.add(fixedPrecision); roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton fixedDecimals = new JRadioButton("Fixed Decimal Places"); + fixedDecimals.addActionListener(e -> this.presenter.setRoundingType(RoundingType.DECIMAL_PLACES)); roundingRuleButtons.add(fixedDecimals); roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton relativePrecision = new JRadioButton("Scientific Precision"); + relativePrecision.setEnabled(false); + relativePrecision.addActionListener(e -> this.presenter.setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); @@ -848,7 +894,7 @@ final class UnitConverterGUI { sigDigSlider.setPaintLabels(true); sigDigSlider.addChangeListener( - e -> this.presenter.setSignificantFigures(sigDigSlider.getValue())); + e -> this.presenter.setPrecision(sigDigSlider.getValue())); } { // prefix repetition settings @@ -861,15 +907,18 @@ final class UnitConverterGUI { final ButtonGroup prefixRuleButtons = new ButtonGroup(); final JRadioButton noRepetition = new JRadioButton("No Repetition"); - noRepetition.setSelected(true); + noRepetition.setEnabled(false); prefixRuleButtons.add(noRepetition); prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton noRestriction = new JRadioButton("No Restriction"); + noRestriction.setSelected(true); + noRestriction.setEnabled(false); prefixRuleButtons.add(noRestriction); prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton customRepetition = new JRadioButton("Custom Repetition Rule"); + customRepetition.setEnabled(false); prefixRuleButtons.add(customRepetition); prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); } @@ -884,19 +933,22 @@ final class UnitConverterGUI { final ButtonGroup searchRuleButtons = new ButtonGroup(); final JRadioButton noPrefixes = new JRadioButton("Never Include Prefixed Units"); - noPrefixes.setSelected(true); + noPrefixes.setEnabled(false); searchRuleButtons.add(noPrefixes); searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton fixedPrefixes = new JRadioButton("Include Some Prefixes"); + fixedPrefixes.setEnabled(false); searchRuleButtons.add(fixedPrefixes); searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton explicitPrefixes = new JRadioButton("Include Explicit Prefixes"); + explicitPrefixes.setEnabled(false); searchRuleButtons.add(explicitPrefixes); searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton alwaysInclude = new JRadioButton("Include All Single Prefixes"); + alwaysInclude.setEnabled(false); searchRuleButtons.add(alwaysInclude); searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); } @@ -909,9 +961,11 @@ final class UnitConverterGUI { final JCheckBox showAllVariations = new JCheckBox("Show Symbols in \"Convert Units\""); showAllVariations.setSelected(true); + showAllVariations.setEnabled(false); miscPanel.add(showAllVariations, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); final JButton unitFileButton = new JButton("Manage Unit Data Files"); + unitFileButton.setEnabled(false); miscPanel.add(unitFileButton, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); } } -- cgit v1.2.3 From 6cd11b76080a98b6f26416cb5954f430aad8d8e1 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 26 Jul 2020 12:09:13 -0500 Subject: NameSymbol now picks a primary name when possible and not available. --- src/org/unitConverter/unit/NameSymbol.java | 180 ++++++++++++++--------------- 1 file changed, 87 insertions(+), 93 deletions(-) diff --git a/src/org/unitConverter/unit/NameSymbol.java b/src/org/unitConverter/unit/NameSymbol.java index 96fab45..8d49c82 100644 --- a/src/org/unitConverter/unit/NameSymbol.java +++ b/src/org/unitConverter/unit/NameSymbol.java @@ -19,6 +19,7 @@ package org.unitConverter.unit; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -35,14 +36,11 @@ 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 + * @param name name to use + * @param symbol symbol to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if name or symbol is null + * @throws NullPointerException if name or symbol is null */ public static final NameSymbol of(final String name, final String symbol) { return new NameSymbol(Optional.of(name), Optional.of(symbol), new HashSet<>()); @@ -51,16 +49,12 @@ 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 + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ public static final NameSymbol of(final String name, final String symbol, final Set otherNames) { return new NameSymbol(Optional.of(name), Optional.of(symbol), @@ -68,18 +62,15 @@ public final class NameSymbol { } /** - * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * 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 + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ public static final NameSymbol of(final String name, final String symbol, final String... otherNames) { return new NameSymbol(Optional.of(name), Optional.of(symbol), @@ -87,20 +78,16 @@ public final class NameSymbol { } /** - * Gets a {@code NameSymbol} with a primary name, a symbol and an additional name. + * Gets a {@code NameSymbol} with a primary name, a symbol and an additional + * name. * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use - * @param name2 - * alternate name + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @param name2 alternate name * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ public static final NameSymbol of(final String name, final String symbol, final String name2) { final Set otherNames = new HashSet<>(); @@ -111,20 +98,14 @@ 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 - * @param name2 - * alternate name - * @param name3 - * alternate name + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @param name2 alternate name + * @param name3 alternate name * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3) { final Set otherNames = new HashSet<>(); @@ -136,22 +117,15 @@ 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 - * @param name2 - * alternate name - * @param name3 - * alternate name - * @param name4 - * alternate name + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @param name2 alternate name + * @param name3 alternate name + * @param name4 alternate name * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3, final String name4) { @@ -165,12 +139,10 @@ public final class NameSymbol { /** * Gets a {@code NameSymbol} with a primary name, no symbol, and no other names. * - * @param name - * name to use + * @param name name to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if name is null + * @throws NullPointerException if name is null */ public static final NameSymbol ofName(final String name) { return new NameSymbol(Optional.of(name), Optional.empty(), new HashSet<>()); @@ -179,50 +151,73 @@ public final class NameSymbol { /** * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. *

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

+ * If {@code name} is null and {@code otherNames} is not empty, a primary name + * will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-11-26 */ public static final NameSymbol ofNullable(final String name, final String symbol, final Set otherNames) { - return new NameSymbol(Optional.ofNullable(name), Optional.ofNullable(symbol), - otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); + return NameSymbol.create(name, symbol, otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); } /** - * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * Creates a {@code NameSymbol}, ensuring that if primaryName is null and + * otherNames is not empty, one name is moved from otherNames to primaryName + * + * Ensure that otherNames is a copy of the inputted argument. + */ + private static final NameSymbol create(final String name, final String symbol, final Set otherNames) { + final Optional primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + Iterator it = otherNames.iterator(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), otherNames); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + *

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

- * If any argument is null, this static factory replaces it with an empty Optional or empty Set. + * If {@code name} is null and {@code otherNames} is not empty, a primary name + * will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-11-26 */ public static final NameSymbol ofNullable(final String name, final String symbol, final String... otherNames) { - return new NameSymbol(Optional.ofNullable(name), Optional.ofNullable(symbol), - otherNames == null ? new HashSet<>() : new HashSet<>(Arrays.asList(otherNames))); + return create(name, symbol, otherNames == null ? new HashSet<>() : new HashSet<>(Arrays.asList(otherNames))); } /** * Gets a {@code NameSymbol} with a symbol and no names. * - * @param symbol - * symbol to use + * @param symbol symbol to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if symbol is null + * @throws NullPointerException if symbol is null */ public static final NameSymbol ofSymbol(final String symbol) { return new NameSymbol(Optional.empty(), Optional.of(symbol), new HashSet<>()); @@ -236,18 +231,17 @@ 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 + * @param primaryName primary name of unit + * @param symbol symbol used to represent unit + * @param otherNames other names and/or spellings, should be a mutable copy of + * the argument * @since 2019-10-21 */ private NameSymbol(final Optional primaryName, final Optional symbol, final Set otherNames) { this.primaryName = primaryName; this.symbol = symbol; + otherNames.remove(null); this.otherNames = Collections.unmodifiableSet(otherNames); } -- cgit v1.2.3 From c6199890be59cd324c8a08c74920e4315a2346a4 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 26 Jul 2020 12:24:26 -0500 Subject: Added equals, hashCode and isEmpty to NameSymbol --- src/org/unitConverter/unit/NameSymbol.java | 92 ++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/src/org/unitConverter/unit/NameSymbol.java b/src/org/unitConverter/unit/NameSymbol.java index 8d49c82..7fa5304 100644 --- a/src/org/unitConverter/unit/NameSymbol.java +++ b/src/org/unitConverter/unit/NameSymbol.java @@ -33,6 +33,28 @@ import java.util.Set; public final class NameSymbol { public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), Optional.empty(), new HashSet<>()); + /** + * Creates a {@code NameSymbol}, ensuring that if primaryName is null and + * otherNames is not empty, one name is moved from otherNames to primaryName + * + * Ensure that otherNames is a copy of the inputted argument. + */ + private static final NameSymbol create(final String name, final String symbol, final Set otherNames) { + final Optional primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + Iterator it = otherNames.iterator(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), otherNames); + } + /** * Gets a {@code NameSymbol} with a primary name, a symbol and no other names. * @@ -168,28 +190,6 @@ public final class NameSymbol { return NameSymbol.create(name, symbol, otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); } - /** - * Creates a {@code NameSymbol}, ensuring that if primaryName is null and - * otherNames is not empty, one name is moved from otherNames to primaryName - * - * Ensure that otherNames is a copy of the inputted argument. - */ - private static final NameSymbol create(final String name, final String symbol, final Set otherNames) { - final Optional primaryName; - - if (name == null && !otherNames.isEmpty()) { - // get primary name and remove it from savedNames - Iterator it = otherNames.iterator(); - assert it.hasNext(); - primaryName = Optional.of(it.next()); - otherNames.remove(primaryName.get()); - } else { - primaryName = Optional.ofNullable(name); - } - - return new NameSymbol(primaryName, Optional.ofNullable(symbol), otherNames); - } - /** * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional * names. @@ -243,6 +243,34 @@ public final class NameSymbol { this.symbol = symbol; otherNames.remove(null); this.otherNames = Collections.unmodifiableSet(otherNames); + + if (this.primaryName.isEmpty()) + assert this.otherNames.isEmpty(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof NameSymbol)) + return false; + NameSymbol other = (NameSymbol) obj; + if (otherNames == null) { + if (other.otherNames != null) + return false; + } else if (!otherNames.equals(other.otherNames)) + return false; + if (primaryName == null) { + if (other.primaryName != null) + return false; + } else if (!primaryName.equals(other.primaryName)) + return false; + if (symbol == null) { + if (other.symbol != null) + return false; + } else if (!symbol.equals(other.symbol)) + return false; + return true; } /** @@ -260,7 +288,7 @@ public final class NameSymbol { public final Optional getPrimaryName() { return this.primaryName; } - + /** * @return symbol * @since 2019-10-21 @@ -268,4 +296,22 @@ public final class NameSymbol { public final Optional getSymbol() { return this.symbol; } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((otherNames == null) ? 0 : otherNames.hashCode()); + result = prime * result + ((primaryName == null) ? 0 : primaryName.hashCode()); + result = prime * result + ((symbol == null) ? 0 : symbol.hashCode()); + return result; + } + + /** + * @return true iff this {@code NameSymbol} contains no names or symbols. + */ + public final boolean isEmpty() { + // if primaryName is empty, otherNames must also be empty + return this.primaryName.isEmpty() && this.symbol.isEmpty(); + } } \ No newline at end of file -- cgit v1.2.3 From b495b7029ebdcd56a41ea09811343a55c432b0a0 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 26 Jul 2020 14:43:11 -0500 Subject: Added UnitValue and LinearUnitValue --- src/org/unitConverter/unit/LinearUnit.java | 12 +- src/org/unitConverter/unit/LinearUnitValue.java | 300 ++++++++++++++++++++++++ src/org/unitConverter/unit/UnitValue.java | 114 +++++++++ 3 files changed, 420 insertions(+), 6 deletions(-) create mode 100644 src/org/unitConverter/unit/LinearUnitValue.java create mode 100644 src/org/unitConverter/unit/UnitValue.java diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 1e5ae53..762572a 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -246,16 +246,16 @@ public final class LinearUnit extends Unit { * @since 2019-03-17 * @since v0.2.0 */ - public LinearUnit minus(final LinearUnit subtrahendend) { - Objects.requireNonNull(subtrahendend, "addend must not be null."); + public LinearUnit minus(final LinearUnit subtrahend) { + Objects.requireNonNull(subtrahend, "addend must not be null."); // reject subtrahends that cannot be added to this unit - if (!this.getBase().equals(subtrahendend.getBase())) + if (!this.getBase().equals(subtrahend.getBase())) throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend)); + String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahend)); // subtract the units - return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); + return valueOf(this.getBase(), this.getConversionFactor() - subtrahend.getConversionFactor()); } /** @@ -347,7 +347,7 @@ public final class LinearUnit extends Unit { public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); } - + /** * Returns the result of applying {@code prefix} to this unit. *

diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java new file mode 100644 index 0000000..8daabf7 --- /dev/null +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -0,0 +1,300 @@ +/** + * + */ +package org.unitConverter.unit; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.Optional; + +/** + * 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 + */ +public final class LinearUnitValue { + private final LinearUnit unit; + private final double value; + private final double uncertainty; + + /** + * Gets an exact {@code UnitValue} + * + * @param unit unit to express with + * @param value value to express + * @return exact {@code UnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue getExact(LinearUnit unit, double value) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); + } + + /** + * Gets an uncertain {@code UnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code UnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(LinearUnit unit, double value, double uncertainty) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); + } + + /** + * @param unit unit to express as + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @since 2020-07-26 + */ + private LinearUnitValue(LinearUnit unit, double value, double uncertainty) { + this.unit = unit; + this.value = value; + this.uncertainty = uncertainty; + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(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 + */ + public final LinearUnitValue convertTo(LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, value), this.unit.convertTo(other, uncertainty)); + } + + /** + * 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 + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits(other.unit.convertToBase(other.getValue())) + && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double + .doubleToLongBits(other.getRelativeUncertainty()); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(LinearUnitValue other) { + if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + double thisBaseValue = this.unit.convertToBase(this.value); + double otherBaseValue = other.unit.convertToBase(other.value); + double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); + double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); + return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); + } + + /** + * @return the unit + * + * @since 2020-07-26 + */ + public final LinearUnit getUnit() { + return unit; + } + + /** + * @return the value + * + * @since 2020-07-26 + */ + public final double getValue() { + return value; + } + + /** + * @return absolute uncertainty of value + * + * @since 2020-07-26 + */ + public final double getUncertainty() { + return uncertainty; + } + + /** + * @return relative uncertainty of value + * + * @since 2020-07-26 + */ + public final double getRelativeUncertainty() { + return uncertainty / value; + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), this.getRelativeUncertainty()); + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-07-26 + */ + public final boolean isExact() { + return uncertainty == 0; + } + + /** + * 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 + */ + public LinearUnitValue plus(LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value + otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * 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 + */ + public LinearUnitValue minus(LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value - otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + @Override + public String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representing the object.
+ * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + *

+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of single + * numbers. + *

+ * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(boolean showUncertainty) { + Optional primaryName = this.unit.getPrimaryName(); + Optional symbol = this.unit.getSymbol(); + String chosenName = symbol.orElse(primaryName.orElse(null)); + + final double baseValue = this.unit.convertToBase(this.value); + final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + + // get rounded strings + final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; + if (this.isExact()) { + valueString = Double.toString(value); + baseValueString = Double.toString(baseValue); + uncertaintyString = "0"; + baseUncertaintyString = "0"; + } else { + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the uncertainty. + BigDecimal roundedUncertainty = bigUncertainty + .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedUncertainty.unscaledValue().intValue() >= 20) { + roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + + if (primaryName.isEmpty() && symbol.isEmpty()) { + final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); + final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); + + BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { + roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); + } + final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), + RoundingMode.HALF_EVEN); + + baseValueString = roundedBaseValue.toString(); + baseUncertaintyString = roundedBaseUncertainty.toString(); + } else { + // unused + baseValueString = ""; + baseUncertaintyString = ""; + } + } + + // create string + if (showUncertainty) { + if (primaryName.isEmpty() && symbol.isEmpty()) { + return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, + baseValueString, baseUncertaintyString, this.unit.getBase()); + } else { + return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); + } + } else { + if (primaryName.isEmpty() && symbol.isEmpty()) { + return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); + } else { + return String.format("%s %s", valueString, chosenName); + } + } + } +} diff --git a/src/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java new file mode 100644 index 0000000..9e565d9 --- /dev/null +++ b/src/org/unitConverter/unit/UnitValue.java @@ -0,0 +1,114 @@ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +/** + * 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 + */ +public final class UnitValue { + /** + * @param unit unit to use + * @param value value to use + * @return {@code UnitValue} instance + */ + public static UnitValue of(Unit unit, double value) { + return new UnitValue(Objects.requireNonNull(unit, "unit must not be null"), value); + } + + private final Unit unit; + private final double value; + + /** + * @param unit the unit being used + * @param value the value being represented + */ + private UnitValue(Unit unit, double value) { + this.unit = unit; + this.value = value; + } + + /** + * @return the unit + */ + public final Unit getUnit() { + return unit; + } + + /** + * @return the value + */ + public final double getValue() { + return value; + } + + /** + * Converts this {@code UnitValue} into an equivalent {@code LinearUnitValue} by + * using this unit's base unit. + * + * @param newName A new name for the base unit. Use {@link NameSymbol#EMPTY} if + * you don't want one. + */ + public final LinearUnitValue asLinearUnitValue(NameSymbol newName) { + LinearUnit base = LinearUnit.valueOf(unit.getBase(), 1, newName); + return LinearUnitValue.getExact(base, base.convertToBase(value)); + } + + /** + * @param other a {@code Unit} + * @return true iff this value can be represented with {@code other}. + */ + public final boolean canConvertTo(Unit other) { + return this.unit.canConvertTo(other); + } + + /** + * 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.getUnit().convertTo(other, this.getValue())); + } + + /** + * 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. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnitValue)) + return false; + final UnitValue other = (UnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits(other.unit.convertToBase(other.getValue())); + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), this.unit.convertFromBase(this.getValue())); + } + + @Override + public String toString() { + Optional primaryName = this.getUnit().getPrimaryName(); + Optional symbol = this.getUnit().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + double baseValue = this.getUnit().convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), baseValue, this.getUnit().getBase()); + } else { + String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} -- cgit v1.2.3 From 7549ac295f16b6de31abdfd832225b8b1824aaa8 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 28 Jul 2020 10:17:27 -0500 Subject: Added times, dividedBy and toExponent to LinearUnitValue --- .../converterGUI/UnitConverterGUI.java | 4 +- src/org/unitConverter/unit/LinearUnitValue.java | 601 ++++++++++++--------- 2 files changed, 338 insertions(+), 267 deletions(-) diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index daae2c6..986cb52 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -26,10 +26,8 @@ import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.text.DecimalFormat; -import java.text.FieldPosition; import java.text.NumberFormat; import java.text.ParseException; -import java.text.ParsePosition; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -559,7 +557,7 @@ final class UnitConverterGUI { if (value instanceof Double) { return (double) value; } else if (value instanceof Long) { - return (double) (((Long) value).longValue()); + return (((Long) value).longValue()); } else throw new AssertionError(); } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 8daabf7..f16b19f 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -18,283 +18,356 @@ import java.util.Optional; * @since 2020-07-26 */ public final class LinearUnitValue { - private final LinearUnit unit; - private final double value; - private final double uncertainty; - - /** - * Gets an exact {@code UnitValue} - * - * @param unit unit to express with - * @param value value to express - * @return exact {@code UnitValue} instance - * @since 2020-07-26 - */ - public static final LinearUnitValue getExact(LinearUnit unit, double value) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); - } + /** + * 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 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, final double value) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); + } - /** - * Gets an uncertain {@code UnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @return uncertain {@code UnitValue} instance - * @since 2020-07-26 - */ - public static final LinearUnitValue of(LinearUnit unit, double value, double uncertainty) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); - } + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, final double value, final double uncertainty) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); + } - /** - * @param unit unit to express as - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @since 2020-07-26 - */ - private LinearUnitValue(LinearUnit unit, double value, double uncertainty) { - this.unit = unit; - this.value = value; - this.uncertainty = uncertainty; - } + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param relativeUncertainty relative uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-28 + */ + public static final LinearUnitValue ofRelative(final LinearUnit unit, final double value, + final double relativeUncertainty) { + return LinearUnitValue.of(unit, value, relativeUncertainty * value); + } - /** - * @param other a {@code LinearUnit} - * @return true iff this value can be represented with {@code other}. - * @since 2020-07-26 - */ - public final boolean canConvertTo(LinearUnit other) { - return this.unit.canConvertTo(other); - } + private final LinearUnit unit; + private final double value; + private final double uncertainty; - /** - * 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 - */ - public final LinearUnitValue convertTo(LinearUnit other) { - return LinearUnitValue.of(other, this.unit.convertTo(other, value), this.unit.convertTo(other, uncertainty)); - } + /** + * @param unit unit to express as + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final double value, final double uncertainty) { + this.unit = unit; + this.value = value; + this.uncertainty = uncertainty; + } - /** - * 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 - */ - @Override - public boolean equals(Object obj) { - if (!(obj instanceof LinearUnitValue)) - return false; - LinearUnitValue other = (LinearUnitValue) obj; - return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits(other.unit.convertToBase(other.getValue())) - && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double - .doubleToLongBits(other.getRelativeUncertainty()); - } - - /** - * @param other another {@code LinearUnitValue} - * @return true iff this and other are within each other's uncertainty range - * - * @since 2020-07-26 - */ - public boolean equivalent(LinearUnitValue other) { - if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) - return false; - double thisBaseValue = this.unit.convertToBase(this.value); - double otherBaseValue = other.unit.convertToBase(other.value); - double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); - double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); - return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); - } + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(final LinearUnit other) { + return this.unit.canConvertTo(other); + } - /** - * @return the unit - * - * @since 2020-07-26 - */ - public final LinearUnit getUnit() { - return unit; - } + /** + * 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 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), + this.unit.convertTo(other, this.uncertainty)); + } - /** - * @return the value - * - * @since 2020-07-26 - */ - public final double getValue() { - return value; - } + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value / divisor, this.uncertainty / divisor); + } - /** - * @return absolute uncertainty of value - * - * @since 2020-07-26 - */ - public final double getUncertainty() { - return uncertainty; - } + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), this.value / divisor.value, + Math.hypot(this.getRelativeUncertainty(), divisor.getRelativeUncertainty())); + } - /** - * @return relative uncertainty of value - * - * @since 2020-07-26 - */ - public final double getRelativeUncertainty() { - return uncertainty / value; - } - - @Override - public int hashCode() { - return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), this.getRelativeUncertainty()); - } + /** + * 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 + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits(other.unit.convertToBase(other.getValue())) + && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double + .doubleToLongBits(other.getRelativeUncertainty()); + } - /** - * @return true iff the value has no uncertainty - * - * @since 2020-07-26 - */ - public final boolean isExact() { - return uncertainty == 0; - } - - /** - * 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 - */ - public LinearUnitValue plus(LinearUnitValue addend) { - Objects.requireNonNull(addend, "addend may not be null"); - - if (!this.canConvertTo(addend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); - - final LinearUnitValue otherConverted = addend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value + otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } - - /** - * 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 - */ - public LinearUnitValue minus(LinearUnitValue subtrahend) { - Objects.requireNonNull(subtrahend, "subtrahend may not be null"); - - if (!this.canConvertTo(subtrahend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); - - final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value - otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final double thisBaseValue = this.unit.convertToBase(this.value); + final double otherBaseValue = other.unit.convertToBase(other.value); + final double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); + final double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); + return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); + } - @Override - public String toString() { - return this.toString(!this.isExact()); - } + /** + * @return relative uncertainty of value + * + * @since 2020-07-26 + */ + public final double getRelativeUncertainty() { + return this.uncertainty / this.value; + } - /** - * Returns a string representing the object.
- * If the attached unit has a name or symbol, the string looks like "12 km". - * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". - *

- * If showUncertainty is true, strings like "35 ± 8" are shown instead of single - * numbers. - *

- * Non-exact values are rounded intelligently based on their uncertainty. - * - * @since 2020-07-26 - */ - public String toString(boolean showUncertainty) { - Optional primaryName = this.unit.getPrimaryName(); - Optional symbol = this.unit.getSymbol(); - String chosenName = symbol.orElse(primaryName.orElse(null)); - - final double baseValue = this.unit.convertToBase(this.value); - final double baseUncertainty = this.unit.convertToBase(this.uncertainty); - - // get rounded strings - final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; - if (this.isExact()) { - valueString = Double.toString(value); - baseValueString = Double.toString(baseValue); - uncertaintyString = "0"; - baseUncertaintyString = "0"; - } else { - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the uncertainty. - BigDecimal roundedUncertainty = bigUncertainty - .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedUncertainty.unscaledValue().intValue() >= 20) { - roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - - if (primaryName.isEmpty() && symbol.isEmpty()) { - final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); - final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); - - BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { - roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); - } - final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), - RoundingMode.HALF_EVEN); - - baseValueString = roundedBaseValue.toString(); - baseUncertaintyString = roundedBaseUncertainty.toString(); - } else { - // unused - baseValueString = ""; - baseUncertaintyString = ""; - } - } + /** + * @return absolute uncertainty of value + * + * @since 2020-07-26 + */ + public final double getUncertainty() { + return this.uncertainty; + } + + /** + * @return the unit + * + * @since 2020-07-26 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * + * @since 2020-07-26 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), + this.getRelativeUncertainty()); + } - // create string - if (showUncertainty) { - if (primaryName.isEmpty() && symbol.isEmpty()) { - return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, - baseValueString, baseUncertaintyString, this.unit.getBase()); - } else { - return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); - } - } else { - if (primaryName.isEmpty() && symbol.isEmpty()) { - return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); - } else { - return String.format("%s %s", valueString, chosenName); - } + /** + * @return true iff the value has no uncertainty + * + * @since 2020-07-26 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * 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 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value - otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * 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 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value + otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value * multiplier, this.uncertainty * multiplier); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), this.value * multiplier.value, + Math.hypot(this.getRelativeUncertainty(), multiplier.getRelativeUncertainty())); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), Math.pow(this.value, exponent), + this.getRelativeUncertainty() * Math.sqrt(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representing the object.
+ * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + *

+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of single + * numbers. + *

+ * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional primaryName = this.unit.getPrimaryName(); + final Optional symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final double baseValue = this.unit.convertToBase(this.value); + final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + + // get rounded strings + final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; + if (this.isExact()) { + valueString = Double.toString(this.value); + baseValueString = Double.toString(baseValue); + uncertaintyString = "0"; + baseUncertaintyString = "0"; + } else { + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the uncertainty. + BigDecimal roundedUncertainty = bigUncertainty + .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedUncertainty.unscaledValue().intValue() >= 20) { + roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + + if (primaryName.isEmpty() && symbol.isEmpty()) { + final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); + final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); + + BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { + roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); } + final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), + RoundingMode.HALF_EVEN); + + baseValueString = roundedBaseValue.toString(); + baseUncertaintyString = roundedBaseUncertainty.toString(); + } else { + // unused + baseValueString = ""; + baseUncertaintyString = ""; + } + } + + // create string + if (showUncertainty) { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, + baseValueString, baseUncertaintyString, this.unit.getBase()); + else + return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); + } else { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); } + } } -- cgit v1.2.3 From cf76cf66ea2039cd3e3052c940784dd88a87e2bd Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 28 Jul 2020 10:51:19 -0500 Subject: Added some tests for UnitValue and LinearUnitValue. --- src/org/unitConverter/unit/LinearUnitValue.java | 763 +++++++++++++----------- src/org/unitConverter/unit/UnitTest.java | 77 ++- 2 files changed, 467 insertions(+), 373 deletions(-) diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index f16b19f..74b0400 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -8,6 +8,8 @@ import java.math.RoundingMode; import java.util.Objects; import java.util.Optional; +import org.unitConverter.math.DecimalComparison; + /** * A possibly uncertain value expressed in a linear unit. * @@ -18,356 +20,417 @@ import java.util.Optional; * @since 2020-07-26 */ public final class LinearUnitValue { - /** - * 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 - */ - public static final LinearUnitValue getExact(final LinearUnit unit, final double value) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); - } - - /** - * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @return uncertain {@code LinearUnitValue} instance - * @since 2020-07-26 - */ - public static final LinearUnitValue of(final LinearUnit unit, final double value, final double uncertainty) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); - } - - /** - * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param relativeUncertainty relative uncertainty of value - * @return uncertain {@code LinearUnitValue} instance - * @since 2020-07-28 - */ - public static final LinearUnitValue ofRelative(final LinearUnit unit, final double value, - final double relativeUncertainty) { - return LinearUnitValue.of(unit, value, relativeUncertainty * value); - } - - private final LinearUnit unit; - private final double value; - private final double uncertainty; - - /** - * @param unit unit to express as - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @since 2020-07-26 - */ - private LinearUnitValue(final LinearUnit unit, final double value, final double uncertainty) { - this.unit = unit; - this.value = value; - this.uncertainty = uncertainty; - } - - /** - * @param other a {@code LinearUnit} - * @return true iff this value can be represented with {@code other}. - * @since 2020-07-26 - */ - public final 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 - */ - public final LinearUnitValue convertTo(final LinearUnit other) { - return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), - this.unit.convertTo(other, this.uncertainty)); - } - - /** - * Divides this value by a scalar - * - * @param divisor value to divide by - * @return multiplied value - * @since 2020-07-28 - */ - public LinearUnitValue dividedBy(final double divisor) { - return LinearUnitValue.of(this.unit, this.value / divisor, this.uncertainty / divisor); - } - - /** - * Divides this value by another value - * - * @param divisor value to multiply by - * @return quotient - * @since 2020-07-28 - */ - public LinearUnitValue dividedBy(final LinearUnitValue divisor) { - return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), this.value / divisor.value, - Math.hypot(this.getRelativeUncertainty(), divisor.getRelativeUncertainty())); - } - - /** - * 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 - */ - @Override - public boolean equals(final Object obj) { - if (!(obj instanceof LinearUnitValue)) - return false; - final LinearUnitValue other = (LinearUnitValue) obj; - return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits(other.unit.convertToBase(other.getValue())) - && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double - .doubleToLongBits(other.getRelativeUncertainty()); - } - - /** - * @param other another {@code LinearUnitValue} - * @return true iff this and other are within each other's uncertainty range - * - * @since 2020-07-26 - */ - public boolean equivalent(final LinearUnitValue other) { - if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) - return false; - final double thisBaseValue = this.unit.convertToBase(this.value); - final double otherBaseValue = other.unit.convertToBase(other.value); - final double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); - final double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); - return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); - } - - /** - * @return relative uncertainty of value - * - * @since 2020-07-26 - */ - public final double getRelativeUncertainty() { - return this.uncertainty / this.value; - } - - /** - * @return absolute uncertainty of value - * - * @since 2020-07-26 - */ - public final double getUncertainty() { - return this.uncertainty; - } - - /** - * @return the unit - * - * @since 2020-07-26 - */ - public final LinearUnit getUnit() { - return this.unit; - } - - /** - * @return the value - * - * @since 2020-07-26 - */ - public final double getValue() { - return this.value; - } - - @Override - public int hashCode() { - return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), - this.getRelativeUncertainty()); - } - - /** - * @return true iff the value has no uncertainty - * - * @since 2020-07-26 - */ - public final boolean isExact() { - return this.uncertainty == 0; - } - - /** - * 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 - */ - public LinearUnitValue minus(final LinearUnitValue subtrahend) { - Objects.requireNonNull(subtrahend, "subtrahend may not be null"); - - if (!this.canConvertTo(subtrahend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); - - final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value - otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } - - /** - * 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 - */ - public LinearUnitValue plus(final LinearUnitValue addend) { - Objects.requireNonNull(addend, "addend may not be null"); - - if (!this.canConvertTo(addend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); - - final LinearUnitValue otherConverted = addend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value + otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } - - /** - * Multiplies this value by a scalar - * - * @param multiplier value to multiply by - * @return multiplied value - * @since 2020-07-28 - */ - public LinearUnitValue times(final double multiplier) { - return LinearUnitValue.of(this.unit, this.value * multiplier, this.uncertainty * multiplier); - } - - /** - * Multiplies this value by another value - * - * @param multiplier value to multiply by - * @return product - * @since 2020-07-28 - */ - public LinearUnitValue times(final LinearUnitValue multiplier) { - return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), this.value * multiplier.value, - Math.hypot(this.getRelativeUncertainty(), multiplier.getRelativeUncertainty())); - } - - /** - * Raises a value to an exponent - * - * @param exponent exponent to raise to - * @return result of exponentiation - * @since 2020-07-28 - */ - public LinearUnitValue toExponent(final int exponent) { - return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), Math.pow(this.value, exponent), - this.getRelativeUncertainty() * Math.sqrt(exponent)); - } - - @Override - public String toString() { - return this.toString(!this.isExact()); - } - - /** - * Returns a string representing the object.
- * If the attached unit has a name or symbol, the string looks like "12 km". - * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". - *

- * If showUncertainty is true, strings like "35 ± 8" are shown instead of single - * numbers. - *

- * Non-exact values are rounded intelligently based on their uncertainty. - * - * @since 2020-07-26 - */ - public String toString(final boolean showUncertainty) { - final Optional primaryName = this.unit.getPrimaryName(); - final Optional symbol = this.unit.getSymbol(); - final String chosenName = symbol.orElse(primaryName.orElse(null)); - - final double baseValue = this.unit.convertToBase(this.value); - final double baseUncertainty = this.unit.convertToBase(this.uncertainty); - - // get rounded strings - final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; - if (this.isExact()) { - valueString = Double.toString(this.value); - baseValueString = Double.toString(baseValue); - uncertaintyString = "0"; - baseUncertaintyString = "0"; - } else { - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the uncertainty. - BigDecimal roundedUncertainty = bigUncertainty - .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedUncertainty.unscaledValue().intValue() >= 20) { - roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - - if (primaryName.isEmpty() && symbol.isEmpty()) { - final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); - final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); - - BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { - roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); - } - final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), - RoundingMode.HALF_EVEN); - - baseValueString = roundedBaseValue.toString(); - baseUncertaintyString = roundedBaseUncertainty.toString(); - } else { - // unused - baseValueString = ""; - baseUncertaintyString = ""; - } + /** + * 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 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, + final double value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value, 0); } - - // create string - if (showUncertainty) { - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, - baseValueString, baseUncertaintyString, this.unit.getBase()); - else - return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); - } else { - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); - else - return String.format("%s %s", valueString, chosenName); + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, + final double value, final double uncertainty) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value, + uncertainty); + } + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param relativeUncertainty relative uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-28 + */ + public static final LinearUnitValue ofRelative(final LinearUnit unit, + final double value, final double relativeUncertainty) { + return LinearUnitValue.of(unit, value, relativeUncertainty * value); + } + + private final LinearUnit unit; + private final double value; + private final double uncertainty; + + /** + * @param unit unit to express as + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final double value, + final double uncertainty) { + this.unit = unit; + this.value = value; + this.uncertainty = uncertainty; + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final 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 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), + this.unit.convertTo(other, this.uncertainty)); + } + + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value / divisor, + this.uncertainty / divisor); + } + + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), + this.value / divisor.value, + Math.hypot(this.getRelativeUncertainty(), + divisor.getRelativeUncertainty())); + } + + /** + * 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 + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits( + this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.unit.convertToBase(other.getValue())) + && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double + .doubleToLongBits(other.getRelativeUncertainty()); + } + + /** + * 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. + *

+ * If avoidFPErrors is true, this method will attempt to avoid floating-point + * errors, at the cost of not always being transitive. + * + * @since 2020-07-28 + */ + 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; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && DecimalComparison.equals(this.unit.convertToBase(this.value), + other.unit.convertToBase(other.value)) + && DecimalComparison.equals(this.getRelativeUncertainty(), + other.getRelativeUncertainty()); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null + || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final double thisBaseValue = this.unit.convertToBase(this.value); + final double otherBaseValue = other.unit.convertToBase(other.value); + final double thisBaseUncertainty = this.unit + .convertToBase(this.uncertainty); + final double otherBaseUncertainty = other.unit + .convertToBase(other.uncertainty); + return Math.abs(thisBaseValue - otherBaseValue) <= Math + .min(thisBaseUncertainty, otherBaseUncertainty); + } + + /** + * @return relative uncertainty of value + * + * @since 2020-07-26 + */ + public final double getRelativeUncertainty() { + return this.uncertainty / this.value; + } + + /** + * @return absolute uncertainty of value + * + * @since 2020-07-26 + */ + public final double getUncertainty() { + return this.uncertainty; + } + + /** + * @return the unit + * + * @since 2020-07-26 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * + * @since 2020-07-26 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), + this.unit.convertToBase(this.getValue()), + this.getRelativeUncertainty()); + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-07-26 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * 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 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", + this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value - otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * 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 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this.unit, + addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value + otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value * multiplier, + this.uncertainty * multiplier); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), + this.value * multiplier.value, + Math.hypot(this.getRelativeUncertainty(), + multiplier.getRelativeUncertainty())); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), + Math.pow(this.value, exponent), + this.getRelativeUncertainty() * Math.sqrt(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representing the object.
+ * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + *

+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of + * single numbers. + *

+ * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional primaryName = this.unit.getPrimaryName(); + final Optional symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final double baseValue = this.unit.convertToBase(this.value); + final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + + // get rounded strings + final String valueString, baseValueString, uncertaintyString, + baseUncertaintyString; + if (this.isExact()) { + valueString = Double.toString(this.value); + baseValueString = Double.toString(baseValue); + uncertaintyString = "0"; + baseUncertaintyString = "0"; + } else { + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal + // point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the + // uncertainty. + BigDecimal roundedUncertainty = bigUncertainty.setScale( + bigUncertainty.scale() - bigUncertainty.precision() + 2, + RoundingMode.HALF_EVEN); + if (roundedUncertainty.unscaledValue().intValue() >= 20) { + roundedUncertainty = bigUncertainty.setScale( + bigUncertainty.scale() - bigUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedValue = bigValue + .setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + + if (primaryName.isEmpty() && symbol.isEmpty()) { + final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); + final BigDecimal bigBaseUncertainty = BigDecimal + .valueOf(baseUncertainty); + + BigDecimal roundedBaseUncertainty = bigBaseUncertainty + .setScale( + bigBaseUncertainty.scale() + - bigBaseUncertainty.precision() + 2, + RoundingMode.HALF_EVEN); + if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { + roundedBaseUncertainty = bigBaseUncertainty + .setScale( + bigBaseUncertainty.scale() + - bigBaseUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedBaseValue = bigBaseValue.setScale( + roundedBaseUncertainty.scale(), RoundingMode.HALF_EVEN); + + baseValueString = roundedBaseValue.toString(); + baseUncertaintyString = roundedBaseUncertainty.toString(); + } else { + // unused + baseValueString = ""; + baseUncertaintyString = ""; + } + } + + // create string + if (showUncertainty) { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", + valueString, uncertaintyString, baseValueString, + baseUncertaintyString, this.unit.getBase()); + else + return String.format("(%s ± %s) %s", valueString, uncertaintyString, + chosenName); + } else { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); + } } - } } diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index c078cfc..2cf3126 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -17,6 +17,8 @@ package org.unitConverter.unit; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @@ -25,7 +27,8 @@ import org.junit.jupiter.api.Test; import org.unitConverter.math.DecimalComparison; /** - * Testing the various Unit classes. This is NOT part of this program's public API. + * Testing the various Unit classes. This is NOT part of this program's public + * API. * * @author Adrien Hopkins * @since 2018-12-22 @@ -34,69 +37,97 @@ import org.unitConverter.math.DecimalComparison; class UnitTest { /** A random number generator */ private static final Random rng = ThreadLocalRandom.current(); - + @Test public void testAdditionAndSubtraction() { - final LinearUnit inch = SI.METRE.times(0.0254); - final LinearUnit foot = SI.METRE.times(0.3048); - + final LinearUnit inch = SI.METRE.times(0.0254) + .withName(NameSymbol.of("inch", "in")); + final LinearUnit foot = SI.METRE.times(0.3048) + .withName(NameSymbol.of("foot", "ft")); + assertEquals(inch.plus(foot), SI.METRE.times(0.3302)); assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); + + // test with LinearUnitValue + final LinearUnitValue value1 = LinearUnitValue.getExact(SI.METRE, 15); + final LinearUnitValue value2 = LinearUnitValue.getExact(foot, 120); + final LinearUnitValue value3 = LinearUnitValue.getExact(SI.METRE, 0.5); + final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60); + + // make sure addition is done correctly + assertEquals(51.576, value1.plus(value2).getValue(), 0.001); + assertEquals(15.5, value1.plus(value3).getValue()); + assertEquals(52.076, value1.plus(value2).plus(value3).getValue(), 0.001); + + // make sure addition uses the correct unit, and is still associative + // (ignoring floating-point rounding errors) + assertEquals(SI.METRE, value1.plus(value2).getUnit()); + assertEquals(SI.METRE, value1.plus(value2).plus(value3).getUnit()); + assertEquals(foot, value2.plus(value1).getUnit()); + assertTrue(value1.plus(value2).equals(value2.plus(value1), true)); + + // make sure errors happen when they should + assertThrows(IllegalArgumentException.class, () -> value1.plus(value4)); } - + @Test public void testConversion() { final LinearUnit metre = SI.METRE; final Unit inch = metre.times(0.0254); - + + final UnitValue 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++) { // initiate random values - final double conversionFactor = rng.nextDouble() * 1000000; - final double testValue = rng.nextDouble() * 1000000; + final double conversionFactor = UnitTest.rng.nextDouble() * 1000000; + final double testValue = UnitTest.rng.nextDouble() * 1000000; final double expected = testValue * conversionFactor; - + // test final Unit unit = SI.METRE.times(conversionFactor); final double actual = unit.convertToBase(testValue); - - assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON); + + assertEquals(actual, expected, + expected * DecimalComparison.DOUBLE_EPSILON); } } - + @Test public void testEquals() { final LinearUnit metre = SI.METRE; final Unit meter = SI.BaseUnits.METRE.asLinearUnit(); - + assertEquals(metre, meter); } - + @Test public void testMultiplicationAndDivision() { // test unit-times-unit multiplication - final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); + final LinearUnit generatedJoule = SI.KILOGRAM + .times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); final LinearUnit actualJoule = SI.JOULE; - + assertEquals(generatedJoule, actualJoule); - + // test multiplication by conversion factors final LinearUnit kilometre = SI.METRE.times(1000); final LinearUnit hour = SI.SECOND.times(3600); final LinearUnit generatedKPH = kilometre.dividedBy(hour); - + final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6); - + assertEquals(generatedKPH, actualKPH); } - + @Test public void testPrefixes() { final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO); final LinearUnit actualKilometre = SI.METRE.times(1000); - + assertEquals(generatedKilometre, actualKilometre); } } -- cgit v1.2.3 From 77c7c962a6ed810b12685aa9ace4bd8a62761cea Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Aug 2020 16:08:22 -0500 Subject: Made UnitConverterGUI use UnitValue and LinearUnitValue --- .../converterGUI/UnitConverterGUI.java | 687 +++++++----- src/org/unitConverter/unit/LinearUnitValue.java | 12 + src/org/unitConverter/unit/UnitDatabase.java | 1163 ++++++++++++-------- 3 files changed, 1119 insertions(+), 743 deletions(-) diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 986cb52..8c70df4 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -59,10 +59,13 @@ import org.unitConverter.math.ObjectProduct; import org.unitConverter.unit.BaseDimension; import org.unitConverter.unit.BritishImperial; import org.unitConverter.unit.LinearUnit; +import org.unitConverter.unit.LinearUnitValue; +import org.unitConverter.unit.NameSymbol; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; import org.unitConverter.unit.UnitDatabase; import org.unitConverter.unit.UnitPrefix; +import org.unitConverter.unit.UnitValue; /** * @author Adrien Hopkins @@ -74,20 +77,10 @@ final class UnitConverterGUI { * A tab in the View. */ private enum Pane { - UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, SETTINGS; + UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, + SETTINGS; } - /** - * Different types of rounding. - * - * Significant digits: Rounds to a number of digits. i.e. with precision 5, 12345.6789 rounds to 12346. - * Decimal places: Rounds to a number of digits after the decimal point, i.e. with precision 5, 12345.6789 rounds to 12345.67890. - * Scientific: Rounds based on the number of digits and operations, following standard scientific rounding. - */ - private static enum RoundingType { - SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; - } - private static class Presenter { /** * Adds default units and dimensions to a database. @@ -110,40 +103,40 @@ final class UnitConverterGUI { // nonlinear units - must be loaded manually database.addUnit("tempCelsius", SI.CELSIUS); database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); - + // load initial dimensions database.addDimension("LENGTH", SI.Dimensions.LENGTH); database.addDimension("MASS", SI.Dimensions.MASS); database.addDimension("TIME", SI.Dimensions.TIME); database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } - + /** The presenter's associated view. */ private final View view; - + /** The units known by the program. */ private final UnitDatabase database; - + /** The names of all of the units */ private final List unitNames; - + /** The names of all of the prefixes */ private final List prefixNames; - + /** The names of all of the dimensions */ private final List dimensionNames; - + private final Comparator prefixNameComparator; - + /* - * Rounding-related settings. I am using my own system, and not MathContext, - * because MathContext does not support decimal place based or scientific - * rounding, only significant digit based rounding. + * Rounding-related settings. I am using my own system, and not + * MathContext, because MathContext does not support decimal place based + * or scientific rounding, only significant digit based rounding. */ private int precision = 6; - + private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; - + /** * Creates the presenter. * @@ -153,14 +146,14 @@ final class UnitConverterGUI { */ Presenter(final View view) { this.view = view; - + // load initial units this.database = new UnitDatabase(); Presenter.addDefaults(this.database); - + this.database.loadUnitsFile(new File("unitsfile.txt")); this.database.loadDimensionFile(new File("dimensionfile.txt")); - + // a comparator that can be used to compare prefix names // any name that does not exist is less than a name that does. // otherwise, they are compared by value @@ -169,37 +162,43 @@ final class UnitConverterGUI { return -1; else if (!Presenter.this.database.containsPrefixName(o2)) return 1; - + final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); - + if (p1.getMultiplier() < p2.getMultiplier()) return -1; else if (p1.getMultiplier() > p2.getMultiplier()) return 1; - + return o1.compareTo(o2); }; - - this.unitNames = new ArrayList<>(this.database.unitMapPrefixless().keySet()); + + this.unitNames = new ArrayList<>( + this.database.unitMapPrefixless().keySet()); this.unitNames.sort(null); // sorts it using Comparable - + this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); - this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator - - this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.database.dimensionMap().keySet())); + this.prefixNames.sort(this.prefixNameComparator); // sorts it using my + // comparator + + this.dimensionNames = new DelegateListModel<>( + new ArrayList<>(this.database.dimensionMap().keySet())); this.dimensionNames.sort(null); // sorts it using Comparable - + // a Predicate that returns true iff the argument is a full base unit - final Predicate isFullBase = unit -> unit instanceof LinearUnit && ((LinearUnit) unit).isBase(); - + final Predicate isFullBase = unit -> unit instanceof LinearUnit + && ((LinearUnit) unit).isBase(); + // print out unit counts - System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n", + System.out.printf( + "Successfully loaded %d units with %d unit names (%d base units).%n", new HashSet<>(this.database.unitMapPrefixless().values()).size(), this.database.unitMapPrefixless().size(), - new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count()); + new HashSet<>(this.database.unitMapPrefixless().values()) + .stream().filter(isFullBase).count()); } - + /** * Converts in the dimension-based converter * @@ -209,7 +208,8 @@ final class UnitConverterGUI { public final void convertDimensionBased() { final String fromSelection = this.view.getFromSelection(); if (fromSelection == null) { - this.view.showErrorDialog("Error", "No unit selected in From field"); + this.view.showErrorDialog("Error", + "No unit selected in From field"); return; } final String toSelection = this.view.getToSelection(); @@ -217,31 +217,35 @@ final class UnitConverterGUI { this.view.showErrorDialog("Error", "No unit selected in To field"); return; } - + final Unit from = this.database.getUnit(fromSelection); - final Unit to = this.database.getUnit(toSelection); - - final double beforeValue; + final Unit to = this.database.getUnit(toSelection) + .withName(NameSymbol.ofName(toSelection)); + + final UnitValue beforeValue; try { - beforeValue = this.view.getDimensionConverterInput(); - } catch (ParseException e) { - this.view.showErrorDialog("Error", "Error in parsing: " + e.getMessage()); + beforeValue = UnitValue.of(from, + this.view.getDimensionConverterInput()); + } catch (final ParseException e) { + this.view.showErrorDialog("Error", + "Error in parsing: " + e.getMessage()); return; } - final double value = from.convertTo(to, beforeValue); - - final String output = this.getRoundedString(new BigDecimal(value)); - - this.view.setDimensionConverterOutputText(String.format("%s %s = %s %s", - this.view.getDimensionConverterText(), fromSelection, output, toSelection)); + final UnitValue value = beforeValue.convertTo(to); + + final String output = this.getRoundedString(value); + + this.view.setDimensionConverterOutputText( + String.format("%s = %s", beforeValue, output)); } - + /** * Runs whenever the convert button is pressed. * *

- * Reads and parses a unit expression from the from and to boxes, then converts - * {@code from} to {@code to}. Any errors are shown in JOptionPanes. + * Reads and parses a unit expression from the from and to boxes, then + * converts {@code from} to {@code to}. Any errors are shown in + * JOptionPanes. *

* * @since 2019-01-26 @@ -250,64 +254,78 @@ final class UnitConverterGUI { public final void convertExpressions() { final String fromUnitString = this.view.getFromText(); final String toUnitString = this.view.getToText(); - + if (fromUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the From: box."); + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the From: box."); return; } if (toUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the To: box."); + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the To: box."); return; } - - // try to parse from - final Unit from; + + final LinearUnitValue from; + final Unit to; try { - from = this.database.getUnitFromExpression(fromUnitString); + from = this.database.evaluateUnitExpression(fromUnitString); } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); + this.view.showErrorDialog("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); return; } - - final double value; - // try to parse to - final Unit to; try { - if (this.database.containsUnitName(toUnitString)) { - // if it's a unit, convert to that - to = this.database.getUnit(toUnitString); - } else { - to = this.database.getUnitFromExpression(toUnitString); - } + to = this.database.getUnitFromExpression(toUnitString); } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); + this.view.showErrorDialog("Parse Error", + "Could not recognize text in To entry: " + e.getMessage()); return; } - - if (from.canConvertTo(to)) { - value = from.convertTo(to, 1); - - // round value - final String output = this.getRoundedString(new BigDecimal(value)); - - this.view.setExpressionConverterOutputText( - String.format("%s = %s %s", fromUnitString, output, toUnitString)); - } else if (from instanceof LinearUnit && SI.ONE.dividedBy((LinearUnit) from).canConvertTo(to)) { - // reciprocal conversion (like seconds to hertz) - value = SI.ONE.dividedBy((LinearUnit) from).convertTo(to, 1); - - // round value - final String output = this.getRoundedString(new BigDecimal(value)); - + + if (to instanceof LinearUnit) { + // convert to LinearUnitValue + final LinearUnitValue from2; + final LinearUnit to2 = (LinearUnit) to; + final boolean useSlash; + + if (from.canConvertTo(to2)) { + from2 = from; + useSlash = false; + } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) { + from2 = LinearUnitValue.ONE.dividedBy(from); + useSlash = true; + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + return; + } + + final LinearUnitValue converted = from2.convertTo(to2); this.view.setExpressionConverterOutputText( - String.format("1 / %s = %s %s", fromUnitString, output, toUnitString)); + (useSlash ? "1 / " : "") + String.format("%s = %s", + fromUnitString, this.getRoundedString(converted))); + return; } else { - // if I can't convert, leave - this.view.showErrorDialog("Conversion Error", - String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); + // convert to UnitValue + final UnitValue from2 = from.asUnitValue(); + if (from2.canConvertTo(to)) { + final UnitValue converted = from2.convertTo(to); + + this.view + .setExpressionConverterOutputText(String.format("%s = %s", + fromUnitString, this.getRoundedString(converted))); + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + } } } - + /** * @return a list of all of the unit dimensions * @since 2019-04-13 @@ -316,7 +334,7 @@ final class UnitConverterGUI { public final List dimensionNameList() { return this.dimensionNames; } - + /** * @return a comparator to compare prefix names * @since 2019-04-14 @@ -325,20 +343,21 @@ final class UnitConverterGUI { public final Comparator getPrefixNameComparator() { return this.prefixNameComparator; } - + /** * @param value value to round - * @return string of that value rounded to {@code significantDigits} significant - * digits. + * @return string of that value rounded to {@code significantDigits} + * significant digits. * @since 2019-04-14 * @since v0.2.0 */ private final String getRoundedString(final BigDecimal value) { // round value based on rounding type final BigDecimal roundedValue; - switch (roundingType) { + switch (this.roundingType) { case DECIMAL_PLACES: - roundedValue = value.setScale(precision, RoundingMode.HALF_EVEN); + roundedValue = value.setScale(this.precision, + RoundingMode.HALF_EVEN); break; case SCIENTIFIC: throw new UnsupportedOperationException("Not yet implemented."); @@ -350,7 +369,7 @@ final class UnitConverterGUI { } String output = roundedValue.toString(); - + // remove trailing zeroes if (output.contains(".")) { while (output.endsWith("0")) { @@ -360,10 +379,68 @@ final class UnitConverterGUI { output = output.substring(0, output.length() - 1); } } - + return output; } - + + /** + * Like {@link LinearUnitValue#toString(boolean)} with parameter + * {@code false}, but obeys this unit converter's rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final LinearUnitValue value) { + switch (this.roundingType) { + case DECIMAL_PLACES: + case SIGNIFICANT_DIGITS: + return this.getRoundedString(value.asUnitValue()); + case SCIENTIFIC: + return value.toString(false); + default: + throw new AssertionError("Invalid switch condition."); + } + } + + /** + * Like {@link UnitValue#toString()}, but obeys this unit converter's + * rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final UnitValue value) { + final BigDecimal unrounded = new BigDecimal(value.getValue()); + final BigDecimal rounded; + int precision = this.precision; + switch (this.roundingType) { + case DECIMAL_PLACES: + rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); + break; + case SCIENTIFIC: + precision = 12; + //$FALL-THROUGH$ + case SIGNIFICANT_DIGITS: + rounded = unrounded + .round(new MathContext(precision, RoundingMode.HALF_EVEN)); + break; + default: + throw new AssertionError("Invalid switch condition."); + } + + String output = rounded.toString(); + + // remove trailing zeroes + if (output.contains(".")) { + while (output.endsWith("0")) { + output = output.substring(0, output.length() - 1); + } + if (output.endsWith(".")) { + output = output.substring(0, output.length() - 1); + } + } + + return output + " " + value.getUnit().getPrimaryName().get(); + } + /** * @return a set of all prefix names in the database * @since 2019-04-14 @@ -372,7 +449,7 @@ final class UnitConverterGUI { public final Set prefixNameSet() { return this.database.prefixMap().keySet(); } - + /** * Runs whenever a prefix is selected in the viewer. *

@@ -388,11 +465,12 @@ final class UnitConverterGUI { return; else { final UnitPrefix prefix = this.database.getPrefix(prefixName); - - this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier())); + + this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", + prefixName, prefix.getMultiplier())); } } - + /** * @param precision new value of precision * @since 2019-01-15 @@ -401,10 +479,18 @@ final class UnitConverterGUI { public final void setPrecision(final int precision) { this.precision = precision; } - + + /** + * @param roundingType the roundingType to set + * @since 2020-07-16 + */ + public final void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + } + /** - * Returns true if and only if the unit represented by {@code unitName} has the - * dimension represented by {@code dimensionName}. + * Returns true if and only if the unit represented by {@code unitName} + * has the dimension represented by {@code dimensionName}. * * @param unitName name of unit to test * @param dimensionName name of dimension to test @@ -412,12 +498,14 @@ final class UnitConverterGUI { * @since 2019-04-13 * @since v0.2.0 */ - public final boolean unitMatchesDimension(final String unitName, final String dimensionName) { + public final boolean unitMatchesDimension(final String unitName, + final String dimensionName) { final Unit unit = this.database.getUnit(unitName); - final ObjectProduct dimension = this.database.getDimension(dimensionName); + final ObjectProduct dimension = this.database + .getDimension(dimensionName); return unit.getDimension().equals(dimension); } - + /** * Runs whenever a unit is selected in the viewer. *

@@ -433,11 +521,11 @@ final class UnitConverterGUI { return; else { final Unit unit = this.database.getUnit(unitName); - + this.view.setUnitTextBoxText(unit.toString()); } } - + /** * @return a set of all of the unit names * @since 2019-04-14 @@ -446,16 +534,21 @@ final class UnitConverterGUI { public final Set unitNameSet() { return this.database.unitMapPrefixless().keySet(); } - - /** - * @param roundingType the roundingType to set - * @since 2020-07-16 - */ - public final void setRoundingType(RoundingType roundingType) { - this.roundingType = roundingType; - } } - + + /** + * Different types of rounding. + * + * Significant digits: Rounds to a number of digits. i.e. with precision 5, + * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits + * after the decimal point, i.e. with precision 5, 12345.6789 rounds to + * 12345.67890. Scientific: Rounds based on the number of digits and + * operations, following standard scientific rounding. + */ + private static enum RoundingType { + SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; + } + private static class View { private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); @@ -465,7 +558,7 @@ final class UnitConverterGUI { private final Presenter presenter; /** The master pane containing all of the tabs. */ private final JTabbedPane masterPane; - + // DIMENSION-BASED CONVERTER /** The panel for inputting values in the dimension-based converter */ private final JTextField valueInput; @@ -475,7 +568,7 @@ final class UnitConverterGUI { private final SearchBoxList toSearch; /** The output area in the dimension-based converter */ private final JTextArea dimensionBasedOutput; - + // EXPRESSION-BASED CONVERTER /** The "From" entry in the conversion panel */ private final JTextField fromEntry; @@ -483,7 +576,7 @@ final class UnitConverterGUI { private final JTextField toEntry; /** The output area in the conversion panel */ private final JTextArea output; - + // UNIT AND PREFIX VIEWERS /** The searchable list of unit names in the unit viewer */ private final SearchBoxList unitNameList; @@ -493,7 +586,7 @@ final class UnitConverterGUI { private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; - + /** * Creates the {@code View}. * @@ -505,7 +598,7 @@ final class UnitConverterGUI { this.frame = new JFrame("Unit Converter"); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); this.masterPane = new JTabbedPane(); - + // create the components this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), @@ -519,13 +612,13 @@ final class UnitConverterGUI { this.fromEntry = new JTextField(); this.toEntry = new JTextField(); this.output = new JTextArea(2, 32); - + // create more components this.initComponents(); - + this.frame.pack(); } - + /** * @return the currently selected pane. * @throws AssertionError if no pane (or an invalid pane) is selected @@ -546,19 +639,20 @@ final class UnitConverterGUI { throw new AssertionError("No selected pane, or invalid pane."); } } - + /** * @return value in dimension-based converter - * @throws ParseException + * @throws ParseException * @since 2020-07-07 */ public double getDimensionConverterInput() throws ParseException { - Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); - if (value instanceof Double) { + final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); + if (value instanceof Double) return (double) value; - } else if (value instanceof Long) { - return (((Long) value).longValue()); - } else throw new AssertionError(); + else if (value instanceof Long) + return ((Long) value).longValue(); + else + throw new AssertionError(); } /** @@ -569,7 +663,7 @@ final class UnitConverterGUI { public String getDimensionConverterText() { return this.valueInput.getText(); } - + /** * @return selection in "From" selector in dimension-based converter * @since 2019-04-13 @@ -578,7 +672,7 @@ final class UnitConverterGUI { public String getFromSelection() { return this.fromSearch.getSelectedValue(); } - + /** * @return text in "From" box in converter panel * @since 2019-01-15 @@ -587,7 +681,7 @@ final class UnitConverterGUI { public String getFromText() { return this.fromEntry.getText(); } - + /** * @return index of selected prefix in prefix viewer * @since 2019-01-15 @@ -596,7 +690,7 @@ final class UnitConverterGUI { public String getPrefixViewerSelection() { return this.prefixNameList.getSelectedValue(); } - + /** * @return selection in "To" selector in dimension-based converter * @since 2019-04-13 @@ -605,7 +699,7 @@ final class UnitConverterGUI { public String getToSelection() { return this.toSearch.getSelectedValue(); } - + /** * @return text in "To" box in converter panel * @since 2019-01-26 @@ -614,7 +708,7 @@ final class UnitConverterGUI { public String getToText() { return this.toEntry.getText(); } - + /** * @return index of selected unit in unit viewer * @since 2019-01-15 @@ -623,7 +717,7 @@ final class UnitConverterGUI { public String getUnitViewerSelection() { return this.unitNameList.getSelectedValue(); } - + /** * Starts up the application. * @@ -633,7 +727,7 @@ final class UnitConverterGUI { public final void init() { this.frame.setVisible(true); } - + /** * Initializes the view's components. * @@ -643,199 +737,213 @@ final class UnitConverterGUI { private final void initComponents() { final JPanel masterPanel = new JPanel(); this.frame.add(masterPanel); - + masterPanel.setLayout(new BorderLayout()); - + { // pane with all of the tabs masterPanel.add(this.masterPane, BorderLayout.CENTER); - + { // a panel for unit conversion using a selector final JPanel convertUnitPanel = new JPanel(); this.masterPane.addTab("Convert Units", convertUnitPanel); this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); - + convertUnitPanel.setLayout(new BorderLayout()); - + { // panel for input part final JPanel inputPanel = new JPanel(); convertUnitPanel.add(inputPanel, BorderLayout.CENTER); - + inputPanel.setLayout(new GridLayout(1, 3)); - + final JComboBox dimensionSelector = new JComboBox<>( - this.presenter.dimensionNameList().toArray(new String[0])); + this.presenter.dimensionNameList() + .toArray(new String[0])); dimensionSelector.setSelectedItem("LENGTH"); - + // handle dimension filter - final MutablePredicate dimensionFilter = new MutablePredicate<>(s -> true); - + final MutablePredicate dimensionFilter = new MutablePredicate<>( + s -> true); + // panel for From things inputPanel.add(this.fromSearch); - + this.fromSearch.addSearchFilter(dimensionFilter); - - { // for dimension selector and arrow that represents conversion + + { // for dimension selector and arrow that represents + // conversion final JPanel inBetweenPanel = new JPanel(); inputPanel.add(inBetweenPanel); - + inBetweenPanel.setLayout(new BorderLayout()); - + { // dimension selector - inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START); + inBetweenPanel.add(dimensionSelector, + BorderLayout.PAGE_START); } - + { // the arrow in the middle final JLabel arrowLabel = new JLabel("->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); } } - + // panel for To things - + inputPanel.add(this.toSearch); - + this.toSearch.addSearchFilter(dimensionFilter); - + // code for dimension filter dimensionSelector.addItemListener(e -> { - dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); }); - - // apply the item listener once because I have a default selection - dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); + + // apply the item listener once because I have a default + // selection + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); } - + { // panel for submit and output, and also value entry final JPanel outputPanel = new JPanel(); convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); - + outputPanel.setLayout(new GridLayout(3, 1)); - + { // unit input final JPanel valueInputPanel = new JPanel(); outputPanel.add(valueInputPanel); - + valueInputPanel.setLayout(new BorderLayout()); - + { // prompt - final JLabel valuePrompt = new JLabel("Value to convert: "); - valueInputPanel.add(valuePrompt, BorderLayout.LINE_START); + final JLabel valuePrompt = new JLabel( + "Value to convert: "); + valueInputPanel.add(valuePrompt, + BorderLayout.LINE_START); } - + { // value to convert - valueInputPanel.add(this.valueInput, BorderLayout.CENTER); + valueInputPanel.add(this.valueInput, + BorderLayout.CENTER); } } - + { // button to convert final JButton convertButton = new JButton("Convert"); outputPanel.add(convertButton); - - convertButton.addActionListener(e -> this.presenter.convertDimensionBased()); + + convertButton.addActionListener( + e -> this.presenter.convertDimensionBased()); convertButton.setMnemonic(KeyEvent.VK_ENTER); } - + { // output of conversion outputPanel.add(this.dimensionBasedOutput); this.dimensionBasedOutput.setEditable(false); } } } - + { // panel for unit conversion using expressions final JPanel convertExpressionPanel = new JPanel(); - this.masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); + this.masterPane.addTab("Convert Unit Expressions", + convertExpressionPanel); this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); - + convertExpressionPanel.setLayout(new GridLayout(4, 1)); - + { // panel for units to convert from final JPanel fromPanel = new JPanel(); convertExpressionPanel.add(fromPanel); - + fromPanel.setBorder(BorderFactory.createTitledBorder("From")); fromPanel.setLayout(new GridLayout(1, 1)); - + { // entry for units fromPanel.add(this.fromEntry); } } - + { // panel for units to convert to final JPanel toPanel = new JPanel(); convertExpressionPanel.add(toPanel); - + toPanel.setBorder(BorderFactory.createTitledBorder("To")); toPanel.setLayout(new GridLayout(1, 1)); - + { // entry for units toPanel.add(this.toEntry); } } - + { // button to convert final JButton convertButton = new JButton("Convert"); convertExpressionPanel.add(convertButton); - - convertButton.addActionListener(e -> this.presenter.convertExpressions()); + + convertButton.addActionListener( + e -> this.presenter.convertExpressions()); convertButton.setMnemonic(KeyEvent.VK_ENTER); } - + { // output of conversion final JPanel outputPanel = new JPanel(); convertExpressionPanel.add(outputPanel); - - outputPanel.setBorder(BorderFactory.createTitledBorder("Output")); + + outputPanel + .setBorder(BorderFactory.createTitledBorder("Output")); outputPanel.setLayout(new GridLayout(1, 1)); - + { // output outputPanel.add(this.output); this.output.setEditable(false); } } } - + { // panel to look up units final JPanel unitLookupPanel = new JPanel(); this.masterPane.addTab("Unit Viewer", unitLookupPanel); this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); - + unitLookupPanel.setLayout(new GridLayout()); - + { // search panel unitLookupPanel.add(this.unitNameList); - - this.unitNameList.getSearchList() - .addListSelectionListener(e -> this.presenter.unitNameSelected()); + + this.unitNameList.getSearchList().addListSelectionListener( + e -> this.presenter.unitNameSelected()); } - + { // the text box for unit's toString unitLookupPanel.add(this.unitTextBox); this.unitTextBox.setEditable(false); this.unitTextBox.setLineWrap(true); } } - + { // panel to look up prefixes final JPanel prefixLookupPanel = new JPanel(); this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); - + prefixLookupPanel.setLayout(new GridLayout(1, 2)); - + { // panel for listing and seaching prefixLookupPanel.add(this.prefixNameList); - - this.prefixNameList.getSearchList() - .addListSelectionListener(e -> this.presenter.prefixSelected()); + + this.prefixNameList.getSearchList().addListSelectionListener( + e -> this.presenter.prefixSelected()); } - + { // the text box for prefix's toString prefixLookupPanel.add(this.prefixTextBox); this.prefixTextBox.setEditable(false); @@ -848,77 +956,104 @@ final class UnitConverterGUI { this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); this.masterPane.setMnemonicAt(4, KeyEvent.VK_S); - settingsPanel.setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); + settingsPanel.setLayout( + new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); { // rounding settings final JPanel roundingPanel = new JPanel(); settingsPanel.add(roundingPanel); - roundingPanel.setBorder(new TitledBorder("Rounding Settings")); + roundingPanel + .setBorder(new TitledBorder("Rounding Settings")); roundingPanel.setLayout(new GridBagLayout()); // rounding rule selection final ButtonGroup roundingRuleButtons = new ButtonGroup(); final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); - roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton fixedPrecision = new JRadioButton("Fixed Precision"); + final JRadioButton fixedPrecision = new JRadioButton( + "Fixed Precision"); fixedPrecision.setSelected(true); - fixedPrecision.addActionListener(e -> this.presenter.setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + fixedPrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); roundingRuleButtons.add(fixedPrecision); - roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton fixedDecimals = new JRadioButton("Fixed Decimal Places"); - fixedDecimals.addActionListener(e -> this.presenter.setRoundingType(RoundingType.DECIMAL_PLACES)); + final JRadioButton fixedDecimals = new JRadioButton( + "Fixed Decimal Places"); + fixedDecimals.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.DECIMAL_PLACES)); roundingRuleButtons.add(fixedDecimals); - roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton relativePrecision = new JRadioButton("Scientific Precision"); + final JRadioButton relativePrecision = new JRadioButton( + "Scientific Precision"); relativePrecision.setEnabled(false); - relativePrecision.addActionListener(e -> this.presenter.setRoundingType(RoundingType.SCIENTIFIC)); + relativePrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); - roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); final JLabel sliderLabel = new JLabel("Precision:"); - roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) + .setAnchor(GridBagConstraints.LINE_START).build()); final JSlider sigDigSlider = new JSlider(0, 12); - roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5).setAnchor(GridBagConstraints.LINE_START).build()); - + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) + .setAnchor(GridBagConstraints.LINE_START).build()); + sigDigSlider.setMajorTickSpacing(4); sigDigSlider.setMinorTickSpacing(1); sigDigSlider.setSnapToTicks(true); sigDigSlider.setPaintTicks(true); sigDigSlider.setPaintLabels(true); - - sigDigSlider.addChangeListener( - e -> this.presenter.setPrecision(sigDigSlider.getValue())); + + sigDigSlider.addChangeListener(e -> this.presenter + .setPrecision(sigDigSlider.getValue())); } { // prefix repetition settings final JPanel prefixRepetitionPanel = new JPanel(); settingsPanel.add(prefixRepetitionPanel); - prefixRepetitionPanel.setBorder(new TitledBorder("Prefix Repetition Settings")); + prefixRepetitionPanel.setBorder( + new TitledBorder("Prefix Repetition Settings")); prefixRepetitionPanel.setLayout(new GridBagLayout()); // prefix rules final ButtonGroup prefixRuleButtons = new ButtonGroup(); - final JRadioButton noRepetition = new JRadioButton("No Repetition"); + final JRadioButton noRepetition = new JRadioButton( + "No Repetition"); noRepetition.setEnabled(false); prefixRuleButtons.add(noRepetition); - prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + prefixRepetitionPanel.add(noRepetition, + new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START) + .build()); - final JRadioButton noRestriction = new JRadioButton("No Restriction"); + final JRadioButton noRestriction = new JRadioButton( + "No Restriction"); noRestriction.setSelected(true); noRestriction.setEnabled(false); prefixRuleButtons.add(noRestriction); - prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + prefixRepetitionPanel.add(noRestriction, + new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START) + .build()); - final JRadioButton customRepetition = new JRadioButton("Custom Repetition Rule"); + final JRadioButton customRepetition = new JRadioButton( + "Custom Repetition Rule"); customRepetition.setEnabled(false); prefixRuleButtons.add(customRepetition); - prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + prefixRepetitionPanel.add(customRepetition, + new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START) + .build()); } { // search settings @@ -930,46 +1065,59 @@ final class UnitConverterGUI { // searching rules final ButtonGroup searchRuleButtons = new ButtonGroup(); - final JRadioButton noPrefixes = new JRadioButton("Never Include Prefixed Units"); + final JRadioButton noPrefixes = new JRadioButton( + "Never Include Prefixed Units"); noPrefixes.setEnabled(false); searchRuleButtons.add(noPrefixes); - searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton fixedPrefixes = new JRadioButton("Include Some Prefixes"); + final JRadioButton fixedPrefixes = new JRadioButton( + "Include Some Prefixes"); fixedPrefixes.setEnabled(false); searchRuleButtons.add(fixedPrefixes); - searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton explicitPrefixes = new JRadioButton("Include Explicit Prefixes"); + final JRadioButton explicitPrefixes = new JRadioButton( + "Include Explicit Prefixes"); explicitPrefixes.setEnabled(false); searchRuleButtons.add(explicitPrefixes); - searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton alwaysInclude = new JRadioButton("Include All Single Prefixes"); + final JRadioButton alwaysInclude = new JRadioButton( + "Include All Single Prefixes"); alwaysInclude.setEnabled(false); searchRuleButtons.add(alwaysInclude); - searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); } { // miscellaneous settings final JPanel miscPanel = new JPanel(); settingsPanel.add(miscPanel); - miscPanel.setBorder(new TitledBorder("Miscellaneous Settings")); + miscPanel + .setBorder(new TitledBorder("Miscellaneous Settings")); miscPanel.setLayout(new GridBagLayout()); - final JCheckBox showAllVariations = new JCheckBox("Show Symbols in \"Convert Units\""); + final JCheckBox showAllVariations = new JCheckBox( + "Show Symbols in \"Convert Units\""); showAllVariations.setSelected(true); showAllVariations.setEnabled(false); - miscPanel.add(showAllVariations, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + miscPanel.add(showAllVariations, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JButton unitFileButton = new JButton("Manage Unit Data Files"); + final JButton unitFileButton = new JButton( + "Manage Unit Data Files"); unitFileButton.setEnabled(false); - miscPanel.add(unitFileButton, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + miscPanel.add(unitFileButton, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); } } } } - + /** * Sets the text in the output of the dimension-based converter. * @@ -980,7 +1128,7 @@ final class UnitConverterGUI { public void setDimensionConverterOutputText(final String text) { this.dimensionBasedOutput.setText(text); } - + /** * Sets the text in the output of the conversion panel. * @@ -991,7 +1139,7 @@ final class UnitConverterGUI { public void setExpressionConverterOutputText(final String text) { this.output.setText(text); } - + /** * Sets the text of the prefix text box in the prefix viewer. * @@ -1002,7 +1150,7 @@ final class UnitConverterGUI { public void setPrefixTextBoxText(final String text) { this.prefixTextBox.setText(text); } - + /** * Sets the text of the unit text box in the unit viewer. * @@ -1013,7 +1161,7 @@ final class UnitConverterGUI { public void setUnitTextBoxText(final String text) { this.unitTextBox.setText(text); } - + /** * Shows an error dialog. * @@ -1023,10 +1171,11 @@ final class UnitConverterGUI { * @since v0.1.0 */ public void showErrorDialog(final String title, final String message) { - JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); } } - + public static void main(final String[] args) { new View().init(); } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 74b0400..7096738 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -20,6 +20,8 @@ import org.unitConverter.math.DecimalComparison; * @since 2020-07-26 */ public final class LinearUnitValue { + public static final LinearUnitValue ONE = getExact(SI.ONE, 1); + /** * Gets an exact {@code LinearUnitValue} * @@ -81,6 +83,15 @@ public final class LinearUnitValue { this.uncertainty = uncertainty; } + /** + * @return this value as a {@code UnitValue}. All uncertainty information is + * removed from the returned value. + * @since 2020-08-04 + */ + public final UnitValue asUnitValue() { + return UnitValue.of(this.unit, this.value); + } + /** * @param other a {@code LinearUnit} * @return true iff this value can be represented with {@code other}. @@ -135,6 +146,7 @@ public final class LinearUnitValue { * km) returns true. * * @since 2020-07-26 + * @see #equals(Object, boolean) */ @Override public boolean equals(final Object obj) { diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 507266d..0246630 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -55,29 +56,33 @@ public final class UnitDatabase { /** * A map for units that allows the use of prefixes. *

- * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is - * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the - * maps that were passed in during construction. + * As this map implementation is intended to be used as a sort of "augmented + * view" of a unit and prefix map, it is unmodifiable but instead reflects + * the changes to the maps passed into it. Do not edit this map, instead edit + * the maps that were passed in during construction. *

*

* The rules for applying prefixes onto units are the following: *

    *
  • Prefixes can only be applied to linear units.
  • - *
  • Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if - * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with - * the prefix "A", even though they have the same string.
  • - *
  • Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and - * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".
  • + *
  • Before attempting to search for prefixes in a unit name, this map will + * first search for a unit name. So, if there are two units, "B" and "AB", + * and a prefix "A", this map will favour the unit "AB" over the unit "B" + * with the prefix "A", even though they have the same string.
  • + *
  • Longer prefixes are preferred to shorter prefixes. So, if you have + * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return + * the unit "C" with the prefix "AB", not "BC" with the prefix "A".
  • *
*

*

- * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some - * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an + * This map is infinite in size if there is at least one unit and at least + * one prefix. If it is infinite, some operations that only work with finite + * collections, like converting name/entry sets to arrays, will throw an * {@code IllegalStateException}. *

*

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), {@link #containsValue} and {@link #values()} - * currently ignore prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), + * {@link #containsValue} and {@link #values()} currently ignore prefixes. *

* * @author Adrien Hopkins @@ -89,16 +94,19 @@ public final class UnitDatabase { * The class used for entry sets. * *

- * 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 set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. + * 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 + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. *

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitEntrySet extends AbstractSet> { + private static final class PrefixedUnitEntrySet + extends AbstractSet> { /** * The entry for this set. * @@ -106,17 +114,16 @@ public final class UnitDatabase { * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntry implements Entry { + private static final class PrefixedUnitEntry + implements Entry { private final String key; private final Unit value; - + /** * Creates the {@code PrefixedUnitEntry}. * - * @param key - * key - * @param value - * value + * @param key key + * @param value value * @since 2019-04-14 * @since v0.2.0 */ @@ -124,7 +131,7 @@ public final class UnitDatabase { this.key = key; this.value = value; } - + /** * @since 2019-05-03 */ @@ -136,34 +143,38 @@ public final class UnitDatabase { return Objects.equals(this.getKey(), other.getKey()) && Objects.equals(this.getValue(), other.getValue()); } - + @Override public String getKey() { return this.key; } - + @Override public Unit getValue() { return this.value; } - + /** * @since 2019-05-03 */ @Override public int hashCode() { return (this.getKey() == null ? 0 : this.getKey().hashCode()) - ^ (this.getValue() == null ? 0 : this.getValue().hashCode()); + ^ (this.getValue() == null ? 0 + : this.getValue().hashCode()); } - + @Override public Unit setValue(final Unit value) { - throw new UnsupportedOperationException("Cannot set value in an immutable entry"); + throw new UnsupportedOperationException( + "Cannot set value in an immutable entry"); } - + /** - * Returns a string representation of the entry. The format of the string is the string representation - * of the key, then the equals ({@code =}) character, then the string representation of the value. + * Returns a string representation of the entry. The format of the + * string is the string representation of the key, then the equals + * ({@code =}) character, then the string representation of the + * value. * * @since 2019-05-03 */ @@ -172,27 +183,30 @@ public final class UnitDatabase { return this.getKey() + "=" + this.getValue(); } } - + /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntryIterator implements Iterator> { + private static final class PrefixedUnitEntryIterator + implements Iterator> { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); - + // values from the unit entry set private final Map map; private transient final List unitNames; private transient final List prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -202,7 +216,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -214,10 +228,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -229,7 +243,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -238,127 +252,142 @@ public final class UnitDatabase { */ private void incrementPosition() { this.unitNamePosition++; - + if (this.unitNamePosition >= this.unitNames.size()) { // we have used all of our units, go to a different prefix this.unitNamePosition = 0; - + // if the prefix coordinates are empty, then set it to [0] if (this.prefixCoordinates.isEmpty()) { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + if (i < 0) { // we need to add a new coordinate this.prefixCoordinates.add(0, 0); } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); } } } } } - + @Override public Entry next() { // get next element final Entry nextEntry = this.peek(); - + // iterate to next position this.incrementPosition(); - + return nextEntry; } - + /** - * @return the next element in the iterator, without iterating over it + * @return the next element in the iterator, without iterating over + * it * @since 2019-05-03 */ private Entry peek() { if (!this.hasNext()) throw new NoSuchElementException("No units left!"); - + // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units + // since all of the unprefixed stuff is done, just remove + // nonlinear units if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { this.unitNames.remove(this.unitNamePosition); } } - + final String nextName = this.getCurrentUnitName(); - + return new PrefixedUnitEntry(nextName, this.map.get(nextName)); } - + /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. * * @since 2019-05-03 */ @Override public String toString() { - return String.format("Iterator iterating over name-unit entries; next value is \"%s\"", + return String.format( + "Iterator iterating over name-unit entries; next value is \"%s\"", this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * - * @param map - * map that created this set + * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitEntrySet(final PrefixedUnitMap map) { this.map = map; } - + @Override public boolean add(final Map.Entry e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override - public boolean addAll(final Collection> c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + public boolean addAll( + final Collection> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); } - + @Override public boolean contains(final Object o) { // get the entry final Entry entry; - + try { - // This is OK because I'm in a try-catch block, catching the exact exception that would be thrown. + // This is OK because I'm in a try-catch block, catching the + // exact exception that would be thrown. @SuppressWarnings("unchecked") final Entry tempEntry = (Entry) o; entry = tempEntry; } catch (final ClassCastException e) { - throw new IllegalArgumentException("Attempted to test for an entry using a non-entry."); + throw new IllegalArgumentException( + "Attempted to test for an entry using a non-entry."); } - - return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue()); + + return this.map.containsKey(entry.getKey()) + && this.map.get(entry.getKey()).equals(entry.getValue()); } - + @Override public boolean containsAll(final Collection c) { for (final Object o : c) @@ -366,37 +395,42 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator> iterator() { return new PrefixedUnitEntryIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override - public boolean removeIf(final Predicate> filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + public boolean removeIf( + final Predicate> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -409,10 +443,9 @@ public final class UnitDatabase { 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() { @@ -420,12 +453,12 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + 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[] toArray(final T[] a) { @@ -433,53 +466,61 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + 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", + 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. * *

- * 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 set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. + * 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 + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. *

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitNameSet extends AbstractSet { + private static final class PrefixedUnitNameSet + extends AbstractSet { /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitNameIterator implements Iterator { + private static final class PrefixedUnitNameIterator + implements Iterator { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); - + // values from the unit name set private final Map map; private transient final List unitNames; private transient final List prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -489,7 +530,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -501,10 +542,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -516,7 +557,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -525,109 +566,121 @@ public final class UnitDatabase { */ private void incrementPosition() { this.unitNamePosition++; - + if (this.unitNamePosition >= this.unitNames.size()) { // we have used all of our units, go to a different prefix this.unitNamePosition = 0; - + // if the prefix coordinates are empty, then set it to [0] if (this.prefixCoordinates.isEmpty()) { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + if (i < 0) { // we need to add a new coordinate this.prefixCoordinates.add(0, 0); } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); } } } } } - + @Override public String next() { final String nextName = this.peek(); - + this.incrementPosition(); - + return nextName; } - + /** - * @return the next element in the iterator, without iterating over it + * @return the next element in the iterator, without iterating over + * it * @since 2019-05-03 */ private String peek() { if (!this.hasNext()) throw new NoSuchElementException("No units left!"); // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units + // since all of the unprefixed stuff is done, just remove + // nonlinear units if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { this.unitNames.remove(this.unitNamePosition); } } - + return this.getCurrentUnitName(); } - + /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. * * @since 2019-05-03 */ @Override public String toString() { - return String.format("Iterator iterating over unit names; next value is \"%s\"", this.peek()); + return String.format( + "Iterator iterating over unit names; next value is \"%s\"", + this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * - * @param map - * map that created this set + * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitNameSet(final PrefixedUnitMap map) { this.map = map; } - + @Override public boolean add(final String e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public boolean addAll(final Collection c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); } - + @Override public boolean contains(final Object o) { return this.map.containsKey(o); } - + @Override public boolean containsAll(final Collection c) { for (final Object o : c) @@ -635,37 +688,41 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator iterator() { return new PrefixedUnitNameIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeIf(final Predicate filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -678,10 +735,9 @@ public final class UnitDatabase { 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() { @@ -689,13 +745,13 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - + 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[] toArray(final T[] a) { @@ -703,19 +759,21 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + 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", + 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. * @@ -723,7 +781,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * The available prefixes for use. * @@ -731,95 +789,106 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map prefixes; - + // caches private transient Collection values = null; private transient Set keySet = null; private transient Set> entrySet = null; - + /** * Creates the {@code PrefixedUnitMap}. * - * @param units - * map mapping unit names to units - * @param prefixes - * map mapping prefix names to prefixes + * @param units map mapping unit names to units + * @param prefixes map mapping prefix names to prefixes * @since 2019-04-13 * @since v0.2.0 */ - public PrefixedUnitMap(final Map units, final Map prefixes) { - // I am making unmodifiable maps to ensure I don't accidentally make changes. + public PrefixedUnitMap(final Map units, + final Map prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make + // changes. this.units = Collections.unmodifiableMap(units); this.prefixes = Collections.unmodifiableMap(prefixes); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable map"); + throw new UnsupportedOperationException( + "Cannot clear an immutable map"); } - + @Override public Unit compute(final String key, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override - public Unit computeIfAbsent(final String key, final Function mappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + public Unit computeIfAbsent(final String key, + final Function mappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override public Unit computeIfPresent(final String key, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override public boolean containsKey(final Object key) { // First, test if there is a unit with the key if (this.units.containsKey(key)) return true; - + // Next, try to cast it to String if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to test for a unit using a non-string name."); + throw new IllegalArgumentException( + "Attempted to test for a unit using a non-string name."); final String unitName = (String) key; - + // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; int longestLength = 0; - + for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } - + return longestPrefix != null; } - + /** * {@inheritDoc} * *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method only tests for prefixless units. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method only tests for prefixless units. *

*/ @Override public boolean containsValue(final Object value) { return this.units.containsValue(value); } - + @Override public Set> entrySet() { if (this.entrySet == null) { @@ -827,56 +896,62 @@ public final class UnitDatabase { } return this.entrySet; } - + @Override public Unit get(final Object key) { // First, test if there is a unit with the key if (this.units.containsKey(key)) return this.units.get(key); - + // Next, try to cast it to String if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name."); + throw new IllegalArgumentException( + "Attempted to obtain a unit using a non-string name."); final String unitName = (String) key; - + // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; int longestLength = 0; - + for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } - + // 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 + // 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); } } - + @Override public boolean isEmpty() { return this.units.isEmpty(); } - + @Override public Set keySet() { if (this.keySet == null) { @@ -884,53 +959,64 @@ public final class UnitDatabase { } return this.keySet; } - + @Override public Unit merge(final String key, final Unit value, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot merge into an immutable map"); + throw new UnsupportedOperationException( + "Cannot merge into an immutable map"); } - + @Override public Unit put(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public void putAll(final Map m) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public Unit putIfAbsent(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public Unit remove(final Object key) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); } - + @Override public boolean remove(final Object key, final Object value) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); } - + @Override public Unit replace(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override - public boolean replace(final String key, final Unit oldValue, final Unit newValue) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + public boolean replace(final String key, final Unit oldValue, + final Unit newValue) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override - public void replaceAll(final BiFunction function) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + public void replaceAll( + final BiFunction function) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override public int size() { if (this.units.isEmpty()) @@ -943,66 +1029,80 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + @Override public String toString() { if (this.units.isEmpty() || this.prefixes.isEmpty()) return super.toString(); else - return String.format("Infinite map of name-unit entries created from units %s and prefixes %s", + return String.format( + "Infinite map of name-unit entries created from units %s and prefixes %s", this.units, this.prefixes); } - + /** * {@inheritDoc} * *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method ignores prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method ignores prefixes. *

*/ @Override public Collection values() { if (this.values == null) { - this.values = Collections.unmodifiableCollection(this.units.values()); + this.values = Collections + .unmodifiableCollection(this.units.values()); } return this.values; } } - + /** * Replacements done to *all* expression types */ private static final Map EXPRESSION_REPLACEMENTS = new HashMap<>(); - + // add data to expression replacements static { - // place brackets around any expression of the form "number unit", with or without the space + // 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 + + "(?:\\.\\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") + + "(?!-?\\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.compile("(\\S+)\\s+(\\S.*)"); - + private static final Pattern NAME_EXPRESSION = Pattern + .compile("(\\S+)\\s+(\\S.*)"); + /** * The exponent operator * - * @param base - * base of exponentiation - * @param exponentUnit - * exponent + * @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, final LinearUnit exponentUnit) { + private static final LinearUnit exponentiateUnits(final LinearUnit base, + final LinearUnit exponentUnit) { // exponent function - first check if o2 is a number, if (exponentUnit.getBase().equals(SI.ONE.getBase())) { // then check if it is an integer, @@ -1012,12 +1112,39 @@ public final class UnitDatabase { return base.toExponent((int) (exponent + 0.5)); else // not an integer - throw new UnsupportedOperationException("Decimal exponents are currently not supported."); + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); } else // not a number throw new IllegalArgumentException("Exponents must be numbers."); } - + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2020-08-04 + */ + private static final LinearUnitValue exponentiateUnitValues( + final LinearUnitValue base, final LinearUnitValue exponentValue) { + // exponent function - first check if o2 is a number, + if (exponentValue.canConvertTo(SI.ONE)) { + // then check if it is an integer, + final double exponent = exponentValue.getValue(); + 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."); + } + /** * The units in this system, excluding prefixes. * @@ -1025,7 +1152,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixlessUnits; - + /** * The unit prefixes in this system. * @@ -1033,7 +1160,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixes; - + /** * The dimensions in this system. * @@ -1041,7 +1168,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map> dimensions; - + /** * A map mapping strings to units (including prefixes) * @@ -1049,7 +1176,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * A parser that can parse unit expressions. * @@ -1059,21 +1186,41 @@ public final class UnitDatabase { private final ExpressionParser 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.times(o2), 1) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); - + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2) + .build(); + + /** + * A parser that can parse unit value expressions. + * + * @since 2020-08-04 + */ + private final ExpressionParser 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("^", UnitDatabase::exponentiateUnitValues, 2) + .build(); + /** * A parser that can parse unit prefix expressions * * @since 2019-04-13 * @since v0.2.0 */ - private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build(); - + private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>( + this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", + (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1) + .build(); + /** * A parser that can parse unit dimension expressions. * @@ -1081,9 +1228,10 @@ public final class UnitDatabase { * @since v0.2.0 */ private final ExpressionParser> unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") + this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); - + /** * Creates the {@code UnitsDatabase}. * @@ -1096,43 +1244,42 @@ public final class UnitDatabase { this.dimensions = new HashMap<>(); this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); } - + /** * 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 + * @param name dimension's name + * @param dimension dimension to add + * @throws NullPointerException if name or dimension is null * @since 2019-03-14 * @since v0.2.0 */ - public void addDimension(final String name, final ObjectProduct dimension) { - this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), + public void addDimension(final String name, + final ObjectProduct dimension) { + this.dimensions.put( + Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(dimension, "dimension must not be null.")); } - + /** * 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 + * @param line line to look at + * @param lineCounter number of line, for error messages * @since 2019-04-10 * @since v0.2.0 */ - private void addDimensionFromLine(final String line, final long lineCounter) { + private void addDimensionFromLine(final String line, + final long lineCounter) { // ignore lines that start with a # sign - they're comments if (line.isEmpty()) return; if (line.contains("#")) { - this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter); + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), + lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1141,17 +1288,18 @@ public final class UnitDatabase { lineCounter)); final String name = lineMatcher.group(1); final String expression = lineMatcher.group(2); - + if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter); + System.err.printf("Warning - line %d's dimension name ends in a space", + lineCounter); } - + // if expression is "!", search for an existing dimension // if no unit found, throw an error if (expression.equals("!")) { if (!this.containsDimensionName(name)) - throw new IllegalArgumentException( - String.format("! used but no dimension found (line %d).", lineCounter)); + throw new IllegalArgumentException(String.format( + "! used but no dimension found (line %d).", lineCounter)); } else { // it's a unit, get the unit final ObjectProduct dimension; @@ -1161,20 +1309,17 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - + this.addDimension(name, dimension); } } - + /** * 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 + * @param name prefix's name + * @param prefix prefix to add + * @throws NullPointerException if name or prefix is null * @since 2019-01-14 * @since v0.1.0 */ @@ -1182,43 +1327,41 @@ public final class UnitDatabase { this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(prefix, "prefix must not be null.")); } - + /** * Adds a unit to the database. * - * @param name - * unit's name - * @param unit - * unit to add - * @throws NullPointerException - * if unit is null + * @param name unit's name + * @param unit unit to add + * @throws NullPointerException if unit is null * @since 2019-01-10 * @since v0.1.0 */ public void addUnit(final String name, final Unit unit) { - this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."), + this.prefixlessUnits.put( + Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(unit, "unit must not be null.")); } - + /** * 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 + * @param line line to look at + * @param lineCounter number of line, for error messages * @since 2019-04-10 * @since v0.2.0 */ - private void addUnitOrPrefixFromLine(final String line, final long lineCounter) { + private void addUnitOrPrefixFromLine(final String line, + final long lineCounter) { // ignore lines that start with a # sign - they're comments if (line.isEmpty()) return; if (line.contains("#")) { - this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter); + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), + lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1226,18 +1369,20 @@ public final class UnitDatabase { "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 String expression = lineMatcher.group(2); - + if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's unit name ends in a space", lineCounter); + System.err.printf("Warning - line %d's unit name ends in a space", + lineCounter); } - + // if expression is "!", search for an existing unit // if no unit found, throw an error if (expression.equals("!")) { if (!this.containsUnitName(name)) - throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter)); + throw new IllegalArgumentException(String + .format("! used but no unit found (line %d).", lineCounter)); } else { if (name.endsWith("-")) { final UnitPrefix prefix; @@ -1257,17 +1402,16 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - + this.addUnit(name, unit); } } } - + /** * Tests if the database has a unit dimension with this name. * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-03-14 * @since v0.2.0 @@ -1275,12 +1419,11 @@ public final class UnitDatabase { public boolean containsDimensionName(final String name) { return this.dimensions.containsKey(name); } - + /** * Tests if the database has a unit prefix with this name. * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1288,12 +1431,12 @@ public final class UnitDatabase { public boolean containsPrefixName(final String name) { return this.prefixes.containsKey(name); } - + /** - * Tests if the database has a unit with this name, taking prefixes into consideration + * Tests if the database has a unit with this name, taking prefixes into + * consideration * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1301,7 +1444,7 @@ public final class UnitDatabase { public boolean containsUnitName(final String name) { return this.units.containsKey(name); } - + /** * @return a map mapping dimension names to dimensions * @since 2019-04-13 @@ -1310,7 +1453,50 @@ public final class UnitDatabase { public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } - + + /** + * 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 + */ + public LinearUnitValue evaluateUnitExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + 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 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); + } + /** * Gets a unit dimension from the database using its name. * @@ -1318,8 +1504,7 @@ public final class UnitDatabase { * This method accepts exponents, like "L^3" *

* - * @param name - * dimension's name + * @param name dimension's name * @return dimension * @since 2019-03-14 * @since v0.2.0 @@ -1328,86 +1513,87 @@ public final class UnitDatabase { Objects.requireNonNull(name, "name must not be null."); if (name.contains("^")) { final String[] baseAndExponent = name.split("\\^"); - - final ObjectProduct base = this.getDimension(baseAndExponent[0]); - + + final ObjectProduct base = this + .getDimension(baseAndExponent[0]); + final int exponent; try { - exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); + exponent = Integer + .parseInt(baseAndExponent[baseAndExponent.length - 1]); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Exponent must be an integer."); } - + return base.toExponent(exponent); } return this.dimensions.get(name); } - + /** * Uses the database's data to parse an expression into a unit dimension *

* The expression is a series of any of the following: *

    - *
  • The name of a unit dimension, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each - * other is equivalent to multiplication)
  • + *
  • The name of a unit dimension, which multiplies or divides the result + * based on preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two unit dimensions next to each other is equivalent to + * multiplication)
  • *
  • The operator '^' which exponentiates. Exponents must be integers.
  • *
* - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null * @since 2019-04-13 * @since v0.2.0 */ - public ObjectProduct getDimensionFromExpression(final String expression) { + public ObjectProduct getDimensionFromExpression( + final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a dimension as an alias first if (this.containsDimensionName(expression)) return this.getDimension(expression); - + // force operators to have spaces String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + return this.unitDimensionParser.parseExpression(modifiedExpression); } - + /** - * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an - * {@code IllegalArgumentException}. + * 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 + * @param name unit's name * @return unit * @since 2019-03-22 * @since v0.2.0 */ private LinearUnit getLinearUnit(final String name) { // see if I am using a function-unit like tempC(100) + Objects.requireNonNull(name, "name may not be null"); if (name.contains("(") && name.contains(")")) { // break it into function name and value final List parts = Arrays.asList(name.split("\\(")); if (parts.size() != 2) - throw new IllegalArgumentException("Format nonlinear units like: unit(value)."); - + throw new IllegalArgumentException( + "Format nonlinear units like: unit(value)."); + // solve the function final Unit unit = this.getUnit(parts.get(0)); - final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1)); + final double value = Double.parseDouble( + parts.get(1).substring(0, parts.get(1).length() - 1)); return LinearUnit.fromUnitValue(unit, value); } else { // get a linear unit @@ -1415,15 +1601,27 @@ public final class UnitDatabase { if (unit instanceof LinearUnit) return (LinearUnit) unit; else - throw new IllegalArgumentException(String.format("%s is not a linear unit.", name)); + 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 + */ + private LinearUnitValue getLinearUnitValue(final String name) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } + /** * Gets a unit prefix from the database from its name * - * @param name - * prefix's name + * @param name prefix's name * @return prefix * @since 2019-01-10 * @since v0.1.0 @@ -1435,53 +1633,45 @@ public final class UnitDatabase { return this.prefixes.get(name); } } - + /** * Gets a unit prefix from a prefix expression *

- * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of - * another prefix + * Currently, prefix expressions are much simpler than unit expressions: They + * are either a number or the name of another prefix *

* - * @param expression - * expression to input + * @param expression expression to input * @return prefix - * @throws IllegalArgumentException - * if expression cannot be parsed - * @throws NullPointerException - * if any argument is null + * @throws IllegalArgumentException if expression cannot be parsed + * @throws NullPointerException if any argument is null * @since 2019-01-14 * @since v0.1.0 */ public UnitPrefix getPrefixFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a unit as an alias first if (this.containsUnitName(expression)) return this.getPrefix(expression); - + // force operators to have spaces String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + return this.prefixExpressionParser.parseExpression(modifiedExpression); } - + /** * Gets a unit from the database from its name, looking for prefixes. * - * @param name - * unit's name + * @param name unit's name * @return unit * @since 2019-01-10 * @since v0.1.0 @@ -1491,98 +1681,113 @@ public final class UnitDatabase { final double value = Double.parseDouble(name); return SI.ONE.times(value); } catch (final NumberFormatException e) { - return this.units.get(name); + final Unit unit = this.units.get(name); + if (unit.getPrimaryName().isEmpty()) + return unit.withName(NameSymbol.ofName(name)); + else if (!unit.getPrimaryName().get().equals(name)) { + final Set 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)) { + final Set 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; } - + } - + /** * Uses the database's unit data to parse an expression into a unit *

* The expression is a series of any of the following: *

    - *
  • The name of a unit, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each - * other is equivalent to multiplication)
  • + *
  • The name of a unit, which multiplies or divides the result based on + * preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two units or values next to each other is equivalent to + * multiplication)
  • *
  • The operator '^' which exponentiates. Exponents must be integers.
  • *
  • A number which is multiplied or divided
  • *
* This method only works with linear units. * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null * @since 2019-01-07 * @since v0.1.0 */ public Unit getUnitFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a unit as an alias first if (this.containsUnitName(expression)) return this.getUnit(expression); - + // force operators to have spaces String modifiedExpression = expression; modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); modifiedExpression = modifiedExpression.replaceAll("-", " - "); - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry 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) for (int i = 0; i < modifiedExpression.length(); i++) { if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2)))) { + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2); + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); } } - + return this.unitExpressionParser.parseExpression(modifiedExpression); } - + /** - * Adds all dimensions from a file, using data from the database to parse them. + * Adds all dimensions from a file, using data from the database to parse + * them. *

- * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated - * by any number of tab characters. + * Each line in the file should consist of a name and an expression (parsed + * by getDimensionFromExpression) separated by any number of tab characters. *

*

* Allowed exceptions: *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • *
* - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null * @since 2019-01-13 * @since v0.1.0 */ public void loadDimensionFile(final File file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try (FileReader fileReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fileReader)) { + // while the reader has lines to read, read a line, then parse it, then + // add it long lineCounter = 0; while (reader.ready()) { this.addDimensionFromLine(reader.readLine(), ++lineCounter); @@ -1593,36 +1798,38 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * Adds all units from a file, using data from the database to parse them. *

- * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by - * any number of tab characters. + * Each line in the file should consist of a name and an expression (parsed + * by getUnitFromExpression) separated by any number of tab characters. *

*

* Allowed exceptions: *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • *
* - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null * @since 2019-01-13 * @since v0.1.0 */ public void loadUnitsFile(final File file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try (FileReader fileReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fileReader)) { + // while the reader has lines to read, read a line, then parse it, then + // add it long lineCounter = 0; while (reader.ready()) { this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); @@ -1633,7 +1840,7 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * @return a map mapping prefix names to prefixes * @since 2019-04-13 @@ -1642,33 +1849,40 @@ public final class UnitDatabase { public Map prefixMap() { return Collections.unmodifiableMap(this.prefixes); } - + /** - * @return a string stating the number of units, prefixes and dimensions in the database + * @return a string stating the number of units, prefixes and dimensions in + * the database */ @Override public String toString() { - return String.format("Unit Database with %d units, %d unit prefixes and %d dimensions", - this.prefixlessUnits.size(), this.prefixes.size(), this.dimensions.size()); + return String.format( + "Unit Database with %d units, %d unit prefixes and %d dimensions", + this.prefixlessUnits.size(), this.prefixes.size(), + this.dimensions.size()); } - + /** * Returns a map mapping unit names to units, including units with prefixes. *

- * The returned map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, - * some operations that only work with finite collections, like converting name/entry sets to arrays, will throw an - * {@code IllegalStateException}. + * The returned map is infinite in size if there is at least one unit and at + * least one prefix. If it is infinite, some operations that only work with + * finite collections, like converting name/entry sets to arrays, will throw + * an {@code IllegalStateException}. *

*

- * Specifically, the operations that will throw an IllegalStateException if the map is infinite in size are: + * Specifically, the operations that will throw an IllegalStateException if + * the map is infinite in size are: *

    *
  • {@code unitMap.entrySet().toArray()} (either overloading)
  • *
  • {@code unitMap.keySet().toArray()} (either overloading)
  • *
*

*

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

* * @return a map mapping unit names to units, including prefixed names @@ -1676,9 +1890,10 @@ public final class UnitDatabase { * @since v0.2.0 */ public Map unitMap() { - return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map. + return this.units; // PrefixedUnitMap is immutable so I don't need to make + // an unmodifiable map. } - + /** * @return a map mapping unit names to units, ignoring prefixes * @since 2019-04-13 -- cgit v1.2.3 From 618b81da627b55dcb051d889c7faffd91804497a Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Aug 2020 16:42:55 -0500 Subject: Added scientific rounding. --- .../converterGUI/UnitConverterGUI.java | 22 +++++++++++---------- src/org/unitConverter/unit/LinearUnitValue.java | 23 +++++++++++++++++++++- src/org/unitConverter/unit/UnitDatabase.java | 18 ++++++++++++++--- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 8c70df4..f7c3479 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.NoSuchElementException; import java.util.Set; import java.util.function.Predicate; @@ -270,14 +271,14 @@ final class UnitConverterGUI { final Unit to; try { from = this.database.evaluateUnitExpression(fromUnitString); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); return; } try { to = this.database.getUnitFromExpression(toUnitString); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); return; @@ -304,9 +305,10 @@ final class UnitConverterGUI { } final LinearUnitValue converted = from2.convertTo(to2); - this.view.setExpressionConverterOutputText( - (useSlash ? "1 / " : "") + String.format("%s = %s", - fromUnitString, this.getRoundedString(converted))); + this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + + String.format("%s = %s", fromUnitString, + this.getRoundedString(converted, false))); + final String toString = this.getRoundedString(converted, false); return; } else { // convert to UnitValue @@ -384,18 +386,19 @@ final class UnitConverterGUI { } /** - * Like {@link LinearUnitValue#toString(boolean)} with parameter - * {@code false}, but obeys this unit converter's rounding settings. + * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit + * converter's rounding settings. * * @since 2020-08-04 */ - private final String getRoundedString(final LinearUnitValue value) { + private final String getRoundedString(final LinearUnitValue value, + boolean showUncertainty) { switch (this.roundingType) { case DECIMAL_PLACES: case SIGNIFICANT_DIGITS: return this.getRoundedString(value.asUnitValue()); case SCIENTIFIC: - return value.toString(false); + return value.toString(showUncertainty); default: throw new AssertionError("Invalid switch condition."); } @@ -992,7 +995,6 @@ final class UnitConverterGUI { final JRadioButton relativePrecision = new JRadioButton( "Scientific Precision"); - relativePrecision.setEnabled(false); relativePrecision.addActionListener(e -> this.presenter .setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 7096738..5685a6d 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -367,7 +367,7 @@ public final class LinearUnitValue { final double baseUncertainty = this.unit.convertToBase(this.uncertainty); // get rounded strings - final String valueString, baseValueString, uncertaintyString, + String valueString, baseValueString, uncertaintyString, baseUncertaintyString; if (this.isExact()) { valueString = Double.toString(this.value); @@ -438,6 +438,27 @@ public final class LinearUnitValue { return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); } else { + // truncate excess zeroes + if (valueString.contains(".")) { + while (valueString.endsWith("0")) { + valueString = valueString.substring(0, valueString.length() - 1); + } + if (valueString.endsWith(".")) { + valueString = valueString.substring(0, valueString.length() - 1); + } + } + + if (baseValueString.contains(".")) { + while (baseValueString.endsWith("0")) { + baseValueString = baseValueString.substring(0, + baseValueString.length() - 1); + } + if (baseValueString.endsWith(".")) { + baseValueString = baseValueString.substring(0, + baseValueString.length() - 1); + } + } + if (primaryName.isEmpty() && symbol.isEmpty()) return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 0246630..c5432f7 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.math.BigDecimal; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; @@ -1465,7 +1466,7 @@ public final class UnitDatabase { public LinearUnitValue evaluateUnitExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - // attempt to get a unit as an alias first + // attempt to get a unit as an alias, or a number with precision first if (this.containsUnitName(expression)) return this.getLinearUnitValue(expression); @@ -1598,6 +1599,7 @@ public final class UnitDatabase { } else { // get a linear unit final Unit unit = this.getUnit(name); + if (unit instanceof LinearUnit) return (LinearUnit) unit; else @@ -1615,7 +1617,15 @@ public final class UnitDatabase { * @since 2020-08-04 */ private LinearUnitValue getLinearUnitValue(final String name) { - return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + try { + // try to parse it as a number - otherwise it is not a number! + final BigDecimal number = new BigDecimal(name); + + final double uncertainty = Math.pow(10, -number.scale()); + return LinearUnitValue.of(SI.ONE, number.doubleValue(), uncertainty); + } catch (final NumberFormatException e) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } } /** @@ -1682,7 +1692,9 @@ public final class UnitDatabase { return SI.ONE.times(value); } catch (final NumberFormatException e) { final Unit unit = this.units.get(name); - if (unit.getPrimaryName().isEmpty()) + if (unit == null) + throw new NoSuchElementException("No unit " + name); + else if (unit.getPrimaryName().isEmpty()) return unit.withName(NameSymbol.ofName(name)); else if (!unit.getPrimaryName().get().equals(name)) { final Set otherNames = new HashSet<>(unit.getOtherNames()); -- cgit v1.2.3 From 6d7d172e2e706da44c2b30177a04648671aad69e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 26 Aug 2020 12:37:12 -0500 Subject: Added the prefix repetition rule, and the two basic rules. --- .../converterGUI/DefaultPrefixRepetitionRule.java | 42 ++ .../converterGUI/UnitConverterGUI.java | 61 +-- src/org/unitConverter/unit/SI.java | 472 ++++++++++++++------- src/org/unitConverter/unit/UnitDatabase.java | 88 +++- src/org/unitConverter/unit/UnitDatabaseTest.java | 142 ++++--- 5 files changed, 542 insertions(+), 263 deletions(-) create mode 100644 src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java diff --git a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..34d8467 --- /dev/null +++ b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,42 @@ +/** + * @since 2020-08-26 + */ +package org.unitConverter.converterGUI; + +import java.util.List; +import java.util.function.Predicate; + +import org.unitConverter.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +enum DefaultPrefixRepetitionRule implements Predicate> { + NO_REPETITION { + @Override + public boolean test(List prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List prefixes) { + // TODO method stub + return false; + } + }; +} diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index f7c3479..eff0c47 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -149,7 +149,8 @@ final class UnitConverterGUI { this.view = view; // load initial units - this.database = new UnitDatabase(); + this.database = new UnitDatabase( + DefaultPrefixRepetitionRule.NO_RESTRICTION); Presenter.addDefaults(this.database); this.database.loadUnitsFile(new File("unitsfile.txt")); @@ -308,7 +309,6 @@ final class UnitConverterGUI { this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + String.format("%s = %s", fromUnitString, this.getRoundedString(converted, false))); - final String toString = this.getRoundedString(converted, false); return; } else { // convert to UnitValue @@ -346,45 +346,6 @@ final class UnitConverterGUI { return this.prefixNameComparator; } - /** - * @param value value to round - * @return string of that value rounded to {@code significantDigits} - * significant digits. - * @since 2019-04-14 - * @since v0.2.0 - */ - private final String getRoundedString(final BigDecimal value) { - // round value based on rounding type - final BigDecimal roundedValue; - switch (this.roundingType) { - case DECIMAL_PLACES: - roundedValue = value.setScale(this.precision, - RoundingMode.HALF_EVEN); - break; - case SCIENTIFIC: - throw new UnsupportedOperationException("Not yet implemented."); - case SIGNIFICANT_DIGITS: - roundedValue = value.round(new MathContext(this.precision)); - break; - default: - throw new AssertionError("Invalid switch condition."); - } - - String output = roundedValue.toString(); - - // remove trailing zeroes - if (output.contains(".")) { - while (output.endsWith("0")) { - output = output.substring(0, output.length() - 1); - } - if (output.endsWith(".")) { - output = output.substring(0, output.length() - 1); - } - } - - return output; - } - /** * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit * converter's rounding settings. @@ -414,6 +375,7 @@ final class UnitConverterGUI { final BigDecimal unrounded = new BigDecimal(value.getValue()); final BigDecimal rounded; int precision = this.precision; + switch (this.roundingType) { case DECIMAL_PLACES: rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); @@ -483,6 +445,15 @@ final class UnitConverterGUI { this.precision = precision; } + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + this.database.setPrefixRepetitionRule(prefixRepetitionRule); + } + /** * @param roundingType the roundingType to set * @since 2020-07-16 @@ -1031,7 +1002,9 @@ final class UnitConverterGUI { final JRadioButton noRepetition = new JRadioButton( "No Repetition"); - noRepetition.setEnabled(false); + noRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_REPETITION)); prefixRuleButtons.add(noRepetition); prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0) @@ -1041,7 +1014,9 @@ final class UnitConverterGUI { final JRadioButton noRestriction = new JRadioButton( "No Restriction"); noRestriction.setSelected(true); - noRestriction.setEnabled(false); + noRestriction.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_RESTRICTION)); prefixRuleButtons.add(noRestriction); prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1) diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java index 1a9eaf1..a4fbd5f 100644 --- a/src/org/unitConverter/unit/SI.java +++ b/src/org/unitConverter/unit/SI.java @@ -16,13 +16,19 @@ */ package org.unitConverter.unit; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.unitConverter.math.ObjectProduct; /** - * All of the units, prefixes and dimensions that are used by the SI, as well as some outside the SI. + * All of the units, prefixes and dimensions that are used by the SI, as well as + * some outside the SI. * *

- * This class does not include prefixed units. To obtain prefixed units, use {@link LinearUnit#withPrefix}: + * This class does not include prefixed units. To obtain prefixed units, use + * {@link LinearUnit#withPrefix}: * *

  * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
@@ -36,42 +42,61 @@ public final class SI {
 	/// dimensions used by SI units
 	// base dimensions, as BaseDimensions
 	public static final class BaseDimensions {
-		public static final BaseDimension LENGTH = BaseDimension.valueOf("Length", "L");
-		public static final BaseDimension MASS = BaseDimension.valueOf("Mass", "M");
-		public static final BaseDimension TIME = BaseDimension.valueOf("Time", "T");
-		public static final BaseDimension ELECTRIC_CURRENT = BaseDimension.valueOf("Electric Current", "I");
-		public static final BaseDimension TEMPERATURE = BaseDimension.valueOf("Temperature", "\u0398"); // theta symbol
-		public static final BaseDimension QUANTITY = BaseDimension.valueOf("Quantity", "N");
-		public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension.valueOf("Luminous Intensity", "J");
-		public static final BaseDimension INFORMATION = BaseDimension.valueOf("Information", "Info"); // non-SI
-		public static final BaseDimension CURRENCY = BaseDimension.valueOf("Currency", "$$"); // non-SI
-
+		public static final BaseDimension LENGTH = BaseDimension.valueOf("Length",
+				"L");
+		public static final BaseDimension MASS = BaseDimension.valueOf("Mass",
+				"M");
+		public static final BaseDimension TIME = BaseDimension.valueOf("Time",
+				"T");
+		public static final BaseDimension ELECTRIC_CURRENT = BaseDimension
+				.valueOf("Electric Current", "I");
+		public static final BaseDimension TEMPERATURE = BaseDimension
+				.valueOf("Temperature", "\u0398"); // theta symbol
+		public static final BaseDimension QUANTITY = BaseDimension
+				.valueOf("Quantity", "N");
+		public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension
+				.valueOf("Luminous Intensity", "J");
+		public static final BaseDimension INFORMATION = BaseDimension
+				.valueOf("Information", "Info"); // non-SI
+		public static final BaseDimension CURRENCY = BaseDimension
+				.valueOf("Currency", "$$"); // non-SI
+		
 		// You may NOT get SI.BaseDimensions instances!
 		private BaseDimensions() {
 			throw new AssertionError();
 		}
 	}
-
+	
 	/// 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)
 	@SuppressWarnings("hiding")
 	public static final class BaseUnits {
-		public static final BaseUnit METRE = BaseUnit.valueOf(BaseDimensions.LENGTH, "metre", "m");
-		public static final BaseUnit KILOGRAM = BaseUnit.valueOf(BaseDimensions.MASS, "kilogram", "kg");
-		public static final BaseUnit SECOND = BaseUnit.valueOf(BaseDimensions.TIME, "second", "s");
-		public static final BaseUnit AMPERE = BaseUnit.valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
-		public static final BaseUnit KELVIN = BaseUnit.valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
-		public static final BaseUnit MOLE = BaseUnit.valueOf(BaseDimensions.QUANTITY, "mole", "mol");
-		public static final BaseUnit CANDELA = BaseUnit.valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
-		public static final BaseUnit BIT = BaseUnit.valueOf(BaseDimensions.INFORMATION, "bit", "b");
-		public static final BaseUnit DOLLAR = BaseUnit.valueOf(BaseDimensions.CURRENCY, "dollar", "$");
-
+		public static final BaseUnit METRE = BaseUnit
+				.valueOf(BaseDimensions.LENGTH, "metre", "m");
+		public static final BaseUnit KILOGRAM = BaseUnit
+				.valueOf(BaseDimensions.MASS, "kilogram", "kg");
+		public static final BaseUnit SECOND = BaseUnit
+				.valueOf(BaseDimensions.TIME, "second", "s");
+		public static final BaseUnit AMPERE = BaseUnit
+				.valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
+		public static final BaseUnit KELVIN = BaseUnit
+				.valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
+		public static final BaseUnit MOLE = BaseUnit
+				.valueOf(BaseDimensions.QUANTITY, "mole", "mol");
+		public static final BaseUnit CANDELA = BaseUnit
+				.valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
+		public static final BaseUnit BIT = BaseUnit
+				.valueOf(BaseDimensions.INFORMATION, "bit", "b");
+		public static final BaseUnit DOLLAR = BaseUnit
+				.valueOf(BaseDimensions.CURRENCY, "dollar", "$");
+		
 		// You may NOT get SI.BaseUnits instances!
 		private BaseUnits() {
 			throw new AssertionError();
 		}
 	}
-
+	
 	/**
 	 * Constants that relate to the SI or other systems.
 	 * 
@@ -79,189 +104,320 @@ public final class SI {
 	 * @since 2019-11-08
 	 */
 	public static final class Constants {
-		public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND).dividedBy(SECOND).times(9.80665);
+		public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND)
+				.dividedBy(SECOND).times(9.80665);
 	}
-
+	
 	// dimensions used in the SI, as ObjectProducts
 	public static final class Dimensions {
-		public static final ObjectProduct EMPTY = ObjectProduct.empty();
-		public static final ObjectProduct LENGTH = ObjectProduct.oneOf(BaseDimensions.LENGTH);
-		public static final ObjectProduct MASS = ObjectProduct.oneOf(BaseDimensions.MASS);
-		public static final ObjectProduct TIME = ObjectProduct.oneOf(BaseDimensions.TIME);
+		public static final ObjectProduct EMPTY = ObjectProduct
+				.empty();
+		public static final ObjectProduct LENGTH = ObjectProduct
+				.oneOf(BaseDimensions.LENGTH);
+		public static final ObjectProduct MASS = ObjectProduct
+				.oneOf(BaseDimensions.MASS);
+		public static final ObjectProduct TIME = ObjectProduct
+				.oneOf(BaseDimensions.TIME);
 		public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct
 				.oneOf(BaseDimensions.ELECTRIC_CURRENT);
-		public static final ObjectProduct TEMPERATURE = ObjectProduct.oneOf(BaseDimensions.TEMPERATURE);
-		public static final ObjectProduct QUANTITY = ObjectProduct.oneOf(BaseDimensions.QUANTITY);
+		public static final ObjectProduct TEMPERATURE = ObjectProduct
+				.oneOf(BaseDimensions.TEMPERATURE);
+		public static final ObjectProduct QUANTITY = ObjectProduct
+				.oneOf(BaseDimensions.QUANTITY);
 		public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct
 				.oneOf(BaseDimensions.LUMINOUS_INTENSITY);
-		public static final ObjectProduct INFORMATION = ObjectProduct.oneOf(BaseDimensions.INFORMATION);
-		public static final ObjectProduct CURRENCY = ObjectProduct.oneOf(BaseDimensions.CURRENCY);
+		public static final ObjectProduct INFORMATION = ObjectProduct
+				.oneOf(BaseDimensions.INFORMATION);
+		public static final ObjectProduct CURRENCY = ObjectProduct
+				.oneOf(BaseDimensions.CURRENCY);
 		// derived dimensions without named SI units
-		public static final ObjectProduct AREA = LENGTH.times(LENGTH);
-
-		public static final ObjectProduct VOLUME = AREA.times(LENGTH);
-		public static final ObjectProduct VELOCITY = LENGTH.dividedBy(TIME);
-		public static final ObjectProduct ACCELERATION = VELOCITY.dividedBy(TIME);
-		public static final ObjectProduct WAVENUMBER = EMPTY.dividedBy(LENGTH);
-		public static final ObjectProduct MASS_DENSITY = MASS.dividedBy(VOLUME);
-		public static final ObjectProduct SURFACE_DENSITY = MASS.dividedBy(AREA);
-		public static final ObjectProduct SPECIFIC_VOLUME = VOLUME.dividedBy(MASS);
-		public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA);
-		public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH);
-		public static final ObjectProduct CONCENTRATION = QUANTITY.dividedBy(VOLUME);
-		public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION.times(MASS);
-		public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA);
-		public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY);
-		public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY);
-		public static final ObjectProduct ANGLE = LENGTH.dividedBy(LENGTH);
-		public static final ObjectProduct SOLID_ANGLE = AREA.dividedBy(AREA);
-
+		public static final ObjectProduct AREA = LENGTH
+				.times(LENGTH);
+		
+		public static final ObjectProduct VOLUME = AREA
+				.times(LENGTH);
+		public static final ObjectProduct VELOCITY = LENGTH
+				.dividedBy(TIME);
+		public static final ObjectProduct ACCELERATION = VELOCITY
+				.dividedBy(TIME);
+		public static final ObjectProduct WAVENUMBER = EMPTY
+				.dividedBy(LENGTH);
+		public static final ObjectProduct MASS_DENSITY = MASS
+				.dividedBy(VOLUME);
+		public static final ObjectProduct SURFACE_DENSITY = MASS
+				.dividedBy(AREA);
+		public static final ObjectProduct SPECIFIC_VOLUME = VOLUME
+				.dividedBy(MASS);
+		public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT
+				.dividedBy(AREA);
+		public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT
+				.dividedBy(LENGTH);
+		public static final ObjectProduct CONCENTRATION = QUANTITY
+				.dividedBy(VOLUME);
+		public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION
+				.times(MASS);
+		public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY
+				.dividedBy(AREA);
+		public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY
+				.dividedBy(VELOCITY);
+		public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY
+				.times(EMPTY);
+		public static final ObjectProduct ANGLE = LENGTH
+				.dividedBy(LENGTH);
+		public static final ObjectProduct SOLID_ANGLE = AREA
+				.dividedBy(AREA);
+		
 		// derived dimensions with named SI units
-		public static final ObjectProduct FREQUENCY = EMPTY.dividedBy(TIME);
-		public static final ObjectProduct FORCE = MASS.times(ACCELERATION);
-		public static final ObjectProduct ENERGY = FORCE.times(LENGTH);
-		public static final ObjectProduct POWER = ENERGY.dividedBy(TIME);
-		public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME);
-		public static final ObjectProduct VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE);
-		public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE);
-		public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT);
-		public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE);
-		public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE.times(TIME);
-		public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA);
-		public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT);
-		public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE);
-		public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA);
-		public static final ObjectProduct SPECIFIC_ENERGY = ENERGY.dividedBy(MASS);
-		public static final ObjectProduct CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME);
-
+		public static final ObjectProduct FREQUENCY = EMPTY
+				.dividedBy(TIME);
+		public static final ObjectProduct FORCE = MASS
+				.times(ACCELERATION);
+		public static final ObjectProduct ENERGY = FORCE
+				.times(LENGTH);
+		public static final ObjectProduct POWER = ENERGY
+				.dividedBy(TIME);
+		public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT
+				.times(TIME);
+		public static final ObjectProduct VOLTAGE = ENERGY
+				.dividedBy(ELECTRIC_CHARGE);
+		public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE
+				.dividedBy(VOLTAGE);
+		public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE
+				.dividedBy(ELECTRIC_CURRENT);
+		public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT
+				.dividedBy(VOLTAGE);
+		public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE
+				.times(TIME);
+		public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX
+				.dividedBy(AREA);
+		public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX
+				.dividedBy(ELECTRIC_CURRENT);
+		public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY
+				.times(SOLID_ANGLE);
+		public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX
+				.dividedBy(AREA);
+		public static final ObjectProduct SPECIFIC_ENERGY = ENERGY
+				.dividedBy(MASS);
+		public static final ObjectProduct CATALYTIC_ACTIVITY = QUANTITY
+				.dividedBy(TIME);
+		
 		// You may NOT get SI.Dimension instances!
 		private Dimensions() {
 			throw new AssertionError();
 		}
 	}
-
+	
 	/// The units of the SI
-	public static final LinearUnit ONE = LinearUnit.valueOf(ObjectProduct.empty(), 1);
+	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()
 			.withName(NameSymbol.of("kilogram", "kg"));
 	public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit()
 			.withName(NameSymbol.of("second", "s", "sec"));
-	public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit().withName(NameSymbol.of("ampere", "A"));
-	public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit().withName(NameSymbol.of("kelvin", "K"));
-	public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit().withName(NameSymbol.of("mole", "mol"));
-	public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit().withName(NameSymbol.of("candela", "cd"));
-	public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit().withName(NameSymbol.of("bit", "b"));
-	public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit().withName(NameSymbol.of("dollar", "$"));
-
+	public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit()
+			.withName(NameSymbol.of("ampere", "A"));
+	public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit()
+			.withName(NameSymbol.of("kelvin", "K"));
+	public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit()
+			.withName(NameSymbol.of("mole", "mol"));
+	public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit()
+			.withName(NameSymbol.of("candela", "cd"));
+	public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit()
+			.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).withName(NameSymbol.of("hertz", "Hz"));
+	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)
+			.withName(NameSymbol.of("hertz", "Hz"));
 	// for periodic phenomena
-	public static final LinearUnit NEWTON = KILOGRAM.times(METRE).dividedBy(SECOND.times(SECOND))
+	public static final LinearUnit NEWTON = KILOGRAM.times(METRE)
+			.dividedBy(SECOND.times(SECOND))
 			.withName(NameSymbol.of("newton", "N"));
 	public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE))
 			.withName(NameSymbol.of("pascal", "Pa"));
-	public static final LinearUnit JOULE = NEWTON.times(METRE).withName(NameSymbol.of("joule", "J"));
-	public static final LinearUnit WATT = JOULE.dividedBy(SECOND).withName(NameSymbol.of("watt", "W"));
-	public static final LinearUnit COULOMB = AMPERE.times(SECOND).withName(NameSymbol.of("coulomb", "C"));
-	public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB).withName(NameSymbol.of("volt", "V"));
-	public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT).withName(NameSymbol.of("farad", "F"));
-	public static final LinearUnit OHM = VOLT.dividedBy(AMPERE).withName(NameSymbol.of("ohm", "\u03A9")); // omega
-	public static final LinearUnit SIEMENS = ONE.dividedBy(OHM).withName(NameSymbol.of("siemens", "S"));
-	public static final LinearUnit WEBER = VOLT.times(SECOND).withName(NameSymbol.of("weber", "Wb"));
-	public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("tesla", "T"));
-	public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE).withName(NameSymbol.of("henry", "H"));
-	public static final LinearUnit LUMEN = CANDELA.times(STERADIAN).withName(NameSymbol.of("lumen", "lm"));
-	public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("lux", "lx"));
-	public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND).withName(NameSymbol.of("bequerel", "Bq"));
+	public static final LinearUnit JOULE = NEWTON.times(METRE)
+			.withName(NameSymbol.of("joule", "J"));
+	public static final LinearUnit WATT = JOULE.dividedBy(SECOND)
+			.withName(NameSymbol.of("watt", "W"));
+	public static final LinearUnit COULOMB = AMPERE.times(SECOND)
+			.withName(NameSymbol.of("coulomb", "C"));
+	public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB)
+			.withName(NameSymbol.of("volt", "V"));
+	public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT)
+			.withName(NameSymbol.of("farad", "F"));
+	public static final LinearUnit OHM = VOLT.dividedBy(AMPERE)
+			.withName(NameSymbol.of("ohm", "\u03A9")); // omega
+	public static final LinearUnit SIEMENS = ONE.dividedBy(OHM)
+			.withName(NameSymbol.of("siemens", "S"));
+	public static final LinearUnit WEBER = VOLT.times(SECOND)
+			.withName(NameSymbol.of("weber", "Wb"));
+	public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE))
+			.withName(NameSymbol.of("tesla", "T"));
+	public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE)
+			.withName(NameSymbol.of("henry", "H"));
+	public static final LinearUnit LUMEN = CANDELA.times(STERADIAN)
+			.withName(NameSymbol.of("lumen", "lm"));
+	public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE))
+			.withName(NameSymbol.of("lux", "lx"));
+	public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND)
+			.withName(NameSymbol.of("bequerel", "Bq"));
 	// for activity referred to a nucleotide
-	public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("grey", "Gy"));
+	public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM)
+			.withName(NameSymbol.of("grey", "Gy"));
 	// for absorbed dose
-	public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("sievert", "Sv"));
+	public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM)
+			.withName(NameSymbol.of("sievert", "Sv"));
 	// for dose equivalent
-	public static final LinearUnit KATAL = MOLE.dividedBy(SECOND).withName(NameSymbol.of("katal", "kat"));
-
+	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 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"));
+			.withName(NameSymbol.of("square metre", "m^2", "square meter",
+					"metre squared", "meter squared"));
 	public static final LinearUnit CUBIC_METRE = METRE.toExponent(3)
-			.withName(NameSymbol.of("cubic metre", "m^3", "cubic meter", "metre cubed", "meter cubed"));
+			.withName(NameSymbol.of("cubic metre", "m^3", "cubic meter",
+					"metre cubed", "meter cubed"));
 	public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND)
-			.withName(NameSymbol.of("metre per second", "m/s", "meter per 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)
+			.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).withName(NameSymbol.of("hour", "h", "hr"));
-	public static final LinearUnit DAY = HOUR.times(60).withName(NameSymbol.of("day", "d"));
-	public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000).dividedBy(HOUR)
-			.withName(NameSymbol.of("kilometre per hour", "km/h", "kilometer per hour"));
+	public static final LinearUnit MINUTE = SECOND.times(60)
+			.withName(NameSymbol.of("minute", "min"));
+	public static final LinearUnit HOUR = MINUTE.times(60)
+			.withName(NameSymbol.of("hour", "h", "hr"));
+	public static final LinearUnit DAY = HOUR.times(60)
+			.withName(NameSymbol.of("day", "d"));
+	public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000)
+			.dividedBy(HOUR).withName(NameSymbol.of("kilometre per hour", "km/h",
+					"kilometer per hour"));
 	public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI))
 			.withName(NameSymbol.of("degree", "\u00B0", "deg"));
-	public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60).withName(NameSymbol.of("arcminute", "arcmin"));
-	public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60).withName(NameSymbol.of("arcsecond", "arcsec"));
-	public static final LinearUnit ASTRONOMICAL_UNIT = METRE.times(149597870700.0)
+	public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60)
+			.withName(NameSymbol.of("arcminute", "arcmin"));
+	public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60)
+			.withName(NameSymbol.of("arcsecond", "arcsec"));
+	public static final LinearUnit ASTRONOMICAL_UNIT = METRE
+			.times(149597870700.0)
 			.withName(NameSymbol.of("astronomical unit", "au"));
-	public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT.dividedBy(ARCSECOND)
-			.withName(NameSymbol.of("parsec", "pc"));
-	public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0).withName(NameSymbol.of("hectare", "ha"));
-	public static final LinearUnit LITRE = METRE.times(METRE).times(METRE).dividedBy(1000.0)
-			.withName(NameSymbol.of("litre", "L", "l", "liter"));
-	public static final LinearUnit TONNE = KILOGRAM.times(1000.0).withName(NameSymbol.of("tonne", "t", "metric ton"));
+	public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT
+			.dividedBy(ARCSECOND).withName(NameSymbol.of("parsec", "pc"));
+	public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0)
+			.withName(NameSymbol.of("hectare", "ha"));
+	public static final LinearUnit LITRE = METRE.times(METRE).times(METRE)
+			.dividedBy(1000.0).withName(NameSymbol.of("litre", "L", "l", "liter"));
+	public static final LinearUnit TONNE = KILOGRAM.times(1000.0)
+			.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
+			.withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate
+																								// 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).withName(NameSymbol.of("byte", "B"));
-	public static final Unit NEPER = Unit
-			.fromConversionFunctions(ONE.getBase(), pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
+	public static final LinearUnit BYTE = BIT.times(8)
+			.withName(NameSymbol.of("byte", "B"));
+	public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(),
+			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))
+	public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(),
+			pr -> Math.log10(pr), dB -> Math.pow(10, dB))
 			.withName(NameSymbol.of("bel", "B"));
 	public static final Unit DECIBEL = Unit
-			.fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), dB -> Math.pow(10, dB / 10))
+			.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).withName(NameSymbol.of("giga", "G"));
-	public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12).withName(NameSymbol.of("tera", "T"));
-	public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15).withName(NameSymbol.of("peta", "P"));
-	public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18).withName(NameSymbol.of("exa", "E"));
-	public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21).withName(NameSymbol.of("zetta", "Z"));
-	public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24).withName(NameSymbol.of("yotta", "Y"));
-
+	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)
+			.withName(NameSymbol.of("giga", "G"));
+	public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12)
+			.withName(NameSymbol.of("tera", "T"));
+	public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15)
+			.withName(NameSymbol.of("peta", "P"));
+	public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18)
+			.withName(NameSymbol.of("exa", "E"));
+	public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21)
+			.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).withName(NameSymbol.of("nano", "n"));
-	public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12).withName(NameSymbol.of("pico", "p"));
-	public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15).withName(NameSymbol.of("femto", "f"));
-	public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18).withName(NameSymbol.of("atto", "a"));
-	public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21).withName(NameSymbol.of("zepto", "z"));
-	public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24).withName(NameSymbol.of("yocto", "y"));
-
+	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)
+			.withName(NameSymbol.of("nano", "n"));
+	public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12)
+			.withName(NameSymbol.of("pico", "p"));
+	public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15)
+			.withName(NameSymbol.of("femto", "f"));
+	public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18)
+			.withName(NameSymbol.of("atto", "a"));
+	public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21)
+			.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).withName(NameSymbol.of("deci", "d"));
-	public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2).withName(NameSymbol.of("centi", "c"));
-	public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024).withName(NameSymbol.of("kibi", "Ki"));
-	public static final UnitPrefix MEBI = KIBI.times(1024).withName(NameSymbol.of("mebi", "Mi"));
-	public static final UnitPrefix GIBI = MEBI.times(1024).withName(NameSymbol.of("gibi", "Gi"));
-	public static final UnitPrefix TEBI = GIBI.times(1024).withName(NameSymbol.of("tebi", "Ti"));
-	public static final UnitPrefix PEBI = TEBI.times(1024).withName(NameSymbol.of("pebi", "Pi"));
-	public static final UnitPrefix EXBI = PEBI.times(1024).withName(NameSymbol.of("exbi", "Ei"));
-
+	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)
+			.withName(NameSymbol.of("deci", "d"));
+	public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2)
+			.withName(NameSymbol.of("centi", "c"));
+	public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024)
+			.withName(NameSymbol.of("kibi", "Ki"));
+	public static final UnitPrefix MEBI = KIBI.times(1024)
+			.withName(NameSymbol.of("mebi", "Mi"));
+	public static final UnitPrefix GIBI = MEBI.times(1024)
+			.withName(NameSymbol.of("gibi", "Gi"));
+	public static final UnitPrefix TEBI = GIBI.times(1024)
+			.withName(NameSymbol.of("tebi", "Ti"));
+	public static final UnitPrefix PEBI = TEBI.times(1024)
+			.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 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 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 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 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 REDUCING_PREFIXES = new HashSet<>(
+			Arrays.asList(DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO,
+					ZEPTO, YOCTO));
+	
 	// You may NOT get SI instances!
 	private SI() {
 		throw new AssertionError();
diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java
index c5432f7..56846a1 100644
--- a/src/org/unitConverter/unit/UnitDatabase.java
+++ b/src/org/unitConverter/unit/UnitDatabase.java
@@ -42,6 +42,7 @@ import java.util.function.Predicate;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.unitConverter.math.ConditionalExistenceCollections;
 import org.unitConverter.math.DecimalComparison;
 import org.unitConverter.math.ExpressionParser;
 import org.unitConverter.math.ObjectProduct;
@@ -1178,6 +1179,18 @@ public final class UnitDatabase {
 	 */
 	private final Map units;
 	
+	/**
+	 * The rule that specifies when prefix repetition is allowed. It takes in one
+	 * argument: a list of the prefixes being applied to the unit
+	 * 

+ * The prefixes are inputted in application order. This means that + * testing whether "kilomegagigametre" is a valid unit is equivalent to + * running the following code (assuming all variables are defined correctly): + *
+ * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} + */ + private Predicate> prefixRepetitionRule; + /** * A parser that can parse unit expressions. * @@ -1240,10 +1253,25 @@ public final class UnitDatabase { * @since v0.1.0 */ public UnitDatabase() { + this(prefixes -> true); + } + + /** + * Creates the {@code UnitsDatabase} + * + * @param prefixRepetitionRule the rule that determines when prefix + * repetition is allowed + * @since 2020-08-26 + */ + public UnitDatabase(Predicate> prefixRepetitionRule) { this.prefixlessUnits = new HashMap<>(); this.prefixes = new HashMap<>(); this.dimensions = new HashMap<>(); - this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); + this.prefixRepetitionRule = prefixRepetitionRule; + this.units = ConditionalExistenceCollections.conditionalExistenceMap( + new PrefixedUnitMap(this.prefixlessUnits, this.prefixes), + entry -> this.prefixRepetitionRule + .test(this.getPrefixesFromName(entry.getKey()))); } /** @@ -1644,6 +1672,47 @@ public final class UnitDatabase { } } + /** + * 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 + */ + List getPrefixesFromName(final String unitName) { + if (this.prefixlessUnits.containsKey(unitName)) + return new ArrayList<>(); + else { + // find the longest prefix + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsUnitName(rest) + && this.getUnit(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + final List prefixes = this + .getPrefixesFromName(unitName.substring(longestLength)); + prefixes.add(this.getPrefix(longestPrefix)); + return prefixes; + } + } + /** * Gets a unit prefix from a prefix expression *

@@ -1678,6 +1747,14 @@ public final class UnitDatabase { return this.prefixExpressionParser.parseExpression(modifiedExpression); } + /** + * @return the prefixRepetitionRule + * @since 2020-08-26 + */ + public final Predicate> getPrefixRepetitionRule() { + return this.prefixRepetitionRule; + } + /** * Gets a unit from the database from its name, looking for prefixes. * @@ -1862,6 +1939,15 @@ public final class UnitDatabase { return Collections.unmodifiableMap(this.prefixes); } + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public final void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + } + /** * @return a string stating the number of units, prefixes and dimensions in * the database diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java index 164172b..96a0b83 100644 --- a/src/org/unitConverter/unit/UnitDatabaseTest.java +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -18,17 +18,22 @@ package org.unitConverter.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 static org.junit.jupiter.api.Assertions.fail; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; /** - * A test for the {@link UnitDatabase} class. This is NOT part of this program's public API. + * A test for the {@link UnitDatabase} class. This is NOT part of this program's + * public API. * * @author Adrien Hopkins * @since 2019-04-14 @@ -39,23 +44,26 @@ class UnitDatabaseTest { private static final Unit U = SI.METRE; private static final Unit V = SI.KILOGRAM; private static final Unit W = SI.SECOND; - + // used for testing expressions // J = U^2 * V / W^2 - private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); + private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)) + .dividedBy(SI.SECOND.toExponent(2)); private static final LinearUnit K = SI.KELVIN; - - private static final Unit NONLINEAR = Unit.fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); - + + private static final Unit NONLINEAR = Unit + .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); + // make the prefix values prime so I can tell which multiplications were made private static final UnitPrefix A = UnitPrefix.valueOf(2); private static final UnitPrefix B = UnitPrefix.valueOf(3); private static final UnitPrefix C = UnitPrefix.valueOf(5); private static final UnitPrefix AB = UnitPrefix.valueOf(7); private static final UnitPrefix BC = UnitPrefix.valueOf(11); - + /** - * Confirms that operations that shouldn't function for infinite databases throw an {@code IllegalStateException}. + * Confirms that operations that shouldn't function for infinite databases + * throw an {@code IllegalStateException}. * * @since 2019-05-03 */ @@ -63,14 +71,14 @@ class UnitDatabaseTest { public void testInfiniteSetExceptions() { // load units final UnitDatabase infiniteDatabase = new UnitDatabase(); - + infiniteDatabase.addUnit("J", J); infiniteDatabase.addUnit("K", K); - + infiniteDatabase.addPrefix("A", A); infiniteDatabase.addPrefix("B", B); infiniteDatabase.addPrefix("C", C); - + { boolean exceptionThrown = false; try { @@ -84,7 +92,7 @@ class UnitDatabaseTest { } } } - + { boolean exceptionThrown = false; try { @@ -99,7 +107,7 @@ class UnitDatabaseTest { } } } - + /** * Test that prefixes correctly apply to units. * @@ -109,23 +117,28 @@ class UnitDatabaseTest { @Test public void testPrefixes() { final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + + // test the getPrefixesFromName method + final List expected = Arrays.asList(C, B, A); + assertEquals(expected, database.getPrefixesFromName("ABCU")); + // get the product final Unit abcuNonlinear = database.getUnit("ABCU"); assert abcuNonlinear instanceof LinearUnit; - + final LinearUnit abcu = (LinearUnit) abcuNonlinear; - assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15); + assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), + abcu.getConversionFactor(), 1e-15); } - + /** * Tests the functionnalites of the prefixless unit map. * @@ -140,21 +153,22 @@ class UnitDatabaseTest { public void testPrefixlessUnitMap() { final UnitDatabase database = new UnitDatabase(); final Map prefixlessUnits = database.unitMapPrefixless(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + // this should work because the map should be an auto-updating view assertTrue(prefixlessUnits.containsKey("U")); assertFalse(prefixlessUnits.containsKey("Z")); - + assertTrue(prefixlessUnits.containsValue(U)); assertFalse(prefixlessUnits.containsValue(NONLINEAR)); } - + /** - * Tests that the database correctly stores and retrieves units, ignoring prefixes. + * Tests that the database correctly stores and retrieves units, ignoring + * prefixes. * * @since 2019-04-14 * @since v0.2.0 @@ -162,18 +176,18 @@ class UnitDatabaseTest { @Test public void testPrefixlessUnits() { final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + assertTrue(database.containsUnitName("U")); assertFalse(database.containsUnitName("Z")); - + assertEquals(U, database.getUnit("U")); - assertEquals(null, database.getUnit("Z")); + assertThrows(NoSuchElementException.class, () -> database.getUnit("Z")); } - + /** * Test that unit expressions return the correct value. * @@ -184,30 +198,31 @@ class UnitDatabaseTest { public void testUnitExpressions() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); database.addUnit("fj", J.times(5)); database.addUnit("ej", J.times(8)); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + // first test - test prefixes and operations - final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C); + final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C) + .withPrefix(C); final Unit 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"); - + assertEquals(expected2, actual2); } - + /** * Tests both the unit name iterator and the name-unit entry iterator * @@ -218,59 +233,64 @@ class UnitDatabaseTest { public void testUnitIterator() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("J", J); database.addUnit("K", K); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + final int NUM_UNITS = database.unitMapPrefixless().size(); final int NUM_PREFIXES = database.prefixMap().size(); - - final Iterator nameIterator = database.unitMap().keySet().iterator(); - final Iterator> entryIterator = database.unitMap().entrySet().iterator(); - + + final Iterator nameIterator = database.unitMap().keySet() + .iterator(); + final Iterator> entryIterator = database.unitMap() + .entrySet().iterator(); + int expectedLength = 1; int unitsWithThisLengthSoFar = 0; - + // loop 1000 times for (int i = 0; i < 1000; i++) { // expected length of next - if (unitsWithThisLengthSoFar >= NUM_UNITS * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { + if (unitsWithThisLengthSoFar >= NUM_UNITS + * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { expectedLength++; unitsWithThisLengthSoFar = 0; } - + // test that stuff is valid final String nextName = nameIterator.next(); final Unit nextUnit = database.getUnit(nextName); final Entry nextEntry = entryIterator.next(); - + assertEquals(expectedLength, nextName.length()); assertEquals(nextName, nextEntry.getKey()); assertEquals(nextUnit, nextEntry.getValue()); - + unitsWithThisLengthSoFar++; } - + // test toString for consistency final String entryIteratorString = entryIterator.toString(); for (int i = 0; i < 3; i++) { assertEquals(entryIteratorString, entryIterator.toString()); } - + final String nameIteratorString = nameIterator.toString(); for (int i = 0; i < 3; i++) { assertEquals(nameIteratorString, nameIterator.toString()); } } - + /** - * Determine, given a unit name that could mean multiple things, which meaning is chosen. + * Determine, given a unit name that could mean multiple things, which + * meaning is chosen. *

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

* * @since 2019-04-14 @@ -280,28 +300,28 @@ class UnitDatabaseTest { public void testUnitPrefixCombinations() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("J", J); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); database.addPrefix("AB", AB); database.addPrefix("BC", BC); - + // 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"); - + assertEquals(expected1, actual1); - + // test 2 - ABC-J vs AB-CJ vs AB-C-J database.addUnit("CJ", J.times(13)); database.addPrefix("ABC", UnitPrefix.valueOf(17)); - + final Unit expected2 = J.times(17); final Unit actual2 = database.getUnit("ABCJ"); - + assertEquals(expected2, actual2); } } -- cgit v1.2.3 From 0245594222bfa0bd9a47d8326ed323c7356ac27c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 27 Aug 2020 07:06:35 -0500 Subject: Added Complex Repetition. --- .../converterGUI/DefaultPrefixRepetitionRule.java | 57 +++- .../converterGUI/UnitConverterGUI.java | 6 +- .../math/ConditionalExistenceCollections.java | 295 +++++++++++++-------- src/org/unitConverter/unit/UnitDatabase.java | 43 ++- src/org/unitConverter/unit/UnitDatabaseTest.java | 44 +-- 5 files changed, 268 insertions(+), 177 deletions(-) diff --git a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java index 34d8467..bdc3a2e 100644 --- a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java +++ b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -6,6 +6,7 @@ package org.unitConverter.converterGUI; import java.util.List; import java.util.function.Predicate; +import org.unitConverter.unit.SI; import org.unitConverter.unit.UnitPrefix; /** @@ -35,8 +36,60 @@ enum DefaultPrefixRepetitionRule implements Predicate> { COMPLEX_REPETITION { @Override public boolean test(List prefixes) { - // TODO method stub - return false; + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!SI.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (SI.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) { + part = 0; + } else if (SI.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; } }; } diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index eff0c47..5fe4ee5 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -1024,8 +1024,10 @@ final class UnitConverterGUI { .build()); final JRadioButton customRepetition = new JRadioButton( - "Custom Repetition Rule"); - customRepetition.setEnabled(false); + "Complex Repetition"); + customRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); prefixRuleButtons.add(customRepetition); prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2) diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java index 9522885..ac1c0cf 100644 --- a/src/org/unitConverter/math/ConditionalExistenceCollections.java +++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java @@ -30,20 +30,25 @@ import java.util.function.Predicate; /** * Elements in these wrapper collections only exist if they pass a condition. *

- * All of the collections in this class are "views" of the provided collections. They are mutable if the provided - * collections are mutable, they allow null if the provided collections allow null, they will reflect changes in the + * All of the collections in this class are "views" of the provided collections. + * They are mutable if the provided collections are mutable, they allow null if + * the provided collections allow null, they will reflect changes in the * provided collection, etc. *

- * The modification operations will always run the corresponding operations, even if the conditional existence - * collection doesn't change. For example, if you have a set that ignores even numbers, add(2) will still add a 2 to the + * The modification operations will always run the corresponding operations, + * even if the conditional existence collection doesn't change. For example, if + * you have a set that ignores even numbers, add(2) will still add a 2 to the * backing set (but the conditional existence set will say it doesn't exist). *

- * The returned collections do not pass the hashCode and equals operations through to the backing collections, - * but rely on {@code Object}'s {@code equals} and {@code hashCode} methods. This is necessary to preserve the contracts - * of these operations in the case that the backing collections are sets or lists. + * The returned collections do not pass the hashCode and equals + * operations through to the backing collections, but rely on {@code Object}'s + * {@code equals} and {@code hashCode} methods. This is necessary to preserve + * the contracts of these operations in the case that the backing collections + * are sets or lists. *

- * Other than that, 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. + * Other than that, 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. * * * @author Adrien Hopkins @@ -56,13 +61,13 @@ public final class ConditionalExistenceCollections { * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * type of element in collection + * @param type of element in collection */ - static final class ConditionalExistenceCollection extends AbstractCollection { + static final class ConditionalExistenceCollection + extends AbstractCollection { final Collection collection; final Predicate existenceCondition; - + /** * Creates the {@code ConditionalExistenceCollection}. * @@ -70,67 +75,89 @@ public final class ConditionalExistenceCollections { * @param existenceCondition * @since 2019-10-17 */ - private ConditionalExistenceCollection(final Collection collection, final Predicate existenceCondition) { + private ConditionalExistenceCollection(final Collection collection, + final Predicate existenceCondition) { this.collection = collection; this.existenceCondition = existenceCondition; } - + @Override public boolean add(final E e) { return this.collection.add(e) && this.existenceCondition.test(e); } - + @Override public void clear() { this.collection.clear(); } - + @Override public boolean contains(final Object o) { if (!this.collection.contains(o)) return false; - + // this collection can only contain instances of E - // since the object is in the collection, we know that it must be an instance of E + // since the object is in the collection, we know that it must be an + // instance of E // therefore this cast will always work @SuppressWarnings("unchecked") final E e = (E) o; - + return this.existenceCondition.test(e); } - + @Override public Iterator iterator() { - return conditionalExistenceIterator(this.collection.iterator(), this.existenceCondition); + return conditionalExistenceIterator(this.collection.iterator(), + this.existenceCondition); } - + @Override public boolean remove(final Object o) { - // remove() must be first in the && statement, otherwise it may not execute + // remove() must be first in the && statement, otherwise it may not + // execute final boolean containedObject = this.contains(o); return this.collection.remove(o) && containedObject; } - + @Override public int size() { - return (int) this.collection.stream().filter(this.existenceCondition).count(); + return (int) this.collection.stream().filter(this.existenceCondition) + .count(); + } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(a); } } - + /** * Elements in this wrapper iterator only exist if they pass a condition. * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * type of elements in iterator + * @param type of elements in iterator */ static final class ConditionalExistenceIterator implements Iterator { final Iterator iterator; final Predicate existenceCondition; E nextElement; boolean hasNext; - + /** * Creates the {@code ConditionalExistenceIterator}. * @@ -138,12 +165,13 @@ public final class ConditionalExistenceCollections { * @param condition * @since 2019-10-17 */ - private ConditionalExistenceIterator(final Iterator iterator, final Predicate condition) { + private ConditionalExistenceIterator(final Iterator iterator, + final Predicate condition) { this.iterator = iterator; this.existenceCondition = condition; this.getAndSetNextElement(); } - + /** * Gets the next element, and sets nextElement and hasNext accordingly. * @@ -160,12 +188,12 @@ public final class ConditionalExistenceCollections { } while (!this.existenceCondition.test(this.nextElement)); this.hasNext = true; } - + @Override public boolean hasNext() { return this.hasNext; } - + @Override public E next() { if (this.hasNext()) { @@ -175,27 +203,25 @@ public final class ConditionalExistenceCollections { } else throw new NoSuchElementException(); } - + @Override public void remove() { this.iterator.remove(); } } - + /** * Mappings in this map only exist if the entry passes some condition. * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * key type - * @param - * value type + * @param key type + * @param value type */ static final class ConditionalExistenceMap extends AbstractMap { Map map; Predicate> entryExistenceCondition; - + /** * Creates the {@code ConditionalExistenceMap}. * @@ -203,205 +229,240 @@ public final class ConditionalExistenceCollections { * @param entryExistenceCondition * @since 2019-10-17 */ - private ConditionalExistenceMap(final Map map, final Predicate> entryExistenceCondition) { + private ConditionalExistenceMap(final Map map, + final Predicate> entryExistenceCondition) { this.map = map; this.entryExistenceCondition = entryExistenceCondition; } - + @Override public boolean containsKey(final Object key) { if (!this.map.containsKey(key)) return false; - + // 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; - + // get and test entry final V value = this.map.get(key); final Entry entry = new SimpleEntry<>(keyAsK, value); return this.entryExistenceCondition.test(entry); } - + @Override public Set> entrySet() { - return conditionalExistenceSet(this.map.entrySet(), this.entryExistenceCondition); + return conditionalExistenceSet(this.map.entrySet(), + this.entryExistenceCondition); } - + @Override public V get(final Object key) { return this.containsKey(key) ? this.map.get(key) : null; } - + + private final Entry getEntry(K key) { + return new Entry() { + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return ConditionalExistenceMap.this.map.get(key); + } + + @Override + public V setValue(V value) { + return ConditionalExistenceMap.this.map.put(key, value); + } + }; + } + @Override public Set keySet() { - // maybe change this to use ConditionalExistenceSet - return super.keySet(); + return conditionalExistenceSet(super.keySet(), + k -> this.entryExistenceCondition.test(this.getEntry(k))); } - + @Override public V put(final K key, final V value) { final V oldValue = this.map.put(key, value); - + // get and test entry final Entry entry = new SimpleEntry<>(key, oldValue); return this.entryExistenceCondition.test(entry) ? oldValue : null; } - + @Override public V remove(final Object key) { final V oldValue = this.map.remove(key); return this.containsKey(key) ? oldValue : null; } - + @Override public Collection values() { // maybe change this to use ConditionalExistenceCollection return super.values(); } - } - + /** * Elements in this set only exist if a certain condition is true. * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * type of element in set + * @param type of element in set */ static final class ConditionalExistenceSet extends AbstractSet { private final Set set; private final Predicate existenceCondition; - + /** * Creates the {@code ConditionalNonexistenceSet}. * - * @param set - * set to use - * @param existenceCondition - * condition where element exists + * @param set set to use + * @param existenceCondition condition where element exists * @since 2019-10-17 */ - private ConditionalExistenceSet(final Set set, final Predicate existenceCondition) { + private ConditionalExistenceSet(final Set set, + final Predicate existenceCondition) { this.set = set; this.existenceCondition = existenceCondition; } - + /** * {@inheritDoc} *

- * Note that this method returns {@code false} if {@code e} does not pass the existence condition. + * Note that this method returns {@code false} if {@code e} does not pass + * the existence condition. */ @Override public boolean add(final E e) { return this.set.add(e) && this.existenceCondition.test(e); } - + @Override public void clear() { this.set.clear(); } - + @Override public boolean contains(final Object o) { if (!this.set.contains(o)) return false; - + // this set can only contain instances of E - // since the object is in the set, we know that it must be an instance of E + // since the object is in the set, we know that it must be an instance + // of E // therefore this cast will always work @SuppressWarnings("unchecked") final E e = (E) o; - + return this.existenceCondition.test(e); } - + @Override public Iterator iterator() { - return conditionalExistenceIterator(this.set.iterator(), this.existenceCondition); + return conditionalExistenceIterator(this.set.iterator(), + this.existenceCondition); } - + @Override public boolean remove(final Object o) { - // remove() must be first in the && statement, otherwise it may not execute + // remove() must be first in the && statement, otherwise it may not + // execute final boolean containedObject = this.contains(o); return this.set.remove(o) && containedObject; } - + @Override public int size() { return (int) this.set.stream().filter(this.existenceCondition).count(); } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(a); + } } - + /** - * Elements in the returned wrapper collection are ignored if they don't pass a condition. + * Elements in the returned wrapper collection are ignored if they don't pass + * a condition. * - * @param - * type of elements in collection - * @param collection - * collection to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param 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 */ - public static final Collection conditionalExistenceCollection(final Collection collection, + public static final Collection conditionalExistenceCollection( + final Collection collection, final Predicate existenceCondition) { - return new ConditionalExistenceCollection<>(collection, existenceCondition); + return new ConditionalExistenceCollection<>(collection, + existenceCondition); } - + /** - * Elements in the returned wrapper iterator are ignored if they don't pass a condition. + * Elements in the returned wrapper iterator are ignored if they don't pass a + * condition. * - * @param - * type of elements in iterator - * @param iterator - * iterator to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param 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 */ - public static final Iterator conditionalExistenceIterator(final Iterator iterator, - final Predicate existenceCondition) { + public static final Iterator conditionalExistenceIterator( + final Iterator iterator, final Predicate existenceCondition) { return new ConditionalExistenceIterator<>(iterator, existenceCondition); } - + /** - * Mappings in the returned wrapper map are ignored if the corresponding entry doesn't pass a condition + * Mappings in the returned wrapper map are ignored if the corresponding + * entry doesn't pass a condition * - * @param - * type of key in map - * @param - * type of value in map - * @param map - * map to wrap - * @param entryExistenceCondition - * mappings only exist if this returns true + * @param type of key in map + * @param 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 */ - public static final Map conditionalExistenceMap(final Map map, + public static final Map conditionalExistenceMap( + final Map map, final Predicate> entryExistenceCondition) { return new ConditionalExistenceMap<>(map, entryExistenceCondition); } - + /** - * Elements in the returned wrapper set are ignored if they don't pass a condition. + * Elements in the returned wrapper set are ignored if they don't pass a + * condition. * - * @param - * type of elements in set - * @param set - * set to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param 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 */ - public static final Set conditionalExistenceSet(final Set set, final Predicate existenceCondition) { + public static final Set conditionalExistenceSet(final Set set, + final Predicate existenceCondition) { return new ConditionalExistenceSet<>(set, existenceCondition); } } diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 56846a1..9812bd0 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -1680,37 +1680,30 @@ public final class UnitDatabase { * @since 2020-08-26 */ List getPrefixesFromName(final String unitName) { - if (this.prefixlessUnits.containsKey(unitName)) - return new ArrayList<>(); - else { + final List prefixes = new ArrayList<>(); + String name = unitName; + + while (!this.prefixlessUnits.containsKey(name)) { // find the longest prefix - String longestPrefix = null; - int longestLength = 0; + String longestPrefixName = null; + int longestLength = name.length(); - for (final String prefixName : this.prefixes.keySet()) { - // a prefix name is valid if: - // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am - // looking for the longest valid prefix) - // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only - // linear units can have prefixes) - if (unitName.startsWith(prefixName) - && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); - if (this.containsUnitName(rest) - && this.getUnit(rest) instanceof LinearUnit) { - longestPrefix = prefixName; - longestLength = prefixName.length(); - } + while (longestPrefixName == null) { + longestLength--; + if (longestLength <= 0) + throw new AssertionError( + "No prefix found in " + name + ", but it is not a unit!"); + if (this.prefixes.containsKey(name.substring(0, longestLength))) { + longestPrefixName = name.substring(0, longestLength); } } - final List prefixes = this - .getPrefixesFromName(unitName.substring(longestLength)); - prefixes.add(this.getPrefix(longestPrefix)); - return prefixes; + // longest prefix found! + final UnitPrefix prefix = this.getPrefix(longestPrefixName); + prefixes.add(0, prefix); + name = name.substring(longestLength); } + return prefixes; } /** diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java index 96a0b83..2b981b6 100644 --- a/src/org/unitConverter/unit/UnitDatabaseTest.java +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import java.util.Arrays; import java.util.Iterator; @@ -28,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -55,9 +55,12 @@ class UnitDatabaseTest { .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); // make the prefix values prime so I can tell which multiplications were made - private static final UnitPrefix A = UnitPrefix.valueOf(2); - private static final UnitPrefix B = UnitPrefix.valueOf(3); - private static final UnitPrefix C = UnitPrefix.valueOf(5); + private static final UnitPrefix A = UnitPrefix.valueOf(2) + .withName(NameSymbol.ofName("A")); + private static final UnitPrefix B = UnitPrefix.valueOf(3) + .withName(NameSymbol.ofName("B")); + private static final UnitPrefix C = UnitPrefix.valueOf(5) + .withName(NameSymbol.ofName("C")); private static final UnitPrefix AB = UnitPrefix.valueOf(7); private static final UnitPrefix BC = UnitPrefix.valueOf(11); @@ -68,6 +71,7 @@ class UnitDatabaseTest { * @since 2019-05-03 */ @Test + // @Timeout(value = 5, unit = TimeUnit.SECONDS) public void testInfiniteSetExceptions() { // load units final UnitDatabase infiniteDatabase = new UnitDatabase(); @@ -79,33 +83,11 @@ class UnitDatabaseTest { infiniteDatabase.addPrefix("B", B); infiniteDatabase.addPrefix("C", C); - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().entrySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } - - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().keySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } + final Set> entrySet = infiniteDatabase.unitMap() + .entrySet(); + final Set keySet = infiniteDatabase.unitMap().keySet(); + assertThrows(IllegalStateException.class, () -> entrySet.toArray()); + assertThrows(IllegalStateException.class, () -> keySet.toArray()); } /** -- cgit v1.2.3 From e2f141427e441daa9d6be0ba8a30b844ca4391e0 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 27 Aug 2020 08:07:21 -0500 Subject: Added the ability to restrict conversion to customary->metric. --- .../unitConverter/converterGUI/SearchBoxList.java | 122 +++++----- .../converterGUI/UnitConverterGUI.java | 89 ++++++-- src/org/unitConverter/unit/SI.java | 59 ++--- src/org/unitConverter/unit/Unit.java | 250 ++++++++++++--------- 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 itemsToFilter; private final DelegateListModel listModel; private final JTextField searchBox; private final JList 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 customSearchFilter = o -> true; private final Comparator 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 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 itemsToFilter, final Comparator defaultOrdering, + public SearchBoxList(final Collection itemsToFilter, + final Comparator 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 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 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 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. *

@@ -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 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 prefixNameComparator; + // conditions for existence of From and To entries + // used for one-way conversion + private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( + s -> true); + private final MutablePredicate 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 @@ -337,6 +345,16 @@ final class UnitConverterGUI { return this.dimensionNames; } + /** + * @return a list of all the entries in the dimension-based converter's + * From box + * @since 2020-08-27 + */ + public final Set fromEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.fromExistenceCondition); + } + /** * @return a comparator to compare prefix names * @since 2019-04-14 @@ -436,6 +454,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 @@ -462,6 +499,16 @@ final class UnitConverterGUI { this.roundingType = roundingType; } + /** + * @return a list of all the entries in the dimension-based converter's To + * box + * @since 2020-08-27 + */ + public final Set 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 unitNameSet() { + private final Set 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(); @@ -629,15 +676,6 @@ final class UnitConverterGUI { throw new AssertionError(); } - /** - * @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 @@ -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 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 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 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 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 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 REDUCING_PREFIXES = new HashSet<>( - Arrays.asList(DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO, - ZEPTO, YOCTO)); + public static final Set 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 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 THOUSAND_PREFIXES = setOf(KILO, MEGA, + GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI, MICRO, NANO, PICO, FEMTO, + ATTO, ZEPTO, YOCTO); + public static final Set MAGNIFYING_PREFIXES = setOf(DEKA, HECTO, + KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, KIBI, MEBI, GIBI, + TEBI, PEBI, EXBI); + public static final Set 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 Set 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. * *

- * 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);} *

* - * @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 base, - final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { + public static final Unit fromConversionFunctions( + final ObjectProduct 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. * *

- * 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);} *

* - * @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 base, - final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo, final NameSymbol ns) { + public static final Unit fromConversionFunctions( + final ObjectProduct 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 unitBase; - + /** * The primary name used by this unit. */ private final Optional primaryName; - + /** * A short symbol used to represent this unit. */ private final Optional symbol; - + /** * A set of any additional names and/or spellings that the unit uses. */ private final Set otherNames; - + /** * Cache storing the result of getDimension() * * @since 2019-10-16 */ private transient ObjectProduct 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 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 otherNames) { + Unit(final String primaryName, final String symbol, + final Set 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. *

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

*

- * If this unit is a base unit, this method should return {@code value}. + * If this unit is a base unit, this method should return + * {@code value}. *

* - * @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 base unit + * @param value value expressed in base unit * @return value expressed in this 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. *

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

*

- * If this unit is a base unit, this method should return {@code value}. + * If this unit is a base unit, this method should return + * {@code value}. *

* - * @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 this unit + * @param value value expressed in this unit * @return value expressed in base 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 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 mapping = this.unitBase.exponentMap(); final Map 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 getOtherNames() { return this.otherNames; } - + /** * @return primaryName * @since 2019-10-21 @@ -281,7 +284,7 @@ public abstract class Unit { public final Optional getPrimaryName() { return this.primaryName; } - + /** * @return symbol * @since 2019-10-21 @@ -289,25 +292,64 @@ public abstract class Unit { public final Optional getSymbol() { return this.symbol; } - + + /** + * Returns true iff this unit is metric. + *

+ * "Metric" is defined by three conditions: + *

    + *
  • Must be an instance of {@link LinearUnit}.
  • + *
  • Must be based on the SI base units (as determined by getBase())
  • + *
  • The conversion factor must be a power of 10.
  • + *
+ *

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

+ * 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; @@ -104,6 +105,17 @@ class UnitTest { assertEquals(metre, meter); } + @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 -- cgit v1.2.3 From 21dc2db1cccfd9781bbdbbbafdacf88630cab5c0 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 27 Aug 2020 08:28:01 -0500 Subject: Added exceptions to the one-way conversion rules. --- metric_exceptions.txt | 19 ++++++++++++ .../converterGUI/UnitConverterGUI.java | 35 ++++++++++++++++++++-- unitsfile.txt | 5 +++- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 metric_exceptions.txt diff --git a/metric_exceptions.txt b/metric_exceptions.txt new file mode 100644 index 0000000..73748c0 --- /dev/null +++ b/metric_exceptions.txt @@ -0,0 +1,19 @@ +# This is a list of exceptions for the one-way conversion mode +# Units in this list will be included in both From: and To: +# regardless of whether or not one-way conversion is enabled. + +tempC +tempCelsius +s +second +min +minute +h +hour +d +day +wk +week +gregorianmonth +gregorianyear +km/h \ No newline at end of file diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 75ab16d..38a9de1 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -21,7 +21,10 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.KeyEvent; +import java.io.BufferedReader; import java.io.File; +import java.io.FileReader; +import java.io.IOException; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; @@ -128,6 +131,9 @@ final class UnitConverterGUI { /** The names of all of the dimensions */ private final List dimensionNames; + /** Unit names that are ignored by the metric-only/imperial-only filter */ + private final Set metricExceptions; + private final Comparator prefixNameComparator; // conditions for existence of From and To entries @@ -164,6 +170,29 @@ final class UnitConverterGUI { this.database.loadUnitsFile(new File("unitsfile.txt")); this.database.loadDimensionFile(new File("dimensionfile.txt")); + // load metric exceptions + final File exceptions = new File("metric_exceptions.txt"); + this.metricExceptions = new HashSet<>(); + try (FileReader fileReader = new FileReader(exceptions); + BufferedReader reader = new BufferedReader(fileReader)) { + while (reader.ready()) { + String line = reader.readLine(); + + // # can be used for comments + if (line.contains("#")) { + line = line.substring(line.indexOf("#")); + } + + // don't read black lines + if (!line.isBlank()) { + this.metricExceptions.add(line.strip()); + } + } + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } + // a comparator that can be used to compare prefix names // any name that does not exist is less than a name that does. // otherwise, they are compared by value @@ -464,9 +493,11 @@ final class UnitConverterGUI { public final void setOneWay(boolean oneWay) { if (oneWay) { this.fromExistenceCondition.setPredicate( - unitName -> !this.database.getUnit(unitName).isMetric()); + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName).isMetric()); this.toExistenceCondition.setPredicate( - unitName -> this.database.getUnit(unitName).isMetric()); + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); } else { this.fromExistenceCondition.setPredicate(unitName -> true); this.toExistenceCondition.setPredicate(unitName -> true); diff --git a/unitsfile.txt b/unitsfile.txt index a067d14..eafe885 100644 --- a/unitsfile.txt +++ b/unitsfile.txt @@ -157,8 +157,11 @@ hour 3600 second h hour day 86400 second d day +week 7 day +wk week julianyear 365.25 day gregorianyear 365.2425 day +gregorianmonth gregorianyear / 12 # Other non-SI "metric" units litre 0.001 m^3 @@ -180,7 +183,7 @@ waterdensity kilogram / litre # Imperial length units foot 0.3048 m ft foot -inch 1 / 12 foot +inch foot / 12 in inch yard 3 foot yd yard -- cgit v1.2.3 From 5a7e8f6fcb175b238eb1d5481513b35039107a3e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 7 Sep 2020 15:15:20 -0500 Subject: Created an UncertainDouble class for uncertainty operations. --- .settings/org.eclipse.jdt.core.prefs | 100 ++++++ src/org/unitConverter/math/DecimalComparison.java | 207 +++++++---- src/org/unitConverter/math/UncertainDouble.java | 411 ++++++++++++++++++++++ src/org/unitConverter/unit/LinearUnit.java | 302 +++++++++------- src/org/unitConverter/unit/LinearUnitValue.java | 252 +++---------- src/org/unitConverter/unit/UnitDatabase.java | 6 +- src/org/unitConverter/unit/UnitTest.java | 7 +- 7 files changed, 885 insertions(+), 400 deletions(-) create mode 100644 src/org/unitConverter/math/UncertainDouble.java diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index ea7a397..f77f6a1 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,13 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve @@ -6,11 +15,102 @@ org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=enabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning +org.eclipse.jdt.core.compiler.problem.fallthroughCase=error +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=warning +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=info +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=info +org.eclipse.jdt.core.compiler.problem.rawTypeReference=error +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=info +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=info +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=info +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=info org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=info +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=error +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=info +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=info +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=disabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=info +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=info +org.eclipse.jdt.core.compiler.problem.unusedParameter=info +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=info +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=info +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java index 859e8da..0f5b91e 100644 --- a/src/org/unitConverter/math/DecimalComparison.java +++ b/src/org/unitConverter/math/DecimalComparison.java @@ -27,42 +27,45 @@ import java.math.BigDecimal; */ 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. + * 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 */ public static final double DOUBLE_EPSILON = 1.0e-15; - + /** - * The value used for float comparison. If two float values are within this value multiplied by the larger value, - * they are considered equal. + * 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 */ public static final float FLOAT_EPSILON = 1.0e-6f; - + /** * Tests for equality of double values using {@link #DOUBLE_EPSILON}. *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon using {@link #equals(double, double, double)} (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon using + * {@link #equals(double, double, double)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test + * @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 @@ -71,57 +74,61 @@ public final class DecimalComparison { public static final boolean equals(final double a, final double b) { return DecimalComparison.equals(a, b, DOUBLE_EPSILON); } - + /** * Tests for double equality using a custom epsilon value. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly - * reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final double a, final double b, final double epsilon) { + public static final 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}. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon using {@link #equals(float, float, float)} (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon using {@link #equals(float, float, float)} + * (this does not make a violation of transitivity impossible, it just + * significantly reduces the chances of it happening) + *
  4. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test + * @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 @@ -129,53 +136,121 @@ public final class DecimalComparison { public static final boolean equals(final float a, final float b) { return DecimalComparison.equals(a, b, FLOAT_EPSILON); } - + /** * Tests for float equality using a custom epsilon value. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly - * reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final float a, final float b, final float epsilon) { + public static final 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)); } - + + /** + * Tests for equality of {@code UncertainDouble} values using + * {@link #DOUBLE_EPSILON}. + *

+ * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon using + * {@link #equals(UncertainDouble, UncertainDouble, double)} (this does not + * make a violation of transitivity impossible, it just significantly reduces + * the chances of it happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @return whether they are equal + * @since 2020-09-07 + * @see #hashCode(double) + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b) { + return DecimalComparison.equals(a.value(), b.value()) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty()); + } + + /** + * Tests for {@code UncertainDouble} equality using a custom epsilon value. + * + *

+ * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final 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. + * 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 + * @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/org/unitConverter/math/UncertainDouble.java b/src/org/unitConverter/math/UncertainDouble.java new file mode 100644 index 0000000..e948df9 --- /dev/null +++ b/src/org/unitConverter/math/UncertainDouble.java @@ -0,0 +1,411 @@ +/** + * @since 2020-09-07 + */ +package org.unitConverter.math; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A double with an associated uncertainty value. For example, 3.2 ± 0.2. + *

+ * All methods in this class throw a NullPointerException if any of their + * arguments is null. + * + * @since 2020-09-07 + */ +public final class UncertainDouble implements Comparable { + /** + * The exact value 0 + */ + public static final UncertainDouble ZERO = UncertainDouble.of(0, 0); + + /** + * A regular expression that can recognize toString forms + */ + private static final Pattern TO_STRING = Pattern + .compile("([a-zA-Z_0-9\\.\\,]+)" // a number + // optional "± [number]" + + "(?:\\s*(?:±|\\+-)\\s*([a-zA-Z_0-9\\.\\,]+))?"); + + /** + * Parses a string in the form of {@link UncertainDouble#toString(boolean)} + * and returns the corresponding {@code UncertainDouble} instance. + *

+ * This method allows some alternative forms of the string representation, + * 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 + */ + public static final UncertainDouble fromString(String s) { + Objects.requireNonNull(s, "s may not be null"); + final Matcher matcher = TO_STRING.matcher(s); + + double value, uncertainty; + try { + value = Double.parseDouble(matcher.group(1)); + } catch (IllegalStateException | NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + + final String uncertaintyString = matcher.group(2); + if (uncertaintyString == null) { + uncertainty = 0; + } else { + try { + uncertainty = Double.parseDouble(uncertaintyString); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + } + + return UncertainDouble.of(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and absolute + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble of(double value, double uncertainty) { + return new UncertainDouble(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and relative + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble ofRelative(double value, + double relativeUncertainty) { + return new UncertainDouble(value, value * relativeUncertainty); + } + + private final double value; + + private final double uncertainty; + + /** + * @param value + * @param uncertainty + * @since 2020-09-07 + */ + private UncertainDouble(double value, double uncertainty) { + this.value = value; + // uncertainty should only ever be positive + this.uncertainty = Math.abs(uncertainty); + } + + /** + * Compares this {@code UncertainDouble} with another + * {@code UncertainDouble}. + *

+ * This method only compares the values, not the uncertainties. So 3.1 ± 0.5 + * is considered less than 3.2 ± 0.5, even though they are equivalent. + *

+ * Note: The natural ordering of this class is inconsistent with + * equals. Specifically, if two {@code UncertainDouble} instances {@code a} + * and {@code b} have the same value but different uncertainties, + * {@code a.compareTo(b)} will return 0 but {@code a.equals(b)} will return + * {@code false}. + */ + @Override + public final int compareTo(UncertainDouble o) { + return Double.compare(this.value, o.value); + } + + /** + * Returns the quotient of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final 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())); + } + + /** + * Returns the quotient of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedByExact(double other) { + return UncertainDouble.of(this.value / other, this.uncertainty / other); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UncertainDouble)) + return false; + final UncertainDouble other = (UncertainDouble) obj; + if (Double.doubleToLongBits(this.uncertainty) != Double + .doubleToLongBits(other.uncertainty)) + return false; + if (Double.doubleToLongBits(this.value) != Double + .doubleToLongBits(other.value)) + return false; + return true; + } + + /** + * @param other another {@code UncertainDouble} + * @return true iff this and {@code other} are within each other's + * uncertainty range. + * @since 2020-09-07 + */ + public final 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); + } + + /** + * Gets the preferred scale for rounding a value for toString. + * + * @since 2020-09-07 + */ + private final int getDisplayScale() { + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal + // point), rounds + // so that uncertainty has 2 significant digits. + // 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); + + // the scale that will give the uncertainty two decimal places + final int twoDecimalPlacesScale = bigUncertainty.scale() + - bigUncertainty.precision() + 2; + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN); + + if (roundedUncertainty.unscaledValue().intValue() >= 20) + return twoDecimalPlacesScale - 1; // one decimal place + else + return twoDecimalPlacesScale; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(this.uncertainty); + result = prime * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.value); + result = prime * result + (int) (temp ^ temp >>> 32); + return result; + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-09-07 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * Returns the difference of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final 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)); + } + + /** + * Returns the difference of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minusExact(double other) { + return UncertainDouble.of(this.value - other, this.uncertainty); + } + + /** + * Returns the sum of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final 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)); + } + + /** + * Returns the sum of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plusExact(double other) { + return UncertainDouble.of(this.value + other, this.uncertainty); + } + + /** + * @return relative uncertainty + * @since 2020-09-07 + */ + public final double relativeUncertainty() { + return this.uncertainty / this.value; + } + + /** + * Returns the product of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final 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())); + } + + /** + * Returns the product of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final 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}. + * + * @since 2020-09-07 + */ + public final 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( + other.value * this.relativeUncertainty(), + Math.log(this.value) * other.uncertainty); + + return UncertainDouble.ofRelative(result, relativeUncertainty); + } + + /** + * Returns the result of {@code this} raised the exact exponent + * {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponentExact(double other) { + return UncertainDouble.ofRelative(Math.pow(this.value, other), + this.relativeUncertainty() * other); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

+ * This method returns the same value as {@link #toString(boolean)}, but + * {@code showUncertainty} is true if and only if the uncertainty is + * non-zero. + * + *

+ * Examples: + * + *

+	 * 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"
+	 * 
+ * + * @since 2020-09-07 + */ + @Override + public final String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

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

+ * VALUE represents a string representation of this {@code UncertainDouble}'s + * value. If the uncertainty is non-zero, the string will be rounded to the + * same precision as the uncertainty, otherwise it will not be rounded. The + * string is still rounded if {@code showUncertainty} is false.
+ * UNCERTAINTY represents a string representation of this + * {@code UncertainDouble}'s uncertainty. If the uncertainty ends in 1X + * (where X represents any digit) it will be rounded to two significant + * digits otherwise it will be rounded to one significant digit. + *

+ * Examples: + * + *

+	 * 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.13).toString(false) = "3.27"
+	 * 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"
+	 * 
+ * + * @since 2020-09-07 + */ + public final String toString(boolean showUncertainty) { + String valueString, uncertaintyString; + + // generate the string representation of value and uncertainty + if (this.isExact()) { + uncertaintyString = "0.0"; + valueString = Double.toString(this.value); + + } else { + // round the value and uncertainty according to getDisplayScale() + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + final int displayScale = this.getDisplayScale(); + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(displayScale, RoundingMode.HALF_EVEN); + final BigDecimal roundedValue = bigValue.setScale(displayScale, + RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + } + + // return "value" or "value ± uncertainty" depending on showUncertainty + return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); + } + + /** + * @return absolute uncertainty + * @since 2020-09-07 + */ + public final double uncertainty() { + return this.uncertainty; + } + + /** + * @return value without uncertainty + * @since 2020-09-07 + */ + public final double value() { + return this.value; + } +} diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 762572a..2d63ca7 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -20,89 +20,83 @@ import java.util.Objects; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; /** - * A unit that can be expressed as a product of its base and a number. For example, kilometres, inches and pounds. + * 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 */ 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' + * 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 unit unit to convert + * @param value value to convert * @return value expressed as a {@code LinearUnit} * @since 2019-10-16 - * @throws NullPointerException - * if unit is null + * @throws NullPointerException if unit is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), NameSymbol.EMPTY); } - + /** - * 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' + * 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 + * @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 - * @throws NullPointerException - * if unit or ns is null + * @throws NullPointerException if unit or ns is null */ - public static LinearUnit fromUnitValue(final Unit unit, final double value, final NameSymbol ns) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + public static LinearUnit fromUnitValue(final Unit unit, final double value, + final NameSymbol ns) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), ns); } - + /** - * 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}. + * 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 unitBase unit base to multiply by + * @param conversionFactor number to multiply base by * @return product of base and conversion factor * @since 2019-10-16 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor) { + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor) { return new LinearUnit(unitBase, conversionFactor, 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}. + * 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 + * @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 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor, - final NameSymbol ns) { + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { return new LinearUnit(unitBase, conversionFactor, ns); } - + /** * The value of this unit as represented in its base form. Mathematically, * @@ -113,21 +107,20 @@ public final class LinearUnit extends Unit { * @since 2019-10-16 */ private final double conversionFactor; - + /** * Creates the {@code LinearUnit}. * - * @param unitBase - * base of linear unit - * @param conversionFactor - * conversion factor between base and unit + * @param unitBase base of linear unit + * @param conversionFactor conversion factor between base and unit * @since 2019-10-16 */ - private LinearUnit(final ObjectProduct unitBase, final double conversionFactor, final NameSymbol ns) { + private LinearUnit(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { super(unitBase, ns); this.conversionFactor = conversionFactor; } - + /** * {@inheritDoc} * @@ -137,7 +130,32 @@ public final class LinearUnit extends Unit { protected double convertFromBase(final double value) { return value / this.getConversionFactor(); } - + + /** + * 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 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if value or other is null + */ + public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) { + Objects.requireNonNull(other, "other must not be null."); + Objects.requireNonNull(value, "value may not be null."); + 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)); + + } + /** * {@inheritDoc} * @@ -147,12 +165,20 @@ public final class LinearUnit extends Unit { protected double convertToBase(final double value) { return value * this.getConversionFactor(); } - + + /** + * Converts an {@code UncertainDouble} to the base unit. + * + * @since 2020-09-07 + */ + UncertainDouble convertToBase(final UncertainDouble value) { + return value.timesExact(this.getConversionFactor()); + } + /** * Divides this unit by a scalar. * - * @param divisor - * scalar to divide by + * @param divisor scalar to divide by * @return quotient * @since 2018-12-23 * @since v0.1.0 @@ -160,26 +186,26 @@ public final class LinearUnit extends Unit { public LinearUnit dividedBy(final double divisor) { return valueOf(this.getBase(), this.getConversionFactor() / divisor); } - + /** * Returns the quotient of this unit and another. * - * @param divisor - * unit to divide by + * @param divisor unit to divide by * @return quotient of two units - * @throws NullPointerException - * if {@code divisor} is null + * @throws NullPointerException if {@code divisor} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit dividedBy(final LinearUnit divisor) { Objects.requireNonNull(divisor, "other must not be null"); - + // divide the units - final ObjectProduct base = this.getBase().dividedBy(divisor.getBase()); - return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); + final ObjectProduct base = this.getBase() + .dividedBy(divisor.getBase()); + return valueOf(base, + this.getConversionFactor() / divisor.getConversionFactor()); } - + /** * {@inheritDoc} * @@ -191,9 +217,10 @@ public final class LinearUnit extends Unit { return false; final LinearUnit other = (LinearUnit) obj; return Objects.equals(this.getBase(), other.getBase()) - && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); + && DecimalComparison.equals(this.getConversionFactor(), + other.getConversionFactor()); } - + /** * @return conversion factor * @since 2019-10-16 @@ -201,7 +228,7 @@ public final class LinearUnit extends Unit { public double getConversionFactor() { return this.conversionFactor; } - + /** * {@inheritDoc} * @@ -209,18 +236,20 @@ public final class LinearUnit extends Unit { */ @Override public int hashCode() { - return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor()); + return 31 * this.getBase().hashCode() + + DecimalComparison.hash(this.getConversionFactor()); } - + /** - * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there is a {@code BaseUnit b} where + * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there + * is a {@code BaseUnit b} where * {@code b.asLinearUnit().equals(this)} returns {@code true}.) * @since 2019-10-16 */ public boolean isBase() { return this.isCoherent() && this.getBase().isSingleObject(); } - + /** * @return whether this unit is coherent (i.e. has conversion factor 1) * @since 2019-10-16 @@ -228,70 +257,73 @@ public final class LinearUnit extends Unit { public boolean isCoherent() { return this.getConversionFactor() == 1; } - + /** * Returns the difference of this unit and another. *

- * Two units can be subtracted if they have the same base. Note that {@link #canConvertTo} can be used to determine - * this. If {@code subtrahend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be subtracted if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code subtrahend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. *

* - * @param subtrahend - * unit to subtract + * @param subtrahend unit to subtract * @return difference of units - * @throws IllegalArgumentException - * if {@code subtrahend} is not compatible for subtraction as described above - * @throws NullPointerException - * if {@code subtrahend} is null + * @throws IllegalArgumentException if {@code subtrahend} is not compatible + * for subtraction as described above + * @throws NullPointerException if {@code subtrahend} is null * @since 2019-03-17 * @since v0.2.0 */ public LinearUnit minus(final LinearUnit subtrahend) { Objects.requireNonNull(subtrahend, "addend must not be null."); - + // reject subtrahends that cannot be added to this unit if (!this.getBase().equals(subtrahend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahend)); - + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", this, + subtrahend)); + // subtract the units - return valueOf(this.getBase(), this.getConversionFactor() - subtrahend.getConversionFactor()); + return valueOf(this.getBase(), + this.getConversionFactor() - subtrahend.getConversionFactor()); } - + /** * Returns the sum of this unit and another. *

- * Two units can be added if they have the same base. Note that {@link #canConvertTo} can be used to determine this. - * If {@code addend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be added if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code addend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. *

* - * @param addend - * unit to add + * @param addend unit to add * @return sum of units - * @throws IllegalArgumentException - * if {@code addend} is not compatible for addition as described above - * @throws NullPointerException - * if {@code addend} is null + * @throws IllegalArgumentException if {@code addend} is not compatible for + * addition as described above + * @throws NullPointerException if {@code addend} is null * @since 2019-03-17 * @since v0.2.0 */ public LinearUnit plus(final LinearUnit addend) { Objects.requireNonNull(addend, "addend must not be null."); - + // reject addends that cannot be added to this unit if (!this.getBase().equals(addend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); - + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this, + addend)); + // add the units - return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); + return valueOf(this.getBase(), + this.getConversionFactor() + addend.getConversionFactor()); } - + /** * Multiplies this unit by a scalar. * - * @param multiplier - * scalar to multiply by + * @param multiplier scalar to multiply by * @return product * @since 2018-12-23 * @since v0.1.0 @@ -299,39 +331,39 @@ public final class LinearUnit extends Unit { public LinearUnit times(final double multiplier) { return valueOf(this.getBase(), this.getConversionFactor() * multiplier); } - + /** * Returns the product of this unit and another. * - * @param multiplier - * unit to multiply by + * @param multiplier unit to multiply by * @return product of two units - * @throws NullPointerException - * if {@code multiplier} is null + * @throws NullPointerException if {@code multiplier} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit times(final LinearUnit multiplier) { Objects.requireNonNull(multiplier, "other must not be null"); - + // multiply the units - final ObjectProduct base = this.getBase().times(multiplier.getBase()); - return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); + final ObjectProduct base = this.getBase() + .times(multiplier.getBase()); + return valueOf(base, + this.getConversionFactor() * multiplier.getConversionFactor()); } - + /** * Returns this unit but to an exponent. * - * @param exponent - * exponent to exponentiate unit to + * @param exponent exponent to exponentiate unit to * @return exponentiated unit * @since 2019-01-15 * @since v0.1.0 */ public LinearUnit toExponent(final int exponent) { - return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); + return valueOf(this.getBase().toExponent(exponent), + Math.pow(this.conversionFactor, exponent)); } - + /** * @return a string providing a definition of this unit * @since 2019-10-21 @@ -339,10 +371,13 @@ public final class LinearUnit extends Unit { @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "") + ", " - + Double.toString(this.conversionFactor) + " * " + this.getBase().toString(u -> u.getSymbol().get()); + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", " + Double.toString(this.conversionFactor) + " * " + + this.getBase().toString(u -> u.getSymbol().get()); } - + @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); @@ -351,37 +386,38 @@ public final class LinearUnit extends Unit { /** * Returns the result of applying {@code prefix} to this unit. *

- * If this unit and the provided prefix have a primary name, the returned unit will have a primary name (prefix's - * name + unit's name).
- * If this unit and the provided prefix have a symbol, the returned unit will have a symbol.
- * This method ignores alternate names of both this unit and the provided prefix. + * If this unit and the provided prefix have a primary name, the returned + * unit will have a primary name (prefix's name + unit's name).
+ * If this unit and the provided prefix have a symbol, the returned unit will + * have a symbol.
+ * This method ignores alternate names of both this unit and the provided + * prefix. * - * @param prefix - * prefix to apply + * @param prefix prefix to apply * @return unit with prefix * @since 2019-03-18 * @since v0.2.0 - * @throws NullPointerException - * if prefix is null + * @throws NullPointerException if prefix is null */ public LinearUnit withPrefix(final UnitPrefix prefix) { final LinearUnit unit = this.times(prefix.getMultiplier()); - + // create new name and symbol, if possible final String name; - if (this.getPrimaryName().isPresent() && prefix.getPrimaryName().isPresent()) { + if (this.getPrimaryName().isPresent() + && prefix.getPrimaryName().isPresent()) { name = prefix.getPrimaryName().get() + this.getPrimaryName().get(); } else { name = null; } - + final String symbol; if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { symbol = prefix.getSymbol().get() + this.getSymbol().get(); } else { symbol = null; } - + return unit.withName(NameSymbol.ofNullable(name, symbol)); } } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 5685a6d..d86d344 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -3,12 +3,11 @@ */ package org.unitConverter.unit; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Objects; import java.util.Optional; import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.UncertainDouble; /** * A possibly uncertain value expressed in a linear unit. @@ -33,7 +32,8 @@ public final class LinearUnitValue { public static final LinearUnitValue getExact(final LinearUnit unit, final double value) { return new LinearUnitValue( - Objects.requireNonNull(unit, "unit must not be null"), value, 0); + Objects.requireNonNull(unit, "unit must not be null"), + UncertainDouble.of(value, 0)); } /** @@ -46,41 +46,23 @@ public final class LinearUnitValue { * @since 2020-07-26 */ public static final LinearUnitValue of(final LinearUnit unit, - final double value, final double uncertainty) { + final UncertainDouble value) { return new LinearUnitValue( - Objects.requireNonNull(unit, "unit must not be null"), value, - uncertainty); - } - - /** - * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param relativeUncertainty relative uncertainty of value - * @return uncertain {@code LinearUnitValue} instance - * @since 2020-07-28 - */ - public static final LinearUnitValue ofRelative(final LinearUnit unit, - final double value, final double relativeUncertainty) { - return LinearUnitValue.of(unit, value, relativeUncertainty * value); + Objects.requireNonNull(unit, "unit must not be null"), + Objects.requireNonNull(value, "value may not be null")); } private final LinearUnit unit; - private final double value; - private final double uncertainty; + private final UncertainDouble value; /** - * @param unit unit to express as - * @param value value to express - * @param uncertainty absolute uncertainty of value + * @param unit unit to express as + * @param value value to express * @since 2020-07-26 */ - private LinearUnitValue(final LinearUnit unit, final double value, - final double uncertainty) { + private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { this.unit = unit; this.value = value; - this.uncertainty = uncertainty; } /** @@ -89,7 +71,7 @@ public final class LinearUnitValue { * @since 2020-08-04 */ public final UnitValue asUnitValue() { - return UnitValue.of(this.unit, this.value); + return UnitValue.of(this.unit, this.value.value()); } /** @@ -110,8 +92,7 @@ public final class LinearUnitValue { * @since 2020-07-26 */ public final LinearUnitValue convertTo(final LinearUnit other) { - return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), - this.unit.convertTo(other, this.uncertainty)); + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); } /** @@ -122,8 +103,7 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue dividedBy(final double divisor) { - return LinearUnitValue.of(this.unit, this.value / divisor, - this.uncertainty / divisor); + return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); } /** @@ -134,10 +114,8 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue dividedBy(final LinearUnitValue divisor) { - return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), - this.value / divisor.value, - Math.hypot(this.getRelativeUncertainty(), - divisor.getRelativeUncertainty())); + return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), + this.value.dividedBy(divisor.value)); } /** @@ -154,12 +132,8 @@ public final class LinearUnitValue { return false; final LinearUnitValue other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits( - this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits( - other.unit.convertToBase(other.getValue())) - && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double - .doubleToLongBits(other.getRelativeUncertainty()); + && this.unit.convertToBase(this.value) + .equals(other.unit.convertToBase(other.value)); } /** @@ -180,9 +154,7 @@ public final class LinearUnitValue { final LinearUnitValue other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) && DecimalComparison.equals(this.unit.convertToBase(this.value), - other.unit.convertToBase(other.value)) - && DecimalComparison.equals(this.getRelativeUncertainty(), - other.getRelativeUncertainty()); + other.unit.convertToBase(other.value)); } /** @@ -195,32 +167,11 @@ public final class LinearUnitValue { if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) return false; - final double thisBaseValue = this.unit.convertToBase(this.value); - final double otherBaseValue = other.unit.convertToBase(other.value); - final double thisBaseUncertainty = this.unit - .convertToBase(this.uncertainty); - final double otherBaseUncertainty = other.unit - .convertToBase(other.uncertainty); - return Math.abs(thisBaseValue - otherBaseValue) <= Math - .min(thisBaseUncertainty, otherBaseUncertainty); - } - - /** - * @return relative uncertainty of value - * - * @since 2020-07-26 - */ - public final double getRelativeUncertainty() { - return this.uncertainty / this.value; - } - - /** - * @return absolute uncertainty of value - * - * @since 2020-07-26 - */ - public final double getUncertainty() { - return this.uncertainty; + final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); + final LinearUnitValue thisBase = this.convertTo(base); + final LinearUnitValue otherBase = other.convertTo(base); + + return thisBase.value.equivalent(otherBase.value); } /** @@ -237,24 +188,22 @@ public final class LinearUnitValue { * * @since 2020-07-26 */ - public final double getValue() { + public final UncertainDouble getValue() { return this.value; } + /** + * @return the exact value + * @since 2020-09-07 + */ + public final double getValueExact() { + return this.value.value(); + } + @Override public int hashCode() { return Objects.hash(this.unit.getBase(), - this.unit.convertToBase(this.getValue()), - this.getRelativeUncertainty()); - } - - /** - * @return true iff the value has no uncertainty - * - * @since 2020-07-26 - */ - public final boolean isExact() { - return this.uncertainty == 0; + this.unit.convertToBase(this.getValue())); } /** @@ -276,8 +225,8 @@ public final class LinearUnitValue { this.unit, subtrahend.unit)); final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value - otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); + return LinearUnitValue.of(this.unit, + this.value.minus(otherConverted.value)); } /** @@ -298,8 +247,8 @@ public final class LinearUnitValue { addend.unit)); final LinearUnitValue otherConverted = addend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value + otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); + return LinearUnitValue.of(this.unit, + this.value.plus(otherConverted.value)); } /** @@ -310,8 +259,7 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue times(final double multiplier) { - return LinearUnitValue.of(this.unit, this.value * multiplier, - this.uncertainty * multiplier); + return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); } /** @@ -322,10 +270,8 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue times(final LinearUnitValue multiplier) { - return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), - this.value * multiplier.value, - Math.hypot(this.getRelativeUncertainty(), - multiplier.getRelativeUncertainty())); + return LinearUnitValue.of(this.unit.times(multiplier.unit), + this.value.times(multiplier.value)); } /** @@ -336,14 +282,13 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue toExponent(final int exponent) { - return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), - Math.pow(this.value, exponent), - this.getRelativeUncertainty() * Math.sqrt(exponent)); + return LinearUnitValue.of(this.unit.toExponent(exponent), + this.value.toExponentExact(exponent)); } @Override public String toString() { - return this.toString(!this.isExact()); + return this.toString(!this.value.isExact()); } /** @@ -363,107 +308,22 @@ public final class LinearUnitValue { final Optional symbol = this.unit.getSymbol(); final String chosenName = symbol.orElse(primaryName.orElse(null)); - final double baseValue = this.unit.convertToBase(this.value); - final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + final UncertainDouble baseValue = this.unit.convertToBase(this.value); // get rounded strings - String valueString, baseValueString, uncertaintyString, - baseUncertaintyString; - if (this.isExact()) { - valueString = Double.toString(this.value); - baseValueString = Double.toString(baseValue); - uncertaintyString = "0"; - baseUncertaintyString = "0"; - } else { - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal - // point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the - // uncertainty. - BigDecimal roundedUncertainty = bigUncertainty.setScale( - bigUncertainty.scale() - bigUncertainty.precision() + 2, - RoundingMode.HALF_EVEN); - if (roundedUncertainty.unscaledValue().intValue() >= 20) { - roundedUncertainty = bigUncertainty.setScale( - bigUncertainty.scale() - bigUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedValue = bigValue - .setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - - if (primaryName.isEmpty() && symbol.isEmpty()) { - final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); - final BigDecimal bigBaseUncertainty = BigDecimal - .valueOf(baseUncertainty); - - BigDecimal roundedBaseUncertainty = bigBaseUncertainty - .setScale( - bigBaseUncertainty.scale() - - bigBaseUncertainty.precision() + 2, - RoundingMode.HALF_EVEN); - if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { - roundedBaseUncertainty = bigBaseUncertainty - .setScale( - bigBaseUncertainty.scale() - - bigBaseUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedBaseValue = bigBaseValue.setScale( - roundedBaseUncertainty.scale(), RoundingMode.HALF_EVEN); - - baseValueString = roundedBaseValue.toString(); - baseUncertaintyString = roundedBaseUncertainty.toString(); - } else { - // unused - baseValueString = ""; - baseUncertaintyString = ""; - } - } + // if showUncertainty is true, add brackets around the string + final String valueString = showUncertainty ? "(" + : "" + this.value.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + final String baseValueString = showUncertainty ? "(" + : "" + baseValue.toString(showUncertainty) + + (showUncertainty ? ")" : ""); // create string - if (showUncertainty) { - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", - valueString, uncertaintyString, baseValueString, - baseUncertaintyString, this.unit.getBase()); - else - return String.format("(%s ± %s) %s", valueString, uncertaintyString, - chosenName); - } else { - // truncate excess zeroes - if (valueString.contains(".")) { - while (valueString.endsWith("0")) { - valueString = valueString.substring(0, valueString.length() - 1); - } - if (valueString.endsWith(".")) { - valueString = valueString.substring(0, valueString.length() - 1); - } - } - - if (baseValueString.contains(".")) { - while (baseValueString.endsWith("0")) { - baseValueString = baseValueString.substring(0, - baseValueString.length() - 1); - } - if (baseValueString.endsWith(".")) { - baseValueString = baseValueString.substring(0, - baseValueString.length() - 1); - } - } - - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("%s unnamed unit (= %s %s)", valueString, - baseValueString, this.unit.getBase()); - else - return String.format("%s %s", valueString, chosenName); - } + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); } } diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 9812bd0..9ca9617 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -46,6 +46,7 @@ import org.unitConverter.math.ConditionalExistenceCollections; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ExpressionParser; import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; /** * A database of units, prefixes and dimensions, and their names. @@ -1134,7 +1135,7 @@ public final class UnitDatabase { // exponent function - first check if o2 is a number, if (exponentValue.canConvertTo(SI.ONE)) { // then check if it is an integer, - final double exponent = exponentValue.getValue(); + final double exponent = exponentValue.getValueExact(); if (DecimalComparison.equals(exponent % 1, 0)) // then exponentiate return base.toExponent((int) (exponent + 0.5)); @@ -1650,7 +1651,8 @@ public final class UnitDatabase { final BigDecimal number = new BigDecimal(name); final double uncertainty = Math.pow(10, -number.scale()); - return LinearUnitValue.of(SI.ONE, number.doubleValue(), uncertainty); + return LinearUnitValue.of(SI.ONE, + UncertainDouble.of(number.doubleValue(), uncertainty)); } catch (final NumberFormatException e) { return LinearUnitValue.getExact(this.getLinearUnit(name), 1); } diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index ff83805..c0711dc 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -56,9 +56,10 @@ class UnitTest { final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60); // make sure addition is done correctly - assertEquals(51.576, value1.plus(value2).getValue(), 0.001); - assertEquals(15.5, value1.plus(value3).getValue()); - assertEquals(52.076, value1.plus(value2).plus(value3).getValue(), 0.001); + assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001); + assertEquals(15.5, value1.plus(value3).getValueExact()); + assertEquals(52.076, value1.plus(value2).plus(value3).getValueExact(), + 0.001); // make sure addition uses the correct unit, and is still associative // (ignoring floating-point rounding errors) -- cgit v1.2.3 From cd33f886dfbd35c0ee3d8cf5b553ea3481b0b3a1 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 2 Oct 2020 10:16:10 -0500 Subject: Added Unitlike objects --- src/org/unitConverter/math/UncertainDouble.java | 15 +- src/org/unitConverter/unit/FunctionalUnitlike.java | 72 ++++++ src/org/unitConverter/unit/LinearUnit.java | 18 ++ src/org/unitConverter/unit/LinearUnitValue.java | 22 +- src/org/unitConverter/unit/Nameable.java | 59 +++++ src/org/unitConverter/unit/Unit.java | 118 ++++++---- src/org/unitConverter/unit/UnitValue.java | 149 ++++++++---- src/org/unitConverter/unit/Unitlike.java | 260 +++++++++++++++++++++ src/org/unitConverter/unit/UnitlikeValue.java | 172 ++++++++++++++ 9 files changed, 785 insertions(+), 100 deletions(-) create mode 100644 src/org/unitConverter/unit/FunctionalUnitlike.java create mode 100644 src/org/unitConverter/unit/Nameable.java create mode 100644 src/org/unitConverter/unit/Unitlike.java create mode 100644 src/org/unitConverter/unit/UnitlikeValue.java diff --git a/src/org/unitConverter/math/UncertainDouble.java b/src/org/unitConverter/math/UncertainDouble.java index e948df9..9601c75 100644 --- a/src/org/unitConverter/math/UncertainDouble.java +++ b/src/org/unitConverter/math/UncertainDouble.java @@ -1,5 +1,18 @@ /** - * @since 2020-09-07 + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . */ package org.unitConverter.math; diff --git a/src/org/unitConverter/unit/FunctionalUnitlike.java b/src/org/unitConverter/unit/FunctionalUnitlike.java new file mode 100644 index 0000000..21c1fca --- /dev/null +++ b/src/org/unitConverter/unit/FunctionalUnitlike.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unitlike form that converts using two conversion functions. + * + * @since 2020-09-07 + */ +final class FunctionalUnitlike extends Unitlike { + /** + * 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 converterFrom; + + /** + * A function that accepts a value in the unitlike form and returns a value + * in the unitlike form's base. + */ + private final ToDoubleFunction 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 unitBase, NameSymbol ns, + DoubleFunction converterFrom, ToDoubleFunction 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/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 2d63ca7..b7f33d5 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -64,6 +64,24 @@ public final class LinearUnit extends Unit { unit.convertToBase(value), ns); } + /** + * @return the base unit associated with {@code unit}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + 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 diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index d86d344..8de734e 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -1,5 +1,18 @@ /** - * + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . */ package org.unitConverter.unit; @@ -53,6 +66,7 @@ public final class LinearUnitValue { } private final LinearUnit unit; + private final UncertainDouble value; /** @@ -176,8 +190,7 @@ public final class LinearUnitValue { /** * @return the unit - * - * @since 2020-07-26 + * @since 2020-09-29 */ public final LinearUnit getUnit() { return this.unit; @@ -185,8 +198,7 @@ public final class LinearUnitValue { /** * @return the value - * - * @since 2020-07-26 + * @since 2020-09-29 */ public final UncertainDouble getValue() { return this.value; diff --git a/src/org/unitConverter/unit/Nameable.java b/src/org/unitConverter/unit/Nameable.java new file mode 100644 index 0000000..36740ab --- /dev/null +++ b/src/org/unitConverter/unit/Nameable.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java index eb9b000..0a3298f 100644 --- a/src/org/unitConverter/unit/Unit.java +++ b/src/org/unitConverter/unit/Unit.java @@ -16,12 +16,10 @@ */ package org.unitConverter.unit; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.function.DoubleUnaryOperator; @@ -34,7 +32,7 @@ import org.unitConverter.math.ObjectProduct; * @author Adrien Hopkins * @since 2019-10-16 */ -public abstract class Unit { +public abstract class Unit implements Nameable { /** * Returns a unit from its base and the functions it uses to convert to and * from its base. @@ -98,19 +96,11 @@ public abstract class Unit { private final ObjectProduct unitBase; /** - * The primary name used by this unit. - */ - private final Optional primaryName; - - /** - * A short symbol used to represent this unit. - */ - private final Optional symbol; - - /** - * A set of any additional names and/or spellings that the unit uses. + * This unit's name(s) and symbol + * + * @since 2020-09-07 */ - private final Set otherNames; + private final NameSymbol nameSymbol; /** * Cache storing the result of getDimension() @@ -120,20 +110,17 @@ public abstract class Unit { private transient ObjectProduct dimension = null; /** - * Creates the {@code AbstractUnit}. + * Creates the {@code 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 */ - protected Unit(final ObjectProduct unitBase, final NameSymbol ns) { + Unit(ObjectProduct unitBase, NameSymbol ns) { 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(); + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); } /** @@ -147,18 +134,25 @@ public abstract class Unit { 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.nameSymbol = NameSymbol.of(primaryName, symbol, + new HashSet<>(otherNames)); + } + + /** + * @return this unit as a {@link Unitlike} + * @since 2020-09-07 + */ + public final Unitlike 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 to test with - * @return true if the units are compatible + * @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 @@ -168,6 +162,21 @@ public abstract class Unit { return Objects.equals(this.getBase(), other.getBase()); } + /** + * 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 boolean canConvertTo(final Unitlike 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. @@ -218,6 +227,34 @@ public abstract class Unit { 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 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 convertTo(final Unitlike 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)); + } + /** * Converts from a value expressed in this unit to a value expressed in this * unit's base unit. @@ -270,27 +307,12 @@ public abstract class Unit { } /** - * @return additionalNames - * @since 2019-10-21 - */ - public final Set getOtherNames() { - return this.otherNames; - } - - /** - * @return primaryName - * @since 2019-10-21 + * @return the nameSymbol + * @since 2020-09-07 */ - public final Optional getPrimaryName() { - return this.primaryName; - } - - /** - * @return symbol - * @since 2019-10-21 - */ - public final Optional getSymbol() { - return this.symbol; + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; } /** diff --git a/src/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java index 9e565d9..8932ccc 100644 --- a/src/org/unitConverter/unit/UnitValue.java +++ b/src/org/unitConverter/unit/UnitValue.java @@ -1,3 +1,19 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ package org.unitConverter.unit; import java.util.Objects; @@ -14,60 +30,57 @@ import java.util.Optional; */ 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 */ public static UnitValue of(Unit unit, double value) { - return new UnitValue(Objects.requireNonNull(unit, "unit must not be null"), value); + return new UnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value); } - + private final Unit unit; private final double value; - + /** * @param unit the unit being used * @param value the value being represented */ - private UnitValue(Unit unit, double value) { + private UnitValue(Unit unit, Double value) { this.unit = unit; this.value = value; } - + /** - * @return the unit + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 */ - public final Unit getUnit() { - return unit; + public final boolean canConvertTo(Unit other) { + return this.unit.canConvertTo(other); } - + /** - * @return the value + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 */ - public final double getValue() { - return value; + public final boolean canConvertTo(Unitlike other) { + return this.unit.canConvertTo(other); } - + /** - * Converts this {@code UnitValue} into an equivalent {@code LinearUnitValue} by - * using this unit's base unit. + * Returns a UnitValue that represents the same value expressed in a + * different unit * - * @param newName A new name for the base unit. Use {@link NameSymbol#EMPTY} if - * you don't want one. - */ - public final LinearUnitValue asLinearUnitValue(NameSymbol newName) { - LinearUnit base = LinearUnit.valueOf(unit.getBase(), 1, newName); - return LinearUnitValue.getExact(base, base.convertToBase(value)); - } - - /** - * @param other a {@code Unit} - * @return true iff this value can be represented with {@code other}. + * @param other new unit to express value in + * @return value expressed in {@code other} */ - public final boolean canConvertTo(Unit other) { - return this.unit.canConvertTo(other); + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.getUnit().convertTo(other, this.getValue())); } - + /** * Returns a UnitValue that represents the same value expressed in a * different unit @@ -75,39 +88,83 @@ public final class UnitValue { * @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.getUnit().convertTo(other, this.getValue())); + public final UnitlikeValue convertTo(Unitlike other) { + return UnitlikeValue.of(other, + this.getUnit().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.unit).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.getUnit().convertTo(other, this.getValue())); + } + /** - * 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. + * 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. */ @Override public boolean equals(Object obj) { if (!(obj instanceof UnitValue)) return false; final UnitValue other = (UnitValue) obj; - return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits(other.unit.convertToBase(other.getValue())); + return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) + && Double.doubleToLongBits( + this.getUnit().convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.getUnit().convertToBase(other.getValue())); } - + + /** + * @return the unit + * @since 2020-09-29 + */ + public final Unit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final double getValue() { + return this.value; + } + @Override public int hashCode() { - return Objects.hash(this.unit.getBase(), this.unit.convertFromBase(this.getValue())); + return Objects.hash(this.getUnit().getBase(), + this.getUnit().convertFromBase(this.getValue())); } - + @Override public String toString() { - Optional primaryName = this.getUnit().getPrimaryName(); - Optional symbol = this.getUnit().getSymbol(); + final Optional primaryName = this.getUnit().getPrimaryName(); + final Optional symbol = this.getUnit().getSymbol(); if (primaryName.isEmpty() && symbol.isEmpty()) { - double baseValue = this.getUnit().convertToBase(this.getValue()); - return String.format("%s unnamed unit (= %s %s)", this.getValue(), baseValue, this.getUnit().getBase()); + final double baseValue = this.getUnit().convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnit().getBase()); } else { - String unitName = symbol.orElse(primaryName.get()); + final String unitName = symbol.orElse(primaryName.get()); return this.getValue() + " " + unitName; } } diff --git a/src/org/unitConverter/unit/Unitlike.java b/src/org/unitConverter/unit/Unitlike.java new file mode 100644 index 0000000..a6ddb04 --- /dev/null +++ b/src/org/unitConverter/unit/Unitlike.java @@ -0,0 +1,260 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.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 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 Unitlike fromConversionFunctions( + final ObjectProduct base, + final DoubleFunction converterFrom, + final ToDoubleFunction 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 Unitlike fromConversionFunctions( + final ObjectProduct base, + final DoubleFunction converterFrom, + final ToDoubleFunction 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 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 dimension = null; + + /** + * @param unitBase + * @since 2020-09-07 + */ + Unitlike(ObjectProduct 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 boolean canConvertTo(final Unitlike 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 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 convertTo(final Unitlike 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 getBase() { + return this.unitBase; + } + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct getDimension() { + if (this.dimension == null) { + final Map mapping = this.unitBase.exponentMap(); + final Map 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 withName(final NameSymbol ns) { + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, + Objects.requireNonNull(ns, "ns must not be null.")); + } +} diff --git a/src/org/unitConverter/unit/UnitlikeValue.java b/src/org/unitConverter/unit/UnitlikeValue.java new file mode 100644 index 0000000..669a123 --- /dev/null +++ b/src/org/unitConverter/unit/UnitlikeValue.java @@ -0,0 +1,172 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.Optional; + +/** + * + * @since 2020-09-07 + */ +final class UnitlikeValue { + /** + * Gets a {@code UnitlikeValue}. + * + * @since 2020-10-02 + */ + public static UnitlikeValue of(Unitlike unitlike, V value) { + return new UnitlikeValue<>(unitlike, value); + } + + private final Unitlike unitlike; + private final V value; + + /** + * @param unitlike + * @param value + * @since 2020-09-07 + */ + private UnitlikeValue(Unitlike 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 boolean canConvertTo(Unitlike other) { + return this.unitlike.canConvertTo(other); + } + + /** + * 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 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 UnitlikeValue convertTo(Unitlike other) { + return UnitlikeValue.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 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 primaryName = this.getUnitlike().getPrimaryName(); + final Optional 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; + } + } +} -- cgit v1.2.3 From 0169e644908c4285536d9d23ab82b0a3a46d9d8b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 3 Oct 2020 11:40:17 -0500 Subject: Added the MultiUnit --- src/org/unitConverter/unit/MultiUnit.java | 160 ++++++++++++++++++++++++++ src/org/unitConverter/unit/MultiUnitTest.java | 106 +++++++++++++++++ src/org/unitConverter/unit/UnitValue.java | 15 +-- src/org/unitConverter/unit/Unitlike.java | 6 +- src/org/unitConverter/unit/UnitlikeValue.java | 24 ++-- 5 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 src/org/unitConverter/unit/MultiUnit.java create mode 100644 src/org/unitConverter/unit/MultiUnitTest.java diff --git a/src/org/unitConverter/unit/MultiUnit.java b/src/org/unitConverter/unit/MultiUnit.java new file mode 100644 index 0000000..a1623f8 --- /dev/null +++ b/src/org/unitConverter/unit/MultiUnit.java @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.unitConverter.math.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> { + /** + * 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 units) { + if (units.size() < 1) + throw new IllegalArgumentException("Must have at least one unit"); + final ObjectProduct 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 units; + + /** + * Creates a {@code MultiUnit}. + * + * @since 2020-10-03 + */ + private MultiUnit(List units, ObjectProduct unitBase, + NameSymbol ns) { + super(unitBase, ns); + this.units = units; + } + + @Override + protected List convertFromBase(double value) { + final List 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 , V> V convertTo(U other, + double... values) { + final List 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 valueList = new ArrayList<>(values.length); + for (final double d : values) { + valueList.add(d); + } + + return this.convertTo(other, valueList); + } + + @Override + protected double convertToBase(List 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/org/unitConverter/unit/MultiUnitTest.java b/src/org/unitConverter/unit/MultiUnitTest.java new file mode 100644 index 0000000..5ea9d07 --- /dev/null +++ b/src/org/unitConverter/unit/MultiUnitTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.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 { + + @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(SI.METRE.withPrefix(SI.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 feetAndInches = SI.METRE.withPrefix(SI.MILLI) + .convertTo(footInch, millimetres); + assertEquals(feet, feetAndInches.get(0), 1e-10); + assertEquals(inches, feetAndInches.get(1), 1e-10); + } + } + + /** + * Test method for + * {@link org.unitConverter.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 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 feetAndInches = footInch.convertFromBase(metres); + assertEquals(feet, feetAndInches.get(0), 1e-10); + assertEquals(inches, feetAndInches.get(1), 1e-10); + } + } + + /** + * Test method for + * {@link org.unitConverter.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/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java index 8932ccc..c138332 100644 --- a/src/org/unitConverter/unit/UnitValue.java +++ b/src/org/unitConverter/unit/UnitValue.java @@ -70,15 +70,16 @@ public final class UnitValue { } /** - * Returns a UnitValue that represents the same value expressed in a - * different unit + * 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 UnitValue convertTo(Unit other) { - return UnitValue.of(other, - this.getUnit().convertTo(other, this.getValue())); + public final , W> UnitlikeValue convertTo( + U other) { + return UnitlikeValue.of(other, + this.unit.convertTo(other, this.getValue())); } /** @@ -88,8 +89,8 @@ public final class UnitValue { * @param other new unit to express value in * @return value expressed in {@code other} */ - public final UnitlikeValue convertTo(Unitlike other) { - return UnitlikeValue.of(other, + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, this.getUnit().convertTo(other, this.getValue())); } diff --git a/src/org/unitConverter/unit/Unitlike.java b/src/org/unitConverter/unit/Unitlike.java index a6ddb04..8077771 100644 --- a/src/org/unitConverter/unit/Unitlike.java +++ b/src/org/unitConverter/unit/Unitlike.java @@ -102,7 +102,7 @@ public abstract class Unitlike implements Nameable { * @param unitBase * @since 2020-09-07 */ - Unitlike(ObjectProduct unitBase, NameSymbol ns) { + protected Unitlike(ObjectProduct unitBase, NameSymbol ns) { this.unitBase = Objects.requireNonNull(unitBase, "unitBase may not be null"); this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); @@ -148,7 +148,7 @@ public abstract class Unitlike implements Nameable { * {@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 @@ -175,7 +175,7 @@ public abstract class Unitlike implements Nameable { * {@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 type of value to convert to diff --git a/src/org/unitConverter/unit/UnitlikeValue.java b/src/org/unitConverter/unit/UnitlikeValue.java index 669a123..79201c4 100644 --- a/src/org/unitConverter/unit/UnitlikeValue.java +++ b/src/org/unitConverter/unit/UnitlikeValue.java @@ -22,17 +22,18 @@ import java.util.Optional; * * @since 2020-09-07 */ -final class UnitlikeValue { +final class UnitlikeValue, V> { /** * Gets a {@code UnitlikeValue}. * * @since 2020-10-02 */ - public static UnitlikeValue of(Unitlike unitlike, V value) { + public static , V> UnitlikeValue of(T unitlike, + V value) { return new UnitlikeValue<>(unitlike, value); } - private final Unitlike unitlike; + private final T unitlike; private final V value; /** @@ -40,7 +41,7 @@ final class UnitlikeValue { * @param value * @since 2020-09-07 */ - private UnitlikeValue(Unitlike unitlike, V value) { + private UnitlikeValue(T unitlike, V value) { this.unitlike = unitlike; this.value = value; } @@ -62,14 +63,15 @@ final class UnitlikeValue { } /** - * Returns a UnitValue that represents the same value expressed in a - * different unit + * 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 UnitValue convertTo(Unit other) { - return UnitValue.of(other, + public final , W> UnitlikeValue convertTo( + U other) { + return UnitlikeValue.of(other, this.unitlike.convertTo(other, this.getValue())); } @@ -80,8 +82,8 @@ final class UnitlikeValue { * @param other new unit to express value in * @return value expressed in {@code other} */ - public final UnitlikeValue convertTo(Unitlike other) { - return UnitlikeValue.of(other, + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, this.unitlike.convertTo(other, this.getValue())); } @@ -114,7 +116,7 @@ final class UnitlikeValue { return true; if (!(obj instanceof UnitlikeValue)) return false; - final UnitlikeValue other = (UnitlikeValue) obj; + final UnitlikeValue other = (UnitlikeValue) obj; if (this.getUnitlike() == null) { if (other.getUnitlike() != null) return false; -- cgit v1.2.3 From 3b1f4fd5b58053a2240401ccf199dcd42bbfe9a9 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 11 Dec 2020 10:25:34 -0500 Subject: Expression no longer fails to convert to an expression (eg. A h) --- .settings/org.eclipse.jdt.core.prefs | 101 --------------------- .../converterGUI/UnitConverterGUI.java | 3 +- 2 files changed, 2 insertions(+), 102 deletions(-) diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index f77f6a1..2dd1838 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,13 +1,4 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull -org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= -org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable -org.eclipse.jdt.core.compiler.annotation.nullable.secondary= -org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve @@ -15,102 +6,10 @@ org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.APILeak=warning -org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=enabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning -org.eclipse.jdt.core.compiler.problem.fallthroughCase=error -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=warning -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning -org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error -org.eclipse.jdt.core.compiler.problem.nullReference=error -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=info -org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=info -org.eclipse.jdt.core.compiler.problem.rawTypeReference=error -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=info -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=info -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=info -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=info org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=info -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=ignore -org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.terminalDeprecation=error -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning -org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled -org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=info -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=info -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=disabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedImport=info -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=info -org.eclipse.jdt.core.compiler.problem.unusedParameter=info -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=info -org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=info -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 38a9de1..82ba048 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -325,7 +325,8 @@ final class UnitConverterGUI { if (to instanceof LinearUnit) { // convert to LinearUnitValue final LinearUnitValue from2; - final LinearUnit to2 = (LinearUnit) to; + final LinearUnit to2 = ((LinearUnit) to) + .withName(NameSymbol.ofName(toUnitString)); final boolean useSlash; if (from.canConvertTo(to2)) { -- cgit v1.2.3 From 77a798f3da39731886b55f0036be2af7dfa44263 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 17 Feb 2021 12:36:15 -0500 Subject: Added the ability to save and load settings. --- .gitignore | 3 +- .../converterGUI/UnitConverterGUI.java | 139 ++++++++++++++++++++- 2 files changed, 138 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 52a523a..1d7e13f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin/ target/ *.class -*~ \ No newline at end of file +*~ +settings.txt \ No newline at end of file diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 82ba048..b8b8894 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -22,12 +22,15 @@ import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; @@ -87,6 +90,9 @@ final class UnitConverterGUI { } private static class Presenter { + /** The default place where settings are stored. */ + private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** * Adds default units and dimensions to a database. * @@ -136,6 +142,12 @@ final class UnitConverterGUI { private final Comparator prefixNameComparator; + /** A boolean remembering whether or not one-way conversion is on */ + private boolean oneWay = true; + + /** The prefix rule */ + private DefaultPrefixRepetitionRule prefixRule = null; + // conditions for existence of From and To entries // used for one-way conversion private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( @@ -193,6 +205,9 @@ final class UnitConverterGUI { e); } + // load settings - requires database to exist + this.loadSettings(); + // a comparator that can be used to compare prefix names // any name that does not exist is less than a name that does. // otherwise, they are compared by value @@ -454,6 +469,72 @@ final class UnitConverterGUI { return output + " " + value.getUnit().getPrimaryName().get(); } + /** + * @return The file where settings are stored; + * @since 2020-12-11 + */ + private final Path getSettingsFile() { + return Path.of(DEFAULT_SETTINGS_FILEPATH); + } + + /** + * Loads settings from the settings file. + * + * @since 2021-02-17 + */ + public final void loadSettings() { + try (BufferedReader reader = Files + .newBufferedReader(this.getSettingsFile())) { + + // read file line by line + final int lineNum = 0; + String line; + while ((line = reader.readLine()) != null) { + final int equalsIndex = line.indexOf('='); + if (equalsIndex == -1) + throw new IllegalStateException( + "Settings file is malformed at line " + lineNum); + + final String param = line.substring(0, equalsIndex); + final String value = line.substring(equalsIndex + 1); + + switch (param) { + // set manually to avoid the unnecessary saving of the non-manual + // methods + case "precision": + this.precision = Integer.valueOf(value); + break; + case "rounding_type": + this.roundingType = RoundingType.valueOf(value); + break; + case "prefix_rule": + this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value); + this.database.setPrefixRepetitionRule(this.prefixRule); + break; + case "one_way": + this.oneWay = Boolean.valueOf(value); + if (this.oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName) + .isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + break; + default: + System.err.printf("Warning: unrecognized setting \"%s\".", + param); + break; + } + } + } catch (final IOException e) {} + } + /** * @return a set of all prefix names in the database * @since 2019-04-14 @@ -484,6 +565,27 @@ final class UnitConverterGUI { } } + /** + * Saves the settings to the settings file. + * + * @since 2021-02-17 + */ + public final void saveSettings() { + try (BufferedWriter writer = Files + .newBufferedWriter(this.getSettingsFile())) { + writer.write(String.format("precision=%d\n", this.precision)); + writer.write( + String.format("rounding_type=%s\n", this.roundingType)); + writer.write(String.format("prefix_rule=%s\n", this.prefixRule)); + writer.write(String.format("one_way=%s\n", this.oneWay)); + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorDialog("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + } + } + /** * Enables or disables one-way conversion. * @@ -492,6 +594,7 @@ final class UnitConverterGUI { * @since 2020-08-27 */ public final void setOneWay(boolean oneWay) { + this.oneWay = oneWay; if (oneWay) { this.fromExistenceCondition.setPredicate( unitName -> this.metricExceptions.contains(unitName) @@ -503,6 +606,8 @@ final class UnitConverterGUI { this.fromExistenceCondition.setPredicate(unitName -> true); this.toExistenceCondition.setPredicate(unitName -> true); } + + this.saveSettings(); } /** @@ -512,6 +617,8 @@ final class UnitConverterGUI { */ public final void setPrecision(final int precision) { this.precision = precision; + + this.saveSettings(); } /** @@ -520,7 +627,14 @@ final class UnitConverterGUI { */ public void setPrefixRepetitionRule( Predicate> prefixRepetitionRule) { + if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) { + this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule; + } else { + this.prefixRule = null; + } this.database.setPrefixRepetitionRule(prefixRepetitionRule); + + this.saveSettings(); } /** @@ -529,6 +643,8 @@ final class UnitConverterGUI { */ public final void setRoundingType(RoundingType roundingType) { this.roundingType = roundingType; + + this.saveSettings(); } /** @@ -1022,7 +1138,9 @@ final class UnitConverterGUI { final JRadioButton fixedPrecision = new JRadioButton( "Fixed Precision"); - fixedPrecision.setSelected(true); + if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { + fixedPrecision.setSelected(true); + } fixedPrecision.addActionListener(e -> this.presenter .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); roundingRuleButtons.add(fixedPrecision); @@ -1031,6 +1149,9 @@ final class UnitConverterGUI { final JRadioButton fixedDecimals = new JRadioButton( "Fixed Decimal Places"); + if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { + fixedDecimals.setSelected(true); + } fixedDecimals.addActionListener(e -> this.presenter .setRoundingType(RoundingType.DECIMAL_PLACES)); roundingRuleButtons.add(fixedDecimals); @@ -1039,6 +1160,9 @@ final class UnitConverterGUI { final JRadioButton relativePrecision = new JRadioButton( "Scientific Precision"); + if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { + relativePrecision.setSelected(true); + } relativePrecision.addActionListener(e -> this.presenter .setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); @@ -1058,6 +1182,7 @@ final class UnitConverterGUI { sigDigSlider.setSnapToTicks(true); sigDigSlider.setPaintTicks(true); sigDigSlider.setPaintLabels(true); + sigDigSlider.setValue(this.presenter.precision); sigDigSlider.addChangeListener(e -> this.presenter .setPrecision(sigDigSlider.getValue())); @@ -1075,6 +1200,9 @@ final class UnitConverterGUI { final JRadioButton noRepetition = new JRadioButton( "No Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { + noRepetition.setSelected(true); + } noRepetition.addActionListener( e -> this.presenter.setPrefixRepetitionRule( DefaultPrefixRepetitionRule.NO_REPETITION)); @@ -1086,7 +1214,9 @@ final class UnitConverterGUI { final JRadioButton noRestriction = new JRadioButton( "No Restriction"); - noRestriction.setSelected(true); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { + noRestriction.setSelected(true); + } noRestriction.addActionListener( e -> this.presenter.setPrefixRepetitionRule( DefaultPrefixRepetitionRule.NO_RESTRICTION)); @@ -1098,6 +1228,9 @@ final class UnitConverterGUI { final JRadioButton customRepetition = new JRadioButton( "Complex Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { + customRepetition.setSelected(true); + } customRepetition.addActionListener( e -> this.presenter.setPrefixRepetitionRule( DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); @@ -1155,7 +1288,7 @@ final class UnitConverterGUI { final JCheckBox oneWay = new JCheckBox( "Convert One Way Only"); - oneWay.setSelected(false); + oneWay.setSelected(this.presenter.oneWay); oneWay.addItemListener( e -> this.presenter.setOneWay(e.getStateChange() == 1)); miscPanel.add(oneWay, new GridBagBuilder(0, 0) -- cgit v1.2.3 From 8def3bbda9331b9178e24400d8189afbdaf47e36 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Mar 2021 14:58:01 -0500 Subject: Small internal changes to some classes (no feature changes) --- src/org/unitConverter/math/UncertainDouble.java | 13 +- src/org/unitConverter/unit/BaseUnit.java | 86 ++++++----- src/org/unitConverter/unit/NameSymbol.java | 193 ++++++++++-------------- 3 files changed, 124 insertions(+), 168 deletions(-) diff --git a/src/org/unitConverter/math/UncertainDouble.java b/src/org/unitConverter/math/UncertainDouble.java index 9601c75..3651bd5 100644 --- a/src/org/unitConverter/math/UncertainDouble.java +++ b/src/org/unitConverter/math/UncertainDouble.java @@ -164,11 +164,9 @@ public final class UncertainDouble implements Comparable { if (!(obj instanceof UncertainDouble)) return false; final UncertainDouble other = (UncertainDouble) obj; - if (Double.doubleToLongBits(this.uncertainty) != Double - .doubleToLongBits(other.uncertainty)) + if (Double.compare(this.value, other.value) != 0) return false; - if (Double.doubleToLongBits(this.value) != Double - .doubleToLongBits(other.value)) + if (Double.compare(this.uncertainty, other.uncertainty) != 0) return false; return true; } @@ -216,11 +214,8 @@ public final class UncertainDouble implements Comparable { public final int hashCode() { final int prime = 31; int result = 1; - long temp; - temp = Double.doubleToLongBits(this.uncertainty); - result = prime * result + (int) (temp ^ temp >>> 32); - temp = Double.doubleToLongBits(this.value); - result = prime * result + (int) (temp ^ temp >>> 32); + result = prime * result + Double.hashCode(this.value); + result = prime * result + Double.hashCode(this.uncertainty); return result; } diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java index d9f7965..6757bd0 100644 --- a/src/org/unitConverter/unit/BaseUnit.java +++ b/src/org/unitConverter/unit/BaseUnit.java @@ -23,8 +23,9 @@ import java.util.Set; /** * A unit that other units are defined by. *

- * Note that BaseUnits must 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. + * Note that BaseUnits must 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 @@ -33,63 +34,56 @@ 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 * @return base unit * @since 2019-10-16 */ - public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) { + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol) { return new BaseUnit(dimension, name, symbol, new HashSet<>()); } - + /** * 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 * @return base unit * @since 2019-10-21 */ - public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol, - final Set otherNames) { + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol, final Set otherNames) { return new BaseUnit(dimension, name, symbol, otherNames); } - + /** * 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 + * @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 */ - private BaseUnit(final BaseDimension dimension, final String primaryName, final String symbol, - final Set otherNames) { + private BaseUnit(final BaseDimension dimension, final String primaryName, + final String symbol, final Set otherNames) { super(primaryName, symbol, otherNames); - this.dimension = Objects.requireNonNull(dimension, "dimension must not be null."); + this.dimension = Objects.requireNonNull(dimension, + "dimension must not be null."); } - + /** - * 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. + * 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 @@ -97,17 +91,17 @@ public final class BaseUnit extends Unit { public LinearUnit asLinearUnit() { return LinearUnit.valueOf(this.getBase(), 1); } - + @Override - public double convertFromBase(final double value) { + protected double convertFromBase(final double value) { return value; } - + @Override - public double convertToBase(final double value) { + protected double convertToBase(final double value) { return value; } - + /** * @return dimension * @since 2019-10-16 @@ -115,21 +109,25 @@ public final class BaseUnit extends Unit { public final BaseDimension getBaseDimension() { return this.dimension; } - + @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : ""); + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : ""); } - + @Override public BaseUnit withName(final NameSymbol ns) { Objects.requireNonNull(ns, "ns must not be null."); if (!ns.getPrimaryName().isPresent()) - throw new IllegalArgumentException("BaseUnits must have primary names."); + throw new IllegalArgumentException( + "BaseUnits must have primary names."); if (!ns.getSymbol().isPresent()) throw new IllegalArgumentException("BaseUnits must have symbols."); - return BaseUnit.valueOf(this.getBaseDimension(), ns.getPrimaryName().get(), ns.getSymbol().get(), + return BaseUnit.valueOf(this.getBaseDimension(), + ns.getPrimaryName().get(), ns.getSymbol().get(), ns.getOtherNames()); } } diff --git a/src/org/unitConverter/unit/NameSymbol.java b/src/org/unitConverter/unit/NameSymbol.java index 7fa5304..8d8302a 100644 --- a/src/org/unitConverter/unit/NameSymbol.java +++ b/src/org/unitConverter/unit/NameSymbol.java @@ -31,32 +31,36 @@ import java.util.Set; * @since 2019-10-21 */ public final class NameSymbol { - public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), Optional.empty(), new HashSet<>()); - + public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), + Optional.empty(), new HashSet<>()); + /** * Creates a {@code NameSymbol}, ensuring that if primaryName is null and * otherNames is not empty, one name is moved from otherNames to primaryName * * Ensure that otherNames is a copy of the inputted argument. */ - private static final NameSymbol create(final String name, final String symbol, final Set otherNames) { + private static final NameSymbol create(final String name, + final String symbol, final Set otherNames) { final Optional primaryName; - + if (name == null && !otherNames.isEmpty()) { // get primary name and remove it from savedNames - Iterator it = otherNames.iterator(); + final Iterator it = otherNames.iterator(); assert it.hasNext(); primaryName = Optional.of(it.next()); otherNames.remove(primaryName.get()); } else { primaryName = Optional.ofNullable(name); } - - return new NameSymbol(primaryName, Optional.ofNullable(symbol), otherNames); + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), + otherNames); } - + /** - * Gets a {@code NameSymbol} with a primary name, a symbol and no other names. + * Gets a {@code NameSymbol} with a primary name, a symbol and no other + * names. * * @param name name to use * @param symbol symbol to use @@ -65,11 +69,13 @@ public final class NameSymbol { * @throws NullPointerException if name or symbol is null */ public static final NameSymbol of(final String name, final String symbol) { - return new NameSymbol(Optional.of(name), Optional.of(symbol), new HashSet<>()); + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>()); } - + /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. * * @param name name to use * @param symbol symbol to use @@ -78,11 +84,13 @@ public final class NameSymbol { * @since 2019-10-21 * @throws NullPointerException if any argument is null */ - public static final NameSymbol of(final String name, final String symbol, final Set otherNames) { + public static final NameSymbol of(final String name, final String symbol, + final Set otherNames) { return new NameSymbol(Optional.of(name), Optional.of(symbol), - new HashSet<>(Objects.requireNonNull(otherNames, "otherNames must not be null."))); + new HashSet<>(Objects.requireNonNull(otherNames, + "otherNames must not be null."))); } - + /** * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional * names. @@ -94,72 +102,16 @@ public final class NameSymbol { * @since 2019-10-21 * @throws NullPointerException if any argument is null */ - public static final NameSymbol of(final String name, final String symbol, final String... otherNames) { + public static final NameSymbol of(final String name, final String symbol, + final String... otherNames) { return new NameSymbol(Optional.of(name), Optional.of(symbol), - new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, "otherNames must not be null.")))); + new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, + "otherNames must not be null.")))); } - - /** - * Gets a {@code NameSymbol} with a primary name, a symbol and an additional - * name. - * - * @param name name to use - * @param symbol symbol to use - * @param otherNames other names to use - * @param name2 alternate name - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if any argument is null - */ - public static final NameSymbol of(final String name, final String symbol, final String name2) { - final Set otherNames = new HashSet<>(); - otherNames.add(Objects.requireNonNull(name2, "name2 must not be null.")); - return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames); - } - - /** - * 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 - * @param name2 alternate name - * @param name3 alternate name - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if any argument is null - */ - public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3) { - final Set otherNames = new HashSet<>(); - otherNames.add(Objects.requireNonNull(name2, "name2 must not be null.")); - otherNames.add(Objects.requireNonNull(name3, "name3 must not be null.")); - return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames); - } - - /** - * 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 - * @param name2 alternate name - * @param name3 alternate name - * @param name4 alternate name - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if any argument is null - */ - public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3, - final String name4) { - final Set otherNames = new HashSet<>(); - otherNames.add(Objects.requireNonNull(name2, "name2 must not be null.")); - otherNames.add(Objects.requireNonNull(name3, "name3 must not be null.")); - otherNames.add(Objects.requireNonNull(name4, "name4 must not be null.")); - return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames); - } - + /** - * Gets a {@code NameSymbol} with a primary name, no symbol, and no other names. + * Gets a {@code NameSymbol} with a primary name, no symbol, and no other + * names. * * @param name name to use * @return NameSymbol instance @@ -167,17 +119,19 @@ public final class NameSymbol { * @throws NullPointerException if name is null */ public static final NameSymbol ofName(final String name) { - return new NameSymbol(Optional.of(name), Optional.empty(), new HashSet<>()); + return new NameSymbol(Optional.of(name), Optional.empty(), + new HashSet<>()); } - + /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. *

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

- * If {@code name} is null and {@code otherNames} is not empty, a primary name - * will be picked from {@code otherNames}. This name will not appear in + * 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 @@ -186,10 +140,12 @@ public final class NameSymbol { * @return NameSymbol instance * @since 2019-11-26 */ - public static final NameSymbol ofNullable(final String name, final String symbol, final Set otherNames) { - return NameSymbol.create(name, symbol, otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); + public static final NameSymbol ofNullable(final String name, + final String symbol, final Set otherNames) { + return NameSymbol.create(name, symbol, + otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); } - + /** * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional * names. @@ -197,8 +153,8 @@ public final class NameSymbol { * If any argument is null, this static factory replaces it with an empty * Optional or empty Set. *

- * If {@code name} is null and {@code otherNames} is not empty, a primary name - * will be picked from {@code otherNames}. This name will not appear in + * 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 @@ -207,10 +163,12 @@ public final class NameSymbol { * @return NameSymbol instance * @since 2019-11-26 */ - public static final NameSymbol ofNullable(final String name, final String symbol, final String... otherNames) { - return create(name, symbol, otherNames == null ? new HashSet<>() : new HashSet<>(Arrays.asList(otherNames))); + public static final NameSymbol ofNullable(final String name, + final String symbol, final String... otherNames) { + return create(name, symbol, otherNames == null ? new HashSet<>() + : new HashSet<>(Arrays.asList(otherNames))); } - + /** * Gets a {@code NameSymbol} with a symbol and no names. * @@ -220,59 +178,61 @@ public final class NameSymbol { * @throws NullPointerException if symbol is null */ public static final NameSymbol ofSymbol(final String symbol) { - return new NameSymbol(Optional.empty(), Optional.of(symbol), new HashSet<>()); + return new NameSymbol(Optional.empty(), Optional.of(symbol), + new HashSet<>()); } - + private final Optional primaryName; private final Optional symbol; - + private final Set otherNames; - + /** * Creates the {@code NameSymbol}. * * @param primaryName primary name of unit * @param symbol symbol used to represent unit - * @param otherNames other names and/or spellings, should be a mutable copy of - * the argument + * @param otherNames other names and/or spellings, should be a mutable copy + * of the argument * @since 2019-10-21 */ - private NameSymbol(final Optional primaryName, final Optional symbol, - final Set otherNames) { + private NameSymbol(final Optional primaryName, + final Optional symbol, final Set otherNames) { this.primaryName = primaryName; this.symbol = symbol; otherNames.remove(null); this.otherNames = Collections.unmodifiableSet(otherNames); - if (this.primaryName.isEmpty()) + if (this.primaryName.isEmpty()) { assert this.otherNames.isEmpty(); + } } - + @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof NameSymbol)) return false; - NameSymbol other = (NameSymbol) obj; - if (otherNames == null) { + final NameSymbol other = (NameSymbol) obj; + if (this.otherNames == null) { if (other.otherNames != null) return false; - } else if (!otherNames.equals(other.otherNames)) + } else if (!this.otherNames.equals(other.otherNames)) return false; - if (primaryName == null) { + if (this.primaryName == null) { if (other.primaryName != null) return false; - } else if (!primaryName.equals(other.primaryName)) + } else if (!this.primaryName.equals(other.primaryName)) return false; - if (symbol == null) { + if (this.symbol == null) { if (other.symbol != null) return false; - } else if (!symbol.equals(other.symbol)) + } else if (!this.symbol.equals(other.symbol)) return false; return true; } - + /** * @return otherNames * @since 2019-10-21 @@ -280,7 +240,7 @@ public final class NameSymbol { public final Set getOtherNames() { return this.otherNames; } - + /** * @return primaryName * @since 2019-10-21 @@ -296,17 +256,20 @@ public final class NameSymbol { public final Optional getSymbol() { return this.symbol; } - + @Override public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((otherNames == null) ? 0 : otherNames.hashCode()); - result = prime * result + ((primaryName == null) ? 0 : primaryName.hashCode()); - result = prime * result + ((symbol == null) ? 0 : symbol.hashCode()); + result = prime * result + + (this.otherNames == null ? 0 : this.otherNames.hashCode()); + result = prime * result + + (this.primaryName == null ? 0 : this.primaryName.hashCode()); + result = prime * result + + (this.symbol == null ? 0 : this.symbol.hashCode()); return result; } - + /** * @return true iff this {@code NameSymbol} contains no names or symbols. */ -- cgit v1.2.3 From c5f7ad25645450fbb3ab539f76da189b5b8fcfe0 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Mar 2021 15:23:00 -0500 Subject: Added About tab with copyright information --- src/about.txt | 12 ++++++ .../converterGUI/UnitConverterGUI.java | 46 ++++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/about.txt diff --git a/src/about.txt b/src/about.txt new file mode 100644 index 0000000..1bad9e8 --- /dev/null +++ b/src/about.txt @@ -0,0 +1,12 @@ +About Unit Converter v0.2.0 + +Copyright Notice: + +Unit Converter Copyright (C) 2018-2021 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 + +or read the LICENSE file. \ No newline at end of file diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index b8b8894..69f188b 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018-2021 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -41,6 +41,7 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.BoxLayout; @@ -59,6 +60,8 @@ import javax.swing.JSlider; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; import javax.swing.WindowConstants; import javax.swing.border.TitledBorder; @@ -85,7 +88,7 @@ final class UnitConverterGUI { * A tab in the View. */ private enum Pane { - UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, + UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT, SETTINGS; } @@ -766,9 +769,19 @@ final class UnitConverterGUI { this.presenter = new Presenter(this); this.frame = new JFrame("Unit Converter"); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - this.masterPane = new JTabbedPane(); + + // enable system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | UnsupportedLookAndFeelException e) { + // oh well, just use default theme + System.err.println("Failed to enable system look-and-feel."); + e.printStackTrace(); + } // create the components + this.masterPane = new JTabbedPane(); this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), this.presenter.getPrefixNameComparator(), true); @@ -803,6 +816,8 @@ final class UnitConverterGUI { case 3: return Pane.PREFIX_VIEWER; case 4: + return Pane.ABOUT; + case 5: return Pane.SETTINGS; default: throw new AssertionError("No selected pane, or invalid pane."); @@ -1114,10 +1129,33 @@ final class UnitConverterGUI { } } + { // Info panel + final JPanel 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); + + // get info text + final String infoText; + try { + final Path aboutFile = Path.of("src", "about.txt"); + infoText = Files.readAllLines(aboutFile).stream() + .filter(s -> !s.trim().startsWith("#")) + .collect(Collectors.joining("\n")); + } catch (final IOException e) { + throw new AssertionError("I/O exception loading about.txt"); + } + infoTextArea.setText(infoText); + } + { // Settings panel final JPanel settingsPanel = new JPanel(); this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); - this.masterPane.setMnemonicAt(4, KeyEvent.VK_S); + this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); settingsPanel.setLayout( new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); -- cgit v1.2.3 From 184b7cc697ffc2dcbd49cfb3d0fd7b14bdac8803 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Mar 2021 15:55:35 -0500 Subject: Upgraded to Java 11, and upgraded file code to the new I/O --- .classpath | 6 +-- .settings/org.eclipse.jdt.core.prefs | 11 +++-- .../converterGUI/UnitConverterGUI.java | 57 +++++++++++----------- src/org/unitConverter/unit/UnitDatabase.java | 27 ++++------ 4 files changed, 45 insertions(+), 56 deletions(-) diff --git a/.classpath b/.classpath index 35e2ca9..a6325e3 100644 --- a/.classpath +++ b/.classpath @@ -1,10 +1,6 @@ - - - - - + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 2dd1838..01cf56c 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,15 +1,16 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.compliance=11 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.processAnnotations=disabled -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 69f188b..6ddc4a0 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -21,10 +21,7 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.KeyEvent; -import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.math.BigDecimal; import java.math.MathContext; @@ -95,6 +92,14 @@ final class UnitConverterGUI { private static class Presenter { /** The default place where settings are stored. */ private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** The default place where units are stored. */ + private static final Path DEFAULT_UNITS_FILE = Path.of("unitsfile.txt"); + /** The default place where dimensions are stored. */ + private static final Path DEFAULT_DIMENSION_FILE = Path + .of("dimensionfile.txt"); + /** The default place where exceptions are stored. */ + private static final Path DEFAULT_EXCEPTIONS_FILE = Path + .of("metric_exceptions.txt"); /** * Adds default units and dimensions to a database. @@ -125,6 +130,15 @@ final class UnitConverterGUI { database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } + /** + * @return {@code line} with any comments removed. + * @since 2021-03-13 + */ + private static final String withoutComments(String line) { + final int index = line.indexOf('#'); + return index == -1 ? line : line.substring(index); + } + /** The presenter's associated view. */ private final View view; @@ -150,11 +164,11 @@ final class UnitConverterGUI { /** The prefix rule */ private DefaultPrefixRepetitionRule prefixRule = null; - // conditions for existence of From and To entries // used for one-way conversion private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( s -> true); + private final MutablePredicate toExistenceCondition = new MutablePredicate<>( s -> true); @@ -182,27 +196,14 @@ final class UnitConverterGUI { DefaultPrefixRepetitionRule.NO_RESTRICTION); Presenter.addDefaults(this.database); - this.database.loadUnitsFile(new File("unitsfile.txt")); - this.database.loadDimensionFile(new File("dimensionfile.txt")); + this.database.loadUnitsFile(DEFAULT_UNITS_FILE); + this.database.loadDimensionFile(DEFAULT_DIMENSION_FILE); // load metric exceptions - final File exceptions = new File("metric_exceptions.txt"); - this.metricExceptions = new HashSet<>(); - try (FileReader fileReader = new FileReader(exceptions); - BufferedReader reader = new BufferedReader(fileReader)) { - while (reader.ready()) { - String line = reader.readLine(); - - // # can be used for comments - if (line.contains("#")) { - line = line.substring(line.indexOf("#")); - } - - // don't read black lines - if (!line.isBlank()) { - this.metricExceptions.add(line.strip()); - } - } + try { + this.metricExceptions = Files.readAllLines(DEFAULT_EXCEPTIONS_FILE) + .stream().map(Presenter::withoutComments) + .filter(s -> !s.isBlank()).collect(Collectors.toSet()); } catch (final IOException e) { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); @@ -486,13 +487,11 @@ final class UnitConverterGUI { * @since 2021-02-17 */ public final void loadSettings() { - try (BufferedReader reader = Files - .newBufferedReader(this.getSettingsFile())) { - + try { // read file line by line final int lineNum = 0; - String line; - while ((line = reader.readLine()) != null) { + for (final String line : Files + .readAllLines(this.getSettingsFile())) { final int equalsIndex = line.indexOf('='); if (equalsIndex == -1) throw new IllegalStateException( @@ -1144,7 +1143,7 @@ final class UnitConverterGUI { try { final Path aboutFile = Path.of("src", "about.txt"); infoText = Files.readAllLines(aboutFile).stream() - .filter(s -> !s.trim().startsWith("#")) + .map(Presenter::withoutComments) .collect(Collectors.joining("\n")); } catch (final IOException e) { throw new AssertionError("I/O exception loading about.txt"); diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 9ca9617..000acf5 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -16,12 +16,11 @@ */ package org.unitConverter.unit; -import java.io.BufferedReader; -import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; @@ -1866,15 +1865,12 @@ public final class UnitDatabase { * @since 2019-01-13 * @since v0.1.0 */ - public void loadDimensionFile(final File file) { + public void loadDimensionFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); - BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then - // add it + try { long lineCounter = 0; - while (reader.ready()) { - this.addDimensionFromLine(reader.readLine(), ++lineCounter); + for (final String line : Files.readAllLines(file)) { + this.addDimensionFromLine(line, ++lineCounter); } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); @@ -1908,15 +1904,12 @@ public final class UnitDatabase { * @since 2019-01-13 * @since v0.1.0 */ - public void loadUnitsFile(final File file) { + public void loadUnitsFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); - BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then - // add it + try { long lineCounter = 0; - while (reader.ready()) { - this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); + for (final String line : Files.readAllLines(file)) { + this.addUnitOrPrefixFromLine(line, ++lineCounter); } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); -- cgit v1.2.3