summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.classpath8
-rw-r--r--.gitignore3
-rw-r--r--.settings/org.eclipse.core.resources.prefs2
-rw-r--r--.settings/org.eclipse.jdt.core.prefs11
-rw-r--r--CHANGELOG.org16
-rw-r--r--README.org2
-rw-r--r--build.gradle2
-rw-r--r--docs/design.org75
-rw-r--r--docs/design.pdfbin242954 -> 346620 bytes
-rw-r--r--docs/design.tex145
-rw-r--r--docs/diagrams/convert-expressions.plantuml.pngbin0 -> 36460 bytes
-rw-r--r--docs/diagrams/convert-expressions.plantuml.txt22
-rw-r--r--docs/diagrams/convert-units.plantuml.pngbin0 -> 28368 bytes
-rw-r--r--docs/diagrams/convert-units.plantuml.txt19
-rw-r--r--docs/diagrams/overview-diagram.plantuml.pngbin0 -> 11363 bytes
-rw-r--r--docs/diagrams/overview-diagram.plantuml.txt20
-rw-r--r--docs/manual.org13
-rw-r--r--docs/manual.pdfbin172013 -> 170946 bytes
-rw-r--r--docs/manual.tex44
-rw-r--r--screenshots/main-interface-dimension-converter.pngbin11281 -> 11281 bytes
-rw-r--r--screenshots/main-interface-expression-converter.pngbin8780 -> 8780 bytes
-rw-r--r--screenshots/main-interface-settings.pngbin25953 -> 25953 bytes
-rw-r--r--src/main/java/sevenUnits/ProgramInfo.java12
-rw-r--r--src/main/java/sevenUnits/converterGUI/MutablePredicate.java70
-rw-r--r--src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java1505
-rw-r--r--src/main/java/sevenUnits/unit/BaseDimension.java51
-rw-r--r--src/main/java/sevenUnits/unit/BaseUnit.java2
-rw-r--r--src/main/java/sevenUnits/unit/BritishImperial.java4
-rw-r--r--src/main/java/sevenUnits/unit/FunctionalUnit.java1
-rw-r--r--src/main/java/sevenUnits/unit/FunctionalUnitlike.java1
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnit.java22
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnitValue.java10
-rw-r--r--src/main/java/sevenUnits/unit/Metric.java133
-rw-r--r--src/main/java/sevenUnits/unit/MultiUnit.java1
-rw-r--r--src/main/java/sevenUnits/unit/Unit.java44
-rw-r--r--src/main/java/sevenUnits/unit/UnitDatabase.java67
-rw-r--r--src/main/java/sevenUnits/unit/UnitPrefix.java136
-rw-r--r--src/main/java/sevenUnits/unit/UnitType.java58
-rw-r--r--src/main/java/sevenUnits/unit/UnitValue.java2
-rw-r--r--src/main/java/sevenUnits/unit/Unitlike.java2
-rw-r--r--src/main/java/sevenUnits/unit/UnitlikeValue.java2
-rw-r--r--src/main/java/sevenUnits/utils/NameSymbol.java (renamed from src/main/java/sevenUnits/unit/NameSymbol.java)33
-rw-r--r--src/main/java/sevenUnits/utils/Nameable.java (renamed from src/main/java/sevenUnits/unit/Nameable.java)22
-rw-r--r--src/main/java/sevenUnits/utils/ObjectProduct.java48
-rw-r--r--src/main/java/sevenUnits/utils/SemanticVersionNumber.java716
-rw-r--r--src/main/java/sevenUnits/utils/UncertainDouble.java24
-rw-r--r--src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java (renamed from src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java)4
-rw-r--r--src/main/java/sevenUnitsGUI/DelegateListModel.java (renamed from src/main/java/sevenUnits/converterGUI/DelegateListModel.java)2
-rw-r--r--src/main/java/sevenUnitsGUI/ExpressionConversionView.java49
-rw-r--r--src/main/java/sevenUnitsGUI/FilterComparator.java (renamed from src/main/java/sevenUnits/converterGUI/FilterComparator.java)73
-rw-r--r--src/main/java/sevenUnitsGUI/GridBagBuilder.java (renamed from src/main/java/sevenUnits/converterGUI/GridBagBuilder.java)2
-rw-r--r--src/main/java/sevenUnitsGUI/Main.java38
-rw-r--r--src/main/java/sevenUnitsGUI/PrefixSearchRule.java171
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java851
-rw-r--r--src/main/java/sevenUnitsGUI/SearchBoxList.java (renamed from src/main/java/sevenUnits/converterGUI/SearchBoxList.java)75
-rw-r--r--src/main/java/sevenUnitsGUI/StandardDisplayRules.java254
-rw-r--r--src/main/java/sevenUnitsGUI/TabbedView.java831
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionRecord.java207
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionView.java120
-rw-r--r--src/main/java/sevenUnitsGUI/View.java115
-rw-r--r--src/main/java/sevenUnitsGUI/ViewBot.java508
-rw-r--r--src/main/java/sevenUnitsGUI/package-info.java (renamed from src/main/java/sevenUnits/converterGUI/package-info.java)9
-rw-r--r--src/main/resources/about.txt4
-rw-r--r--src/main/resources/dimensionfile.txt16
-rw-r--r--src/main/resources/unitsfile.txt13
-rw-r--r--src/test/java/sevenUnits/unit/MultiUnitTest.java16
-rw-r--r--src/test/java/sevenUnits/unit/UnitDatabaseTest.java3
-rw-r--r--src/test/java/sevenUnits/unit/UnitTest.java15
-rw-r--r--src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java54
-rw-r--r--src/test/java/sevenUnits/utils/SemanticVersionTest.java399
-rw-r--r--src/test/java/sevenUnits/utils/UncertainDoubleTest.java20
-rw-r--r--src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java123
-rw-r--r--src/test/java/sevenUnitsGUI/PrefixSearchTest.java158
-rw-r--r--src/test/java/sevenUnitsGUI/PresenterTest.java423
-rw-r--r--src/test/java/sevenUnitsGUI/RoundingTest.java287
-rw-r--r--src/test/java/sevenUnitsGUI/TabbedViewTest.java95
76 files changed, 6302 insertions, 1981 deletions
diff --git a/.classpath b/.classpath
index e21da51..d4e759d 100644
--- a/.classpath
+++ b/.classpath
@@ -14,13 +14,17 @@
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
+ <attribute name="test" value="true"/>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
- <attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="src/test/resources"/>
- <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
+ <attributes>
+ <attribute name="module" value="true"/>
+ </attributes>
+ </classpathentry>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>
diff --git a/.gitignore b/.gitignore
index 5be4b77..bbda9ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,9 @@
# Ignore Gradle build output directory
build
-# Unit Converter gitignore files
+# 7Units gitignore files
*.class
*~
settings.txt
+/src/test/resources/test-settings.txt
/bin/
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..9bca13f
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/settings.txt=UTF-8
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 18ad895..cd8d089 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,15 @@
eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+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=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=warning
+org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=11
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 5630737..90f6cfa 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -1,5 +1,21 @@
* Changelog
All notable changes in this project will be shown in this file.
+** v0.4.0 - [2022-07-17 Sun]
+*** Added
+ - *Added tests for the GUI*
+ - *Added search rules that determine which prefixes are shown in the unit conversion view.*
+ - Added an object for the version numbers (SemanticVersionNumber)
+ - Added some toString methods to NameSymbol and Nameable
+*** Changed
+ - *Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve*
+ - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional<String> instead of String, even though they will always succeed.
+ - The UnitDatabase's units, prefixes and dimensions are now always named
+ - The toString method of the common unit classes is now simpler. Alternate toString functions that describe the full unit are provided.
+ - UncertainDouble and LinearUnitValue accept a RoundingMode in their complicated toString functions.
+ - Rounding rules are now in their own classes
+ - The "Show Duplicates" setting now affects the prefix viewer in addition to units
+ - Tweaked the look of the unit and expression conversion sections of the view
+ - Default dimension names are in title case, not uppercase.
** v0.3.2 - [2021-12-02 Thu]
*** Added
- Added lots more tests for the backend and utilities
diff --git a/README.org b/README.org
index 2b4ffa0..bce9006 100644
--- a/README.org
+++ b/README.org
@@ -1,4 +1,4 @@
-* 7Units v0.3.2
+* 7Units Version 0.4.0
(this project uses Semantic Versioning)
** What is it?
This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in.
diff --git a/build.gradle b/build.gradle
index 4484e9d..8b90060 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ java {
sourceCompatibility = JavaVersion.VERSION_11
}
-mainClassName = "sevenUnits.converterGUI.SevenUnitsGUI"
+mainClassName = "sevenUnitsGUI.Main"
repositories {
mavenCentral()
diff --git a/docs/design.org b/docs/design.org
index be345f2..0e3a477 100644
--- a/docs/design.org
+++ b/docs/design.org
@@ -1,20 +1,53 @@
#+TITLE: 7Units Design Document
-#+SUBTITLE: For version 0.3.1
-#+DATE: 2021 August 26
+#+SUBTITLE: For version 0.4.0
+#+DATE: 2022 July 8
#+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry}
#+LaTeX_HEADER: \usepackage{xurl}
#+LaTeX: \newpage
* Introduction
7Units is a program that can convert between units. This document details the internal design of 7Units, intended to be used by current and future developers.
-
- The frontend code is currently subject to change, so it is not included in the current version of this document.
+* System Overview
+ #+CAPTION: A big-picture diagram of 7Units, containing all of the major classes.
+ #+attr_latex: :height 144px
+ [[./diagrams/overview-diagram.plantuml.png]]
+** Packages of 7Units
+ 7Units splits its code into three main packages:
+ - ~sevenUnits.unit~ :: The [[*Unit System Design][unit system]]
+ - ~sevenUnits.utils~ :: Extra classes that aid the unit system.
+ - ~sevenUnitsGUI~ :: The [[*Front-End Design][front end]] code
+ ~sevenUnits.unit~ depends on ~sevenUnits.utils~, while ~sevenUnitsGUI~ depends on both ~sevenUnits~ packages. There is only one class that isn't in any of these packages, ~sevenUnits.VersionInfo~.
+** Major Classes of 7Units
+ - [[*Unit Classes][sevenUnits.unit.Unit]] :: The class representing a unit
+ - [[*The Unit Database][sevenUnits.unit.UnitDatabase]] :: A class that stores collections of units, prefixes and dimensions.
+ - [[*The View][sevenUnitsGUI.View]] :: The class that handles interaction between the user and the program.
+ - [[*The Presenter][sevenUnitsGUI.Presenter]] :: The class that handles communication between the ~View~ and the unit system.
+#+LaTeX: \newpage
+** Process of Unit Conversion
+ #+CAPTION: The process of converting units
+ [[./diagrams/convert-units.plantuml.png]]
+ 1. The ~View~ triggers a unit conversion method from the ~Presenter~.
+ 2. The ~Presenter~ gets raw input data from the ~View~'s public methods (from unit, to unit, from value).
+ 3. The ~Presenter~ uses the ~UnitDatabase~'s methods to convert the raw data from the ~View~ into actual units.
+ 4. The ~Presenter~ performs the unit conversion using the provided unit objects.
+ 5. The ~Presenter~ calls the ~View~'s methods to show the result of the conversion.
+#+LaTeX: \newpage
+** Process of Expression Conversion
+ The process of expression conversion is similar to that of unit conversion.
+ #+CAPTION: The process of converting expressions
+ [[./diagrams/convert-expressions.plantuml.png]]
+ 1. The ~View~ triggers a unit conversion method from the ~Presenter~.
+ 2. The ~Presenter~ gets raw input data from the ~View~'s public methods (unit conversion: from unit, to unit, from value; expression conversion: input expression, output expression).
+ 3. The ~Presenter~ uses the ~UnitDatabase~'s methods to parse the expressions, converting the input into a ~LinearUnitValue~ and the output into a ~Unit~.
+ 4. The ~Presenter~ converts the provided value into the provided unit.
+ 5. The ~Presenter~ calls the ~View~'s methods to show the result of the conversion.
+#+LaTeX: \newpage
* Unit System Design
Any code related to the backend unit system is stored in the ~sevenUnits.unit~ package.
Here is a class diagram of the system. Unimportant methods, methods inherited from Object, getters and setters have been omitted.
- [[./diagrams/units-class-diagram.plantuml.png]]
#+CAPTION: Class diagram of sevenUnits.unit
+ [[./diagrams/units-class-diagram.plantuml.png]]
#+LaTeX: \newpage
** Dimensions
Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an [[*ObjectProduct][ObjectProduct]]<BaseDimension>, where ~BaseDimension~ is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions.
@@ -56,6 +89,38 @@
Dimension files are similar, only for dimensions instead of units and prefixes.
#+LaTeX: \newpage
+* Front-End Design
+ The front-end of 7Units is based on an MVP model. There are two major frontend classes, the *View* and the *Presenter*.
+** The View
+ The ~View~ is the part of the frontend code that directly interacts with the user. It handles input and output, but does not do any processing. Processing is handled by the presenter and the backend code.
+
+ The ~View~ is an interface, not a single class, so that I can easily create multiple views without having to rewrite any processing code. This allows me to easily prototype changes to the GUI without messing with existing code.
+
+ In addition, I have decided to move some functions of the ~View~ into two subinterfaces, ~UnitConversionView~ and ~ExpressionConversionView~. This is because 7Units supports two kinds of unit conversion: unit conversion (select two compatible units, specify a value then convert) and expression conversion (enter two expressions and convert the first to a multiple of the second). Putting these functions into subinterfaces allows a ~View~ to do one type of conversion without forcing it to support the other.
+
+ There are currently two implementations of the ~View~:
+ - TabbedView :: A Swing GUI implementation that uses tabs to separate the two types of conversion. The default GUI used by 7Units.
+ - ViewBot :: A simulated view that allows programs to set the output of its public methods (i.e. every getter in ~View~ has a setter in ~ViewBot~). Intended for testing, and is already used by ~PresenterTest~.
+ Both of these ~View~ implementations implement ~UnitConversionView~ and ~ExpressionConversionView~.
+** The Presenter
+ The ~Presenter~ is an intermediary between the ~View~ and the backend code. It accepts the user's input and passes it to the backend, then accepts the backend's output and passes it to the frontend for user viewing. Its main functions do not have arguments or return values; instead it takes input from and provides output to the ~View~ via its public methods.
+*** Rules
+ The ~Presenter~ has a set of function-object rules that determine some of its behaviours. Each corresponds to a setting in the ~View~, but they can be set to other values via the ~Presenter~'s setters (although nonstandard rules cannot be saved and loaded):
+ - numberDisplayRule :: A function that determines how numbers are displayed. This controls the rounding rules.
+ - prefixRepetitionRule :: A function that determines which combinations of prefixes are valid in unit expressions. This controls the unit prefix rules.
+ - searchRule :: A function that determines which prefixes are shown in the unit lists in unit conversion (which prefixed units are searchable).
+
+ These rules have been made this way to enable an incredible level of customization of these behaviours. Because any function object with the correct arguments and return type is accepted, these rules (especially the number display rule) can do much more than the default behaviours.
+** Utility Classes
+ The frontend has many miscellaneous utility classes. Many of them are package-private. Here is a list of them, with a brief description of what they do and where they are used:
+ - DefaultPrefixRepetitionRule :: An enum containing the available rules determining when you can repeat prefixes. Used by the ~TabbedView~ for selecting the rule and by the ~Presenter~ for loading it from a file.
+ - DelegateListModel :: A ~javax.swing.ListModel~ implementation that delegates all of its methods to a ~List~. Implements ~List~ by also delegating to the underlying list. Used by the ~SearchBoxList~ to create an easily mutable ~ListModel~.
+ - FilterComparator :: A ~Comparator~ that sorts objects according to whether or not they match a filter. Used by the ~SearchBoxList~ for item sorting.
+ - GridBagBuilder :: A convenience class for generating ~GridBagConstraints~ objects for Swing's ~GridBagLayout~. Used by ~TabbedView~ for constructing the GUI.
+ - SearchBoxList :: A Swing component that combines a text field and a ~JList~ to create a searchable list. Used by the ~TabbedView~'s unit conversion mode.
+ - StandardDisplayRules :: A static utility class that allows you to generate display/rounding rules. Used by ~TabbedView~ for generating these rules and ~Presenter~ for loading them from a file.
+ - UnitConversionRecord :: A record-like object that contains the results of a unit or expression conversion. Used by ~UnitConversionView~ and ~ExpressionConversionView~ for accepting the results to be displayed.
+#+LaTeX: \newpage
* Utility Classes
7Units has a few general "utility" classes. They aren't directly related to units, but are used in the units system.
** ObjectProduct
diff --git a/docs/design.pdf b/docs/design.pdf
index fd1f2f1..936928a 100644
--- a/docs/design.pdf
+++ b/docs/design.pdf
Binary files differ
diff --git a/docs/design.tex b/docs/design.tex
index 178a1c8..35d2004 100644
--- a/docs/design.tex
+++ b/docs/design.tex
@@ -1,4 +1,4 @@
-% Created 2021-08-26 Thu 11:25
+% Created 2022-07-17 Sun 16:22
% Intended LaTeX compiler: pdflatex
\documentclass[11pt]{article}
\usepackage[utf8]{inputenc}
@@ -16,9 +16,9 @@
\usepackage{hyperref}
\usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry}
\usepackage{xurl}
-\date{2021 August 26}
+\date{2022 July 8}
\title{7Units Design Document\\\medskip
-\large For version 0.3.1}
+\large For version 0.4.0}
\hypersetup{
pdfauthor={},
pdftitle={7Units Design Document},
@@ -34,25 +34,81 @@
\newpage
\section{Introduction}
-\label{sec:orgc81fddd}
+\label{sec:orgb154108}
7Units is a program that can convert between units. This document details the internal design of 7Units, intended to be used by current and future developers.
-
-The frontend code is currently subject to change, so it is not included in the current version of this document.
+\section{System Overview}
+\label{sec:org064d7bc}
+\begin{figure}[htbp]
+\centering
+\includegraphics[height=144px]{./diagrams/overview-diagram.plantuml.png}
+\caption{A big-picture diagram of 7Units, containing all of the major classes.}
+\end{figure}
+\subsection{Packages of 7Units}
+\label{sec:org8a6985b}
+7Units splits its code into three main packages:
+\begin{description}
+\item[{\texttt{sevenUnits.unit}}] The \hyperref[sec:org76c2c26]{unit system}
+\item[{\texttt{sevenUnits.utils}}] Extra classes that aid the unit system.
+\item[{\texttt{sevenUnitsGUI}}] The \hyperref[sec:orgbb1d346]{front end} code
+\end{description}
+\texttt{sevenUnits.unit} depends on \texttt{sevenUnits.utils}, while \texttt{sevenUnitsGUI} depends on both \texttt{sevenUnits} packages. There is only one class that isn't in any of these packages, \texttt{sevenUnits.VersionInfo}.
+\subsection{Major Classes of 7Units}
+\label{sec:org031cf3b}
+\begin{description}
+\item[{\hyperref[sec:org195924f]{sevenUnits.unit.Unit}}] The class representing a unit
+\item[{\hyperref[sec:org1a286b2]{sevenUnits.unit.UnitDatabase}}] A class that stores collections of units, prefixes and dimensions.
+\item[{\hyperref[sec:orgae93ac0]{sevenUnitsGUI.View}}] The class that handles interaction between the user and the program.
+\item[{\hyperref[sec:org76432e4]{sevenUnitsGUI.Presenter}}] The class that handles communication between the \texttt{View} and the unit system.
+\end{description}
+\newpage
+\subsection{Process of Unit Conversion}
+\label{sec:org0699189}
+\begin{figure}[htbp]
+\centering
+\includegraphics[width=.9\linewidth]{./diagrams/convert-units.plantuml.png}
+\caption{The process of converting units}
+\end{figure}
+\begin{enumerate}
+\item The \texttt{View} triggers a unit conversion method from the \texttt{Presenter}.
+\item The \texttt{Presenter} gets raw input data from the \texttt{View}'s public methods (from unit, to unit, from value).
+\item The \texttt{Presenter} uses the \texttt{UnitDatabase}'s methods to convert the raw data from the \texttt{View} into actual units.
+\item The \texttt{Presenter} performs the unit conversion using the provided unit objects.
+\item The \texttt{Presenter} calls the \texttt{View}'s methods to show the result of the conversion.
+\end{enumerate}
+\newpage
+\subsection{Process of Expression Conversion}
+\label{sec:orgbdb2960}
+The process of expression conversion is similar to that of unit conversion.
+\begin{figure}[htbp]
+\centering
+\includegraphics[width=.9\linewidth]{./diagrams/convert-expressions.plantuml.png}
+\caption{The process of converting expressions}
+\end{figure}
+\begin{enumerate}
+\item The \texttt{View} triggers a unit conversion method from the \texttt{Presenter}.
+\item The \texttt{Presenter} gets raw input data from the \texttt{View}'s public methods (unit conversion: from unit, to unit, from value; expression conversion: input expression, output expression).
+\item The \texttt{Presenter} uses the \texttt{UnitDatabase}'s methods to parse the expressions, converting the input into a \texttt{LinearUnitValue} and the output into a \texttt{Unit}.
+\item The \texttt{Presenter} converts the provided value into the provided unit.
+\item The \texttt{Presenter} calls the \texttt{View}'s methods to show the result of the conversion.
+\end{enumerate}
+\newpage
\section{Unit System Design}
-\label{sec:org9d0655d}
+\label{sec:org76c2c26}
Any code related to the backend unit system is stored in the \texttt{sevenUnits.unit} package.
Here is a class diagram of the system. Unimportant methods, methods inherited from Object, getters and setters have been omitted.
-\begin{center}
+\begin{figure}[htbp]
+\centering
\includegraphics[width=.9\linewidth]{./diagrams/units-class-diagram.plantuml.png}
-\end{center}
+\caption{Class diagram of sevenUnits.unit}
+\end{figure}
\newpage
\subsection{Dimensions}
-\label{sec:org1e98dda}
-Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:orgc7c5740]{ObjectProduct}<BaseDimension>, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions.
+\label{sec:orgc8f3222}
+Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:org07580ff]{ObjectProduct}<BaseDimension>, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions.
\subsection{Unit Classes}
-\label{sec:orgd5ff0fb}
-Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:orgc7c5740]{ObjectProduct}<BaseUnit> (referred to as the base) that they are based on, a dimension (ObjectProduct<BaseDimension>), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable.
+\label{sec:org195924f}
+Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:org07580ff]{ObjectProduct}<BaseUnit> (referred to as the base) that they are based on, a dimension (ObjectProduct<BaseDimension>), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable.
Units also have two conversion functions - one which converts from a value expressed in this unit to its base unit, and another which converts from a value expressed in the base unit to this unit. In \texttt{Unit}, they are defined as two abstract methods. This allows you to convert from any unit to any other (as long as they have the same base, i.e. you aren't converting metres to pounds). To convert from A to B, first convert from A to its base, then convert from the base to B.
@@ -77,36 +133,79 @@ There are a few more classes which play small roles in the unit system:
\item[{USCustomary}] A static utility class with instances of common units in the US Customary system (not to be confused with the British Imperial system; it has the same unit names but the values of a few units are different).
\end{description}
\subsection{Prefixes}
-\label{sec:org1aaa92c}
+\label{sec:org1504786}
A \texttt{UnitPrefix} is a simple object that can multiply a \texttt{LinearUnit} by a value. It can calculate a new name for the unit by combining its name and the unit's name (symbols are done similarly). It can do multiplication, division and exponentation with a number, as well as multiplication and division with another prefix; all of these work by changing the prefix's multiplier.
\subsection{The Unit Database}
-\label{sec:org7c6cf6d}
+\label{sec:org1a286b2}
The \texttt{UnitDatabase} class stores all of the unit, prefix and dimension data used by this program. It is not a representation of an actual database, just a class that stores lots of data.
Units are stored using a custom \texttt{Map} implementation (\texttt{PrefixedUnitMap}) which maps unit names to units. It is backed by two maps: one for units (without prefixes) and one for prefixes. It is programmed to include prefixes (so if units includes "metre" and prefixes includes "kilo", this map will include "kilometre", mapping it to a unit representing a kilometre). It is immutable, but you can modify the underlying maps, which is reflected in the \texttt{PrefixedUnitMap}. Other than that, it is a normal map implementation.
Prefixes and dimensions are stored in normal maps.
\subsubsection{Parsing Expressions}
-\label{sec:org8392990}
-Each \texttt{UnitDatabase} instance has four \hyperref[sec:orgb091347]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct<BaseDimension>}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating.
+\label{sec:org3608cd5}
+Each \texttt{UnitDatabase} instance has four \hyperref[sec:orgb075c07]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct<BaseDimension>}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating.
\subsubsection{Parsing Files}
-\label{sec:orgd4ad341}
+\label{sec:org262b0a7}
There are two types of data files: unit and dimension.
Unit files contain data about units and prefixes. Each line contains the name of a unit or prefix (prefixes end in a dash, units don't) followed by an expression which defines it, separated by one or more space characters (this behaviour is defined by the static regular expression \texttt{NAME\_EXPRESSION}). Unit files are parsed line by line, each line being run through the \texttt{addUnitOrPrefixFromLine} method, which splits a line into name and expression, determines whether it's a unit or a prefix, and parses the expression. Because all units are defined by others, base units need to be defined with a special expression "!"; \textbf{these units should be added to the database before parsing the file}.
Dimension files are similar, only for dimensions instead of units and prefixes.
\newpage
+\section{Front-End Design}
+\label{sec:orgbb1d346}
+The front-end of 7Units is based on an MVP model. There are two major frontend classes, the \textbf{View} and the \textbf{Presenter}.
+\subsection{The View}
+\label{sec:orgae93ac0}
+The \texttt{View} is the part of the frontend code that directly interacts with the user. It handles input and output, but does not do any processing. Processing is handled by the presenter and the backend code.
+
+The \texttt{View} is an interface, not a single class, so that I can easily create multiple views without having to rewrite any processing code. This allows me to easily prototype changes to the GUI without messing with existing code.
+
+In addition, I have decided to move some functions of the \texttt{View} into two subinterfaces, \texttt{UnitConversionView} and \texttt{ExpressionConversionView}. This is because 7Units supports two kinds of unit conversion: unit conversion (select two compatible units, specify a value then convert) and expression conversion (enter two expressions and convert the first to a multiple of the second). Putting these functions into subinterfaces allows a \texttt{View} to do one type of conversion without forcing it to support the other.
+
+There are currently two implementations of the \texttt{View}:
+\begin{description}
+\item[{TabbedView}] A Swing GUI implementation that uses tabs to separate the two types of conversion. The default GUI used by 7Units.
+\item[{ViewBot}] A simulated view that allows programs to set the output of its public methods (i.e. every getter in \texttt{View} has a setter in \texttt{ViewBot}). Intended for testing, and is already used by \texttt{PresenterTest}.
+\end{description}
+Both of these \texttt{View} implementations implement \texttt{UnitConversionView} and \texttt{ExpressionConversionView}.
+\subsection{The Presenter}
+\label{sec:org76432e4}
+The \texttt{Presenter} is an intermediary between the \texttt{View} and the backend code. It accepts the user's input and passes it to the backend, then accepts the backend's output and passes it to the frontend for user viewing. Its main functions do not have arguments or return values; instead it takes input from and provides output to the \texttt{View} via its public methods.
+\subsubsection{Rules}
+\label{sec:org5218ce5}
+The \texttt{Presenter} has a set of function-object rules that determine some of its behaviours. Each corresponds to a setting in the \texttt{View}, but they can be set to other values via the \texttt{Presenter}'s setters (although nonstandard rules cannot be saved and loaded):
+\begin{description}
+\item[{numberDisplayRule}] A function that determines how numbers are displayed. This controls the rounding rules.
+\item[{prefixRepetitionRule}] A function that determines which combinations of prefixes are valid in unit expressions. This controls the unit prefix rules.
+\item[{searchRule}] A function that determines which prefixes are shown in the unit lists in unit conversion (which prefixed units are searchable).
+\end{description}
+
+These rules have been made this way to enable an incredible level of customization of these behaviours. Because any function object with the correct arguments and return type is accepted, these rules (especially the number display rule) can do much more than the default behaviours.
+\subsection{Utility Classes}
+\label{sec:org0cfabd2}
+The frontend has many miscellaneous utility classes. Many of them are package-private. Here is a list of them, with a brief description of what they do and where they are used:
+\begin{description}
+\item[{DefaultPrefixRepetitionRule}] An enum containing the available rules determining when you can repeat prefixes. Used by the \texttt{TabbedView} for selecting the rule and by the \texttt{Presenter} for loading it from a file.
+\item[{DelegateListModel}] A \texttt{javax.swing.ListModel} implementation that delegates all of its methods to a \texttt{List}. Implements \texttt{List} by also delegating to the underlying list. Used by the \texttt{SearchBoxList} to create an easily mutable \texttt{ListModel}.
+\item[{FilterComparator}] A \texttt{Comparator} that sorts objects according to whether or not they match a filter. Used by the \texttt{SearchBoxList} for item sorting.
+\item[{GridBagBuilder}] A convenience class for generating \texttt{GridBagConstraints} objects for Swing's \texttt{GridBagLayout}. Used by \texttt{TabbedView} for constructing the GUI.
+\item[{SearchBoxList}] A Swing component that combines a text field and a \texttt{JList} to create a searchable list. Used by the \texttt{TabbedView}'s unit conversion mode.
+\item[{StandardDisplayRules}] A static utility class that allows you to generate display/rounding rules. Used by \texttt{TabbedView} for generating these rules and \texttt{Presenter} for loading them from a file.
+\item[{UnitConversionRecord}] A record-like object that contains the results of a unit or expression conversion. Used by \texttt{UnitConversionView} and \texttt{ExpressionConversionView} for accepting the results to be displayed.
+\end{description}
+\newpage
\section{Utility Classes}
-\label{sec:org251cb6a}
+\label{sec:org59e5f0c}
7Units has a few general "utility" classes. They aren't directly related to units, but are used in the units system.
\subsection{ObjectProduct}
-\label{sec:orgc7c5740}
+\label{sec:org07580ff}
An \texttt{ObjectProduct} represents a "product" of elements of some type. The units system uses them to represent coherent units as a product of base units, and dimensions as a product of base dimensions.
Internally, it is represented using a map mapping objects to their exponents in the product. For example, the unit "kg m\textsuperscript{2} / s\textsuperscript{2}" (i.e. a Joule) would be represented with a map like \texttt{[kg: 1, m: 2, s: -2]}.
\subsection{ExpressionParser}
-\label{sec:orgb091347}
+\label{sec:orgb075c07}
The \texttt{ExpressionParser} class is used to parse the unit, prefix and dimension expressions that are used throughout 7Units. An expression is something like "(2 m + 30 J / N) * 8 s)". Each instance represents a type of expression, containing a way to obtain values (such as numbers or units) from the text and operations that can be done on these values (such as addition, subtraction or multiplication). Each operation also has a priority, which controls the order of operations (i.e. multiplication gets a higher priority than addition).
\texttt{ExpressionParser} has a parameterized type \texttt{T}, which represents the type of the value used in the expression. The expression parser currently only supports one type of value per expression; in the expressions used by 7Units numbers are treated as a kind of unit or prefix. Operators are represented by internal types; the system distinguishes between unary operators (those that take a single value, like negation) and binary operators (those that take 2 values, like +, -, * or /).
@@ -123,13 +222,13 @@ Expressions are parsed in 2 steps:
After evaluating the last token, there should be one value left in the stack - the answer. If there isn't, the original expression was malformed.
\end{enumerate}
\subsection{Math Classes}
-\label{sec:orgb4a476a}
+\label{sec:org1e73788}
There are two simple math classes in 7Units:
\begin{description}
\item[{\texttt{UncertainDouble}}] Like a \texttt{double}, but with an uncertainty (e.g. \(2.0 \pm 0.4\)). The operations are like those of the regular Double, only they also calculate the uncertainty of the final value. They also have "exact" versions to help interoperation between \texttt{double} and \texttt{UncertainDouble}. It is used by the converter's Scientific Precision setting.
\item[{\texttt{DecimalComparison}}] A static utility class that contains a few alternate equals() methods for \texttt{double} and \texttt{UncertainDouble}. These methods allow a slight (configurable) difference between values to still be considered equal, to fight roundoff error.
\end{description}
\subsection{Collection Classes}
-\label{sec:org4a3e6a1}
+\label{sec:org02e007a}
The \texttt{ConditionalExistenceCollections} class contains wrapper implementations of \texttt{Collection}, \texttt{Iterator}, \texttt{Map} and \texttt{Set}. These implementations ignore elements that do not pass a certain condition - if an element fails the condition, \texttt{contains} will return false, the iterator will skip past it, it won't be counted in \texttt{size}, etc. even if it exists in the original collection. Effectively, any element of the original collection that fails the test does not exist.
\end{document}
diff --git a/docs/diagrams/convert-expressions.plantuml.png b/docs/diagrams/convert-expressions.plantuml.png
new file mode 100644
index 0000000..f3e9096
--- /dev/null
+++ b/docs/diagrams/convert-expressions.plantuml.png
Binary files differ
diff --git a/docs/diagrams/convert-expressions.plantuml.txt b/docs/diagrams/convert-expressions.plantuml.txt
new file mode 100644
index 0000000..08de283
--- /dev/null
+++ b/docs/diagrams/convert-expressions.plantuml.txt
@@ -0,0 +1,22 @@
+@startuml
+
+actor User
+participant View
+participant Presenter
+database UnitDatabase
+participant ExpressionParser
+participant Unit
+
+User -> View : Set input & output expressions
+View -> Presenter : Call convertExpressions()
+View <-- Presenter : Get user input
+Presenter -> UnitDatabase : Send raw user input
+UnitDatabase -> ExpressionParser : Send inputted expressions
+UnitDatabase <-- ExpressionParser : Return parsed expressions
+Presenter <-- UnitDatabase : Return unit/value objects
+Presenter -> Unit : Convert provided units
+Presenter <-- Unit : Return converted value
+View <-- Presenter : Return converted units
+User <-- View : Show conversion output
+
+@enduml
diff --git a/docs/diagrams/convert-units.plantuml.png b/docs/diagrams/convert-units.plantuml.png
new file mode 100644
index 0000000..318e8c9
--- /dev/null
+++ b/docs/diagrams/convert-units.plantuml.png
Binary files differ
diff --git a/docs/diagrams/convert-units.plantuml.txt b/docs/diagrams/convert-units.plantuml.txt
new file mode 100644
index 0000000..564a766
--- /dev/null
+++ b/docs/diagrams/convert-units.plantuml.txt
@@ -0,0 +1,19 @@
+@startuml
+
+actor User
+participant View
+participant Presenter
+database UnitDatabase
+participant Unit
+
+User -> View : Choose units & input value
+View -> Presenter : Call convertUnits()
+View <-- Presenter : Get user input
+Presenter -> UnitDatabase : Send raw user input
+Presenter <-- UnitDatabase : Return unit objects
+Presenter -> Unit : Convert provided units
+Presenter <-- Unit : Return converted value
+View <-- Presenter : Return converted units
+User <-- View : Show conversion output
+
+@enduml
diff --git a/docs/diagrams/overview-diagram.plantuml.png b/docs/diagrams/overview-diagram.plantuml.png
new file mode 100644
index 0000000..096a65c
--- /dev/null
+++ b/docs/diagrams/overview-diagram.plantuml.png
Binary files differ
diff --git a/docs/diagrams/overview-diagram.plantuml.txt b/docs/diagrams/overview-diagram.plantuml.txt
new file mode 100644
index 0000000..aeedbc5
--- /dev/null
+++ b/docs/diagrams/overview-diagram.plantuml.txt
@@ -0,0 +1,20 @@
+@startuml
+
+package sevenUnitsGUI {
+ interface View {}
+ class Presenter {}
+
+ Presenter "1" o-- View
+ View "0-1" o-- Presenter
+}
+
+package sevenUnits.unit {
+ class Unit {}
+ class UnitDatabase {}
+
+ UnitDatabase *-- Unit
+}
+
+Presenter "1" o- UnitDatabase
+
+@enduml
diff --git a/docs/manual.org b/docs/manual.org
index 086ae5d..47302d3 100644
--- a/docs/manual.org
+++ b/docs/manual.org
@@ -1,6 +1,6 @@
#+TITLE: 7Units User Manual
-#+SUBTITLE: For Version 0.3.1
-#+DATE: 2021 July 6
+#+SUBTITLE: For Version 0.4.0
+#+DATE: 2022 July 8
#+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry}
#+LaTeX: \newpage
@@ -9,7 +9,7 @@
* System Requirements
- Works on all major operating systems \\
*NOTE:* All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown.
- - Java version 11-15 required
+ - Java version 11+ required
# installation instructions go here - wait until git repository is fixed/set up
#+LaTeX: \newpage
* How to Use 7Units
@@ -47,7 +47,7 @@
[[../screenshots/sample-conversion-results-expression-converter.png]]
* 7Units Settings
All settings can be accessed in the tab with the gear icon.
- #+CAPTION: The settings menu, as of version 0.3.0
+ #+CAPTION: The settings menu, as of version 0.4.0
#+ATTR_LaTeX: :height 250px
[[../screenshots/main-interface-settings.png]]
** Rounding Settings
@@ -65,6 +65,11 @@
- any number of yocto or yotta
- they must be in this order
- all prefixes must be of the same sign (either all magnifying or all reducing)
+** Search Settings
+ These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile).
+ - Never Include Prefixed Units :: Prefixed units will only be shown if they are explicitly added to the unitfile.
+ - Include Common Prefixes :: Every coherent unit will have its kilo- and milli- versions included in the list.
+ - Include All Single Prefixes :: Every coherent unit will have every prefixed version of it included in the list.
** Miscellaneous Settings
- Convert One Way Only :: In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units[fn:1] will be shown on the right. Units listed in the exceptions file (~src/main/resources/metric_exceptions.txt~) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected.
- Show Duplicates in "Convert Units" :: If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab.
diff --git a/docs/manual.pdf b/docs/manual.pdf
index e6cbb5d..76ec078 100644
--- a/docs/manual.pdf
+++ b/docs/manual.pdf
Binary files differ
diff --git a/docs/manual.tex b/docs/manual.tex
index 25a5cf7..e82dac5 100644
--- a/docs/manual.tex
+++ b/docs/manual.tex
@@ -1,4 +1,4 @@
-% Created 2021-07-06 Tue 15:22
+% Created 2022-07-17 Sun 16:22
% Intended LaTeX compiler: pdflatex
\documentclass[11pt]{article}
\usepackage[utf8]{inputenc}
@@ -15,9 +15,9 @@
\usepackage{capt-of}
\usepackage{hyperref}
\usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry}
-\date{2021 July 6}
+\date{2022 July 8}
\title{7Units User Manual\\\medskip
-\large For Version 0.3.1}
+\large For Version 0.4.0}
\hypersetup{
pdfauthor={},
pdftitle={7Units User Manual},
@@ -32,21 +32,21 @@
\newpage
\section{Introduction and Purpose}
-\label{sec:org9bdf09d}
+\label{sec:org24a029b}
7Units is a program that can be used to convert units. This document outlines how to use the program.
\section{System Requirements}
-\label{sec:org6fc29c1}
+\label{sec:orgab28d01}
\begin{itemize}
\item Works on all major operating systems \\
\textbf{NOTE:} All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown.
-\item Java version 11-15 required
+\item Java version 11+ required
\end{itemize}
\newpage
\section{How to Use 7Units}
-\label{sec:org12dfe6f}
+\label{sec:org23427ab}
\subsection{Simple Unit Conversion}
-\label{sec:org49a020a}
+\label{sec:org91a1ab6}
\begin{enumerate}
\item Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure \ref{main-interface-dimension}:
\begin{figure}[htbp]
@@ -71,7 +71,7 @@
\end{figure}
\end{enumerate}
\subsection{Complex Unit Conversion}
-\label{sec:org5433cd5}
+\label{sec:orgf8fd5b1}
\begin{enumerate}
\item Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure \ref{main-interface-expression}:
\begin{figure}[htbp]
@@ -79,7 +79,7 @@
\includegraphics[height=250px]{../screenshots/main-interface-expression-converter.png}
\caption{\label{main-interface-expression}Taken in version 0.3.0}
\end{figure}
-\item Enter a \hyperref[sec:org05dd82b]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}".
+\item Enter a \hyperref[sec:org7ac3fe5]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}".
\item Enter a unit name (or another unit expression) in the To box.
\item Press the Convert button. This will calculate the value of the first expression, and convert it to a multiple of the second unit (or expression).
\begin{figure}[htbp]
@@ -89,15 +89,15 @@
\end{figure}
\end{enumerate}
\section{7Units Settings}
-\label{sec:org59fb50d}
+\label{sec:org72dc17b}
All settings can be accessed in the tab with the gear icon.
\begin{figure}[htbp]
\centering
\includegraphics[height=250px]{../screenshots/main-interface-settings.png}
-\caption{The settings menu, as of version 0.3.0}
+\caption{The settings menu, as of version 0.4.0}
\end{figure}
\subsection{Rounding Settings}
-\label{sec:org328f0e1}
+\label{sec:org690a0a5}
These settings control how the output of a unit conversion is rounded.
\begin{description}
\item[{Fixed Precision}] Round to a fixed number of \href{https://en.wikipedia.org/wiki/Significant\_figures}{significant digits}. The number of significant digits is controlled by the precision slider below.
@@ -105,7 +105,7 @@ These settings control how the output of a unit conversion is rounded.
\item[{Scientific Precision}] Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider.
\end{description}
\subsection{Prefix Repetition Settings}
-\label{sec:org859bd80}
+\label{sec:orgef19465}
These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre)
\begin{description}
\item[{No Repetition}] Units may only have one prefix.
@@ -119,16 +119,24 @@ These settings control when you are allowed to repeat unit prefixes (e.g. kiloki
\item all prefixes must be of the same sign (either all magnifying or all reducing)
\end{itemize}
\end{description}
+\subsection{Search Settings}
+\label{sec:org038add7}
+These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile).
+\begin{description}
+\item[{Never Include Prefixed Units}] Prefixed units will only be shown if they are explicitly added to the unitfile.
+\item[{Include Common Prefixes}] Every coherent unit will have its kilo- and milli- versions included in the list.
+\item[{Include All Single Prefixes}] Every coherent unit will have every prefixed version of it included in the list.
+\end{description}
\subsection{Miscellaneous Settings}
-\label{sec:org7345e44}
+\label{sec:orgbd23cc6}
\begin{description}
\item[{Convert One Way Only}] In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units\footnote{7Units's definition of "metric" is stricter than the SI, but all of the common units that are commonly considered metric but not included in 7Units's definition are included in the exceptions file.} will be shown on the right. Units listed in the exceptions file (\texttt{src/main/resources/metric\_exceptions.txt}) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected.
\item[{Show Duplicates in "Convert Units"}] If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab.
\end{description}
\section{Appendices}
-\label{sec:orgee83bb3}
+\label{sec:org4ceffe9}
\subsection{Unit Expressions}
-\label{sec:org05dd82b}
+\label{sec:org7ac3fe5}
A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence):
\begin{itemize}
\item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent
@@ -138,6 +146,6 @@ A unit expression is simply a math expression where the values being operated on
Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \textdegree{} C
\subsection{Other Expressions}
-\label{sec:org8014464}
+\label{sec:orga66137e}
There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future.
\end{document}
diff --git a/screenshots/main-interface-dimension-converter.png b/screenshots/main-interface-dimension-converter.png
index 7d8bd4d..2ad0c7c 100644
--- a/screenshots/main-interface-dimension-converter.png
+++ b/screenshots/main-interface-dimension-converter.png
Binary files differ
diff --git a/screenshots/main-interface-expression-converter.png b/screenshots/main-interface-expression-converter.png
index e5e0b2e..9056053 100644
--- a/screenshots/main-interface-expression-converter.png
+++ b/screenshots/main-interface-expression-converter.png
Binary files differ
diff --git a/screenshots/main-interface-settings.png b/screenshots/main-interface-settings.png
index 97b711c..64c4367 100644
--- a/screenshots/main-interface-settings.png
+++ b/screenshots/main-interface-settings.png
Binary files differ
diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java
index 31e43c7..3fc0ef9 100644
--- a/src/main/java/sevenUnits/ProgramInfo.java
+++ b/src/main/java/sevenUnits/ProgramInfo.java
@@ -16,6 +16,8 @@
*/
package sevenUnits;
+import sevenUnits.utils.SemanticVersionNumber;
+
/**
* Information about 7Units
*
@@ -24,8 +26,14 @@ package sevenUnits;
*/
public final class ProgramInfo {
- public static final String VERSION = "0.3.2";
+ /** The version number (0.4.0) */
+ public static final SemanticVersionNumber VERSION = SemanticVersionNumber
+ .stableVersion(0, 4, 0);
- private ProgramInfo() {}
+ private ProgramInfo() {
+ // this class is only for static variables, you shouldn't be able to
+ // construct an instance
+ throw new AssertionError();
+ }
}
diff --git a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java b/src/main/java/sevenUnits/converterGUI/MutablePredicate.java
deleted file mode 100644
index ae6b7a1..0000000
--- a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Copyright (C) 2019 Adrien Hopkins
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.converterGUI;
-
-import java.util.function.Predicate;
-
-/**
- * A container for a predicate, which can be changed later.
- *
- * @author Adrien Hopkins
- * @since 2019-04-13
- * @since v0.2.0
- */
-final class MutablePredicate<T> implements Predicate<T> {
- /**
- * The predicate stored in this {@code MutablePredicate}
- *
- * @since 2019-04-13
- * @since v0.2.0
- */
- private Predicate<T> predicate;
-
- /**
- * Creates the {@code MutablePredicate}.
- *
- * @since 2019-04-13
- * @since v0.2.0
- */
- public MutablePredicate(final Predicate<T> predicate) {
- this.predicate = predicate;
- }
-
- /**
- * @return predicate
- * @since 2019-04-13
- * @since v0.2.0
- */
- public final Predicate<T> getPredicate() {
- return this.predicate;
- }
-
- /**
- * @param predicate
- * new value of predicate
- * @since 2019-04-13
- * @since v0.2.0
- */
- public final void setPredicate(final Predicate<T> predicate) {
- this.predicate = predicate;
- }
-
- @Override
- public boolean test(final T t) {
- return this.predicate.test(t);
- }
-}
diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java
deleted file mode 100644
index bfd5974..0000000
--- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java
+++ /dev/null
@@ -1,1505 +0,0 @@
-/**
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.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.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-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;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Scanner;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-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.UIManager;
-import javax.swing.UnsupportedLookAndFeelException;
-import javax.swing.WindowConstants;
-import javax.swing.border.TitledBorder;
-
-import sevenUnits.ProgramInfo;
-import sevenUnits.unit.BaseDimension;
-import sevenUnits.unit.BritishImperial;
-import sevenUnits.unit.LinearUnit;
-import sevenUnits.unit.LinearUnitValue;
-import sevenUnits.unit.NameSymbol;
-import sevenUnits.unit.Metric;
-import sevenUnits.unit.Unit;
-import sevenUnits.unit.UnitDatabase;
-import sevenUnits.unit.UnitPrefix;
-import sevenUnits.unit.UnitValue;
-import sevenUnits.utils.ConditionalExistenceCollections;
-import sevenUnits.utils.ObjectProduct;
-
-/**
- * @author Adrien Hopkins
- * @since 2018-12-27
- * @since v0.1.0
- */
-final class SevenUnitsGUI {
- /**
- * A tab in the View.
- */
- private enum Pane {
- UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT,
- SETTINGS;
- }
-
- 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 String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt";
- /** The default place where dimensions are stored. */
- private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt";
- /** The default place where exceptions are stored. */
- private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt";
-
- /**
- * Adds default units and dimensions to a database.
- *
- * @param database database to add to
- * @since 2019-04-14
- * @since v0.2.0
- */
- private static void addDefaults(final UnitDatabase database) {
- database.addUnit("metre", Metric.METRE);
- database.addUnit("kilogram", Metric.KILOGRAM);
- database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000));
- database.addUnit("second", Metric.SECOND);
- database.addUnit("ampere", Metric.AMPERE);
- database.addUnit("kelvin", Metric.KELVIN);
- database.addUnit("mole", Metric.MOLE);
- database.addUnit("candela", Metric.CANDELA);
- database.addUnit("bit", Metric.BIT);
- database.addUnit("unit", Metric.ONE);
- // nonlinear units - must be loaded manually
- database.addUnit("tempCelsius", Metric.CELSIUS);
- database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
-
- // load initial dimensions
- database.addDimension("LENGTH", Metric.Dimensions.LENGTH);
- database.addDimension("MASS", Metric.Dimensions.MASS);
- database.addDimension("TIME", Metric.Dimensions.TIME);
- database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE);
- }
-
- /**
- * Gets the text of a resource file as a set of strings (each one is one
- * line of the text).
- *
- * @param filename filename to get resource from
- * @return contents of file
- * @since 2021-03-27
- */
- public static final List<String> getLinesFromResource(String filename) {
- final List<String> lines = new ArrayList<>();
-
- try (InputStream stream = inputStream(filename);
- Scanner scanner = new Scanner(stream)) {
- while (scanner.hasNextLine()) {
- lines.add(scanner.nextLine());
- }
- } catch (final IOException e) {
- throw new AssertionError(
- "Error occurred while loading file " + filename, e);
- }
-
- return lines;
- }
-
- /**
- * Gets an input stream for a resource file.
- *
- * @param filepath file to use as resource
- * @return obtained Path
- * @since 2021-03-27
- */
- private static final InputStream inputStream(String filepath) {
- return SevenUnitsGUI.class.getResourceAsStream(filepath);
- }
-
- /**
- * @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;
-
- /** The units known by the program. */
- private final UnitDatabase database;
-
- /** The names of all of the units */
- private final List<String> unitNames;
-
- /** The names of all of the prefixes */
- private final List<String> prefixNames;
-
- /** The names of all of the dimensions */
- private final List<String> dimensionNames;
-
- /** Unit names that are ignored by the metric-only/imperial-only filter */
- private final Set<String> metricExceptions;
-
- private final Comparator<String> 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<String> fromExistenceCondition = new MutablePredicate<>(
- s -> true);
-
- private final MutablePredicate<String> toExistenceCondition = new MutablePredicate<>(
- s -> true);
-
- /*
- * Rounding-related settings. I am using my own system, and not
- * MathContext, because MathContext does not support decimal place based
- * or scientific rounding, only significant digit based rounding.
- */
- private int precision = 6;
-
- private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS;
-
- // The "include duplicate units" setting
- private boolean includeDuplicateUnits = true;
-
- /**
- * Creates the presenter.
- *
- * @param view presenter's associated view
- * @since 2018-12-27
- * @since v0.1.0
- */
- Presenter(final View view) {
- this.view = view;
-
- // load initial units
- this.database = new UnitDatabase(
- DefaultPrefixRepetitionRule.NO_RESTRICTION);
- Presenter.addDefaults(this.database);
-
- // load units and prefixes
- try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) {
- this.database.loadUnitsFromStream(units);
- } catch (final IOException e) {
- throw new AssertionError("Loading of unitsfile.txt failed.", e);
- }
-
- // load dimensions
- try (final InputStream dimensions = inputStream(
- DEFAULT_DIMENSIONS_FILEPATH)) {
- this.database.loadDimensionsFromStream(dimensions);
- } catch (final IOException e) {
- throw new AssertionError("Loading of dimensionfile.txt failed.", e);
- }
-
- // load metric exceptions
- try {
- this.metricExceptions = new HashSet<>();
- try (InputStream exceptions = inputStream(
- DEFAULT_EXCEPTIONS_FILEPATH);
- Scanner scanner = new Scanner(exceptions)) {
- while (scanner.hasNextLine()) {
- final String line = Presenter
- .withoutComments(scanner.nextLine());
- if (!line.isBlank()) {
- this.metricExceptions.add(line);
- }
- }
- }
- } catch (final IOException e) {
- throw new AssertionError("Loading of metric_exceptions.txt failed.",
- e);
- }
-
- // load settings - requires database to exist
- if (Files.exists(this.getSettingsFile())) {
- 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
- this.prefixNameComparator = (o1, o2) -> {
- if (!Presenter.this.database.containsPrefixName(o1))
- 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(true).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.dimensionNames.sort(null); // sorts it using Comparable
-
- // a Predicate that returns true iff the argument is a full base unit
- final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit
- && ((LinearUnit) unit).isBase();
-
- // print out unit counts
- System.out.printf(
- "Successfully loaded %d units with %d unit names (%d base units).%n",
- this.database.unitMapPrefixless(false).size(),
- this.database.unitMapPrefixless(true).size(),
- this.database.unitMapPrefixless(false).values().stream()
- .filter(isFullBase).count());
- }
-
- /**
- * Converts in the dimension-based converter
- *
- * @since 2019-04-13
- * @since v0.2.0
- */
- public final void convertDimensionBased() {
- final String fromSelection = this.view.getFromSelection();
- if (fromSelection == null) {
- this.view.showErrorDialog("Error",
- "No unit selected in From field");
- return;
- }
- final String toSelection = this.view.getToSelection();
- if (toSelection == null) {
- 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)
- .withName(NameSymbol.ofName(toSelection));
-
- final UnitValue beforeValue;
- try {
- beforeValue = UnitValue.of(from,
- this.view.getDimensionConverterInput());
- } catch (final ParseException e) {
- this.view.showErrorDialog("Error",
- "Error in parsing: " + e.getMessage());
- return;
- }
- 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.
- *
- * <p>
- * 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.
- * </p>
- *
- * @since 2019-01-26
- * @since v0.1.0
- */
- 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.");
- return;
- }
- if (toUnitString.isEmpty()) {
- this.view.showErrorDialog("Parse Error",
- "Please enter a unit expression in the To: box.");
- return;
- }
-
- final LinearUnitValue from;
- final Unit to;
- try {
- from = this.database.evaluateUnitExpression(fromUnitString);
- } 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 | NoSuchElementException e) {
- this.view.showErrorDialog("Parse Error",
- "Could not recognize text in To entry: " + e.getMessage());
- return;
- }
-
- if (to instanceof LinearUnit) {
- // convert to LinearUnitValue
- final LinearUnitValue from2;
- final LinearUnit to2 = ((LinearUnit) to)
- .withName(NameSymbol.ofName(toUnitString));
- 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((useSlash ? "1 / " : "")
- + String.format("%s = %s", fromUnitString,
- this.getRoundedString(converted, false)));
- return;
- } else {
- // 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
- * @since v0.2.0
- */
- public final List<String> dimensionNameList() {
- 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<String> fromEntries() {
- return ConditionalExistenceCollections.conditionalExistenceSet(
- this.unitNameSet(), this.fromExistenceCondition);
- }
-
- /**
- * @return a comparator to compare prefix names
- * @since 2019-04-14
- * @since v0.2.0
- */
- public final Comparator<String> getPrefixNameComparator() {
- return this.prefixNameComparator;
- }
-
- /**
- * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit
- * converter's rounding settings.
- *
- * @since 2020-08-04
- */
- 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(showUncertainty);
- 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 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 {
- // read file line by line
- final int lineNum = 0;
- for (final String line : Files
- .readAllLines(this.getSettingsFile())) {
- 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;
- case "include_duplicates":
- this.includeDuplicateUnits = Boolean.valueOf(value);
- if (this.view.presenter != null) {
- this.view.update();
- }
- 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
- * @since v0.2.0
- */
- public final Set<String> prefixNameSet() {
- return this.database.prefixMap().keySet();
- }
-
- /**
- * Runs whenever a prefix is selected in the viewer.
- * <p>
- * Shows its information in the text box to the right.
- * </p>
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void prefixSelected() {
- final String prefixName = this.view.getPrefixViewerSelection();
- if (prefixName == null)
- return;
- else {
- final UnitPrefix prefix = this.database.getPrefix(prefixName);
-
- this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s",
- prefixName, prefix.getMultiplier()));
- }
- }
-
- /**
- * 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));
- writer.write(String.format("include_duplicates=%s\n",
- this.includeDuplicateUnits));
- } catch (final IOException e) {
- e.printStackTrace();
- this.view.showErrorDialog("I/O Error",
- "Error occurred while saving settings: "
- + e.getLocalizedMessage());
- }
- }
-
- public final void setIncludeDuplicateUnits(
- boolean includeDuplicateUnits) {
- this.includeDuplicateUnits = includeDuplicateUnits;
-
- this.view.update();
- this.saveSettings();
- }
-
- /**
- * 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) {
- this.oneWay = oneWay;
- if (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);
- }
-
- this.saveSettings();
- }
-
- /**
- * @param precision new value of precision
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void setPrecision(final int precision) {
- this.precision = precision;
-
- this.saveSettings();
- }
-
- /**
- * @param prefixRepetitionRule the prefixRepetitionRule to set
- * @since 2020-08-26
- */
- public void setPrefixRepetitionRule(
- Predicate<List<UnitPrefix>> prefixRepetitionRule) {
- if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) {
- this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule;
- } else {
- this.prefixRule = null;
- }
- this.database.setPrefixRepetitionRule(prefixRepetitionRule);
-
- this.saveSettings();
- }
-
- /**
- * @param roundingType the roundingType to set
- * @since 2020-07-16
- */
- public final void setRoundingType(RoundingType roundingType) {
- this.roundingType = roundingType;
-
- this.saveSettings();
- }
-
- /**
- * @return a list of all the entries in the dimension-based converter's To
- * box
- * @since 2020-08-27
- */
- public final Set<String> toEntries() {
- return ConditionalExistenceCollections.conditionalExistenceSet(
- this.unitNameSet(), this.toExistenceCondition);
- }
-
- /**
- * Returns true if and only if the unit represented by {@code unitName}
- * has the dimension represented by {@code dimensionName}.
- *
- * @param unitName name of unit to test
- * @param dimensionName name of dimension to test
- * @return whether unit has dimenision
- * @since 2019-04-13
- * @since v0.2.0
- */
- public final boolean unitMatchesDimension(final String unitName,
- final String dimensionName) {
- final Unit unit = this.database.getUnit(unitName);
- final ObjectProduct<BaseDimension> dimension = this.database
- .getDimension(dimensionName);
- return unit.getDimension().equals(dimension);
- }
-
- /**
- * Runs whenever a unit is selected in the viewer.
- * <p>
- * Shows its information in the text box to the right.
- * </p>
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void unitNameSelected() {
- final String unitName = this.view.getUnitViewerSelection();
- if (unitName == null)
- 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
- * @since v0.2.0
- */
- public final Set<String> unitNameSet() {
- return this.database.unitMapPrefixless(this.includeDuplicateUnits)
- .keySet();
- }
- }
-
- /**
- * 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();
-
- /** The view's frame. */
- private final JFrame frame;
- /** The view's associated presenter. */
- 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;
- /** The panel for "From" in the dimension-based converter */
- private final SearchBoxList fromSearch;
- /** The panel for "To" in the dimension-based converter */
- 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;
- /** The "To" entry in the conversion panel */
- 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;
- /** The searchable list of prefix names in the prefix viewer */
- private final SearchBoxList prefixNameList;
- /** The text box for unit data in the unit viewer */
- private final JTextArea unitTextBox;
- /** The text box for prefix data in the prefix viewer */
- private final JTextArea prefixTextBox;
-
- /**
- * Creates the {@code View}.
- *
- * @since 2019-01-14
- * @since v0.1.0
- */
- public View() {
- this.presenter = new Presenter(this);
- this.frame = new JFrame("7Units");
- this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
-
- // 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);
- this.unitTextBox = new JTextArea();
- this.prefixTextBox = new JTextArea();
- 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();
- 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
- */
- public Pane getActivePane() {
- switch (this.masterPane.getSelectedIndex()) {
- case 0:
- return Pane.UNIT_CONVERTER;
- case 1:
- return Pane.EXPRESSION_CONVERTER;
- case 2:
- return Pane.UNIT_VIEWER;
- 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.");
- }
- }
-
- /**
- * @return value in dimension-based converter
- * @throws ParseException
- * @since 2020-07-07
- */
- public double getDimensionConverterInput() throws ParseException {
- 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();
- }
-
- /**
- * @return selection in "From" selector in dimension-based converter
- * @since 2019-04-13
- * @since v0.2.0
- */
- public String getFromSelection() {
- return this.fromSearch.getSelectedValue();
- }
-
- /**
- * @return text in "From" box in converter panel
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getFromText() {
- return this.fromEntry.getText();
- }
-
- /**
- * @return index of selected prefix in prefix viewer
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getPrefixViewerSelection() {
- return this.prefixNameList.getSelectedValue();
- }
-
- /**
- * @return selection in "To" selector in dimension-based converter
- * @since 2019-04-13
- * @since v0.2.0
- */
- public String getToSelection() {
- return this.toSearch.getSelectedValue();
- }
-
- /**
- * @return text in "To" box in converter panel
- * @since 2019-01-26
- * @since v0.1.0
- */
- public String getToText() {
- return this.toEntry.getText();
- }
-
- /**
- * @return index of selected unit in unit viewer
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getUnitViewerSelection() {
- return this.unitNameList.getSelectedValue();
- }
-
- /**
- * Starts up the application.
- *
- * @since 2018-12-27
- * @since v0.1.0
- */
- public final void init() {
- this.frame.setVisible(true);
- }
-
- /**
- * Initializes the view's components.
- *
- * @since 2018-12-27
- * @since v0.1.0
- */
- 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);
-
- // 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);
- 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<String> dimensionSelector = new JComboBox<>(
- this.presenter.dimensionNameList()
- .toArray(new String[0]));
- dimensionSelector.setSelectedItem("LENGTH");
-
- // handle dimension filter
- final MutablePredicate<String> 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
- final JPanel inBetweenPanel = new JPanel();
- inputPanel.add(inBetweenPanel);
-
- inBetweenPanel.setLayout(new BorderLayout());
-
- { // dimension selector
- 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()));
- 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()));
- 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);
- }
-
- { // value to convert
- 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.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.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.setMnemonic(KeyEvent.VK_ENTER);
- }
-
- { // output of conversion
- final JPanel outputPanel = new JPanel();
- convertExpressionPanel.add(outputPanel);
-
- 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());
- }
-
- { // 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());
- }
-
- { // the text box for prefix's toString
- prefixLookupPanel.add(this.prefixTextBox);
- this.prefixTextBox.setEditable(false);
- this.prefixTextBox.setLineWrap(true);
- }
- }
-
- { // 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 = Presenter
- .getLinesFromResource("/about.txt").stream()
- .map(Presenter::withoutComments)
- .collect(Collectors.joining("\n"))
- .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION);
- infoTextArea.setText(infoText);
- }
-
- { // Settings panel
- final JPanel settingsPanel = new JPanel();
- this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel));
- this.masterPane.setMnemonicAt(5, 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");
- if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) {
- 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");
- if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) {
- fixedDecimals.setSelected(true);
- }
- 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");
- if (this.presenter.roundingType == RoundingType.SCIENTIFIC) {
- relativePrecision.setSelected(true);
- }
- relativePrecision.addActionListener(e -> this.presenter
- .setRoundingType(RoundingType.SCIENTIFIC));
- roundingRuleButtons.add(relativePrecision);
- roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JLabel sliderLabel = new JLabel("Precision:");
- roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JSlider sigDigSlider = new JSlider(0, 12);
- roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- sigDigSlider.setMajorTickSpacing(4);
- sigDigSlider.setMinorTickSpacing(1);
- sigDigSlider.setSnapToTicks(true);
- sigDigSlider.setPaintTicks(true);
- sigDigSlider.setPaintLabels(true);
- sigDigSlider.setValue(this.presenter.precision);
-
- sigDigSlider.addChangeListener(e -> this.presenter
- .setPrecision(sigDigSlider.getValue()));
- }
-
- { // prefix repetition settings
- 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");
- if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
- noRepetition.setSelected(true);
- }
- noRepetition.addActionListener(
- e -> this.presenter.setPrefixRepetitionRule(
- DefaultPrefixRepetitionRule.NO_REPETITION));
- prefixRuleButtons.add(noRepetition);
- prefixRepetitionPanel.add(noRepetition,
- new GridBagBuilder(0, 0)
- .setAnchor(GridBagConstraints.LINE_START)
- .build());
-
- final JRadioButton noRestriction = new JRadioButton(
- "No Restriction");
- if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
- noRestriction.setSelected(true);
- }
- noRestriction.addActionListener(
- e -> this.presenter.setPrefixRepetitionRule(
- DefaultPrefixRepetitionRule.NO_RESTRICTION));
- prefixRuleButtons.add(noRestriction);
- prefixRepetitionPanel.add(noRestriction,
- new GridBagBuilder(0, 1)
- .setAnchor(GridBagConstraints.LINE_START)
- .build());
-
- final JRadioButton customRepetition = new JRadioButton(
- "Complex Repetition");
- if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
- customRepetition.setSelected(true);
- }
- customRepetition.addActionListener(
- e -> this.presenter.setPrefixRepetitionRule(
- DefaultPrefixRepetitionRule.COMPLEX_REPETITION));
- 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.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());
- }
-
- { // miscellaneous settings
- final JPanel miscPanel = new JPanel();
- settingsPanel.add(miscPanel);
- miscPanel
- .setBorder(new TitledBorder("Miscellaneous Settings"));
- miscPanel.setLayout(new GridBagLayout());
-
- final JCheckBox oneWay = new JCheckBox(
- "Convert One Way Only");
- oneWay.setSelected(this.presenter.oneWay);
- oneWay.addItemListener(
- e -> this.presenter.setOneWay(e.getStateChange() == 1));
- miscPanel.add(oneWay, new GridBagBuilder(0, 0)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JCheckBox showAllVariations = new JCheckBox(
- "Show Duplicates in \"Convert Units\"");
- showAllVariations
- .setSelected(this.presenter.includeDuplicateUnits);
- showAllVariations.addItemListener(e -> this.presenter
- .setIncludeDuplicateUnits(e.getStateChange() == 1));
- 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, 2)
- .setAnchor(GridBagConstraints.LINE_START).build());
- }
- }
- }
- }
-
- /**
- * Sets the text in the output of the dimension-based converter.
- *
- * @param text text to set
- * @since 2019-04-13
- * @since v0.2.0
- */
- public void setDimensionConverterOutputText(final String text) {
- this.dimensionBasedOutput.setText(text);
- }
-
- /**
- * Sets the text in the output of the conversion panel.
- *
- * @param text text to set
- * @since 2019-01-15
- * @since v0.1.0
- */
- public void setExpressionConverterOutputText(final String text) {
- this.output.setText(text);
- }
-
- /**
- * Sets the text of the prefix text box in the prefix viewer.
- *
- * @param text text to set
- * @since 2019-01-15
- * @since v0.1.0
- */
- public void setPrefixTextBoxText(final String text) {
- this.prefixTextBox.setText(text);
- }
-
- /**
- * Sets the text of the unit text box in the unit viewer.
- *
- * @param text text to set
- * @since 2019-01-15
- * @since v0.1.0
- */
- public void setUnitTextBoxText(final String text) {
- this.unitTextBox.setText(text);
- }
-
- /**
- * Shows an error dialog.
- *
- * @param title title of dialog
- * @param message message in dialog
- * @since 2019-01-14
- * @since v0.1.0
- */
- public void showErrorDialog(final String title, final String message) {
- JOptionPane.showMessageDialog(this.frame, message, title,
- JOptionPane.ERROR_MESSAGE);
- }
-
- public void update() {
- this.unitNameList.setItems(this.presenter.unitNameSet());
- this.fromSearch.setItems(this.presenter.fromEntries());
- this.toSearch.setItems(this.presenter.toEntries());
-
- 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) {
- new View().init();
- }
-}
diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java
index d5e98ca..820d48c 100644
--- a/src/main/java/sevenUnits/unit/BaseDimension.java
+++ b/src/main/java/sevenUnits/unit/BaseDimension.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2022 Adrien Hopkins
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -18,70 +18,61 @@ package sevenUnits.unit;
import java.util.Objects;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
+
/**
* A dimension that defines a {@code BaseUnit}
*
* @author Adrien Hopkins
* @since 2019-10-16
*/
-public final class BaseDimension {
+public final class BaseDimension implements Nameable {
/**
* Gets a {@code BaseDimension} with the provided name and symbol.
*
- * @param name
- * name of dimension
- * @param symbol
- * symbol used for dimension
+ * @param name name of dimension
+ * @param symbol symbol used for dimension
* @return dimension
* @since 2019-10-16
*/
public static BaseDimension valueOf(final String name, final String symbol) {
return new BaseDimension(name, symbol);
}
-
+
/**
* The name of the dimension.
*/
private final String name;
/**
- * The symbol used by the dimension. Symbols should be short, generally one or two characters.
+ * The symbol used by the dimension. Symbols should be short, generally one
+ * or two characters.
*/
private final String symbol;
-
+
/**
* Creates the {@code BaseDimension}.
*
- * @param name
- * name of unit
- * @param symbol
- * symbol of unit
- * @throws NullPointerException
- * if any argument is null
+ * @param name name of unit
+ * @param symbol symbol of unit
+ * @throws NullPointerException if any argument is null
* @since 2019-10-16
*/
private BaseDimension(final String name, final String symbol) {
this.name = Objects.requireNonNull(name, "name must not be null.");
this.symbol = Objects.requireNonNull(symbol, "symbol must not be null.");
}
-
- /**
- * @return name
- * @since 2019-10-16
- */
- public final String getName() {
- return this.name;
- }
-
+
/**
- * @return symbol
- * @since 2019-10-16
+ * @since v0.4.0
*/
- public final String getSymbol() {
- return this.symbol;
+ @Override
+ public NameSymbol getNameSymbol() {
+ return NameSymbol.of(this.name, this.symbol);
}
-
+
@Override
public String toString() {
- return String.format("%s (%s)", this.getName(), this.getSymbol());
+ return String.format("%s (%s)", this.name, this.symbol);
}
}
diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java
index ee2c277..dba7f52 100644
--- a/src/main/java/sevenUnits/unit/BaseUnit.java
+++ b/src/main/java/sevenUnits/unit/BaseUnit.java
@@ -20,6 +20,8 @@ import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import sevenUnits.utils.NameSymbol;
+
/**
* A unit that other units are defined by.
* <p>
diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java
index 743beeb..0ecba6d 100644
--- a/src/main/java/sevenUnits/unit/BritishImperial.java
+++ b/src/main/java/sevenUnits/unit/BritishImperial.java
@@ -16,6 +16,8 @@
*/
package sevenUnits.unit;
+import sevenUnits.utils.NameSymbol;
+
/**
* A static utility class that contains units in the British Imperial system.
*
@@ -119,5 +121,5 @@ public final class BritishImperial {
public static final Unit FAHRENHEIT = Unit
.fromConversionFunctions(Metric.KELVIN.getBase(),
tempK -> tempK * 1.8 - 459.67, tempF -> (tempF + 459.67) / 1.8)
- .withName(NameSymbol.of("degrees Fahrenheit", "\u00B0F"));
+ .withName(NameSymbol.of("degree Fahrenheit", "\u00B0F"));
}
diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java
index df457e4..720b0af 100644
--- a/src/main/java/sevenUnits/unit/FunctionalUnit.java
+++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java
@@ -19,6 +19,7 @@ package sevenUnits.unit;
import java.util.Objects;
import java.util.function.DoubleUnaryOperator;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
index 2ee9e19..d6046c0 100644
--- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
+++ b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
@@ -19,6 +19,7 @@ package sevenUnits.unit;
import java.util.function.DoubleFunction;
import java.util.function.ToDoubleFunction;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java
index 25c2e2e..103b7f6 100644
--- a/src/main/java/sevenUnits/unit/LinearUnit.java
+++ b/src/main/java/sevenUnits/unit/LinearUnit.java
@@ -19,6 +19,7 @@ package sevenUnits.unit;
import java.util.Objects;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
import sevenUnits.utils.UncertainDouble;
@@ -369,6 +370,13 @@ public final class LinearUnit extends Unit {
this.getConversionFactor() * multiplier.getConversionFactor());
}
+ @Override
+ public String toDefinitionString() {
+ return Double.toString(this.conversionFactor)
+ + (this.getBase().equals(ObjectProduct.empty()) ? ""
+ : " " + this.getBase().toString(BaseUnit::getShortName));
+ }
+
/**
* Returns this unit but to an exponent.
*
@@ -382,20 +390,6 @@ public final class LinearUnit extends Unit {
Math.pow(this.conversionFactor, exponent));
}
- /**
- * @return a string providing a definition of this unit
- * @since 2019-10-21
- */
- @Override
- public String toString() {
- return this.getPrimaryName().orElse("Unnamed unit")
- + (this.getSymbol().isPresent()
- ? String.format(" (%s)", this.getSymbol().get())
- : "")
- + ", " + Double.toString(this.conversionFactor) + " * "
- + this.getBase().toString(u -> u.getSymbol().get());
- }
-
@Override
public LinearUnit withName(final NameSymbol ns) {
return valueOf(this.getBase(), this.getConversionFactor(), ns);
diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java
index a50e1f5..f91d30b 100644
--- a/src/main/java/sevenUnits/unit/LinearUnitValue.java
+++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java
@@ -16,6 +16,7 @@
*/
package sevenUnits.unit;
+import java.math.RoundingMode;
import java.util.Objects;
import java.util.Optional;
@@ -300,7 +301,7 @@ public final class LinearUnitValue {
@Override
public String toString() {
- return this.toString(!this.value.isExact());
+ return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN);
}
/**
@@ -315,7 +316,8 @@ public final class LinearUnitValue {
*
* @since 2020-07-26
*/
- public String toString(final boolean showUncertainty) {
+ public String toString(final boolean showUncertainty,
+ RoundingMode roundingMode) {
final Optional<String> primaryName = this.unit.getPrimaryName();
final Optional<String> symbol = this.unit.getSymbol();
final String chosenName = symbol.orElse(primaryName.orElse(null));
@@ -325,10 +327,10 @@ public final class LinearUnitValue {
// get rounded strings
// if showUncertainty is true, add brackets around the string
final String valueString = (showUncertainty ? "(" : "")
- + this.value.toString(showUncertainty)
+ + this.value.toString(showUncertainty, roundingMode)
+ (showUncertainty ? ")" : "");
final String baseValueString = (showUncertainty ? "(" : "")
- + baseValue.toString(showUncertainty)
+ + baseValue.toString(showUncertainty, roundingMode)
+ (showUncertainty ? ")" : "");
// create string
diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java
index 3c4d291..05e82ba 100644
--- a/src/main/java/sevenUnits/unit/Metric.java
+++ b/src/main/java/sevenUnits/unit/Metric.java
@@ -18,6 +18,7 @@ package sevenUnits.unit;
import java.util.Set;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
@@ -114,23 +115,30 @@ public final class Metric {
public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct
.empty();
public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct
- .oneOf(BaseDimensions.LENGTH);
+ .oneOf(BaseDimensions.LENGTH)
+ .withName(NameSymbol.of("Length", "L"));
public static final ObjectProduct<BaseDimension> MASS = ObjectProduct
- .oneOf(BaseDimensions.MASS);
+ .oneOf(BaseDimensions.MASS).withName(NameSymbol.of("Mass", "M"));
public static final ObjectProduct<BaseDimension> TIME = ObjectProduct
- .oneOf(BaseDimensions.TIME);
+ .oneOf(BaseDimensions.TIME).withName(NameSymbol.of("Time", "T"));
public static final ObjectProduct<BaseDimension> ELECTRIC_CURRENT = ObjectProduct
- .oneOf(BaseDimensions.ELECTRIC_CURRENT);
+ .oneOf(BaseDimensions.ELECTRIC_CURRENT)
+ .withName(NameSymbol.of("Current", "I"));
public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct
- .oneOf(BaseDimensions.TEMPERATURE);
+ .oneOf(BaseDimensions.TEMPERATURE)
+ .withName(NameSymbol.of("Temperature", "\u0398"));
public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct
- .oneOf(BaseDimensions.QUANTITY);
+ .oneOf(BaseDimensions.QUANTITY)
+ .withName(NameSymbol.of("Quantity", "N"));
public static final ObjectProduct<BaseDimension> LUMINOUS_INTENSITY = ObjectProduct
- .oneOf(BaseDimensions.LUMINOUS_INTENSITY);
+ .oneOf(BaseDimensions.LUMINOUS_INTENSITY)
+ .withName(NameSymbol.of("Luminous Intensity", "J"));
public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct
- .oneOf(BaseDimensions.INFORMATION);
+ .oneOf(BaseDimensions.INFORMATION)
+ .withName(NameSymbol.ofName("Information"));
public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct
- .oneOf(BaseDimensions.CURRENCY);
+ .oneOf(BaseDimensions.CURRENCY)
+ .withName(NameSymbol.ofName("Currency"));
// derived dimensions without named SI units
public static final ObjectProduct<BaseDimension> AREA = LENGTH
@@ -138,7 +146,7 @@ public final class Metric {
public static final ObjectProduct<BaseDimension> VOLUME = AREA
.times(LENGTH);
public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH
- .dividedBy(TIME);
+ .dividedBy(TIME).withName(NameSymbol.ofName("Velocity"));
public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY
.dividedBy(TIME);
public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY
@@ -402,54 +410,89 @@ public final class Metric {
.withName(NameSymbol.of("exbi", "Ei"));
// a few prefixed units
- public static final LinearUnit MICROMETRE = Metric.METRE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIMETRE = Metric.METRE.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOMETRE = Metric.METRE.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAMETRE = Metric.METRE.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROMETRE = Metric.METRE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIMETRE = Metric.METRE
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOMETRE = Metric.METRE
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAMETRE = Metric.METRE
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROLITRE = Metric.LITRE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLILITRE = Metric.LITRE.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOLITRE = Metric.LITRE.withPrefix(Metric.KILO);
- public static final LinearUnit MEGALITRE = Metric.LITRE.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROLITRE = Metric.LITRE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLILITRE = Metric.LITRE
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOLITRE = Metric.LITRE
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGALITRE = Metric.LITRE
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROSECOND = Metric.SECOND.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLISECOND = Metric.SECOND.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOSECOND = Metric.SECOND.withPrefix(Metric.KILO);
- public static final LinearUnit MEGASECOND = Metric.SECOND.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROSECOND = Metric.SECOND
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLISECOND = Metric.SECOND
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOSECOND = Metric.SECOND
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGASECOND = Metric.SECOND
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROGRAM = Metric.GRAM.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIGRAM = Metric.GRAM.withPrefix(Metric.MILLI);
- public static final LinearUnit MEGAGRAM = Metric.GRAM.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROGRAM = Metric.GRAM
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIGRAM = Metric.GRAM
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit MEGAGRAM = Metric.GRAM
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICRONEWTON = Metric.NEWTON.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLINEWTON = Metric.NEWTON.withPrefix(Metric.MILLI);
- public static final LinearUnit KILONEWTON = Metric.NEWTON.withPrefix(Metric.KILO);
- public static final LinearUnit MEGANEWTON = Metric.NEWTON.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICRONEWTON = Metric.NEWTON
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLINEWTON = Metric.NEWTON
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILONEWTON = Metric.NEWTON
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGANEWTON = Metric.NEWTON
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROJOULE = Metric.JOULE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIJOULE = Metric.JOULE.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOJOULE = Metric.JOULE.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAJOULE = Metric.JOULE.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROJOULE = Metric.JOULE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIJOULE = Metric.JOULE
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOJOULE = Metric.JOULE
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAJOULE = Metric.JOULE
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROWATT = Metric.WATT.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIWATT = Metric.WATT.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOWATT = Metric.WATT.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAWATT = Metric.WATT.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROWATT = Metric.WATT
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIWATT = Metric.WATT
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOWATT = Metric.WATT
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAWATT = Metric.WATT
+ .withPrefix(Metric.MEGA);
public static final LinearUnit MICROCOULOMB = Metric.COULOMB
.withPrefix(Metric.MICRO);
public static final LinearUnit MILLICOULOMB = Metric.COULOMB
.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOCOULOMB = Metric.COULOMB.withPrefix(Metric.KILO);
- public static final LinearUnit MEGACOULOMB = Metric.COULOMB.withPrefix(Metric.MEGA);
+ public static final LinearUnit KILOCOULOMB = Metric.COULOMB
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGACOULOMB = Metric.COULOMB
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROAMPERE = Metric.AMPERE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIAMPERE = Metric.AMPERE.withPrefix(Metric.MILLI);
+ public static final LinearUnit MICROAMPERE = Metric.AMPERE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIAMPERE = Metric.AMPERE
+ .withPrefix(Metric.MILLI);
- public static final LinearUnit MICROVOLT = Metric.VOLT.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIVOLT = Metric.VOLT.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOVOLT = Metric.VOLT.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAVOLT = Metric.VOLT.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROVOLT = Metric.VOLT
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIVOLT = Metric.VOLT
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOVOLT = Metric.VOLT
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAVOLT = Metric.VOLT
+ .withPrefix(Metric.MEGA);
public static final LinearUnit KILOOHM = Metric.OHM.withPrefix(Metric.KILO);
public static final LinearUnit MEGAOHM = Metric.OHM.withPrefix(Metric.MEGA);
diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java
index 83cdb03..bc240e3 100644
--- a/src/main/java/sevenUnits/unit/MultiUnit.java
+++ b/src/main/java/sevenUnits/unit/MultiUnit.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java
index 005b6f7..14478ba 100644
--- a/src/main/java/sevenUnits/unit/Unit.java
+++ b/src/main/java/sevenUnits/unit/Unit.java
@@ -22,6 +22,8 @@ import java.util.Objects;
import java.util.function.DoubleUnaryOperator;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
import sevenUnits.utils.ObjectProduct;
/**
@@ -188,7 +190,7 @@ public abstract class Unit implements Nameable {
*
* @implSpec This method is used by {@link #convertTo}, and its behaviour
* affects the behaviour of {@code convertTo}.
- *
+ *
* @param value value expressed in <b>base</b> unit
* @return value expressed in <b>this</b> unit
* @since 2018-12-22
@@ -204,7 +206,7 @@ public abstract class Unit 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
@@ -231,7 +233,7 @@ public abstract class Unit 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 <W> type of value to convert to
@@ -266,7 +268,7 @@ public abstract class Unit implements Nameable {
*
* @implSpec This method is used by {@link #convertTo}, and its behaviour
* affects the behaviour of {@code convertTo}.
- *
+ *
* @param value value expressed in <b>this</b> unit
* @return value expressed in <b>base</b> unit
* @since 2018-12-22
@@ -347,16 +349,34 @@ public abstract class Unit implements Nameable {
.equals(Math.log10(linear.getConversionFactor()) % 1.0, 0);
}
+ /**
+ * @return a string representing this unit's definition
+ * @since 2022-03-10
+ */
+ public String toDefinitionString() {
+ if (!this.unitBase.getNameSymbol().isEmpty())
+ return "derived from " + this.unitBase.getName();
+ else
+ return "derived from "
+ + this.getBase().toString(BaseUnit::getShortName);
+ }
+
+ /**
+ * @return a string containing both this unit's name and its definition
+ * @since 2022-03-10
+ */
+ public final String toFullString() {
+ return this.toString() + " (" + this.toDefinitionString() + ")";
+ }
+
@Override
public String toString() {
- return this.getPrimaryName().orElse("Unnamed unit")
- + (this.getSymbol().isPresent()
- ? String.format(" (%s)", this.getSymbol().get())
- : "")
- + ", derived from "
- + this.getBase().toString(u -> u.getSymbol().get())
- + (this.getOtherNames().isEmpty() ? ""
- : ", also called " + String.join(", ", this.getOtherNames()));
+ if (this.nameSymbol.getPrimaryName().isPresent()
+ && this.nameSymbol.getSymbol().isPresent())
+ return this.nameSymbol.getPrimaryName().orElseThrow() + " ("
+ + this.nameSymbol.getSymbol().orElseThrow() + ")";
+ else
+ return this.getName();
}
/**
diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java
index 18ac619..12b78a7 100644
--- a/src/main/java/sevenUnits/unit/UnitDatabase.java
+++ b/src/main/java/sevenUnits/unit/UnitDatabase.java
@@ -19,7 +19,6 @@ package sevenUnits.unit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractSet;
@@ -47,6 +46,7 @@ import java.util.regex.Pattern;
import sevenUnits.utils.ConditionalExistenceCollections;
import sevenUnits.utils.DecimalComparison;
import sevenUnits.utils.ExpressionParser;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
import sevenUnits.utils.UncertainDouble;
@@ -1160,16 +1160,16 @@ public final class UnitDatabase {
}
/**
- * @return true if entry represents a removable duplicate entry of unitMap.
+ * @return true if entry represents a removable duplicate entry of map.
* @since 2021-05-22
*/
- static boolean isRemovableDuplicate(Map<String, Unit> unitMap,
- Entry<String, Unit> entry) {
- for (final Entry<String, Unit> e : unitMap.entrySet()) {
+ static <T> boolean isRemovableDuplicate(Map<String, T> map,
+ Entry<String, T> entry) {
+ for (final Entry<String, T> e : map.entrySet()) {
final String name = e.getKey();
- final Unit value = e.getValue();
+ final T value = e.getValue();
if (lengthFirstComparator.compare(entry.getKey(), name) < 0
- && Objects.equals(unitMap.get(entry.getKey()), value))
+ && Objects.equals(map.get(entry.getKey()), value))
return true;
}
return false;
@@ -1313,9 +1313,11 @@ public final class UnitDatabase {
*/
public void addDimension(final String name,
final ObjectProduct<BaseDimension> dimension) {
- this.dimensions.put(
- Objects.requireNonNull(name, "name must not be null."),
- Objects.requireNonNull(dimension, "dimension must not be null."));
+ Objects.requireNonNull(name, "name may not be null");
+ Objects.requireNonNull(dimension, "dimension may not be null");
+ final ObjectProduct<BaseDimension> namedDimension = dimension
+ .withName(dimension.getNameSymbol().withExtraName(name));
+ this.dimensions.put(name, namedDimension);
}
/**
@@ -1381,8 +1383,11 @@ public final class UnitDatabase {
* @since v0.1.0
*/
public void addPrefix(final String name, final UnitPrefix prefix) {
+ Objects.requireNonNull(prefix, "prefix may not be null");
+ final var namedPrefix = prefix
+ .withName(prefix.getNameSymbol().withExtraName(name));
this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
- Objects.requireNonNull(prefix, "prefix must not be null."));
+ namedPrefix);
}
/**
@@ -1395,9 +1400,11 @@ public final class UnitDatabase {
* @since v0.1.0
*/
public void addUnit(final String name, final Unit unit) {
+ Objects.requireNonNull(unit, "unit may not be null");
+ final var namedUnit = unit
+ .withName(unit.getNameSymbol().withExtraName(name));
this.prefixlessUnits.put(
- Objects.requireNonNull(name, "name must not be null."),
- Objects.requireNonNull(unit, "unit must not be null."));
+ Objects.requireNonNull(name, "name must not be null."), namedUnit);
}
/**
@@ -1451,7 +1458,8 @@ public final class UnitDatabase {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
- this.addPrefix(name.substring(0, name.length() - 1), prefix);
+ final String prefixName = name.substring(0, name.length() - 1);
+ this.addPrefix(prefixName, prefix);
} else {
// it's a unit, get the unit
final Unit unit;
@@ -1462,13 +1470,23 @@ public final class UnitDatabase {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
-
this.addUnit(name, unit);
}
}
}
/**
+ * Removes all units, prefixes and dimensions from this database.
+ *
+ * @since 2022-02-26
+ */
+ public void clear() {
+ this.dimensions.clear();
+ this.prefixes.clear();
+ this.prefixlessUnits.clear();
+ }
+
+ /**
* Tests if the database has a unit dimension with this name.
*
* @param name name to test
@@ -1685,11 +1703,8 @@ public final class UnitDatabase {
LinearUnitValue getLinearUnitValue(final String name) {
try {
// try to parse it as a number - otherwise it is not a number!
- final BigDecimal number = new BigDecimal(name);
-
- final double uncertainty = Math.pow(10, -number.scale());
return LinearUnitValue.of(Metric.ONE,
- UncertainDouble.of(number.doubleValue(), uncertainty));
+ UncertainDouble.fromRoundedString(name));
} catch (final NumberFormatException e) {
return LinearUnitValue.getExact(this.getLinearUnit(name), 1);
}
@@ -1994,12 +2009,18 @@ public final class UnitDatabase {
}
/**
+ * @param includeDuplicates if false, duplicates are removed from the map
* @return a map mapping prefix names to prefixes
- * @since 2019-04-13
- * @since v0.2.0
+ * @since 2022-04-18
+ * @since v0.4.0
*/
- public Map<String, UnitPrefix> prefixMap() {
- return Collections.unmodifiableMap(this.prefixes);
+ public Map<String, UnitPrefix> prefixMap(boolean includeDuplicates) {
+ if (includeDuplicates)
+ return Collections.unmodifiableMap(this.prefixes);
+ else
+ return Collections.unmodifiableMap(ConditionalExistenceCollections
+ .conditionalExistenceMap(this.prefixes,
+ entry -> !isRemovableDuplicate(this.prefixes, entry)));
}
/**
diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java
index 308f4b0..e1f7788 100644
--- a/src/main/java/sevenUnits/unit/UnitPrefix.java
+++ b/src/main/java/sevenUnits/unit/UnitPrefix.java
@@ -17,68 +17,59 @@
package sevenUnits.unit;
import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
/**
- * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value
+ * A prefix that can be applied to a {@code LinearUnit} to multiply it by some
+ * value
*
* @author Adrien Hopkins
* @since 2019-10-16
*/
-public final class UnitPrefix {
+public final class UnitPrefix implements Nameable {
/**
* Gets a {@code UnitPrefix} from a multiplier
*
- * @param multiplier
- * multiplier of prefix
+ * @param multiplier multiplier of prefix
* @return prefix
* @since 2019-10-16
*/
public static UnitPrefix valueOf(final double multiplier) {
return new UnitPrefix(multiplier, NameSymbol.EMPTY);
}
-
+
/**
* Gets a {@code UnitPrefix} from a multiplier and a name
*
- * @param multiplier
- * multiplier of prefix
- * @param ns
- * name(s) and symbol of prefix
+ * @param multiplier multiplier of prefix
+ * @param ns name(s) and symbol of prefix
* @return prefix
* @since 2019-10-16
- * @throws NullPointerException
- * if ns is null
+ * @throws NullPointerException if ns is null
*/
- public static UnitPrefix valueOf(final double multiplier, final NameSymbol ns) {
- return new UnitPrefix(multiplier, Objects.requireNonNull(ns, "ns must not be null."));
+ public static UnitPrefix valueOf(final double multiplier,
+ final NameSymbol ns) {
+ return new UnitPrefix(multiplier,
+ Objects.requireNonNull(ns, "ns must not be null."));
}
-
- /**
- * This prefix's primary name
- */
- private final Optional<String> primaryName;
-
- /**
- * This prefix's symbol
- */
- private final Optional<String> symbol;
-
+
/**
- * Other names and symbols used by this prefix
+ * This prefix's name(s) and symbol.
+ *
+ * @since 2022-04-16
*/
- private final Set<String> otherNames;
-
+ private final NameSymbol nameSymbol;
+
/**
* The number that this prefix multiplies units by
*
* @since 2019-10-16
*/
private final double multiplier;
-
+
/**
* Creates the {@code DefaultUnitPrefix}.
*
@@ -88,28 +79,24 @@ public final class UnitPrefix {
*/
private UnitPrefix(final double multiplier, final NameSymbol ns) {
this.multiplier = multiplier;
- this.primaryName = ns.getPrimaryName();
- this.symbol = ns.getSymbol();
- this.otherNames = ns.getOtherNames();
+ this.nameSymbol = ns;
}
-
+
/**
* Divides this prefix by a scalar
*
- * @param divisor
- * number to divide by
+ * @param divisor number to divide by
* @return quotient of prefix and scalar
* @since 2019-10-16
*/
public UnitPrefix dividedBy(final double divisor) {
return valueOf(this.getMultiplier() / divisor);
}
-
+
/**
* Divides this prefix by {@code other}.
*
- * @param other
- * prefix to divide by
+ * @param other prefix to divide by
* @return quotient of prefixes
* @since 2019-04-13
* @since v0.2.0
@@ -117,7 +104,7 @@ public final class UnitPrefix {
public UnitPrefix dividedBy(final UnitPrefix other) {
return valueOf(this.getMultiplier() / other.getMultiplier());
}
-
+
/**
* {@inheritDoc}
*
@@ -132,9 +119,10 @@ public final class UnitPrefix {
if (!(obj instanceof UnitPrefix))
return false;
final UnitPrefix other = (UnitPrefix) obj;
- return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier());
+ return DecimalComparison.equals(this.getMultiplier(),
+ other.getMultiplier());
}
-
+
/**
* @return prefix's multiplier
* @since 2019-11-26
@@ -142,31 +130,12 @@ public final class UnitPrefix {
public double getMultiplier() {
return this.multiplier;
}
-
- /**
- * @return other names
- * @since 2019-11-26
- */
- public final Set<String> getOtherNames() {
- return this.otherNames;
- }
-
- /**
- * @return primary name
- * @since 2019-11-26
- */
- public final Optional<String> getPrimaryName() {
- return this.primaryName;
- }
-
- /**
- * @return symbol
- * @since 2019-11-26
- */
- public final Optional<String> getSymbol() {
- return this.symbol;
+
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
}
-
+
/**
* {@inheritDoc}
*
@@ -176,24 +145,22 @@ public final class UnitPrefix {
public int hashCode() {
return DecimalComparison.hash(this.getMultiplier());
}
-
+
/**
* Multiplies this prefix by a scalar
*
- * @param multiplicand
- * number to multiply by
+ * @param multiplicand number to multiply by
* @return product of prefix and scalar
* @since 2019-10-16
*/
public UnitPrefix times(final double multiplicand) {
return valueOf(this.getMultiplier() * multiplicand);
}
-
+
/**
* Multiplies this prefix by {@code other}.
*
- * @param other
- * prefix to multiply by
+ * @param other prefix to multiply by
* @return product of prefixes
* @since 2019-04-13
* @since v0.2.0
@@ -201,12 +168,11 @@ public final class UnitPrefix {
public UnitPrefix times(final UnitPrefix other) {
return valueOf(this.getMultiplier() * other.getMultiplier());
}
-
+
/**
* Raises this prefix to an exponent.
*
- * @param exponent
- * exponent to raise to
+ * @param exponent exponent to raise to
* @return result of exponentiation.
* @since 2019-04-13
* @since v0.2.0
@@ -214,27 +180,27 @@ public final class UnitPrefix {
public UnitPrefix toExponent(final double exponent) {
return valueOf(Math.pow(this.getMultiplier(), exponent));
}
-
+
/**
* @return a string describing the prefix and its multiplier
*/
@Override
public String toString() {
- if (this.primaryName.isPresent())
- return String.format("%s (\u00D7 %s)", this.primaryName.get(), this.multiplier);
- else if (this.symbol.isPresent())
- return String.format("%s (\u00D7 %s)", this.symbol.get(), this.multiplier);
+ if (this.getPrimaryName().isPresent())
+ return String.format("%s (\u00D7 %s)", this.getPrimaryName().get(),
+ this.multiplier);
+ else if (this.getSymbol().isPresent())
+ return String.format("%s (\u00D7 %s)", this.getSymbol().get(),
+ this.multiplier);
else
return String.format("Unit Prefix (\u00D7 %s)", this.multiplier);
}
-
+
/**
- * @param ns
- * name(s) and symbol to use
+ * @param ns name(s) and symbol to use
* @return copy of this prefix with provided name(s) and symbol
* @since 2019-11-26
- * @throws NullPointerException
- * if ns is null
+ * @throws NullPointerException if ns is null
*/
public UnitPrefix withName(final NameSymbol ns) {
return valueOf(this.multiplier, ns);
diff --git a/src/main/java/sevenUnits/unit/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java
new file mode 100644
index 0000000..7cebf2d
--- /dev/null
+++ b/src/main/java/sevenUnits/unit/UnitType.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.unit;
+
+import java.util.function.Predicate;
+
+/**
+ * A type of unit, as chosen by the type of system it is in.
+ * <ul>
+ * <li>{@code METRIC} refers to metric/SI units that pass {@link Unit#isMetric}
+ * <li>{@code SEMI_METRIC} refers to the degree Celsius (which is an official SI
+ * unit but does not pass {@link Unit#isMetric}) and non-metric units intended
+ * for use with the SI.
+ * <li>{@code NON_METRIC} refers to units that are neither metric nor intended
+ * for use with the metric system (e.g. imperial and customary units)
+ * </ul>
+ *
+ * @since 2022-04-10
+ */
+public enum UnitType {
+ METRIC, SEMI_METRIC, NON_METRIC;
+
+ /**
+ * Determines which type a unit is. The type will be:
+ * <ul>
+ * <li>{@code SEMI_METRIC} if the unit passes the provided predicate
+ * <li>{@code METRIC} if it fails the predicate but is metric
+ * <li>{@code NON_METRIC} if it fails the predicate and is not metric
+ * </ul>
+ *
+ * @param u unit to test
+ * @param isSemiMetric predicate to determine if a unit is semi-metric
+ * @return type of unit
+ * @since 2022-04-18
+ */
+ public static final UnitType getType(Unit u, Predicate<Unit> isSemiMetric) {
+ if (isSemiMetric.test(u))
+ return SEMI_METRIC;
+ else if (u.isMetric())
+ return METRIC;
+ else
+ return NON_METRIC;
+ }
+}
diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java
index f6d18f8..339263d 100644
--- a/src/main/java/sevenUnits/unit/UnitValue.java
+++ b/src/main/java/sevenUnits/unit/UnitValue.java
@@ -19,6 +19,8 @@ package sevenUnits.unit;
import java.util.Objects;
import java.util.Optional;
+import sevenUnits.utils.NameSymbol;
+
/**
* A value expressed in a unit.
*
diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java
index d2dcbbb..68de2c2 100644
--- a/src/main/java/sevenUnits/unit/Unitlike.java
+++ b/src/main/java/sevenUnits/unit/Unitlike.java
@@ -22,6 +22,8 @@ import java.util.Objects;
import java.util.function.DoubleFunction;
import java.util.function.ToDoubleFunction;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java
index edc13ca..26354b1 100644
--- a/src/main/java/sevenUnits/unit/UnitlikeValue.java
+++ b/src/main/java/sevenUnits/unit/UnitlikeValue.java
@@ -18,6 +18,8 @@ package sevenUnits.unit;
import java.util.Optional;
+import sevenUnits.utils.NameSymbol;
+
/**
*
* @since 2020-09-07
diff --git a/src/main/java/sevenUnits/unit/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java
index 3e26138..9388f63 100644
--- a/src/main/java/sevenUnits/unit/NameSymbol.java
+++ b/src/main/java/sevenUnits/utils/NameSymbol.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.unit;
+package sevenUnits.utils;
import java.util.Arrays;
import java.util.Collections;
@@ -38,7 +38,7 @@ public final class NameSymbol {
* Creates a {@code NameSymbol}, ensuring that if primaryName is null and
* otherNames is not empty, one name is moved from otherNames to primaryName
*
- * Ensure that otherNames is a copy of the inputted argument.
+ * Ensure that otherNames is not a copy of the inputted argument.
*/
private static final NameSymbol create(final String name,
final String symbol, final Set<String> otherNames) {
@@ -277,4 +277,33 @@ public final class NameSymbol {
// if primaryName is empty, otherNames must also be empty
return this.primaryName.isEmpty() && this.symbol.isEmpty();
}
+
+ @Override
+ public String toString() {
+ if (this.isEmpty())
+ return "NameSymbol.EMPTY";
+ else if (this.primaryName.isPresent() && this.symbol.isPresent())
+ return this.primaryName.orElseThrow() + " ("
+ + this.symbol.orElseThrow() + ")";
+ else
+ return this.primaryName.orElseGet(this.symbol::orElseThrow);
+ }
+
+ /**
+ * Creates and returns a copy of this {@code NameSymbol} with the provided
+ * extra name. If this {@code NameSymbol} has a primary name, the provided
+ * name will become an other name, otherwise it will become the primary name.
+ *
+ * @since v0.4.0
+ * @since 2022-04-19
+ */
+ public final NameSymbol withExtraName(String name) {
+ if (this.primaryName.isPresent()) {
+ final var otherNames = new HashSet<>(this.otherNames);
+ otherNames.add(name);
+ return NameSymbol.ofNullable(this.primaryName.orElse(null),
+ this.symbol.orElse(null), otherNames);
+ } else
+ return NameSymbol.ofNullable(name, this.symbol.orElse(null));
+ }
} \ No newline at end of file
diff --git a/src/main/java/sevenUnits/unit/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java
index ed23687..e469d04 100644
--- a/src/main/java/sevenUnits/unit/Nameable.java
+++ b/src/main/java/sevenUnits/utils/Nameable.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.unit;
+package sevenUnits.utils;
import java.util.Optional;
import java.util.Set;
@@ -27,6 +27,16 @@ import java.util.Set;
*/
public interface Nameable {
/**
+ * @return a name for the object - if there's a primary name, it's that,
+ * otherwise the symbol, otherwise "Unnamed"
+ * @since 2022-02-26
+ */
+ default String getName() {
+ final NameSymbol ns = this.getNameSymbol();
+ return ns.getPrimaryName().or(ns::getSymbol).orElse("Unnamed");
+ }
+
+ /**
* @return a {@code NameSymbol} that contains this object's primary name,
* symbol and other names
* @since 2020-09-07
@@ -50,6 +60,16 @@ public interface Nameable {
}
/**
+ * @return a short name for the object - if there's a symbol, it's that,
+ * otherwise the symbol, otherwise "Unnamed"
+ * @since 2022-02-26
+ */
+ default String getShortName() {
+ final NameSymbol ns = this.getNameSymbol();
+ return ns.getSymbol().or(ns::getPrimaryName).orElse("Unnamed");
+ }
+
+ /**
* @return short symbol representing object
* @since 2020-09-07
*/
diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java
index 5b1b739..66bb773 100644
--- a/src/main/java/sevenUnits/utils/ObjectProduct.java
+++ b/src/main/java/sevenUnits/utils/ObjectProduct.java
@@ -33,7 +33,7 @@ import java.util.function.Function;
* @author Adrien Hopkins
* @since 2019-10-16
*/
-public final class ObjectProduct<T> {
+public class ObjectProduct<T> implements Nameable {
/**
* Returns an empty ObjectProduct of a certain type
*
@@ -83,15 +83,32 @@ public final class ObjectProduct<T> {
final Map<T, Integer> exponents;
/**
- * Creates the {@code ObjectProduct}.
+ * The object's name and symbol
+ */
+ private final NameSymbol nameSymbol;
+
+ /**
+ * Creates a {@code ObjectProduct} without a name/symbol.
*
* @param exponents objects that make up this product
* @since 2019-10-16
*/
- private ObjectProduct(final Map<T, Integer> exponents) {
+ ObjectProduct(final Map<T, Integer> exponents) {
+ this(exponents, NameSymbol.EMPTY);
+ }
+
+ /**
+ * Creates the {@code ObjectProduct}.
+ *
+ * @param exponents objects that make up this product
+ * @param nameSymbol name and symbol of object product
+ * @since 2019-10-16
+ */
+ ObjectProduct(final Map<T, Integer> exponents, NameSymbol nameSymbol) {
this.exponents = Collections.unmodifiableMap(
ConditionalExistenceCollections.conditionalExistenceMap(exponents,
e -> !Integer.valueOf(0).equals(e.getValue())));
+ this.nameSymbol = nameSymbol;
}
/**
@@ -171,6 +188,11 @@ public final class ObjectProduct<T> {
}
@Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
public int hashCode() {
return Objects.hash(this.exponents);
}
@@ -235,16 +257,19 @@ public final class ObjectProduct<T> {
/**
* Converts this product to a string using the objects'
- * {@link Object#toString()} method. If objects have a long toString
- * representation, it is recommended to use {@link #toString(Function)}
- * instead to shorten the returned string.
+ * {@link Object#toString()} method (or {@link Nameable#getShortName} if
+ * available). If objects have a long toString representation, it is
+ * recommended to use {@link #toString(Function)} instead to shorten the
+ * returned string.
*
* <p>
* {@inheritDoc}
*/
@Override
public String toString() {
- return this.toString(Object::toString);
+ return this
+ .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName()
+ : o.toString());
}
/**
@@ -280,4 +305,13 @@ public final class ObjectProduct<T> {
return positiveString + negativeString;
}
+
+ /**
+ * @return named version of this {@code ObjectProduct}, using data from
+ * {@code nameSymbol}
+ * @since 2021-12-15
+ */
+ public ObjectProduct<T> withName(NameSymbol nameSymbol) {
+ return new ObjectProduct<>(this.exponents, nameSymbol);
+ }
}
diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java
new file mode 100644
index 0000000..e80e16e
--- /dev/null
+++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java
@@ -0,0 +1,716 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A version number in the <a href="https://semver.org">Semantic Versioning</a>
+ * scheme
+ * <p>
+ * Each version number has three main parts:
+ * <ol>
+ * <li>The major version, which increments when backwards incompatible changes
+ * are made
+ * <li>The minor version, which increments when backwards compatible feature
+ * changes are made
+ * <li>The patch version, which increments when backwards compatible bug fixes
+ * are made
+ * </ol>
+ *
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+public final class SemanticVersionNumber
+ implements Comparable<SemanticVersionNumber> {
+ /**
+ * A builder that can be used to create complex version numbers.
+ * <p>
+ * Note: None of this builder's methods tolerate null arguments, arrays
+ * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls
+ * throw NullPointerExceptions, everything else throws
+ * IllegalArgumentException.
+ *
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public static final class Builder {
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final List<String> preReleaseIdentifiers;
+ private final List<String> buildMetadata;
+
+ /**
+ * Creates a builder which can be used to create a
+ * {@code SemanticVersionNumber}
+ *
+ * @param major major version number of final version
+ * @param minor minor version number of final version
+ * @param patch patch version number of final version
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ private Builder(int major, int minor, int patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.preReleaseIdentifiers = new ArrayList<>();
+ this.buildMetadata = new ArrayList<>();
+ }
+
+ /**
+ * @return version number created by this builder
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public SemanticVersionNumber build() {
+ return new SemanticVersionNumber(this.major, this.minor, this.patch,
+ this.preReleaseIdentifiers, this.buildMetadata);
+ }
+
+ /**
+ * Adds one or more build metadata identifiers
+ *
+ * @param identifiers build metadata
+ * @return this builder
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public Builder buildMetadata(List<String> identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.buildMetadata.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more build metadata identifiers
+ *
+ * @param identifiers build metadata
+ * @return this builder
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public Builder buildMetadata(String... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.buildMetadata.add(identifier);
+ }
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof Builder))
+ return false;
+ final Builder other = (Builder) obj;
+ return Objects.equals(this.buildMetadata, other.buildMetadata)
+ && this.major == other.major && this.minor == other.minor
+ && this.patch == other.patch && Objects.equals(
+ this.preReleaseIdentifiers, other.preReleaseIdentifiers);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.buildMetadata, this.major, this.minor,
+ this.patch, this.preReleaseIdentifiers);
+ }
+
+ /**
+ * Adds one or more numeric identifiers to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public Builder preRelease(int... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final int identifier : identifiers) {
+ if (identifier < 0)
+ throw new IllegalArgumentException(
+ "Numeric identifiers may not be negative");
+ this.preReleaseIdentifiers.add(Integer.toString(identifier));
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more pre-release identifier(s) to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public Builder preRelease(List<String> identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.preReleaseIdentifiers.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more pre-release identifier(s) to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public Builder preRelease(String... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.preReleaseIdentifiers.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a string identifier and an integer identifer to pre-release data
+ *
+ * @param identifier1 first identifier
+ * @param identifier2 second identifier
+ * @return this builder
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public Builder preRelease(String identifier1, int identifier2) {
+ Objects.requireNonNull(identifier1, "identifier1 may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier1).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier1));
+ if (identifier2 < 0)
+ throw new IllegalArgumentException(
+ "Integer identifier cannot be negative");
+ this.preReleaseIdentifiers.add(identifier1);
+ this.preReleaseIdentifiers.add(Integer.toString(identifier2));
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "Semantic Version Builder: " + this.build().toString();
+ }
+ }
+
+ /**
+ * An alternative comparison method for version numbers. This uses the
+ * version's natural order, but the build metadata will be compared (using
+ * the same rules as pre-release identifiers) if everything else is equal.
+ * <p>
+ * This ordering is consistent with equals, unlike
+ * {@code SemanticVersionNumber}'s natural ordering.
+ */
+ public static final Comparator<SemanticVersionNumber> BUILD_METADATA_COMPARATOR = new Comparator<>() {
+ @Override
+ public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) {
+ Objects.requireNonNull(o1, "o1 may not be null");
+ Objects.requireNonNull(o2, "o2 may not be null");
+ final int naturalComparison = o1.compareTo(o2);
+ if (naturalComparison == 0)
+ return SemanticVersionNumber.compareIdentifiers(o1.buildMetadata,
+ o2.buildMetadata);
+ else
+ return naturalComparison;
+ };
+ };
+
+ /** The alphanumeric pattern all identifiers must follow */
+ private static final Pattern VALID_IDENTIFIER = Pattern
+ .compile("[0-9A-Za-z-]+");
+
+ /** The numeric pattern which causes special behaviour */
+ private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+");
+
+ /** The pattern for a version number */
+ private static final Pattern VERSION_NUMBER = Pattern
+ .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main
+ // version
+ + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release
+ + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data
+
+ /**
+ * Creates a builder that can be used to create a version number
+ *
+ * @param major major version number of final version
+ * @param minor minor version number of final version
+ * @param patch patch version number of final version
+ * @return version number builder
+ * @throws IllegalArgumentException if any argument is negative
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber.Builder builder(int major,
+ int minor, int patch) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ return new SemanticVersionNumber.Builder(major, minor, patch);
+ }
+
+ /**
+ * Compares two lists of strings based on SemVer's precedence rules
+ *
+ * @param a first list
+ * @param b second list
+ * @return result of comparison as in a comparator
+ * @see Comparator
+ * @since v0.4.0
+ * @since 2022-02-20
+ */
+ private static final int compareIdentifiers(List<String> a, List<String> b) {
+ // test pre-release size
+ final int aSize = a.size();
+ final int bSize = b.size();
+
+ // no identifiers is greater than any identifiers
+ if (aSize != 0 && bSize == 0)
+ return -1;
+ else if (aSize == 0 && bSize != 0)
+ return 1;
+
+ // test identifiers one by one
+ for (int i = 0; i < Math.min(aSize, bSize); i++) {
+ final String aElement = a.get(i);
+ final String bElement = b.get(i);
+
+ if (NUMERIC_IDENTIFER.matcher(aElement).matches()) {
+ if (NUMERIC_IDENTIFER.matcher(bElement).matches()) {
+ // both are numbers, compare them
+ final int aNumber = Integer.parseInt(aElement);
+ final int bNumber = Integer.parseInt(bElement);
+
+ if (aNumber < bNumber)
+ return -1;
+ else if (aNumber > bNumber)
+ return 1;
+ } else
+ // aElement is a number and bElement is not a number
+ // by the rules, a goes before b
+ return -1;
+ } else {
+ if (NUMERIC_IDENTIFER.matcher(bElement).matches())
+ // aElement is not a number but bElement is
+ // by the rules, a goes after b
+ return 1;
+ else {
+ // both are not numbers, compare them
+ final int comparison = aElement.compareTo(bElement);
+ if (comparison != 0)
+ return comparison;
+ }
+ }
+ }
+
+ // we just tested the stuff that's in common, maybe someone has more
+ if (aSize < bSize)
+ return -1;
+ else if (aSize > bSize)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * Gets a version number from a string in the official format
+ *
+ * @param versionString string to parse
+ * @return {@code SemanticVersionNumber} instance
+ * @since v0.4.0
+ * @since 2022-02-19
+ * @see {@link #toString}
+ */
+ public static final SemanticVersionNumber fromString(String versionString) {
+ // parse & validate version string
+ Objects.requireNonNull(versionString, "versionString may not be null");
+ final Matcher m = VERSION_NUMBER.matcher(versionString);
+ if (!m.matches())
+ throw new IllegalArgumentException(
+ String.format("Provided string \"%s\" is not a version number",
+ versionString));
+
+ // main parts
+ final int major = Integer.parseInt(m.group(1));
+ final int minor = Integer.parseInt(m.group(2));
+ final int patch = Integer.parseInt(m.group(3));
+
+ // pre release
+ final List<String> preRelease;
+ if (m.group(4) == null) {
+ preRelease = List.of();
+ } else {
+ preRelease = Arrays.asList(m.group(4).split("\\."));
+ }
+
+ // build metadata
+ final List<String> buildMetadata;
+ if (m.group(5) == null) {
+ buildMetadata = List.of();
+ } else {
+ buildMetadata = Arrays.asList(m.group(5).split("\\."));
+ }
+
+ // return number
+ return new SemanticVersionNumber(major, minor, patch, preRelease,
+ buildMetadata);
+ }
+
+ /**
+ * Tests whether a string is a valid Semantic Version string
+ *
+ * @param versionString string to test
+ * @return true iff string is valid
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public static final boolean isValidVersionString(String versionString) {
+ return VERSION_NUMBER.matcher(versionString).matches();
+ }
+
+ /**
+ * Creates a simple pre-release version number of the form
+ * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4).
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @param preReleaseType first pre-release element
+ * @param preReleaseNumber second pre-release element
+ * @return {@code SemanticVersionNumber} instance
+ * @throws IllegalArgumentException if any argument is negative or if the
+ * preReleaseType is null, empty or not
+ * alphanumeric (0-9, A-Z, a-z, - only)
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber preRelease(int major, int minor,
+ int patch, String preReleaseType, int preReleaseNumber) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ Objects.requireNonNull(preReleaseType, "preReleaseType may not be null");
+ if (!VALID_IDENTIFIER.matcher(preReleaseType).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\".", preReleaseType));
+ if (preReleaseNumber < 0)
+ throw new IllegalArgumentException(
+ "Pre-release number must be non-negative.");
+ return new SemanticVersionNumber(major, minor, patch,
+ List.of(preReleaseType, Integer.toString(preReleaseNumber)),
+ List.of());
+ }
+
+ /**
+ * Creates a {@code SemanticVersionNumber} instance without pre-release
+ * identifiers or build metadata.
+ * <p>
+ * Note: this method allows you to create versions with major version number
+ * 0, even though these versions would not be considered stable.
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @return {@code SemanticVersionNumber} instance
+ * @throws IllegalArgumentException if any argument is negative
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber stableVersion(int major, int minor,
+ int patch) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ return new SemanticVersionNumber(major, minor, patch, List.of(),
+ List.of());
+ }
+
+ // parts of the version number
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final List<String> preReleaseIdentifiers;
+ private final List<String> buildMetadata;
+
+ /**
+ * Creates a version number
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @param preReleaseIdentifiers pre-release version data
+ * @param buildMetadata build metadata
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ private SemanticVersionNumber(int major, int minor, int patch,
+ List<String> preReleaseIdentifiers, List<String> buildMetadata) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.preReleaseIdentifiers = preReleaseIdentifiers;
+ this.buildMetadata = buildMetadata;
+ }
+
+ /**
+ * @return build metadata (empty if there is none)
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public List<String> buildMetadata() {
+ return Collections.unmodifiableList(this.buildMetadata);
+ }
+
+ /**
+ * Compares two version numbers according to the official Semantic Versioning
+ * order.
+ * <p>
+ * Note: this ordering is not consistent with equals. Specifically, two
+ * versions that are identical except for their build metadata will be
+ * considered different by equals but the same by this method. This is
+ * required to follow the official Semantic Versioning specification.
+ * <p>
+ */
+ @Override
+ public int compareTo(SemanticVersionNumber o) {
+ // test the three big numbers in order first
+ if (this.major < o.major)
+ return -1;
+ else if (this.major > o.major)
+ return 1;
+
+ if (this.minor < o.minor)
+ return -1;
+ else if (this.minor > o.minor)
+ return 1;
+
+ if (this.patch < o.patch)
+ return -1;
+ else if (this.patch > o.patch)
+ return 1;
+
+ // now we just compare pre-release identifiers
+ // (remember: build metadata is ignored)
+ return SemanticVersionNumber.compareIdentifiers(
+ this.preReleaseIdentifiers, o.preReleaseIdentifiers);
+ }
+
+ /**
+ * Determines the compatibility of code written for this version to
+ * {@code other}. More specifically:
+ * <p>
+ * If this function returns <b>true</b>, then there should be no problems
+ * upgrading code written for this version to version {@code other} as long
+ * as:
+ * <ul>
+ * <li>Semantic Versioning is being used properly
+ * <li>Your code doesn't depend on unintended features (if it does, it isn't
+ * necessarily compatible with any other version)
+ * </ul>
+ * If this function returns <b>false</b>, you may have to change your code to
+ * upgrade it to {@code other}
+ *
+ * <p>
+ * Two version numbers that are identical (ignoring build metadata) are
+ * always compatible. Different version numbers are compatible as long as:
+ * <ul>
+ * <li>The major version number is not 0 (if it is, the API is considered
+ * unstable and any upgrade can be backwards incompatible)
+ * <li>The major version number is the same (changing the major version
+ * number implies backwards incompatible changes)
+ * <li>This version comes before the other one in the official precedence
+ * order (downgrading can remove features you depend on)
+ * </ul>
+ *
+ * @param other version to compare with
+ * @return true if you can definitely upgrade to {@code other} without
+ * changing code
+ * @since v0.4.0
+ * @since 2022-02-20
+ */
+ public boolean compatibleWith(SemanticVersionNumber other) {
+ Objects.requireNonNull(other, "other may not be null");
+
+ return this.compareTo(other) == 0 || this.major != 0
+ && this.major == other.major && this.compareTo(other) < 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof SemanticVersionNumber))
+ return false;
+ final SemanticVersionNumber other = (SemanticVersionNumber) obj;
+ if (this.buildMetadata == null) {
+ if (other.buildMetadata != null)
+ return false;
+ } else if (!this.buildMetadata.equals(other.buildMetadata))
+ return false;
+ if (this.major != other.major)
+ return false;
+ if (this.minor != other.minor)
+ return false;
+ if (this.patch != other.patch)
+ return false;
+ if (this.preReleaseIdentifiers == null) {
+ if (other.preReleaseIdentifiers != null)
+ return false;
+ } else if (!this.preReleaseIdentifiers
+ .equals(other.preReleaseIdentifiers))
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode());
+ result = prime * result + this.major;
+ result = prime * result + this.minor;
+ result = prime * result + this.patch;
+ result = prime * result + (this.preReleaseIdentifiers == null ? 0
+ : this.preReleaseIdentifiers.hashCode());
+ return result;
+ }
+
+ /**
+ * @return true iff this version is stable (major version > 0 and not a
+ * pre-release)
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public boolean isStable() {
+ return this.major > 0 && this.preReleaseIdentifiers.isEmpty();
+ }
+
+ /**
+ * @return the MAJOR version number, incremented when you make backwards
+ * incompatible API changes
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public int majorVersion() {
+ return this.major;
+ }
+
+ /**
+ * @return the MINOR version number, incremented when you add backwards
+ * compatible functionality
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public int minorVersion() {
+ return this.minor;
+ }
+
+ /**
+ * @return the PATCH version number, incremented when you make backwards
+ * compatible bug fixes
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public int patchVersion() {
+ return this.patch;
+ }
+
+ /**
+ * @return identifiers describing this pre-release (empty if not a
+ * pre-release)
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public List<String> preReleaseIdentifiers() {
+ return Collections.unmodifiableList(this.preReleaseIdentifiers);
+ }
+
+ /**
+ * Converts a version number to a string using the official SemVer format.
+ * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If
+ * pre-release identifiers are present, they are separated by periods and
+ * added after a '-'. If build metadata is present, it is separated by
+ * periods and added after a '+'. Pre-release identifiers go before version
+ * metadata.
+ * <p>
+ * For example, the version with major number 3, minor number 2, patch number
+ * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19"
+ * has a string representation "3.2.1-alpha.1+2022-02-19".
+ *
+ * @since v0.4.0
+ * @see <a href="https://semver.org">The official SemVer specification</a>
+ */
+ @Override
+ public String toString() {
+ String versionString = String.format("%d.%d.%d", this.major, this.minor,
+ this.patch);
+ if (!this.preReleaseIdentifiers.isEmpty()) {
+ versionString += "-" + String.join(".", this.preReleaseIdentifiers);
+ }
+ if (!this.buildMetadata.isEmpty()) {
+ versionString += "+" + String.join(".", this.buildMetadata);
+ }
+ return versionString;
+ }
+}
diff --git a/src/main/java/sevenUnits/utils/UncertainDouble.java b/src/main/java/sevenUnits/utils/UncertainDouble.java
index fe41104..ac523b3 100644
--- a/src/main/java/sevenUnits/utils/UncertainDouble.java
+++ b/src/main/java/sevenUnits/utils/UncertainDouble.java
@@ -46,6 +46,21 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
+ "(?:\\s*(?:±|\\+-)\\s*" + NUMBER_REGEX + ")?");
/**
+ * Gets an UncertainDouble from a double string. The uncertainty of the
+ * double will be one of the lowest decimal place of the number. For example,
+ * "12345.678" will become 12345.678 ± 0.001.
+ *
+ * @throws NumberFormatException if the argument is not a number
+ *
+ * @since 2022-04-18
+ */
+ public static final UncertainDouble fromRoundedString(String s) {
+ final BigDecimal value = new BigDecimal(s);
+ final double uncertainty = Math.pow(10, -value.scale());
+ return UncertainDouble.of(value.doubleValue(), uncertainty);
+ }
+
+ /**
* Parses a string in the form of {@link UncertainDouble#toString(boolean)}
* and returns the corresponding {@code UncertainDouble} instance.
* <p>
@@ -348,7 +363,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
*/
@Override
public final String toString() {
- return this.toString(!this.isExact());
+ return this.toString(!this.isExact(), RoundingMode.HALF_EVEN);
}
/**
@@ -379,7 +394,8 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
*
* @since 2020-09-07
*/
- public final String toString(boolean showUncertainty) {
+ public final String toString(boolean showUncertainty,
+ RoundingMode roundingMode) {
String valueString, uncertaintyString;
// generate the string representation of value and uncertainty
@@ -394,9 +410,9 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
final int displayScale = this.getDisplayScale();
final BigDecimal roundedUncertainty = bigUncertainty
- .setScale(displayScale, RoundingMode.HALF_EVEN);
+ .setScale(displayScale, roundingMode);
final BigDecimal roundedValue = bigValue.setScale(displayScale,
- RoundingMode.HALF_EVEN);
+ roundingMode);
valueString = roundedValue.toString();
uncertaintyString = roundedUncertainty.toString();
diff --git a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
index 6b6abf0..b56356d 100644
--- a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java
+++ b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
@@ -1,7 +1,7 @@
/**
* @since 2020-08-26
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.util.List;
import java.util.function.Predicate;
@@ -14,7 +14,7 @@ import sevenUnits.unit.UnitPrefix;
*
* @since 2020-08-26
*/
-enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> {
+public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> {
NO_REPETITION {
@Override
public boolean test(List<UnitPrefix> prefixes) {
diff --git a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java b/src/main/java/sevenUnitsGUI/DelegateListModel.java
index dd8cc97..5938b59 100644
--- a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java
+++ b/src/main/java/sevenUnitsGUI/DelegateListModel.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.util.ArrayList;
import java.util.Collection;
diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
new file mode 100644
index 0000000..5c39788
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (C) 2021 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+/**
+ * A View that can convert unit expressions
+ *
+ * @author Adrien Hopkins
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+public interface ExpressionConversionView extends View {
+ /**
+ * @return unit expression to convert <em>from</em>
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ String getFromExpression();
+
+ /**
+ * @return unit expression to convert <em>to</em>
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ String getToExpression();
+
+ /**
+ * Shows the output of an expression conversion to the user.
+ *
+ * @param uc unit conversion to show
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ void showExpressionConversionOutput(UnitConversionRecord uc);
+}
diff --git a/src/main/java/sevenUnits/converterGUI/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java
index edd00e2..c0a67e8 100644
--- a/src/main/java/sevenUnits/converterGUI/FilterComparator.java
+++ b/src/main/java/sevenUnitsGUI/FilterComparator.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.util.Comparator;
import java.util.Objects;
@@ -22,11 +22,13 @@ import java.util.Objects;
/**
* A comparator that compares strings using a filter.
*
+ * @param <T> type of element being compared
+ *
* @author Adrien Hopkins
* @since 2019-01-15
* @since v0.1.0
*/
-final class FilterComparator implements Comparator<String> {
+final class FilterComparator<T> implements Comparator<T> {
/**
* The filter that the comparator is filtered by.
*
@@ -40,7 +42,7 @@ final class FilterComparator implements Comparator<String> {
* @since 2019-01-15
* @since v0.1.0
*/
- private final Comparator<String> comparator;
+ private final Comparator<T> comparator;
/**
* Whether or not the comparison is case-sensitive.
*
@@ -48,7 +50,7 @@ final class FilterComparator implements Comparator<String> {
* @since v0.2.0
*/
private final boolean caseSensitive;
-
+
/**
* Creates the {@code FilterComparator}.
*
@@ -59,71 +61,76 @@ final class FilterComparator implements Comparator<String> {
public FilterComparator(final String filter) {
this(filter, null);
}
-
+
/**
* Creates the {@code FilterComparator}.
*
- * @param filter
- * string to filter by
- * @param comparator
- * comparator to fall back to if all else fails, null is compareTo.
- * @throws NullPointerException
- * if filter is null
+ * @param filter string to filter by
+ * @param comparator comparator to fall back to if all else fails, null is
+ * compareTo.
+ * @throws NullPointerException if filter is null
* @since 2019-01-15
* @since v0.1.0
*/
- public FilterComparator(final String filter, final Comparator<String> comparator) {
+ public FilterComparator(final String filter,
+ final Comparator<T> comparator) {
this(filter, comparator, false);
}
-
+
/**
* Creates the {@code FilterComparator}.
*
- * @param filter
- * string to filter by
- * @param comparator
- * comparator to fall back to if all else fails, null is compareTo.
- * @param caseSensitive
- * whether or not the comparator is case-sensitive
- * @throws NullPointerException
- * if filter is null
+ * @param filter string to filter by
+ * @param comparator comparator to fall back to if all else fails, null is
+ * compareTo.
+ * @param caseSensitive whether or not the comparator is case-sensitive
+ * @throws NullPointerException if filter is null
* @since 2019-04-14
* @since v0.2.0
*/
- public FilterComparator(final String filter, final Comparator<String> comparator, final boolean caseSensitive) {
- this.filter = Objects.requireNonNull(filter, "filter must not be null.");
+ public FilterComparator(final String filter, final Comparator<T> comparator,
+ final boolean caseSensitive) {
+ Objects.requireNonNull(filter, "filter must not be null.");
+ this.filter = caseSensitive ? filter : filter.toLowerCase();
this.comparator = comparator;
this.caseSensitive = caseSensitive;
}
-
+
+ /**
+ * Compares two objects according to whether or not they match a filter.
+ * Objects whose string representation starts with the filter's text go
+ * first, then those that contain it but don't start with it, then those that
+ * don't contain it. Objects in the same order here are sorted by their
+ * string representation's compareTo or the provided comparator.
+ */
@Override
- public int compare(final String arg0, final String arg1) {
+ public int compare(final T arg0, final T arg1) {
// if this is case insensitive, make them lowercase
final String str0, str1;
if (this.caseSensitive) {
- str0 = arg0;
- str1 = arg1;
+ str0 = arg0.toString();
+ str1 = arg1.toString();
} else {
- str0 = arg0.toLowerCase();
- str1 = arg1.toLowerCase();
+ str0 = arg0.toString().toLowerCase();
+ str1 = arg1.toString().toLowerCase();
}
-
+
// elements that start with the filter always go first
if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
return -1;
else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
return 1;
-
+
// elements that contain the filter but don't start with them go next
if (str0.contains(this.filter) && !str1.contains(this.filter))
return -1;
else if (!str0.contains(this.filter) && !str1.contains(this.filter))
return 1;
-
+
// other elements go last
if (this.comparator == null)
return str0.compareTo(str1);
else
- return this.comparator.compare(str0, str1);
+ return this.comparator.compare(arg0, arg1);
}
}
diff --git a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
index 0b71d78..32e94d7 100644
--- a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java
+++ b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.awt.GridBagConstraints;
import java.awt.Insets;
diff --git a/src/main/java/sevenUnitsGUI/Main.java b/src/main/java/sevenUnitsGUI/Main.java
new file mode 100644
index 0000000..998b373
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/Main.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+/**
+ * The main code for the 7Units GUI
+ *
+ * @since v0.4.0
+ * @since 2022-04-19
+ */
+public final class Main {
+
+ /**
+ * The main method that starts 7Units
+ *
+ * @param args commandline arguments
+ * @since v0.4.0
+ * @since 2022-04-19
+ */
+ public static void main(String[] args) {
+ View.createTabbedView();
+ }
+
+}
diff --git a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java
new file mode 100644
index 0000000..a5034c9
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import sevenUnits.unit.LinearUnit;
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.UnitPrefix;
+
+/**
+ * A search rule that applies a certain set of prefixes to a unit. It always
+ * includes the original unit in the output map.
+ *
+ * @since v0.4.0
+ * @since 2022-07-06
+ */
+public final class PrefixSearchRule implements
+ Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> {
+ /**
+ * A rule that does not add any prefixed versions of units.
+ *
+ * @since v0.4.0
+ */
+ public static final PrefixSearchRule NO_PREFIXES = getUniversalRule(
+ Set.of());
+
+ /**
+ * A rule that gives every unit a common set of prefixes.
+ *
+ * @since v0.4.0
+ */
+ public static final PrefixSearchRule COMMON_PREFIXES = getCoherentOnlyRule(
+ Set.of(Metric.MILLI, Metric.KILO));
+
+ /**
+ * A rule that gives every unit all metric prefixes.
+ *
+ * @since v0.4.0
+ */
+ public static final PrefixSearchRule ALL_METRIC_PREFIXES = getCoherentOnlyRule(
+ Metric.ALL_PREFIXES);
+
+ /**
+ * Gets a rule that applies the provided prefixes to coherent units only (as
+ * defined by {@link LinearUnit#isCoherent}), except the kilogram
+ * (specifically, units named "kilogram").
+ *
+ * @param prefixes prefixes to apply
+ * @return prefix rule
+ * @since v0.4.0
+ * @since 2022-07-06
+ */
+ public static final PrefixSearchRule getCoherentOnlyRule(
+ Set<UnitPrefix> prefixes) {
+ return new PrefixSearchRule(prefixes,
+ u -> u.isCoherent() && !u.getName().equals("kilogram"));
+ }
+
+ /**
+ * Gets a rule that applies the provided prefixes to all units.
+ *
+ * @param prefixes prefixes to apply
+ * @return prefix rule
+ * @since v0.4.0
+ * @since 2022-07-06
+ */
+ public static final PrefixSearchRule getUniversalRule(
+ Set<UnitPrefix> prefixes) {
+ return new PrefixSearchRule(prefixes, u -> true);
+ }
+
+ /**
+ * The set of prefixes that will be applied to the unit.
+ */
+ private final Set<UnitPrefix> prefixes;
+
+ /**
+ * Determines which units are given prefixes.
+ */
+ private final Predicate<LinearUnit> prefixableUnitRule;
+
+ /**
+ * @param prefixes prefixes to add to units
+ * @param prefixableUnitRule function that determines which units get
+ * prefixes
+ * @since v0.4.0
+ * @since 2022-07-06
+ */
+ public PrefixSearchRule(Set<UnitPrefix> prefixes,
+ Predicate<LinearUnit> prefixableUnitRule) {
+ this.prefixes = Collections.unmodifiableSet(new HashSet<>(prefixes));
+ this.prefixableUnitRule = prefixableUnitRule;
+ }
+
+ @Override
+ public Map<String, LinearUnit> apply(Entry<String, LinearUnit> t) {
+ final Map<String, LinearUnit> outputUnits = new HashMap<>();
+ final String originalName = t.getKey();
+ final LinearUnit originalUnit = t.getValue();
+ outputUnits.put(originalName, originalUnit);
+ if (this.prefixableUnitRule.test(originalUnit)) {
+ for (final UnitPrefix prefix : this.prefixes) {
+ outputUnits.put(prefix.getName() + originalName,
+ originalUnit.withPrefix(prefix));
+ }
+ }
+ return Collections.unmodifiableMap(outputUnits);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof PrefixSearchRule))
+ return false;
+ final PrefixSearchRule other = (PrefixSearchRule) obj;
+ return Objects.equals(this.prefixableUnitRule, other.prefixableUnitRule)
+ && Objects.equals(this.prefixes, other.prefixes);
+ }
+
+ /**
+ * @return rule that determines which units get prefixes
+ * @since v0.4.0
+ * @since 2022-07-09
+ */
+ public Predicate<LinearUnit> getPrefixableUnitRule() {
+ return this.prefixableUnitRule;
+ }
+
+ /**
+ * @return the prefixes that are applied by this rule
+ * @since v0.4.0
+ * @since 2022-07-06
+ */
+ public Set<UnitPrefix> getPrefixes() {
+ return this.prefixes;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.prefixableUnitRule, this.prefixes);
+ }
+
+ @Override
+ public String toString() {
+ return "Apply the following prefixes: " + this.prefixes;
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
new file mode 100644
index 0000000..abdd1f6
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -0,0 +1,851 @@
+/**
+ * Copyright (C) 2021-2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import sevenUnits.ProgramInfo;
+import sevenUnits.unit.BaseDimension;
+import sevenUnits.unit.BaseUnit;
+import sevenUnits.unit.BritishImperial;
+import sevenUnits.unit.LinearUnit;
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.Unit;
+import sevenUnits.unit.UnitDatabase;
+import sevenUnits.unit.UnitPrefix;
+import sevenUnits.unit.UnitType;
+import sevenUnits.unit.UnitValue;
+import sevenUnits.utils.Nameable;
+import sevenUnits.utils.ObjectProduct;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * An object that handles interactions between the view and the backend code
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public final class Presenter {
+ /** The default place where settings are stored. */
+ private static final Path DEFAULT_SETTINGS_FILEPATH = Path
+ .of("settings.txt");
+ /** The default place where units are stored. */
+ private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt";
+ /** The default place where dimensions are stored. */
+ private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt";
+ /** The default place where exceptions are stored. */
+ private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt";
+
+ /**
+ * Adds default units and dimensions to a database.
+ *
+ * @param database database to add to
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private static void addDefaults(final UnitDatabase database) {
+ database.addUnit("metre", Metric.METRE);
+ database.addUnit("kilogram", Metric.KILOGRAM);
+ database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000));
+ database.addUnit("second", Metric.SECOND);
+ database.addUnit("ampere", Metric.AMPERE);
+ database.addUnit("kelvin", Metric.KELVIN);
+ database.addUnit("mole", Metric.MOLE);
+ database.addUnit("candela", Metric.CANDELA);
+ database.addUnit("bit", Metric.BIT);
+ database.addUnit("unit", Metric.ONE);
+ // nonlinear units - must be loaded manually
+ database.addUnit("tempCelsius", Metric.CELSIUS);
+ database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
+
+ // load initial dimensions
+ database.addDimension("Length", Metric.Dimensions.LENGTH);
+ database.addDimension("Mass", Metric.Dimensions.MASS);
+ database.addDimension("Time", Metric.Dimensions.TIME);
+ database.addDimension("Temperature", Metric.Dimensions.TEMPERATURE);
+ }
+
+ /**
+ * @return text in About file
+ * @since 2022-02-19
+ */
+ public static final String getAboutText() {
+ return Presenter.getLinesFromResource("/about.txt").stream()
+ .map(Presenter::withoutComments).collect(Collectors.joining("\n"))
+ .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString());
+ }
+
+ /**
+ * Gets the text of a resource file as a set of strings (each one is one line
+ * of the text).
+ *
+ * @param filename filename to get resource from
+ * @return contents of file
+ * @since 2021-03-27
+ */
+ private static final List<String> getLinesFromResource(String filename) {
+ final List<String> lines = new ArrayList<>();
+
+ try (InputStream stream = inputStream(filename);
+ Scanner scanner = new Scanner(stream)) {
+ while (scanner.hasNextLine()) {
+ lines.add(scanner.nextLine());
+ }
+ } catch (final IOException e) {
+ throw new AssertionError(
+ "Error occurred while loading file " + filename, e);
+ }
+
+ return lines;
+ }
+
+ /**
+ * Gets an input stream for a resource file.
+ *
+ * @param filepath file to use as resource
+ * @return obtained Path
+ * @since 2021-03-27
+ */
+ private static final InputStream inputStream(String filepath) {
+ return Presenter.class.getResourceAsStream(filepath);
+ }
+
+ /**
+ * @return true iff a and b have any elements in common
+ * @since 2022-04-19
+ */
+ private static final boolean sharesAnyElements(Set<?> a, Set<?> b) {
+ for (final Object e : a) {
+ if (b.contains(e))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return {@code line} with any comments removed.
+ * @since 2021-03-13
+ */
+ private static final String withoutComments(String line) {
+ final int index = line.indexOf('#');
+ return index == -1 ? line : line.substring(0, index);
+ }
+
+ // ====== SETTINGS ======
+
+ /**
+ * The view that this presenter communicates with
+ */
+ private final View view;
+
+ /**
+ * The database that this presenter communicates with (effectively the model)
+ */
+ final UnitDatabase database;
+
+ /**
+ * The rule used for parsing input numbers. Any number-string inputted into
+ * this program will be parsed using this method. <b>Not implemented yet.</b>
+ */
+ private Function<String, UncertainDouble> numberParsingRule;
+
+ /**
+ * The rule used for displaying the results of unit conversions. The result
+ * of unit conversions will be put into this function, and the resulting
+ * string will be used in the output.
+ */
+ private Function<UncertainDouble, String> numberDisplayRule = StandardDisplayRules
+ .uncertaintyBased();
+
+ /**
+ * A predicate that determines whether or not a certain combination of
+ * prefixes is allowed. If it returns false, a combination of prefixes will
+ * not be allowed. Prefixes are put in the list from right to left.
+ */
+ private Predicate<List<UnitPrefix>> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION;
+
+ /**
+ * A rule that accepts a prefixless name-unit pair and returns a map mapping
+ * names to prefixed versions of that unit (including the unit itself) that
+ * should be searchable.
+ */
+ private Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule = PrefixSearchRule.NO_PREFIXES;
+
+ /**
+ * The set of units that is considered neither metric nor nonmetric for the
+ * purposes of the metric-imperial one-way conversion. These units are
+ * included in both From and To, even if One Way Conversion is enabled.
+ */
+ private final Set<String> metricExceptions;
+
+ /**
+ * If this is true, views that show units as a list will have metric units
+ * removed from the From unit list and imperial/USC units removed from the To
+ * unit list.
+ */
+ private boolean oneWayConversionEnabled = false;
+
+ /**
+ * If this is false, duplicate units and prefixes will be removed from the
+ * unit view in views that show units as a list to choose from.
+ */
+ private boolean showDuplicates = false;
+
+ /**
+ * Creates a Presenter
+ *
+ * @param view the view that this presenter communicates with
+ * @since 2021-12-15
+ */
+ public Presenter(View view) {
+ this.view = view;
+ this.database = new UnitDatabase();
+ addDefaults(this.database);
+
+ // load units and prefixes
+ try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) {
+ this.database.loadUnitsFromStream(units);
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of unitsfile.txt failed.", e);
+ }
+
+ // load dimensions
+ try (final InputStream dimensions = inputStream(
+ DEFAULT_DIMENSIONS_FILEPATH)) {
+ this.database.loadDimensionsFromStream(dimensions);
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of dimensionfile.txt failed.", e);
+ }
+
+ // load metric exceptions
+ try {
+ this.metricExceptions = new HashSet<>();
+ try (InputStream exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
+ Scanner scanner = new Scanner(exceptions)) {
+ while (scanner.hasNextLine()) {
+ final String line = Presenter
+ .withoutComments(scanner.nextLine());
+ if (!line.isBlank()) {
+ this.metricExceptions.add(line);
+ }
+ }
+ }
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of metric_exceptions.txt failed.",
+ e);
+ }
+
+ // set default settings temporarily
+ this.loadSettings(DEFAULT_SETTINGS_FILEPATH);
+
+ // a Predicate that returns true iff the argument is a full base unit
+ final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit
+ && ((LinearUnit) unit).isBase();
+
+ // print out unit counts
+ System.out.printf(
+ "Successfully loaded %d units with %d unit names (%d base units).%n",
+ this.database.unitMapPrefixless(false).size(),
+ this.database.unitMapPrefixless(true).size(),
+ this.database.unitMapPrefixless(false).values().stream()
+ .filter(isFullBase).count());
+ }
+
+ /**
+ * Applies a search rule to an entry in a name-unit map.
+ *
+ * @param e entry
+ * @return stream of entries, ready for flat-mapping
+ * @since 2022-07-06
+ */
+ private final Stream<Map.Entry<String, Unit>> applySearchRule(
+ Map.Entry<String, Unit> e) {
+ final Unit u = e.getValue();
+ if (u instanceof LinearUnit) {
+ final String name = e.getKey();
+ final Map.Entry<String, LinearUnit> linearEntry = Map.entry(name,
+ (LinearUnit) u);
+ return this.searchRule.apply(linearEntry).entrySet().stream().map(
+ entry -> Map.entry(entry.getKey(), (Unit) entry.getValue()));
+ } else
+ return Stream.of(e);
+ }
+
+ /**
+ * Converts from the view's input expression to its output expression.
+ * Displays an error message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * expression-based conversion (does
+ * not implement
+ * {@link ExpressionConversionView})
+ * @since 2021-12-15
+ */
+ public void convertExpressions() {
+ if (this.view instanceof ExpressionConversionView) {
+ final ExpressionConversionView xcview = (ExpressionConversionView) this.view;
+
+ final String fromExpression = xcview.getFromExpression();
+ final String toExpression = xcview.getToExpression();
+
+ // expressions must not be empty
+ if (fromExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the From: box.");
+ return;
+ }
+ if (toExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the To: box.");
+ return;
+ }
+
+ // evaluate expressions
+ final LinearUnitValue from;
+ final Unit to;
+ try {
+ from = this.database.evaluateUnitExpression(fromExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in From entry: " + e.getMessage());
+ return;
+ }
+ try {
+ to = this.database.getUnitFromExpression(toExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in To entry: " + e.getMessage());
+ return;
+ }
+
+ // convert and show output
+ if (from.getUnit().canConvertTo(to)) {
+ final UncertainDouble uncertainValue;
+
+ // uncertainty is meaningless for non-linear units, so we will have
+ // to erase uncertainty information for them
+ if (to instanceof LinearUnit) {
+ final var toLinear = (LinearUnit) to;
+ uncertainValue = from.convertTo(toLinear).getValue();
+ } else {
+ final double value = from.asUnitValue().convertTo(to).getValue();
+ uncertainValue = UncertainDouble.of(value, 0);
+ }
+
+ final UnitConversionRecord uc = UnitConversionRecord.valueOf(
+ fromExpression, toExpression, "",
+ this.numberDisplayRule.apply(uncertainValue));
+ xcview.showExpressionConversionOutput(uc);
+ } else {
+ this.view.showErrorMessage("Conversion Error",
+ "Cannot convert between \"" + fromExpression + "\" and \""
+ + toExpression + "\".");
+ }
+
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is an ExpressionConversionView");
+ }
+
+ /**
+ * Converts from the view's input unit to its output unit. Displays an error
+ * message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * unit-based conversion (does not
+ * implement
+ * {@link UnitConversionView})
+ * @since 2021-12-15
+ */
+ public void convertUnits() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+
+ final Optional<String> fromUnitOptional = ucview.getFromSelection();
+ final Optional<String> toUnitOptional = ucview.getToSelection();
+ final String inputValueString = ucview.getInputValue();
+
+ // extract values from optionals
+ final String fromUnitString, toUnitString;
+ if (fromUnitOptional.isPresent()) {
+ fromUnitString = fromUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a From unit");
+ return;
+ }
+ if (toUnitOptional.isPresent()) {
+ toUnitString = toUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a To unit");
+ return;
+ }
+
+ // convert strings to data, checking if anything is invalid
+ final Unit fromUnit, toUnit;
+ final UncertainDouble uncertainValue;
+
+ if (this.database.containsUnitName(fromUnitString)) {
+ fromUnit = this.database.getUnit(fromUnitString);
+ } else
+ throw this.viewError("Nonexistent From unit: %s", fromUnitString);
+ if (this.database.containsUnitName(toUnitString)) {
+ toUnit = this.database.getUnit(toUnitString);
+ } else
+ throw this.viewError("Nonexistent To unit: %s", toUnitString);
+ try {
+ uncertainValue = UncertainDouble
+ .fromRoundedString(inputValueString);
+ } catch (final NumberFormatException e) {
+ this.view.showErrorMessage("Value Error",
+ "Invalid value " + inputValueString);
+ return;
+ }
+
+ if (!fromUnit.canConvertTo(toUnit))
+ throw this.viewError("Could not convert between %s and %s",
+ fromUnit, toUnit);
+
+ // convert - we will need to erase uncertainty for non-linear units, so
+ // we need to treat linear and non-linear units differently
+ final String outputValueString;
+ if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) {
+ final LinearUnit fromLinear = (LinearUnit) fromUnit;
+ final LinearUnit toLinear = (LinearUnit) toUnit;
+ final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear,
+ uncertainValue);
+ final LinearUnitValue converted = initialValue.convertTo(toLinear);
+
+ outputValueString = this.numberDisplayRule
+ .apply(converted.getValue());
+ } else {
+ final UnitValue initialValue = UnitValue.of(fromUnit,
+ uncertainValue.value());
+ final UnitValue converted = initialValue.convertTo(toUnit);
+
+ outputValueString = this.numberDisplayRule
+ .apply(UncertainDouble.of(converted.getValue(), 0));
+ }
+
+ ucview.showUnitConversionOutput(
+ UnitConversionRecord.valueOf(fromUnitString, toUnitString,
+ inputValueString, outputValueString));
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is a UnitConversionView.");
+ }
+
+ /**
+ * @return true iff duplicate units are shown in unit lists
+ * @since 2022-03-30
+ */
+ public boolean duplicatesShown() {
+ return this.showDuplicates;
+ }
+
+ /**
+ * Gets a name for this dimension using the database
+ *
+ * @param dimension dimension to name
+ * @return name of dimension
+ * @since 2022-04-16
+ */
+ final String getDimensionName(ObjectProduct<BaseDimension> dimension) {
+ // find this dimension in the database and get its name
+ // if it isn't there, use the dimension's toString instead
+ return this.database.dimensionMap().values().stream()
+ .filter(d -> d.equals(dimension)).findAny().map(Nameable::getName)
+ .orElse(dimension.toString(Nameable::getName));
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert numbers into
+ * strings
+ * @since 2022-04-10
+ */
+ public Function<UncertainDouble, String> getNumberDisplayRule() {
+ return this.numberDisplayRule;
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert strings into
+ * numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private Function<String, UncertainDouble> getNumberParsingRule() {
+ return this.numberParsingRule;
+ }
+
+ /**
+ * @return the rule that determines whether a set of prefixes is valid
+ * @since 2022-04-19
+ */
+ public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
+ return this.prefixRepetitionRule;
+ }
+
+ /**
+ * @return the rule that determines which units are prefixed
+ * @since 2022-07-08
+ */
+ public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getSearchRule() {
+ return this.searchRule;
+ }
+
+ /**
+ * @return a search rule that shows all single prefixes
+ * @since 2022-07-08
+ */
+ public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getUniversalSearchRule() {
+ return PrefixSearchRule.getCoherentOnlyRule(
+ new HashSet<>(this.database.prefixMap(true).values()));
+ }
+
+ /**
+ * @return the view associated with this presenter
+ * @since 2022-04-19
+ */
+ public View getView() {
+ return this.view;
+ }
+
+ /**
+ * @return whether or not the provided unit is semi-metric (i.e. an
+ * exception)
+ * @since 2022-04-16
+ */
+ final boolean isSemiMetric(Unit u) {
+ // determine if u is an exception
+ final var primaryName = u.getPrimaryName();
+ final var symbol = u.getSymbol();
+ return primaryName.isPresent()
+ && this.metricExceptions.contains(primaryName.orElseThrow())
+ || symbol.isPresent()
+ && this.metricExceptions.contains(symbol.orElseThrow())
+ || sharesAnyElements(this.metricExceptions, u.getOtherNames());
+ }
+
+ /**
+ * Loads settings from the user's settings file and applies them to the
+ * presenter.
+ *
+ * @param settingsFile file settings should be loaded from
+ * @since 2021-12-15
+ */
+ void loadSettings(Path settingsFile) {
+ try {
+ // read file line by line
+ final int lineNum = 0;
+ for (final String line : Files.readAllLines(settingsFile)) {
+ final int equalsIndex = line.indexOf('=');
+ if (equalsIndex == -1)
+ throw new IllegalStateException(
+ "Settings file is malformed at line " + lineNum);
+
+ final String param = line.substring(0, equalsIndex);
+ final String value = line.substring(equalsIndex + 1);
+
+ switch (param) {
+ // set manually to avoid the unnecessary saving of the non-manual
+ // methods
+ case "number_display_rule":
+ this.numberDisplayRule = StandardDisplayRules
+ .getStandardRule(value);
+ break;
+ case "prefix_rule":
+ this.prefixRepetitionRule = DefaultPrefixRepetitionRule
+ .valueOf(value);
+ this.database.setPrefixRepetitionRule(this.prefixRepetitionRule);
+ break;
+ case "one_way":
+ this.oneWayConversionEnabled = Boolean.valueOf(value);
+ break;
+ case "include_duplicates":
+ this.showDuplicates = Boolean.valueOf(value);
+ break;
+ case "search_prefix_rule":
+ if (PrefixSearchRule.NO_PREFIXES.toString().equals(value)) {
+ this.searchRule = PrefixSearchRule.NO_PREFIXES;
+ } else if (PrefixSearchRule.COMMON_PREFIXES.toString()
+ .equals(value)) {
+ this.searchRule = PrefixSearchRule.COMMON_PREFIXES;
+ } else {
+ this.searchRule = this.getUniversalSearchRule();
+ }
+ break;
+ default:
+ System.err.printf("Warning: unrecognized setting \"%s\".%n",
+ param);
+ break;
+ }
+ }
+ if (this.view.getPresenter() != null) {
+ this.updateView();
+ }
+ } catch (final IOException e) {}
+ }
+
+ /**
+ * @return true iff the One-Way Conversion feature is available (views that
+ * show units as a list will have metric units removed from the From
+ * unit list and imperial/USC units removed from the To unit list)
+ *
+ * @since 2022-03-30
+ */
+ public boolean oneWayConversionEnabled() {
+ return this.oneWayConversionEnabled;
+ }
+
+ /**
+ * Completes creation of the presenter. This part of the initialization
+ * depends on the view's functions, so it cannot be run if the components
+ * they depend on are not created yet.
+ *
+ * @since 2022-02-26
+ */
+ public void postViewInitialize() {
+ // unit conversion specific stuff
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ ucview.setDimensionNames(this.database.dimensionMap().keySet());
+ }
+
+ this.updateView();
+ }
+
+ void prefixSelected() {
+ final Optional<String> selectedPrefixName = this.view
+ .getViewedPrefixName();
+ final Optional<UnitPrefix> selectedPrefix = selectedPrefixName
+ .map(name -> this.database.containsPrefixName(name)
+ ? this.database.getPrefix(name)
+ : null);
+ selectedPrefix
+ .ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(),
+ String.valueOf(prefix.getMultiplier())));
+ }
+
+ /**
+ * Saves the presenter's current settings to its default filepath.
+ *
+ * @since 2022-04-19
+ */
+ public void saveSettings() {
+ this.saveSettings(DEFAULT_SETTINGS_FILEPATH);
+ }
+
+ /**
+ * Saves the presenter's settings to the user settings file.
+ *
+ * @param settingsFile file settings should be saved to
+ * @since 2021-12-15
+ */
+ void saveSettings(Path settingsFile) {
+ try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) {
+ writer.write(String.format("number_display_rule=%s\n",
+ this.numberDisplayRule));
+ writer.write(
+ String.format("prefix_rule=%s\n", this.prefixRepetitionRule));
+ writer.write(
+ String.format("one_way=%s\n", this.oneWayConversionEnabled));
+ writer.write(
+ String.format("include_duplicates=%s\n", this.showDuplicates));
+ writer.write(
+ String.format("search_prefix_rule=%s\n", this.searchRule));
+ } catch (final IOException e) {
+ e.printStackTrace();
+ this.view.showErrorMessage("I/O Error",
+ "Error occurred while saving settings: "
+ + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * @param numberDisplayRule the new rule that will be used by this presenter
+ * to convert numbers into strings
+ * @since 2022-04-10
+ */
+ public void setNumberDisplayRule(
+ Function<UncertainDouble, String> numberDisplayRule) {
+ this.numberDisplayRule = numberDisplayRule;
+ }
+
+ /**
+ * @param numberParsingRule the new rule that will be used by this presenter
+ * to convert strings into numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private void setNumberParsingRule(
+ Function<String, UncertainDouble> numberParsingRule) {
+ this.numberParsingRule = numberParsingRule;
+ }
+
+ /**
+ * @param oneWayConversionEnabled whether not one-way conversion should be
+ * enabled
+ * @since 2022-03-30
+ * @see {@link #isOneWayConversionEnabled}
+ */
+ public void setOneWayConversionEnabled(boolean oneWayConversionEnabled) {
+ this.oneWayConversionEnabled = oneWayConversionEnabled;
+ this.updateView();
+ }
+
+ /**
+ * @param prefixRepetitionRule the rule that determines whether a set of
+ * prefixes is valid
+ * @since 2022-04-19
+ */
+ public void setPrefixRepetitionRule(
+ Predicate<List<UnitPrefix>> prefixRepetitionRule) {
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ this.database.setPrefixRepetitionRule(prefixRepetitionRule);
+ }
+
+ /**
+ * @param searchRule A rule that accepts a prefixless name-unit pair and
+ * returns a map mapping names to prefixed versions of that
+ * unit (including the unit itself) that should be
+ * searchable.
+ * @since 2022-07-08
+ */
+ public void setSearchRule(
+ Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
+ this.searchRule = searchRule;
+ }
+
+ /**
+ * @param showDuplicateUnits whether or not duplicate units should be shown
+ * @since 2022-03-30
+ */
+ public void setShowDuplicates(boolean showDuplicateUnits) {
+ this.showDuplicates = showDuplicateUnits;
+ this.updateView();
+ }
+
+ /**
+ * Shows a unit in the unit viewer
+ *
+ * @param u unit to show
+ * @since 2022-04-16
+ */
+ private final void showUnit(Unit u) {
+ final var nameSymbol = u.getNameSymbol();
+ final boolean isBase = u instanceof BaseUnit
+ || u instanceof LinearUnit && ((LinearUnit) u).isBase();
+ final var definition = isBase ? "(Base unit)" : u.toDefinitionString();
+ final var dimensionString = this.getDimensionName(u.getDimension());
+ final var unitType = UnitType.getType(u, this::isSemiMetric);
+ this.view.showUnit(nameSymbol, definition, dimensionString, unitType);
+ }
+
+ /**
+ * Runs whenever a unit name is selected in the unit viewer. Gets the
+ * description of a unit and displays it.
+ *
+ * @since 2022-04-10
+ */
+ void unitNameSelected() {
+ // get selected unit, if it's there and valid
+ final Optional<String> selectedUnitName = this.view.getViewedUnitName();
+ final Optional<Unit> selectedUnit = selectedUnitName
+ .map(unitName -> this.database.containsUnitName(unitName)
+ ? this.database.getUnit(unitName)
+ : null);
+ selectedUnit.ifPresent(this::showUnit);
+ }
+
+ /**
+ * Updates the view's From and To units, if it has some
+ *
+ * @since 2021-12-15
+ */
+ public void updateView() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var selectedDimensionName = ucview.getSelectedDimensionName();
+
+ // load units & prefixes into viewers
+ this.view.setViewableUnitNames(
+ this.database.unitMapPrefixless(this.showDuplicates).keySet());
+ this.view.setViewablePrefixNames(
+ this.database.prefixMap(this.showDuplicates).keySet());
+
+ // get From and To units
+ var fromUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+ var toUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+
+ // filter by dimension, if one is selected
+ if (selectedDimensionName.isPresent()) {
+ final var viewDimension = this.database
+ .getDimension(selectedDimensionName.orElseThrow());
+ fromUnits = fromUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ toUnits = toUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ }
+
+ // filter by unit type, if desired
+ if (this.oneWayConversionEnabled) {
+ fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.METRIC);
+ toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.NON_METRIC);
+ }
+
+ // set unit names
+ ucview.setFromUnitNames(fromUnits.flatMap(this::applySearchRule)
+ .map(Map.Entry::getKey).collect(Collectors.toSet()));
+ ucview.setToUnitNames(toUnits.flatMap(this::applySearchRule)
+ .map(Map.Entry::getKey).collect(Collectors.toSet()));
+ }
+ }
+
+ /**
+ * @param message message to add
+ * @param args string formatting arguments for message
+ * @return AssertionError stating that an error has happened in the view's
+ * code
+ * @since 2022-04-09
+ */
+ private AssertionError viewError(String message, Object... args) {
+ return new AssertionError("View Programming Error (from " + this.view
+ + "): " + String.format(message, args));
+ }
+}
diff --git a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java
index 2aa9fce..9b41601 100644
--- a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java
+++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.awt.BorderLayout;
import java.awt.Color;
@@ -22,7 +22,10 @@ import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
import java.util.function.Predicate;
import javax.swing.JList;
@@ -31,11 +34,12 @@ import javax.swing.JScrollPane;
import javax.swing.JTextField;
/**
+ * @param <E> type of element in list
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
*/
-final class SearchBoxList extends JPanel {
+final class SearchBoxList<E> extends JPanel {
/**
* @since 2019-04-13
@@ -60,10 +64,10 @@ final class SearchBoxList extends JPanel {
private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192);
// the components
- private final Collection<String> itemsToFilter;
- private final DelegateListModel<String> listModel;
+ private final Collection<E> itemsToFilter;
+ private final DelegateListModel<E> listModel;
private final JTextField searchBox;
- private final JList<String> searchItems;
+ private final JList<E> searchItems;
private boolean searchBoxEmpty = true;
@@ -72,17 +76,26 @@ final class SearchBoxList extends JPanel {
// event.
private boolean searchBoxFocused = false;
- private Predicate<String> customSearchFilter = o -> true;
- private final Comparator<String> defaultOrdering;
+ private Predicate<E> customSearchFilter = o -> true;
+ private final Comparator<E> defaultOrdering;
private final boolean caseSensitive;
/**
+ * Creates an empty SearchBoxList
+ *
+ * @since 2022-02-19
+ */
+ public SearchBoxList() {
+ this(List.of(), null, false);
+ }
+
+ /**
* Creates the {@code SearchBoxList}.
*
* @param itemsToFilter items to put in the list
* @since 2019-04-14
*/
- public SearchBoxList(final Collection<String> itemsToFilter) {
+ public SearchBoxList(final Collection<E> itemsToFilter) {
this(itemsToFilter, null, false);
}
@@ -97,9 +110,8 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
* @since v0.2.0
*/
- public SearchBoxList(final Collection<String> itemsToFilter,
- final Comparator<String> defaultOrdering,
- final boolean caseSensitive) {
+ public SearchBoxList(final Collection<E> itemsToFilter,
+ final Comparator<E> defaultOrdering, final boolean caseSensitive) {
super(new BorderLayout(), true);
this.itemsToFilter = new ArrayList<>(itemsToFilter);
this.defaultOrdering = defaultOrdering;
@@ -140,7 +152,7 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
* @since v0.2.0
*/
- public void addSearchFilter(final Predicate<String> filter) {
+ public void addSearchFilter(final Predicate<E> filter) {
this.customSearchFilter = this.customSearchFilter.and(filter);
}
@@ -155,6 +167,15 @@ final class SearchBoxList extends JPanel {
}
/**
+ * @return items available in search list, including items that are hidden by
+ * the search filter
+ * @since 2022-03-30
+ */
+ public Collection<E> getItems() {
+ return Collections.unmodifiableCollection(this.itemsToFilter);
+ }
+
+ /**
* @return this component's search box component
* @since 2019-04-14
* @since v0.2.0
@@ -170,11 +191,11 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-14
* @since v0.2.0
*/
- private Predicate<String> getSearchFilter(final String searchText) {
+ private Predicate<E> getSearchFilter(final String searchText) {
if (this.caseSensitive)
- return string -> string.contains(searchText);
+ return item -> item.toString().contains(searchText);
else
- return string -> string.toLowerCase()
+ return item -> item.toString().toLowerCase()
.contains(searchText.toLowerCase());
}
@@ -183,12 +204,12 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-14
* @since v0.2.0
*/
- public final JList<String> getSearchList() {
+ public final JList<E> getSearchList() {
return this.searchItems;
}
/**
- * @return index selected in item list
+ * @return index selected in item list, -1 if no selection
* @since 2019-04-14
* @since v0.2.0
*/
@@ -201,8 +222,8 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
* @since v0.2.0
*/
- public String getSelectedValue() {
- return this.searchItems.getSelectedValue();
+ public Optional<E> getSelectedValue() {
+ return Optional.ofNullable(this.searchItems.getSelectedValue());
}
/**
@@ -214,14 +235,14 @@ final class SearchBoxList extends JPanel {
public void reapplyFilter() {
final String searchText = this.searchBoxEmpty ? ""
: this.searchBox.getText();
- final FilterComparator comparator = new FilterComparator(searchText,
+ final FilterComparator<E> comparator = new FilterComparator<>(searchText,
this.defaultOrdering, this.caseSensitive);
- final Predicate<String> searchFilter = this.getSearchFilter(searchText);
+ final Predicate<E> searchFilter = this.getSearchFilter(searchText);
this.listModel.clear();
- this.itemsToFilter.forEach(string -> {
- if (searchFilter.test(string)) {
- this.listModel.add(string);
+ this.itemsToFilter.forEach(item -> {
+ if (searchFilter.test(item)) {
+ this.listModel.add(item);
}
});
@@ -277,9 +298,9 @@ final class SearchBoxList extends JPanel {
}
final String searchText = this.searchBoxEmpty ? ""
: this.searchBox.getText();
- final FilterComparator comparator = new FilterComparator(searchText,
+ final FilterComparator<E> comparator = new FilterComparator<>(searchText,
this.defaultOrdering, this.caseSensitive);
- final Predicate<String> searchFilter = this.getSearchFilter(searchText);
+ final Predicate<E> searchFilter = this.getSearchFilter(searchText);
// initialize list with items that match the filter then sort
this.listModel.clear();
@@ -303,7 +324,7 @@ final class SearchBoxList extends JPanel {
* @param newItems new items to put in list
* @since 2021-05-22
*/
- public void setItems(Collection<String> newItems) {
+ public void setItems(Collection<? extends E> newItems) {
this.itemsToFilter.clear();
this.itemsToFilter.addAll(newItems);
this.reapplyFilter();
diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
new file mode 100644
index 0000000..cc69d31
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
@@ -0,0 +1,254 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * A static utility class that can be used to make display rules for the
+ * presenter.
+ *
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+public final class StandardDisplayRules {
+ /**
+ * A rule that rounds to a fixed number of decimal places.
+ *
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ public static final class FixedDecimals
+ implements Function<UncertainDouble, String> {
+ public static final Pattern TO_STRING_PATTERN = Pattern
+ .compile("Round to (\\d+) decimal places");
+ /**
+ * The number of places to round to.
+ */
+ private final int decimalPlaces;
+
+ /**
+ * @param decimalPlaces
+ * @since 2022-04-18
+ */
+ private FixedDecimals(int decimalPlaces) {
+ this.decimalPlaces = decimalPlaces;
+ }
+
+ @Override
+ public String apply(UncertainDouble t) {
+ final var toRound = new BigDecimal(t.value());
+ return toRound.setScale(this.decimalPlaces, RoundingMode.HALF_EVEN)
+ .toPlainString();
+ }
+
+ /**
+ * @return the number of decimal places this rule rounds to
+ * @since 2022-04-18
+ */
+ public int decimalPlaces() {
+ return this.decimalPlaces;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof FixedDecimals))
+ return false;
+ final FixedDecimals other = (FixedDecimals) obj;
+ if (this.decimalPlaces != other.decimalPlaces)
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 + this.decimalPlaces;
+ }
+
+ @Override
+ public String toString() {
+ return "Round to " + this.decimalPlaces + " decimal places";
+ }
+ }
+
+ /**
+ * A rule that rounds to a fixed number of significant digits.
+ *
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ public static final class FixedPrecision
+ implements Function<UncertainDouble, String> {
+ public static final Pattern TO_STRING_PATTERN = Pattern
+ .compile("Round to (\\d+) significant figures");
+
+ /**
+ * The number of significant figures to round to.
+ */
+ private final MathContext mathContext;
+
+ /**
+ * @param significantFigures
+ * @since 2022-04-18
+ */
+ private FixedPrecision(int significantFigures) {
+ this.mathContext = new MathContext(significantFigures,
+ RoundingMode.HALF_EVEN);
+ }
+
+ @Override
+ public String apply(UncertainDouble t) {
+ final var toRound = new BigDecimal(t.value());
+ return toRound.round(this.mathContext).toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof FixedPrecision))
+ return false;
+ final FixedPrecision other = (FixedPrecision) obj;
+ if (this.mathContext == null) {
+ if (other.mathContext != null)
+ return false;
+ } else if (!this.mathContext.equals(other.mathContext))
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return 127
+ + (this.mathContext == null ? 0 : this.mathContext.hashCode());
+ }
+
+ /**
+ * @return the number of significant figures this rule rounds to
+ * @since 2022-04-18
+ */
+ public int significantFigures() {
+ return this.mathContext.getPrecision();
+ }
+
+ @Override
+ public String toString() {
+ return "Round to " + this.mathContext.getPrecision()
+ + " significant figures";
+ }
+ }
+
+ /**
+ * A rounding rule that rounds based on UncertainDouble's toString method.
+ * This means the output will have around as many significant figures as the
+ * input.
+ *
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ public static final class UncertaintyBased
+ implements Function<UncertainDouble, String> {
+ private UncertaintyBased() {}
+
+ @Override
+ public String apply(UncertainDouble t) {
+ return t.toString(false, RoundingMode.HALF_EVEN);
+ }
+
+ @Override
+ public String toString() {
+ return "Uncertainty-Based Rounding";
+ }
+ }
+
+ /**
+ * For now, I want this to be a singleton. I might want to add a parameter
+ * later, so I won't make it an enum.
+ */
+ private static final UncertaintyBased UNCERTAINTY_BASED_ROUNDING_RULE = new UncertaintyBased();
+
+ /**
+ * @param decimalPlaces decimal places to round to
+ * @return a rounding rule that rounds to fixed number of decimal places
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ public static final FixedDecimals fixedDecimals(int decimalPlaces) {
+ return new FixedDecimals(decimalPlaces);
+ }
+
+ /**
+ * @param significantFigures significant figures to round to
+ * @return a rounding rule that rounds to a fixed number of significant
+ * figures
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ public static final FixedPrecision fixedPrecision(int significantFigures) {
+ return new FixedPrecision(significantFigures);
+ }
+
+ /**
+ * Gets one of the standard rules from its string representation.
+ *
+ * @param ruleToString string representation of the display rule
+ * @return display rule
+ * @throws IllegalArgumentException if the provided string is not that of a
+ * standard rule.
+ * @since v0.4.0
+ * @since 2021-12-24
+ */
+ public static final Function<UncertainDouble, String> getStandardRule(
+ String ruleToString) {
+ if (UNCERTAINTY_BASED_ROUNDING_RULE.toString().equals(ruleToString))
+ return UNCERTAINTY_BASED_ROUNDING_RULE;
+
+ // test if it is a fixed-places rule
+ final var placesMatch = FixedDecimals.TO_STRING_PATTERN
+ .matcher(ruleToString);
+ if (placesMatch.matches())
+ return new FixedDecimals(Integer.valueOf(placesMatch.group(1)));
+
+ // test if it is a fixed-sig-fig rule
+ final var sigFigMatch = FixedPrecision.TO_STRING_PATTERN
+ .matcher(ruleToString);
+ if (sigFigMatch.matches())
+ return new FixedPrecision(Integer.valueOf(sigFigMatch.group(1)));
+
+ throw new IllegalArgumentException(
+ "Provided string does not match any given rules.");
+ }
+
+ /**
+ * @return an UncertainDouble-based rounding rule
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ public static final UncertaintyBased uncertaintyBased() {
+ return UNCERTAINTY_BASED_ROUNDING_RULE;
+ }
+
+ private StandardDisplayRules() {}
+}
diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java
new file mode 100644
index 0000000..6181eae
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/TabbedView.java
@@ -0,0 +1,831 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.awt.BorderLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.GridLayout;
+import java.awt.event.ItemEvent;
+import java.awt.event.KeyEvent;
+import java.util.AbstractSet;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+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.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
+import javax.swing.WindowConstants;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+
+import sevenUnits.ProgramInfo;
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * A View that separates its functions into multiple tabs
+ *
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+final class TabbedView implements ExpressionConversionView, UnitConversionView {
+ /**
+ * A Set-like view of a JComboBox's items
+ *
+ * @param <E> type of item in list
+ *
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ private static final class JComboBoxItemSet<E> extends AbstractSet<E> {
+ private final JComboBox<E> comboBox;
+
+ /**
+ * @param comboBox combo box to get items from
+ * @since 2022-02-19
+ */
+ public JComboBoxItemSet(JComboBox<E> comboBox) {
+ this.comboBox = comboBox;
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return new Iterator<>() {
+ private int index = 0;
+
+ @Override
+ public boolean hasNext() {
+ return this.index < JComboBoxItemSet.this.size();
+ }
+
+ @Override
+ public E next() {
+ if (this.hasNext())
+ return JComboBoxItemSet.this.comboBox.getItemAt(this.index++);
+ else
+ throw new NoSuchElementException(
+ "Iterator has finished iteration");
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return this.comboBox.getItemCount();
+ }
+
+ }
+
+ /**
+ * The standard types of rounding, corresponding to the options on the
+ * TabbedView's settings panel.
+ *
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ private static enum StandardRoundingType {
+ /**
+ * Rounds to a fixed number of significant digits. Precision is used,
+ * representing the number of significant digits to round to.
+ */
+ SIGNIFICANT_DIGITS,
+ /**
+ * Rounds to a fixed number of decimal places. Precision is used,
+ * representing the number of decimal places to round to.
+ */
+ DECIMAL_PLACES,
+ /**
+ * Rounds according to UncertainDouble's toString method. The specified
+ * precision is ignored.
+ */
+ UNCERTAINTY;
+ }
+
+ /**
+ * Creates a TabbedView.
+ *
+ * @param args command line arguments
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public static void main(String[] args) {
+ // This view doesn't need to do anything, the side effects of creating it
+ // are enough to start the program
+ @SuppressWarnings("unused")
+ final View view = new TabbedView();
+ }
+
+ /** The Presenter that handles this View */
+ final Presenter presenter;
+ /** The frame that this view lives on */
+ final JFrame frame;
+ /** The tabbed pane that contains all of the components */
+ final JTabbedPane masterPane;
+
+ // DIMENSION-BASED CONVERTER
+ /** The combo box that selects dimensions */
+ final JComboBox<String> dimensionSelector;
+ /** The panel for inputting values in the dimension-based converter */
+ final JTextField valueInput;
+ /** The panel for "From" in the dimension-based converter */
+ final SearchBoxList<String> fromSearch;
+ /** The panel for "To" in the dimension-based converter */
+ final SearchBoxList<String> toSearch;
+ /** The button used for conversion */
+ final JButton convertUnitButton;
+ /** The output area in the dimension-based converter */
+ final JTextArea unitOutput;
+
+ // EXPRESSION-BASED CONVERTER
+ /** The "From" entry in the conversion panel */
+ final JTextField fromEntry;
+ /** The "To" entry in the conversion panel */
+ final JTextField toEntry;
+ /** The button used for conversion */
+ final JButton convertExpressionButton;
+ /** The output area in the conversion panel */
+ final JTextArea expressionOutput;
+
+ // UNIT AND PREFIX VIEWERS
+ /** The searchable list of unit names in the unit viewer */
+ private final SearchBoxList<String> unitNameList;
+ /** The searchable list of prefix names in the prefix viewer */
+ private final SearchBoxList<String> prefixNameList;
+ /** The text box for unit data in the unit viewer */
+ private final JTextArea unitTextBox;
+ /** The text box for prefix data in the prefix viewer */
+ private final JTextArea prefixTextBox;
+
+ // SETTINGS STUFF
+ private StandardRoundingType roundingType;
+ private int precision;
+
+ /**
+ * Creates the view and makes it visible to the user
+ *
+ * @since v0.4.0
+ * @since 2022-02-19
+ */
+ public TabbedView() {
+ // 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();
+ }
+
+ // initialize important components
+ this.presenter = new Presenter(this);
+ this.frame = new JFrame("7Units " + ProgramInfo.VERSION);
+ this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+ // master components (those that contain everything else within them)
+ this.masterPane = new JTabbedPane();
+ this.frame.add(this.masterPane);
+
+ // ============ UNIT CONVERSION TAB ============
+ 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));
+ inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6));
+
+ this.fromSearch = new SearchBoxList<>();
+ inputPanel.add(this.fromSearch);
+
+ final JPanel inBetweenPanel = new JPanel();
+ inputPanel.add(inBetweenPanel);
+ inBetweenPanel.setLayout(new BorderLayout());
+
+ this.dimensionSelector = new JComboBox<>();
+ inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START);
+ this.dimensionSelector
+ .addItemListener(e -> this.presenter.updateView());
+
+ final JLabel arrowLabel = new JLabel("-->");
+ inBetweenPanel.add(arrowLabel, BorderLayout.CENTER);
+ arrowLabel.setHorizontalAlignment(SwingConstants.CENTER);
+
+ this.toSearch = new SearchBoxList<>();
+ inputPanel.add(this.toSearch);
+ }
+
+ { // panel for submit and output, and also value entry
+ final JPanel outputPanel = new JPanel();
+ convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END);
+ outputPanel.setLayout(new BorderLayout());
+ outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6));
+
+ final JLabel valuePrompt = new JLabel("Value to convert: ");
+ outputPanel.add(valuePrompt, BorderLayout.LINE_START);
+
+ this.valueInput = new JTextField();
+ outputPanel.add(this.valueInput, BorderLayout.CENTER);
+
+ // conversion button
+ this.convertUnitButton = new JButton("Convert");
+ outputPanel.add(this.convertUnitButton, BorderLayout.LINE_END);
+ this.convertUnitButton
+ .addActionListener(e -> this.presenter.convertUnits());
+ this.convertUnitButton.setMnemonic(KeyEvent.VK_ENTER);
+
+ // conversion output
+ this.unitOutput = new JTextArea(2, 32);
+ outputPanel.add(this.unitOutput, BorderLayout.PAGE_END);
+ this.unitOutput.setEditable(false);
+ }
+
+ // ============ EXPRESSION CONVERSION TAB ============
+ final JPanel convertExpressionPanel = new JPanel();
+ this.masterPane.addTab("Convert Unit Expressions",
+ convertExpressionPanel);
+ this.masterPane.setMnemonicAt(1, KeyEvent.VK_E);
+ convertExpressionPanel.setLayout(new GridLayout(4, 1));
+
+ // from and to expressions
+ this.fromEntry = new JTextField();
+ convertExpressionPanel.add(this.fromEntry);
+ this.fromEntry.setBorder(BorderFactory.createTitledBorder("From"));
+
+ this.toEntry = new JTextField();
+ convertExpressionPanel.add(this.toEntry);
+ this.toEntry.setBorder(BorderFactory.createTitledBorder("To"));
+
+ // button to convert
+ this.convertExpressionButton = new JButton("Convert");
+ convertExpressionPanel.add(this.convertExpressionButton);
+
+ this.convertExpressionButton
+ .addActionListener(e -> this.presenter.convertExpressions());
+ this.convertExpressionButton.setMnemonic(KeyEvent.VK_ENTER);
+
+ // output of conversion
+ this.expressionOutput = new JTextArea(2, 32);
+ convertExpressionPanel.add(this.expressionOutput);
+ this.expressionOutput
+ .setBorder(BorderFactory.createTitledBorder("Output"));
+ this.expressionOutput.setEditable(false);
+
+ // =========== UNIT VIEWER ===========
+ final JPanel unitLookupPanel = new JPanel();
+ this.masterPane.addTab("Unit Viewer", unitLookupPanel);
+ this.masterPane.setMnemonicAt(2, KeyEvent.VK_V);
+ unitLookupPanel.setLayout(new GridLayout());
+
+ this.unitNameList = new SearchBoxList<>();
+ unitLookupPanel.add(this.unitNameList);
+ this.unitNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.unitNameSelected());
+
+ // the text box for unit's toString
+ this.unitTextBox = new JTextArea();
+ unitLookupPanel.add(this.unitTextBox);
+ this.unitTextBox.setEditable(false);
+ this.unitTextBox.setLineWrap(true);
+
+ // ============ PREFIX VIEWER =============
+ final JPanel prefixLookupPanel = new JPanel();
+ this.masterPane.addTab("Prefix Viewer", prefixLookupPanel);
+ this.masterPane.setMnemonicAt(3, KeyEvent.VK_P);
+ prefixLookupPanel.setLayout(new GridLayout(1, 2));
+
+ this.prefixNameList = new SearchBoxList<>();
+ prefixLookupPanel.add(this.prefixNameList);
+ this.prefixNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.prefixSelected());
+
+ // the text box for prefix's toString
+ this.prefixTextBox = new JTextArea();
+ prefixLookupPanel.add(this.prefixTextBox);
+ this.prefixTextBox.setEditable(false);
+ this.prefixTextBox.setLineWrap(true);
+
+ // ============ 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);
+ infoTextArea.setText(Presenter.getAboutText());
+
+ // ============ SETTINGS PANEL ============
+ this.masterPane.addTab("\u2699",
+ new JScrollPane(this.createSettingsPanel()));
+ this.masterPane.setMnemonicAt(5, KeyEvent.VK_S);
+
+ // ============ FINALIZE CREATION OF VIEW ============
+ this.presenter.postViewInitialize();
+ this.frame.pack();
+ this.frame.setVisible(true);
+ }
+
+ /**
+ * Creates and returns the settings panel (in its own function to make this
+ * code more organized, as this function is massive!)
+ *
+ * @since 2022-02-19
+ */
+ private JPanel createSettingsPanel() {
+ final JPanel settingsPanel = new JPanel();
+
+ 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();
+ this.roundingType = this.getPresenterRoundingType()
+ .orElseThrow(() -> new AssertionError(
+ "Presenter loaded non-standard rounding rule"));
+ this.precision = this.getPresenterPrecision().orElse(6);
+
+ final JLabel roundingRuleLabel = new JLabel("Rounding Rule:");
+ roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // sigDigSlider needs to be first so that the rounding-type buttons can
+ // show and hide it
+ final JLabel sliderLabel = new JLabel("Precision:");
+ sliderLabel.setVisible(
+ this.roundingType != StandardRoundingType.UNCERTAINTY);
+ roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JSlider sigDigSlider = new JSlider(0, 12);
+ roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ sigDigSlider.setMajorTickSpacing(4);
+ sigDigSlider.setMinorTickSpacing(1);
+ sigDigSlider.setSnapToTicks(true);
+ sigDigSlider.setPaintTicks(true);
+ sigDigSlider.setPaintLabels(true);
+
+ sigDigSlider.setVisible(
+ this.roundingType != StandardRoundingType.UNCERTAINTY);
+ sigDigSlider.setValue(this.precision);
+
+ sigDigSlider.addChangeListener(e -> {
+ this.precision = sigDigSlider.getValue();
+ this.updatePresenterRoundingRule();
+ });
+
+ // significant digit rounding
+ final JRadioButton fixedPrecision = new JRadioButton(
+ "Fixed Precision");
+ if (this.roundingType == StandardRoundingType.SIGNIFICANT_DIGITS) {
+ fixedPrecision.setSelected(true);
+ }
+ fixedPrecision.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.SIGNIFICANT_DIGITS;
+ sliderLabel.setVisible(true);
+ sigDigSlider.setVisible(true);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(fixedPrecision);
+ roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // decimal place rounding
+ final JRadioButton fixedDecimals = new JRadioButton(
+ "Fixed Decimal Places");
+ if (this.roundingType == StandardRoundingType.DECIMAL_PLACES) {
+ fixedDecimals.setSelected(true);
+ }
+ fixedDecimals.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.DECIMAL_PLACES;
+ sliderLabel.setVisible(true);
+ sigDigSlider.setVisible(true);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(fixedDecimals);
+ roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // scientific rounding
+ final JRadioButton relativePrecision = new JRadioButton(
+ "Uncertainty-Based Rounding");
+ if (this.roundingType == StandardRoundingType.UNCERTAINTY) {
+ relativePrecision.setSelected(true);
+ }
+ relativePrecision.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.UNCERTAINTY;
+ sliderLabel.setVisible(false);
+ sigDigSlider.setVisible(false);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(relativePrecision);
+ roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+ }
+
+ // ============ PREFIX REPETITION SETTINGS ============
+ {
+ final JPanel prefixRepetitionPanel = new JPanel();
+ settingsPanel.add(prefixRepetitionPanel);
+ prefixRepetitionPanel
+ .setBorder(new TitledBorder("Prefix Repetition Settings"));
+ prefixRepetitionPanel.setLayout(new GridBagLayout());
+
+ final var prefixRule = this.getPresenterPrefixRule()
+ .orElseThrow(() -> new AssertionError(
+ "Presenter loaded non-standard prefix rule"));
+
+ // prefix rules
+ final ButtonGroup prefixRuleButtons = new ButtonGroup();
+
+ final JRadioButton noRepetition = new JRadioButton("No Repetition");
+ if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
+ noRepetition.setSelected(true);
+ }
+ noRepetition.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_REPETITION);
+ this.presenter.saveSettings();
+ });
+ prefixRuleButtons.add(noRepetition);
+ prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton noRestriction = new JRadioButton("No Restriction");
+ if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
+ noRestriction.setSelected(true);
+ }
+ noRestriction.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_RESTRICTION);
+ this.presenter.saveSettings();
+ });
+ prefixRuleButtons.add(noRestriction);
+ prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton customRepetition = new JRadioButton(
+ "Complex Repetition");
+ if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
+ customRepetition.setSelected(true);
+ }
+ customRepetition.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.COMPLEX_REPETITION);
+ this.presenter.saveSettings();
+ });
+ prefixRuleButtons.add(customRepetition);
+ prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+ }
+
+ // ============ 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 var searchRule = this.presenter.getSearchRule();
+
+ final JRadioButton noPrefixes = new JRadioButton(
+ "Never Include Prefixed Units");
+ noPrefixes.addActionListener(e -> {
+ this.presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES);
+ this.presenter.updateView();
+ this.presenter.saveSettings();
+ });
+ searchRuleButtons.add(noPrefixes);
+ searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton commonPrefixes = new JRadioButton(
+ "Include Common Prefixes");
+ commonPrefixes.addActionListener(e -> {
+ this.presenter.setSearchRule(PrefixSearchRule.COMMON_PREFIXES);
+ this.presenter.updateView();
+ this.presenter.saveSettings();
+ });
+ searchRuleButtons.add(commonPrefixes);
+ searchingPanel.add(commonPrefixes, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton alwaysInclude = new JRadioButton(
+ "Include All Single Prefixes");
+ alwaysInclude.addActionListener(e -> {
+ this.presenter
+ .setSearchRule(this.presenter.getUniversalSearchRule());
+ this.presenter.updateView();
+ this.presenter.saveSettings();
+ });
+ searchRuleButtons.add(alwaysInclude);
+ searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) {
+ noPrefixes.setSelected(true);
+ } else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) {
+ commonPrefixes.setSelected(true);
+ } else {
+ alwaysInclude.setSelected(true);
+ this.presenter
+ .setSearchRule(this.presenter.getUniversalSearchRule());
+ this.presenter.saveSettings();
+ }
+ }
+
+ // ============ OTHER SETTINGS ============
+ {
+ final JPanel miscPanel = new JPanel();
+ settingsPanel.add(miscPanel);
+ miscPanel.setLayout(new GridBagLayout());
+
+ final JCheckBox oneWay = new JCheckBox("Convert One Way Only");
+ oneWay.setSelected(this.presenter.oneWayConversionEnabled());
+ oneWay.addItemListener(e -> {
+ this.presenter.setOneWayConversionEnabled(
+ e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ miscPanel.add(oneWay, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JCheckBox showAllVariations = new JCheckBox(
+ "Show Duplicate Units & Prefixes");
+ showAllVariations.setSelected(this.presenter.duplicatesShown());
+ showAllVariations.addItemListener(e -> {
+ this.presenter
+ .setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ 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, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+ }
+
+ return settingsPanel;
+ }
+
+ @Override
+ public Set<String> getDimensionNames() {
+ return Collections
+ .unmodifiableSet(new JComboBoxItemSet<>(this.dimensionSelector));
+ }
+
+ @Override
+ public String getFromExpression() {
+ return this.fromEntry.getText();
+ }
+
+ @Override
+ public Optional<String> getFromSelection() {
+ return this.fromSearch.getSelectedValue();
+ }
+
+ @Override
+ public Set<String> getFromUnitNames() {
+ // this should work because the only way I can mutate the item list is
+ // with setFromUnits which only accepts a Set
+ return new HashSet<>(this.fromSearch.getItems());
+ }
+
+ @Override
+ public String getInputValue() {
+ return this.valueInput.getText();
+ }
+
+ @Override
+ public Presenter getPresenter() {
+ return this.presenter;
+ }
+
+ /**
+ * @return the precision of the presenter's rounding rule, if that is
+ * meaningful
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ private OptionalInt getPresenterPrecision() {
+ final var presenterRule = this.presenter.getNumberDisplayRule();
+ if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
+ return OptionalInt
+ .of(((StandardDisplayRules.FixedDecimals) presenterRule)
+ .decimalPlaces());
+ else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ return OptionalInt
+ .of(((StandardDisplayRules.FixedPrecision) presenterRule)
+ .significantFigures());
+ else
+ return OptionalInt.empty();
+ }
+
+ /**
+ * @return presenter's prefix repetition rule
+ * @since v0.4.0
+ * @since 2022-04-19
+ */
+ private Optional<DefaultPrefixRepetitionRule> getPresenterPrefixRule() {
+ final var prefixRule = this.presenter.getPrefixRepetitionRule();
+ return prefixRule instanceof DefaultPrefixRepetitionRule
+ ? Optional.of((DefaultPrefixRepetitionRule) prefixRule)
+ : Optional.empty();
+ }
+
+ /**
+ * Determines which rounding type the presenter is currently using, if any.
+ *
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ private Optional<StandardRoundingType> getPresenterRoundingType() {
+ final var presenterRule = this.presenter.getNumberDisplayRule();
+ if (Objects.equals(presenterRule,
+ StandardDisplayRules.uncertaintyBased()))
+ return Optional.of(StandardRoundingType.UNCERTAINTY);
+ else if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
+ return Optional.of(StandardRoundingType.DECIMAL_PLACES);
+ else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ return Optional.of(StandardRoundingType.SIGNIFICANT_DIGITS);
+ else
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<String> getSelectedDimensionName() {
+ final String selectedItem = (String) this.dimensionSelector
+ .getSelectedItem();
+ return Optional.ofNullable(selectedItem);
+ }
+
+ @Override
+ public String getToExpression() {
+ return this.toEntry.getText();
+ }
+
+ @Override
+ public Optional<String> getToSelection() {
+ return this.toSearch.getSelectedValue();
+ }
+
+ @Override
+ public Set<String> getToUnitNames() {
+ // this should work because the only way I can mutate the item list is
+ // with setToUnits which only accepts a Set
+ return new HashSet<>(this.toSearch.getItems());
+ }
+
+ @Override
+ public Optional<String> getViewedPrefixName() {
+ return this.prefixNameList.getSelectedValue();
+ }
+
+ @Override
+ public Optional<String> getViewedUnitName() {
+ return this.unitNameList.getSelectedValue();
+ }
+
+ @Override
+ public void setDimensionNames(Set<String> dimensionNames) {
+ this.dimensionSelector.removeAllItems();
+ for (final String d : dimensionNames) {
+ this.dimensionSelector.addItem(d);
+ }
+ }
+
+ @Override
+ public void setFromUnitNames(Set<String> units) {
+ this.fromSearch.setItems(units);
+ }
+
+ @Override
+ public void setToUnitNames(Set<String> units) {
+ this.toSearch.setItems(units);
+ }
+
+ @Override
+ public void setViewablePrefixNames(Set<String> prefixNames) {
+ this.prefixNameList.setItems(prefixNames);
+ }
+
+ @Override
+ public void setViewableUnitNames(Set<String> unitNames) {
+ this.unitNameList.setItems(unitNames);
+ }
+
+ @Override
+ public void showErrorMessage(String title, String message) {
+ JOptionPane.showMessageDialog(this.frame, message, title,
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ @Override
+ public void showExpressionConversionOutput(UnitConversionRecord uc) {
+ this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(),
+ uc.outputValueString(), uc.toName()));
+ }
+
+ @Override
+ public void showPrefix(NameSymbol name, String multiplierString) {
+ this.prefixTextBox.setText(
+ String.format("%s%nMultiplier: %s", name, multiplierString));
+ }
+
+ @Override
+ public void showUnit(NameSymbol name, String definition,
+ String dimensionName, UnitType type) {
+ this.unitTextBox.setText(
+ String.format("%s%nDefinition: %s%nDimension: %s%nType: %s", name,
+ definition, dimensionName, type));
+ }
+
+ @Override
+ public void showUnitConversionOutput(UnitConversionRecord uc) {
+ this.unitOutput.setText(uc.toString());
+ }
+
+ /**
+ * Sets the presenter's rounding rule to the one specified by the current
+ * settings
+ *
+ * @since v0.4.0
+ * @since 2022-04-18
+ */
+ private void updatePresenterRoundingRule() {
+ final Function<UncertainDouble, String> roundingRule;
+ switch (this.roundingType) {
+ case DECIMAL_PLACES:
+ roundingRule = StandardDisplayRules.fixedDecimals(this.precision);
+ break;
+ case SIGNIFICANT_DIGITS:
+ roundingRule = StandardDisplayRules.fixedPrecision(this.precision);
+ break;
+ case UNCERTAINTY:
+ roundingRule = StandardDisplayRules.uncertaintyBased();
+ break;
+ default:
+ throw new AssertionError();
+ }
+ this.presenter.setNumberDisplayRule(roundingRule);
+ this.presenter.saveSettings();
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
new file mode 100644
index 0000000..fa64ee9
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.math.RoundingMode;
+
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.UnitValue;
+
+/**
+ * A record of a conversion between units or expressions
+ *
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+public final class UnitConversionRecord {
+ /**
+ * Gets a {@code UnitConversionRecord} from two linear unit values
+ *
+ * @param input input unit & value
+ * @param output output unit & value
+ * @return unit conversion record
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord fromLinearValues(LinearUnitValue input,
+ LinearUnitValue output) {
+ return UnitConversionRecord.valueOf(input.getUnit().getName(),
+ output.getUnit().getName(),
+ input.getValue().toString(false, RoundingMode.HALF_EVEN),
+ output.getValue().toString(false, RoundingMode.HALF_EVEN));
+ }
+
+ /**
+ * Gets a {@code UnitConversionRecord} from two unit values
+ *
+ * @param input input unit & value
+ * @param output output unit & value
+ * @return unit conversion record
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord fromValues(UnitValue input,
+ UnitValue output) {
+ return UnitConversionRecord.valueOf(input.getUnit().getName(),
+ output.getUnit().getName(), String.valueOf(input.getValue()),
+ String.valueOf(output.getValue()));
+ }
+
+ /**
+ * Gets a {@code UnitConversionRecord}
+ *
+ * @param fromName name of unit or expression that was converted
+ * from
+ * @param toName name of unit or expression that was converted to
+ * @param inputValueString string representing input value
+ * @param outputValueString string representing output value
+ * @return unit conversion record
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord valueOf(String fromName, String toName,
+ String inputValueString, String outputValueString) {
+ return new UnitConversionRecord(fromName, toName, inputValueString,
+ outputValueString);
+ }
+
+ /**
+ * The name of the unit or expression that was converted from
+ */
+ private final String fromName;
+ /**
+ * The name of the unit or expression that was converted to
+ */
+ private final String toName;
+
+ /**
+ * A string representing the input value. It doesn't need to be the same as
+ * the input value's string representation; it could be rounded, for example.
+ */
+ private final String inputValueString;
+ /**
+ * A string representing the input value. It doesn't need to be the same as
+ * the input value's string representation; it could be rounded, for example.
+ */
+ private final String outputValueString;
+
+ /**
+ * @param fromName name of unit or expression that was converted
+ * from
+ * @param toName name of unit or expression that was converted to
+ * @param inputValueString string representing input value
+ * @param outputValueString string representing output value
+ * @since 2022-04-09
+ */
+ private UnitConversionRecord(String fromName, String toName,
+ String inputValueString, String outputValueString) {
+ this.fromName = fromName;
+ this.toName = toName;
+ this.inputValueString = inputValueString;
+ this.outputValueString = outputValueString;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UnitConversionRecord))
+ return false;
+ final UnitConversionRecord other = (UnitConversionRecord) obj;
+ if (this.fromName == null) {
+ if (other.fromName != null)
+ return false;
+ } else if (!this.fromName.equals(other.fromName))
+ return false;
+ if (this.inputValueString == null) {
+ if (other.inputValueString != null)
+ return false;
+ } else if (!this.inputValueString.equals(other.inputValueString))
+ return false;
+ if (this.outputValueString == null) {
+ if (other.outputValueString != null)
+ return false;
+ } else if (!this.outputValueString.equals(other.outputValueString))
+ return false;
+ if (this.toName == null) {
+ if (other.toName != null)
+ return false;
+ } else if (!this.toName.equals(other.toName))
+ return false;
+ return true;
+ }
+
+ /**
+ * @return name of unit or expression that was converted from
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+ public String fromName() {
+ return this.fromName;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.fromName == null ? 0 : this.fromName.hashCode());
+ result = prime * result + (this.inputValueString == null ? 0
+ : this.inputValueString.hashCode());
+ result = prime * result + (this.outputValueString == null ? 0
+ : this.outputValueString.hashCode());
+ result = prime * result
+ + (this.toName == null ? 0 : this.toName.hashCode());
+ return result;
+ }
+
+ /**
+ * @return string representing input value
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+ public String inputValueString() {
+ return this.inputValueString;
+ }
+
+ /**
+ * @return string representing output value
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+ public String outputValueString() {
+ return this.outputValueString;
+ }
+
+ /**
+ * @return name of unit or expression that was converted to
+ * @since v0.4.0
+ * @since 2022-04-09
+ */
+ public String toName() {
+ return this.toName;
+ }
+
+ @Override
+ public String toString() {
+ final String inputString = this.inputValueString.isBlank() ? this.fromName
+ : this.inputValueString + " " + this.fromName;
+ final String outputString = this.outputValueString.isBlank() ? this.toName
+ : this.outputValueString + " " + this.toName;
+ return inputString + " = " + outputString;
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java
new file mode 100644
index 0000000..0d07823
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java
@@ -0,0 +1,120 @@
+/**
+ * Copyright (C) 2021-2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * A View that supports single unit-based conversion
+ *
+ * @author Adrien Hopkins
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+public interface UnitConversionView extends View {
+ /**
+ * @return dimensions available for filtering
+ * @since v0.4.0
+ * @since 2022-01-29
+ */
+ Set<String> getDimensionNames();
+
+ /**
+ * @return name of unit to convert <em>from</em>
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ Optional<String> getFromSelection();
+
+ /**
+ * @return list of names of units available to convert from
+ * @since v0.4.0
+ * @since 2022-03-30
+ */
+ Set<String> getFromUnitNames();
+
+ /**
+ * @return value to convert between the units (specifically, the numeric
+ * string provided by the user)
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ String getInputValue();
+
+ /**
+ * @return selected dimension
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ Optional<String> getSelectedDimensionName();
+
+ /**
+ * @return name of unit to convert <em>to</em>
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ Optional<String> getToSelection();
+
+ /**
+ * @return list of names of units available to convert to
+ * @since v0.4.0
+ * @since 2022-03-30
+ */
+ Set<String> getToUnitNames();
+
+ /**
+ * Sets the available dimensions for filtering.
+ *
+ * @param dimensionNames names of dimensions to use
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ void setDimensionNames(Set<String> dimensionNames);
+
+ /**
+ * Sets the available units to convert from. {@link #getFromSelection} is not
+ * required to use one of these units; this method is to be used for views
+ * that allow the user to select units from a list.
+ *
+ * @param unitNames names of units to convert from
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ void setFromUnitNames(Set<String> unitNames);
+
+ /**
+ * Sets the available units to convert to. {@link #getToSelection} is not
+ * required to use one of these units; this method is to be used for views
+ * that allow the user to select units from a list.
+ *
+ * @param unitNames names of units to convert to
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ void setToUnitNames(Set<String> unitNames);
+
+ /**
+ * Shows the output of a unit conversion.
+ *
+ * @param input input unit & value (obtained from this view)
+ * @param output output unit & value
+ * @since v0.4.0
+ * @since 2021-12-24
+ */
+ void showUnitConversionOutput(UnitConversionRecord uc);
+}
diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java
new file mode 100644
index 0000000..bb810ec
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/View.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (C) 2021-2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Optional;
+import java.util.Set;
+
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+
+/**
+ * An object that controls user interaction with 7Units
+ *
+ * @author Adrien Hopkins
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+public interface View {
+ /**
+ * @return a new tabbed view
+ * @since v0.4.0
+ * @since 2022-04-19
+ */
+ static View createTabbedView() {
+ return new TabbedView();
+ }
+
+ /**
+ * @return the presenter associated with this view
+ * @since v0.4.0
+ * @since 2022-04-19
+ */
+ Presenter getPresenter();
+
+ /**
+ * @return name of prefix currently being viewed
+ * @since v0.4.0
+ * @since 2022-04-10
+ */
+ Optional<String> getViewedPrefixName();
+
+ /**
+ * @return name of unit currently being viewed
+ * @since v0.4.0
+ * @since 2022-04-10
+ */
+ Optional<String> getViewedUnitName();
+
+ /**
+ * Sets the list of prefixes that are available to be viewed in a prefix
+ * viewer
+ *
+ * @param prefixNames prefix names to view
+ * @since v0.4.0
+ * @since 2022-04-10
+ */
+ void setViewablePrefixNames(Set<String> prefixNames);
+
+ /**
+ * Sets the list of units that are available to be viewed in a unit viewer
+ *
+ * @param unitNames unit names to view
+ * @since v0.4.0
+ * @since 2022-04-10
+ */
+ void setViewableUnitNames(Set<String> unitNames);
+
+ /**
+ * Shows an error message.
+ *
+ * @param title title of error message; on any view that uses an error
+ * dialog, this should be the title of the error dialog.
+ * @param message error message
+ * @since v0.4.0
+ * @since 2021-12-15
+ */
+ void showErrorMessage(String title, String message);
+
+ /**
+ * Shows information about a prefix to the user.
+ *
+ * @param name name(s) and symbol of prefix
+ * @param multiplierString string representation of prefix multiplier
+ * @since v0.4.0
+ * @since 2022-04-10
+ */
+ void showPrefix(NameSymbol name, String multiplierString);
+
+ /**
+ * Shows information about a unit to the user.
+ *
+ * @param name name(s) and symbol of unit
+ * @param definition unit's definition string
+ * @param dimensionName name of unit's dimension
+ * @param type type of unit (metric/semi-metric/non-metric)
+ * @since v0.4.0
+ * @since 2022-04-10
+ */
+ void showUnit(NameSymbol name, String definition, String dimensionName,
+ UnitType type);
+}
diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java
new file mode 100644
index 0000000..e7304c4
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/ViewBot.java
@@ -0,0 +1,508 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
+
+/**
+ * A class that simulates a View (supports both unit and expression conversion)
+ * for testing. Getters and setters work as expected.
+ *
+ * @author Adrien Hopkins
+ * @since v0.4.0
+ * @since 2022-01-29
+ */
+public final class ViewBot
+ implements UnitConversionView, ExpressionConversionView {
+ /**
+ * A record of the parameters given to
+ * {@link View#showPrefix(NameSymbol, String)}, for testing.
+ *
+ * @since 2022-04-16
+ */
+ public static final class PrefixViewingRecord implements Nameable {
+ private final NameSymbol nameSymbol;
+ private final String multiplierString;
+
+ /**
+ * @param nameSymbol
+ * @param multiplierString
+ * @since 2022-04-16
+ */
+ public PrefixViewingRecord(NameSymbol nameSymbol,
+ String multiplierString) {
+ this.nameSymbol = nameSymbol;
+ this.multiplierString = multiplierString;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof PrefixViewingRecord))
+ return false;
+ final PrefixViewingRecord other = (PrefixViewingRecord) obj;
+ return Objects.equals(this.multiplierString, other.multiplierString)
+ && Objects.equals(this.nameSymbol, other.nameSymbol);
+ }
+
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.multiplierString, this.nameSymbol);
+ }
+
+ public String multiplierString() {
+ return this.multiplierString;
+ }
+
+ public NameSymbol nameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("PrefixViewingRecord [nameSymbol=");
+ builder.append(this.nameSymbol);
+ builder.append(", multiplierString=");
+ builder.append(this.multiplierString);
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ /**
+ * A record of the parameters given to
+ * {@link View#showUnit(NameSymbol, String, String, UnitType)}, for testing.
+ *
+ * @since 2022-04-16
+ */
+ public static final class UnitViewingRecord implements Nameable {
+ private final NameSymbol nameSymbol;
+ private final String definition;
+ private final String dimensionName;
+ private final UnitType unitType;
+
+ /**
+ * @since 2022-04-16
+ */
+ public UnitViewingRecord(NameSymbol nameSymbol, String definition,
+ String dimensionName, UnitType unitType) {
+ this.nameSymbol = nameSymbol;
+ this.definition = definition;
+ this.dimensionName = dimensionName;
+ this.unitType = unitType;
+ }
+
+ /**
+ * @return the definition
+ * @since 2022-04-16
+ */
+ public String definition() {
+ return this.definition;
+ }
+
+ /**
+ * @return the dimensionName
+ * @since 2022-04-16
+ */
+ public String dimensionName() {
+ return this.dimensionName;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UnitViewingRecord))
+ return false;
+ final UnitViewingRecord other = (UnitViewingRecord) obj;
+ return Objects.equals(this.definition, other.definition)
+ && Objects.equals(this.dimensionName, other.dimensionName)
+ && Objects.equals(this.nameSymbol, other.nameSymbol)
+ && this.unitType == other.unitType;
+ }
+
+ /**
+ * @return the nameSymbol
+ * @since 2022-04-16
+ */
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.definition, this.dimensionName,
+ this.nameSymbol, this.unitType);
+ }
+
+ public NameSymbol nameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("UnitViewingRecord [nameSymbol=");
+ builder.append(this.nameSymbol);
+ builder.append(", definition=");
+ builder.append(this.definition);
+ builder.append(", dimensionName=");
+ builder.append(this.dimensionName);
+ builder.append(", unitType=");
+ builder.append(this.unitType);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ /**
+ * @return the unitType
+ * @since 2022-04-16
+ */
+ public UnitType unitType() {
+ return this.unitType;
+ }
+ }
+
+ /** The presenter that works with this ViewBot */
+ private final Presenter presenter;
+
+ /** The dimensions available to select from */
+ private Set<String> dimensionNames = Set.of();
+ /** The expression in the From field */
+ private String fromExpression = "";
+ /** The expression in the To field */
+ private String toExpression = "";
+ /**
+ * The user-provided string representing the value in {@code fromSelection}
+ */
+ private String inputValue = "";
+ /** The unit selected in the From selection */
+ private Optional<String> fromSelection = Optional.empty();
+ /** The unit selected in the To selection */
+ private Optional<String> toSelection = Optional.empty();
+ /** The currently selected dimension */
+ private Optional<String> selectedDimensionName = Optional.empty();
+ /** The units available in the From selection */
+ private Set<String> fromUnits = Set.of();
+ /** The units available in the To selection */
+ private Set<String> toUnits = Set.of();
+
+ /** The selected unit in the unit viewer */
+ private Optional<String> unitViewerSelection = Optional.empty();
+ /** The selected unit in the prefix viewer */
+ private Optional<String> prefixViewerSelection = Optional.empty();
+
+ /** Saved outputs of all unit conversions */
+ private final List<UnitConversionRecord> unitConversions;
+ /** Saved outputs of all unit expressions */
+ private final List<UnitConversionRecord> expressionConversions;
+ /** Saved outputs of all unit viewings */
+ private final List<UnitViewingRecord> unitViewingRecords;
+ /** Saved outputs of all prefix viewings */
+ private final List<PrefixViewingRecord> prefixViewingRecords;
+
+ /**
+ * Creates a new {@code ViewBot} with a new presenter.
+ *
+ * @since 2022-01-29
+ */
+ public ViewBot() {
+ this.presenter = new Presenter(this);
+
+ this.unitConversions = new ArrayList<>();
+ this.expressionConversions = new ArrayList<>();
+ this.unitViewingRecords = new ArrayList<>();
+ this.prefixViewingRecords = new ArrayList<>();
+ }
+
+ /**
+ * @return list of records of expression conversions done by this bot
+ * @since 2022-04-09
+ */
+ public List<UnitConversionRecord> expressionConversionList() {
+ return Collections.unmodifiableList(this.expressionConversions);
+ }
+
+ /**
+ * @return the available dimensions
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getDimensionNames() {
+ return this.dimensionNames;
+ }
+
+ @Override
+ public String getFromExpression() {
+ return this.fromExpression;
+ }
+
+ @Override
+ public Optional<String> getFromSelection() {
+ return this.fromSelection;
+ }
+
+ /**
+ * @return the units available for selection in From
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getFromUnitNames() {
+ return Collections.unmodifiableSet(this.fromUnits);
+ }
+
+ @Override
+ public String getInputValue() {
+ return this.inputValue;
+ }
+
+ /**
+ * @return the presenter associated with tihs view
+ * @since 2022-01-29
+ */
+ @Override
+ public Presenter getPresenter() {
+ return this.presenter;
+ }
+
+ @Override
+ public Optional<String> getSelectedDimensionName() {
+ return this.selectedDimensionName;
+ }
+
+ @Override
+ public String getToExpression() {
+ return this.toExpression;
+ }
+
+ @Override
+ public Optional<String> getToSelection() {
+ return this.toSelection;
+ }
+
+ /**
+ * @return the units available for selection in To
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getToUnitNames() {
+ return Collections.unmodifiableSet(this.toUnits);
+ }
+
+ @Override
+ public Optional<String> getViewedPrefixName() {
+ return this.prefixViewerSelection;
+ }
+
+ @Override
+ public Optional<String> getViewedUnitName() {
+ return this.unitViewerSelection;
+ }
+
+ /**
+ * @return list of records of this viewBot's prefix views
+ * @since 2022-04-16
+ */
+ public List<PrefixViewingRecord> prefixViewList() {
+ return Collections.unmodifiableList(this.prefixViewingRecords);
+ }
+
+ @Override
+ public void setDimensionNames(Set<String> dimensionNames) {
+ this.dimensionNames = Objects.requireNonNull(dimensionNames,
+ "dimensions may not be null");
+ }
+
+ /**
+ * Sets the From expression (as in {@link #getFromExpression}).
+ *
+ * @param fromExpression the expression to convert from
+ * @throws NullPointerException if {@code fromExpression} is null
+ * @since 2022-01-29
+ */
+ public void setFromExpression(String fromExpression) {
+ this.fromExpression = Objects.requireNonNull(fromExpression,
+ "fromExpression cannot be null.");
+ }
+
+ /**
+ * @param fromSelection the fromSelection to set
+ * @since 2022-01-29
+ */
+ public void setFromSelection(Optional<String> fromSelection) {
+ this.fromSelection = Objects.requireNonNull(fromSelection,
+ "fromSelection cannot be null");
+ }
+
+ /**
+ * @param fromSelection the fromSelection to set
+ * @since 2022-02-10
+ */
+ public void setFromSelection(String fromSelection) {
+ this.setFromSelection(Optional.of(fromSelection));
+ }
+
+ @Override
+ public void setFromUnitNames(Set<String> units) {
+ this.fromUnits = Objects.requireNonNull(units, "units may not be null");
+ }
+
+ /**
+ * @param inputValue the inputValue to set
+ * @since 2022-01-29
+ */
+ public void setInputValue(String inputValue) {
+ this.inputValue = inputValue;
+ }
+
+ /**
+ * @param selectedDimension the selectedDimension to set
+ * @since 2022-01-29
+ */
+ public void setSelectedDimensionName(
+ Optional<String> selectedDimensionName) {
+ this.selectedDimensionName = selectedDimensionName;
+ }
+
+ public void setSelectedDimensionName(String selectedDimensionName) {
+ this.setSelectedDimensionName(Optional.of(selectedDimensionName));
+ }
+
+ /**
+ * Sets the To expression (as in {@link #getToExpression}).
+ *
+ * @param toExpression the expression to convert to
+ * @throws NullPointerException if {@code toExpression} is null
+ * @since 2022-01-29
+ */
+ public void setToExpression(String toExpression) {
+ this.toExpression = Objects.requireNonNull(toExpression,
+ "toExpression cannot be null.");
+ }
+
+ /**
+ * @param toSelection the toSelection to set
+ * @since 2022-01-29
+ */
+ public void setToSelection(Optional<String> toSelection) {
+ this.toSelection = Objects.requireNonNull(toSelection,
+ "toSelection cannot be null.");
+ }
+
+ public void setToSelection(String toSelection) {
+ this.setToSelection(Optional.of(toSelection));
+ }
+
+ @Override
+ public void setToUnitNames(Set<String> units) {
+ this.toUnits = Objects.requireNonNull(units, "units may not be null");
+ }
+
+ @Override
+ public void setViewablePrefixNames(Set<String> prefixNames) {
+ // do nothing, ViewBot supports selecting any prefix
+ }
+
+ @Override
+ public void setViewableUnitNames(Set<String> unitNames) {
+ // do nothing, ViewBot supports selecting any unit
+ }
+
+ public void setViewedPrefixName(Optional<String> viewedPrefixName) {
+ this.prefixViewerSelection = viewedPrefixName;
+ }
+
+ public void setViewedPrefixName(String viewedPrefixName) {
+ this.setViewedPrefixName(Optional.of(viewedPrefixName));
+ }
+
+ public void setViewedUnitName(Optional<String> viewedUnitName) {
+ this.unitViewerSelection = viewedUnitName;
+ }
+
+ public void setViewedUnitName(String viewedUnitName) {
+ this.setViewedUnitName(Optional.of(viewedUnitName));
+ }
+
+ @Override
+ public void showErrorMessage(String title, String message) {
+ System.err.printf("%s: %s%n", title, message);
+ }
+
+ @Override
+ public void showExpressionConversionOutput(UnitConversionRecord uc) {
+ this.expressionConversions.add(uc);
+ System.out.println("Expression Conversion: " + uc);
+ }
+
+ @Override
+ public void showPrefix(NameSymbol name, String multiplierString) {
+ this.prefixViewingRecords
+ .add(new PrefixViewingRecord(name, multiplierString));
+ }
+
+ @Override
+ public void showUnit(NameSymbol name, String definition,
+ String dimensionName, UnitType type) {
+ this.unitViewingRecords
+ .add(new UnitViewingRecord(name, definition, dimensionName, type));
+ }
+
+ @Override
+ public void showUnitConversionOutput(UnitConversionRecord uc) {
+ this.unitConversions.add(uc);
+ System.out.println("Unit Conversion: " + uc);
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + String.format("[presenter=%s]", this.presenter);
+ }
+
+ /**
+ * @return list of records of every unit conversion made by this bot
+ * @since 2022-04-09
+ */
+ public List<UnitConversionRecord> unitConversionList() {
+ return Collections.unmodifiableList(this.unitConversions);
+ }
+
+ /**
+ * @return list of records of unit viewings made by this bot
+ * @since 2022-04-16
+ */
+ public List<UnitViewingRecord> unitViewList() {
+ return Collections.unmodifiableList(this.unitViewingRecords);
+ }
+}
diff --git a/src/main/java/sevenUnits/converterGUI/package-info.java b/src/main/java/sevenUnitsGUI/package-info.java
index 784664f..cff1ded 100644
--- a/src/main/java/sevenUnits/converterGUI/package-info.java
+++ b/src/main/java/sevenUnitsGUI/package-info.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2021 Adrien Hopkins
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -15,10 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
- * The GUI interface of the Unit Converter.
+ * The MVP GUI of SevenUnits
*
* @author Adrien Hopkins
- * @since 2019-01-25
- * @since v0.2.0
+ * @since 2021-12-15
*/
-package sevenUnits.converterGUI; \ No newline at end of file
+package sevenUnitsGUI; \ No newline at end of file
diff --git a/src/main/resources/about.txt b/src/main/resources/about.txt
index f175396..2fd1368 100644
--- a/src/main/resources/about.txt
+++ b/src/main/resources/about.txt
@@ -1,8 +1,8 @@
-About 7Units v[VERSION]
+About 7Units Version [VERSION]
Copyright Notice:
-Unit Converter Copyright (C) 2018-2021 Adrien Hopkins
+Unit Converter Copyright (C) 2018-2022 Adrien Hopkins
This program comes with ABSOLUTELY NO WARRANTY;
for details read the LICENSE file, section 15
diff --git a/src/main/resources/dimensionfile.txt b/src/main/resources/dimensionfile.txt
index 3485de5..d767e9f 100644
--- a/src/main/resources/dimensionfile.txt
+++ b/src/main/resources/dimensionfile.txt
@@ -6,13 +6,13 @@
# I have excluded electric current, quantity and luminous intensity since their units are exclusively SI.
-LENGTH !
-MASS !
-TIME !
-TEMPERATURE !
+Length !
+Mass !
+Time !
+Temperature !
# Derived Dimensions
-AREA LENGTH^2
-VOLUME LENGTH^3
-VELOCITY LENGTH / TIME
-ENERGY MASS * VELOCITY^2 \ No newline at end of file
+Area Length^2
+Volume Length^3
+Velocity Length / Time
+Energy Mass * Velocity^2 \ No newline at end of file
diff --git a/src/main/resources/unitsfile.txt b/src/main/resources/unitsfile.txt
index 340e8ea..543c821 100644
--- a/src/main/resources/unitsfile.txt
+++ b/src/main/resources/unitsfile.txt
@@ -253,22 +253,21 @@ Wh W h
# Extra units to show in the dimension-based converter
km km
-kilometre km
cm cm
centimetre cm
mm mm
-millimetre mm
mg mg
-milligram mg
mL mL
ml ml
-millilitre mL
-kJ kJ
-kilojoule kJ
MJ MJ
megajoule MJ
kWh kWh
m/s m / s
+metre/second m/s
km/h km / h
+kilometre/hour km/h
ft/s foot / s
-mi/h mile / hour \ No newline at end of file
+foot/second ft/s
+mi/h mile / hour
+mph mile / hour
+mile/hour mi/h \ No newline at end of file
diff --git a/src/test/java/sevenUnits/unit/MultiUnitTest.java b/src/test/java/sevenUnits/unit/MultiUnitTest.java
index 39ee21c..30f2941 100644
--- a/src/test/java/sevenUnits/unit/MultiUnitTest.java
+++ b/src/test/java/sevenUnits/unit/MultiUnitTest.java
@@ -32,30 +32,34 @@ import org.junit.jupiter.api.Test;
*/
class MultiUnitTest {
+ /**
+ * Ensures that the {@code MultiUnit} can convert properly.
+ */
@Test
final void testConvert() {
final Random rng = ThreadLocalRandom.current();
final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT,
BritishImperial.Length.INCH);
- assertEquals(1702.0, footInch.convertTo(Metric.METRE.withPrefix(Metric.MILLI),
- Arrays.asList(5.0, 7.0)), 1.0);
+ assertEquals(1702.0,
+ footInch.convertTo(Metric.METRE.withPrefix(Metric.MILLI),
+ Arrays.asList(5.0, 7.0)),
+ 1.0);
for (int i = 0; i < 1000; i++) {
final double feet = rng.nextInt(1000);
final double inches = rng.nextDouble() * 12;
final double millimetres = feet * 304.8 + inches * 25.4;
- final List<Double> feetAndInches = Metric.METRE.withPrefix(Metric.MILLI)
- .convertTo(footInch, millimetres);
+ final List<Double> feetAndInches = Metric.METRE
+ .withPrefix(Metric.MILLI).convertTo(footInch, millimetres);
assertEquals(feet, feetAndInches.get(0), 1e-10);
assertEquals(inches, feetAndInches.get(1), 1e-10);
}
}
/**
- * Test method for
- * {@link sevenUnits.unit.MultiUnit#convertFromBase(double)}.
+ * Test method for {@link sevenUnits.unit.MultiUnit#convertFromBase(double)}.
*/
@Test
final void testConvertFromBase() {
diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
index 2276d7c..4be33dd 100644
--- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
+++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
@@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.UncertainDouble;
/**
@@ -595,7 +596,7 @@ class UnitDatabaseTest {
database.addPrefix("C", C);
final int NUM_UNITS = database.unitMapPrefixless(true).size();
- final int NUM_PREFIXES = database.prefixMap().size();
+ final int NUM_PREFIXES = database.prefixMap(true).size();
final Iterator<String> nameIterator = database.unitMap().keySet()
.iterator();
diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java
index bb2e6a4..d3699ca 100644
--- a/src/test/java/sevenUnits/unit/UnitTest.java
+++ b/src/test/java/sevenUnits/unit/UnitTest.java
@@ -21,12 +21,14 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.math.RoundingMode;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import org.junit.jupiter.api.Test;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.UncertainDouble;
/**
@@ -163,8 +165,9 @@ class UnitTest {
UncertainDouble.of(10, 0.24));
assertEquals("(10.0 ± 0.2) m", value.toString());
- assertEquals("(10.0 ± 0.2) m", value.toString(true));
- assertEquals("10.0 m", value.toString(false));
+ assertEquals("(10.0 ± 0.2) m",
+ value.toString(true, RoundingMode.HALF_EVEN));
+ assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
}
/**
@@ -178,8 +181,9 @@ class UnitTest {
UncertainDouble.of(10, 0));
assertEquals("10.0 m", value.toString());
- assertEquals("(10.0 ± 0.0) m", value.toString(true));
- assertEquals("10.0 m", value.toString(false));
+ assertEquals("(10.0 ± 0.0) m",
+ value.toString(true, RoundingMode.HALF_EVEN));
+ assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
}
/**
@@ -193,7 +197,8 @@ class UnitTest {
Metric.METRE.withName(NameSymbol.EMPTY),
UncertainDouble.of(10, 0.24));
- assertEquals("10.0 unnamed unit (= 10.0 m)", value.toString(false));
+ assertEquals("10.0 unnamed unit (= 10.0 m)",
+ value.toString(false, RoundingMode.HALF_EVEN));
}
/**
diff --git a/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java b/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java
index d653848..6b5f9cf 100644
--- a/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java
+++ b/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java
@@ -34,13 +34,16 @@ import org.junit.jupiter.api.Test;
import sevenUnits.utils.ConditionalExistenceCollections.ConditionalExistenceIterator;
/**
- * Tests the {@link #ConditionalExistenceCollections}.
+ * Tests the {@link #ConditionalExistenceCollections}. Specifically, it runs
+ * normal operations on conditional existence collections and ensures that
+ * elements that do not pass the existence condition are not included in the
+ * results.
*
* @author Adrien Hopkins
* @since 2019-10-16
*/
class ConditionalExistenceCollectionsTest {
-
+
/**
* The returned iterator ignores elements that don't start with "a".
*
@@ -54,7 +57,7 @@ class ConditionalExistenceCollectionsTest {
.conditionalExistenceIterator(it, s -> s.startsWith("a"));
return cit;
}
-
+
/**
* The returned map ignores mappings where the value is zero.
*
@@ -67,13 +70,14 @@ class ConditionalExistenceCollectionsTest {
map.put("two", 2);
map.put("zero", 0);
map.put("ten", 10);
- final Map<String, Integer> conditionalMap = ConditionalExistenceCollections.conditionalExistenceMap(map,
- e -> !Integer.valueOf(0).equals(e.getValue()));
+ final Map<String, Integer> conditionalMap = ConditionalExistenceCollections
+ .conditionalExistenceMap(map,
+ e -> !Integer.valueOf(0).equals(e.getValue()));
return conditionalMap;
}
-
+
/**
- * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsKey(java.lang.Object)}.
+ * Test method for the ConditionalExistenceMap's containsKey method.
*/
@Test
void testContainsKeyObject() {
@@ -83,9 +87,9 @@ class ConditionalExistenceCollectionsTest {
assertFalse(map.containsKey("five"));
assertFalse(map.containsKey("zero"));
}
-
+
/**
- * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsValue(java.lang.Object)}.
+ * Test method for the ConditionalExistenceMap's containsValue method.
*/
@Test
void testContainsValueObject() {
@@ -95,9 +99,9 @@ class ConditionalExistenceCollectionsTest {
assertFalse(map.containsValue(5));
assertFalse(map.containsValue(0));
}
-
+
/**
- * Test method for {@link org.unitConverter.math.ZeroIsNullMap#entrySet()}.
+ * Test method for the ConditionalExistenceMap's entrySet method.
*/
@Test
void testEntrySet() {
@@ -106,9 +110,9 @@ class ConditionalExistenceCollectionsTest {
assertTrue(e.getValue() != 0);
}
}
-
+
/**
- * Test method for {@link org.unitConverter.math.ZeroIsNullMap#get(java.lang.Object)}.
+ * Test method for the ConditionalExistenceMap's get method.
*/
@Test
void testGetObject() {
@@ -118,43 +122,47 @@ class ConditionalExistenceCollectionsTest {
assertEquals(null, map.get("five"));
assertEquals(null, map.get("zero"));
}
-
+
+ /**
+ * Test method for the ConditionalExistenceCollection's iterator.
+ */
@Test
void testIterator() {
- final ConditionalExistenceIterator<String> testIterator = this.getTestIterator();
-
+ final ConditionalExistenceIterator<String> testIterator = this
+ .getTestIterator();
+
assertTrue(testIterator.hasNext);
assertTrue(testIterator.hasNext());
assertEquals("aa", testIterator.nextElement);
assertEquals("aa", testIterator.next());
-
+
assertTrue(testIterator.hasNext);
assertTrue(testIterator.hasNext());
assertEquals("ab", testIterator.nextElement);
assertEquals("ab", testIterator.next());
-
+
assertFalse(testIterator.hasNext);
assertFalse(testIterator.hasNext());
assertEquals(null, testIterator.nextElement);
assertThrows(NoSuchElementException.class, testIterator::next);
}
-
+
/**
- * Test method for {@link org.unitConverter.math.ZeroIsNullMap#keySet()}.
+ * Test method for the ConditionalExistenceMap's keySet operation.
*/
@Test
void testKeySet() {
final Map<String, Integer> map = this.getTestMap();
assertFalse(map.keySet().contains("zero"));
}
-
+
/**
- * Test method for {@link org.unitConverter.math.ZeroIsNullMap#values()}.
+ * Test method for the ConditionalExistenceMap's values operation.
*/
@Test
void testValues() {
final Map<String, Integer> map = this.getTestMap();
assertFalse(map.values().contains(0));
}
-
+
}
diff --git a/src/test/java/sevenUnits/utils/SemanticVersionTest.java b/src/test/java/sevenUnits/utils/SemanticVersionTest.java
new file mode 100644
index 0000000..877b258
--- /dev/null
+++ b/src/test/java/sevenUnits/utils/SemanticVersionTest.java
@@ -0,0 +1,399 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static sevenUnits.utils.SemanticVersionNumber.BUILD_METADATA_COMPARATOR;
+import static sevenUnits.utils.SemanticVersionNumber.builder;
+import static sevenUnits.utils.SemanticVersionNumber.fromString;
+import static sevenUnits.utils.SemanticVersionNumber.isValidVersionString;
+import static sevenUnits.utils.SemanticVersionNumber.preRelease;
+import static sevenUnits.utils.SemanticVersionNumber.stableVersion;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link SemanticVersionNumber}
+ *
+ * @since 2022-02-19
+ */
+public final class SemanticVersionTest {
+ /**
+ * Test for {@link SemanticVersionNumber#compatible}
+ *
+ * @since 2022-02-20
+ */
+ @Test
+ public void testCompatibility() {
+ assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)),
+ "1.0.0 not compatible with 1.0.5");
+ assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)),
+ "1.3.1 not compatible with 1.4.0");
+
+ // 0.y.z should not be compatible with any other version
+ assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)),
+ "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)");
+
+ // upgrading major version should = incompatible
+ assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)),
+ "1.0.0 compatible with 2.0.0");
+
+ // dowgrade should = incompatible
+ assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)),
+ "1.1.0 compatible with 1.0.0");
+ }
+
+ /**
+ * Tests {@link SemanticVersionNumber#toString} for complex version numbers
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testComplexToString() {
+ final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
+ .build();
+ assertEquals("1.2.3-1.2.3", v1.toString());
+ final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123)
+ .buildMetadata("2022-02-19").build();
+ assertEquals("4.5.6-abc.123+2022-02-19", v2.toString());
+ final SemanticVersionNumber v3 = builder(1, 0, 0)
+ .preRelease("x-y-z", "--").build();
+ assertEquals("1.0.0-x-y-z.--", v3.toString());
+ }
+
+ /**
+ * Tests that complex version can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testComplexVersions() {
+ final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
+ .build();
+ assertEquals(1, v1.majorVersion());
+ assertEquals(2, v1.minorVersion());
+ assertEquals(3, v1.patchVersion());
+ assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers());
+ assertEquals(List.of(), v1.buildMetadata());
+
+ final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123)
+ .buildMetadata("2022-02-19").build();
+ assertEquals(4, v2.majorVersion());
+ assertEquals(5, v2.minorVersion());
+ assertEquals(6, v2.patchVersion());
+ assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers());
+ assertEquals(List.of("2022-02-19"), v2.buildMetadata());
+
+ final SemanticVersionNumber v3 = builder(1, 0, 0)
+ .preRelease("x-y-z", "--").build();
+ assertEquals(1, v3.majorVersion());
+ assertEquals(0, v3.minorVersion());
+ assertEquals(0, v3.patchVersion());
+ assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers());
+ assertEquals(List.of(), v3.buildMetadata());
+ }
+
+ /**
+ * Test that semantic version strings can be parsed correctly
+ *
+ * @since 2022-02-19
+ * @see SemanticVersionNumber#fromString
+ * @see SemanticVersionNumber#isValidVersionString
+ */
+ @Test
+ public void testFromString() {
+ // test that the regex can match version strings
+ assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid");
+ assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid");
+ assertTrue(isValidVersionString("2.0.0-a.1"),
+ "2.0.0-a.1 is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0-a.b.c.d"),
+ "1.0.0-a.b.c.d is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0+abc"),
+ "1.0.0+abc is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0-abc+def"),
+ "1.0.0-abc+def is treated as invalid");
+
+ // test that invalid versions don't match
+ assertFalse(isValidVersionString("1.0"),
+ "1.0 is treated as valid (patch should be required)");
+ assertFalse(isValidVersionString("1.A.0"),
+ "1.A.0 is treated as valid (main versions must be numbers)");
+ assertFalse(isValidVersionString("1.0.0-"),
+ "1.0.0- is treated as valid (pre-release must not be empty)");
+ assertFalse(isValidVersionString("1.0.0+"),
+ "1.0.0+ is treated as valid (build metadata must not be empty)");
+
+ // test that versions can be parsed
+ assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"),
+ "Could not parse 1.0.0");
+ assertEquals(
+ builder(1, 2, 3).preRelease("abc", "56", "def")
+ .buildMetadata("2022abc99").build(),
+ fromString("1.2.3-abc.56.def+2022abc99"),
+ "Could not parse 1.2.3-abc.56.def+2022abc99");
+ }
+
+ /**
+ * Ensures it is impossible to create invalid version numbers
+ */
+ @Test
+ public void testInvalidVersionNumbers() {
+ // stableVersion()
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(1, 0, -1),
+ "Negative patch tolerated by stableVersion");
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(1, -2, 1),
+ "Negative minor version number tolerated by stableVersion");
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(-3, 0, 7),
+ "Negative major version number tolerated by stableVersion");
+
+ // preRelease()
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, -1, "test", 2),
+ "Negative patch tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, -2, 1, "test", 2),
+ "Negative minor version number tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(-3, 0, 7, "test", 2),
+ "Negative major version number tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "test", -1),
+ "Negative pre release number tolerated by preRelease");
+ assertThrows(NullPointerException.class,
+ () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "", 1),
+ "Empty string tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "abc+cde", 1),
+ "Invalid string tolerated by preRelease");
+
+ // builder()
+ assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1),
+ "Negative patch tolerated by builder");
+ assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1),
+ "Negative minor version number tolerated by builder");
+ assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7),
+ "Negative major version number tolerated by builder");
+
+ final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3);
+ // note: builder.buildMetadata(null) doesn't even compile lol
+ // builder.buildMetadata
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.buildMetadata(null, "abc"),
+ "Null tolerated by builder.buildMetadata(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(""),
+ "Empty string tolerated by builder.buildMetadata(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata("c%4"),
+ "Invalid string tolerated by builder.buildMetadata(String...)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.buildMetadata(List.of("abc", null)),
+ "Null tolerated by builder.buildMetadata(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(List.of("")),
+ "Empty string tolerated by builder.buildMetadata(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(List.of("")),
+ "Invalid string tolerated by builder.buildMetadata(List<String>)");
+
+ // builder.preRelease
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(null, "abc"),
+ "Null tolerated by builder.preRelease(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(""),
+ "Empty string tolerated by builder.preRelease(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("c%4"),
+ "Invalid string tolerated by builder.preRelease(String...)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(List.of("abc", null)),
+ "Null tolerated by builder.preRelease(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(List.of("")),
+ "Empty string tolerated by builder.preRelease(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(List.of("")),
+ "Invalid string tolerated by builder.preRelease(List<String>)");
+
+ // the overloadings that accept numeric arguments
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(-1),
+ "Negative number tolerated by builder.preRelease(int...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("abc", -1),
+ "Negative number tolerated by builder.preRelease(String, int)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(null, 1),
+ "Null tolerated by builder.preRelease(String, int)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("", 1),
+ "Empty string tolerated by builder.preRelease(String, int)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("#$#c", 1),
+ "Invalid string tolerated by builder.preRelease(String, int)");
+
+ // ensure all these attempts didn't change the builder
+ assertEquals(builder(1, 2, 3), testBuilder,
+ "Attempts at making invalid version number succeeded despite throwing errors");
+ }
+
+ /**
+ * Test for {@link SemanticVersionNumber#isStable}
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testIsStable() {
+ assertTrue(stableVersion(1, 0, 0).isStable(),
+ "1.0.0 should be stable but is not");
+ assertFalse(stableVersion(0, 1, 2).isStable(),
+ "0.1.2 should not be stable but is");
+ assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(),
+ "1.2.3a5 should not be stable but is");
+ assertTrue(
+ builder(9, 9, 99)
+ .buildMetadata("lots-of-metadata", "abc123", "2022").build()
+ .isStable(),
+ "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not");
+ }
+
+ /**
+ * Tests that the versions are ordered by
+ * {@link SemanticVersionNumber#compareTo} according to official rules. Tests
+ * all of the versions compared in section 11 of the SemVer 2.0.0 document
+ * and some more.
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testOrder() {
+ final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha")
+ .build(); // 1.0.0-alpha
+ final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1
+ final SemanticVersionNumber v100ab = builder(1, 0, 0)
+ .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta
+ final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta")
+ .build(); // 1.0.0-alpha
+ final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2
+ final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11
+ final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ final SemanticVersionNumber v100plus = builder(1, 0, 0)
+ .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah
+ final SemanticVersionNumber v200 = stableVersion(2, 0, 0);
+ final SemanticVersionNumber v201 = stableVersion(2, 0, 1);
+ final SemanticVersionNumber v210 = stableVersion(2, 1, 0);
+ final SemanticVersionNumber v211 = stableVersion(2, 1, 1);
+ final SemanticVersionNumber v300 = stableVersion(3, 0, 0);
+
+ // test order of version numbers
+ assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1");
+ assertTrue(v100a1.compareTo(v100ab) < 0,
+ "1.0.0-alpha.1 >= 1.0.0-alpha.beta");
+ assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta");
+ assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2");
+ assertTrue(v100b2.compareTo(v100b11) < 0,
+ "1.0.0-beta.2 >= 1.0.0-beta.11");
+ assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1");
+ assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0");
+ assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0");
+ assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1");
+ assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0");
+ assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1");
+ assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0");
+
+ // test symmetry - assume previous tests passed
+ assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha");
+ assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1");
+ assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1");
+
+ // test transitivity
+ assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11");
+ assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0");
+ assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0");
+ assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0");
+
+ // test metadata is ignored
+ assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored");
+ // test metadata is NOT ignored by alternative comparator
+ assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0,
+ "Build metadata ignored by BUILD_METADATA_COMPARATOR");
+ }
+
+ /**
+ * Tests that simple stable versions can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleStableVersions() {
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ assertEquals(1, v100.majorVersion());
+ assertEquals(0, v100.minorVersion());
+ assertEquals(0, v100.patchVersion());
+
+ final SemanticVersionNumber v925 = stableVersion(9, 2, 5);
+ assertEquals(9, v925.majorVersion());
+ assertEquals(2, v925.minorVersion());
+ assertEquals(5, v925.patchVersion());
+ }
+
+ /**
+ * Tests that {@link SemanticVersionNumber#toString} works for simple version
+ * numbers
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleToString() {
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ assertEquals("1.0.0", v100.toString());
+
+ final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1);
+ assertEquals("8.4.5-alpha.1", v845a1.toString());
+ }
+
+ /**
+ * Tests that simple unstable versions can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleUnstableVersions() {
+ final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1);
+ assertEquals(3, v350a1.majorVersion(),
+ "Incorrect major version for v3.5.0a1");
+ assertEquals(5, v350a1.minorVersion(),
+ "Incorrect minor version for v3.5.0a1");
+ assertEquals(0, v350a1.patchVersion(),
+ "Incorrect patch version for v3.5.0a1");
+ assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(),
+ "Incorrect pre-release identifiers for v3.5.0a1");
+ }
+}
diff --git a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
index c891f20..5ccef28 100644
--- a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
+++ b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
@@ -19,6 +19,7 @@ package sevenUnits.utils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static sevenUnits.utils.UncertainDouble.fromRoundedString;
import static sevenUnits.utils.UncertainDouble.fromString;
import static sevenUnits.utils.UncertainDouble.of;
@@ -33,6 +34,9 @@ import org.junit.jupiter.api.Test;
* @since 2021-11-29
*/
class UncertainDoubleTest {
+ /**
+ * Ensures that the compareTo function behaves correctly.
+ */
@Test
final void testCompareTo() {
assertTrue(of(2.0, 0.5).compareTo(of(2.0, 0.1)) == 0);
@@ -40,6 +44,9 @@ class UncertainDoubleTest {
assertTrue(of(2.0, 0.5).compareTo(of(3.0, 0.1)) < 0);
}
+ /**
+ * Tests the ___exact operations
+ */
@Test
final void testExactOperations() {
final UncertainDouble x = UncertainDouble.of(Math.PI, 0.1);
@@ -66,6 +73,19 @@ class UncertainDoubleTest {
x.toExponentExact(Math.E).value());
}
+ /**
+ * Test for {@link UncertainDouble#fromRoundedString}
+ *
+ * @since 2022-04-18
+ */
+ @Test
+ final void testFromRoundedString() {
+ assertEquals(of(12345.678, 0.001), fromRoundedString("12345.678"));
+ }
+
+ /**
+ * Test for {@link UncertainDouble#fromString}
+ */
@Test
final void testFromString() {
// valid strings
diff --git a/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java b/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java
new file mode 100644
index 0000000..8ea3fd0
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static sevenUnitsGUI.DefaultPrefixRepetitionRule.COMPLEX_REPETITION;
+import static sevenUnitsGUI.DefaultPrefixRepetitionRule.NO_REPETITION;
+import static sevenUnitsGUI.DefaultPrefixRepetitionRule.NO_RESTRICTION;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import sevenUnits.unit.Metric;
+
+/**
+ * Tests for the default prefix repetition rules.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+class PrefixRepetitionTest {
+ /**
+ * Ensures that the complex repetition rule disallows invalid prefix lists.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testInvalidComplexRepetition() {
+ assertFalse(COMPLEX_REPETITION.test(List.of(Metric.KILO, Metric.YOTTA)),
+ "Complex repetition does not factor order of prefixes");
+ assertFalse(COMPLEX_REPETITION.test(List.of(Metric.MEGA, Metric.KILO)),
+ "\"kilomega\" allowed (should use \"giga\")");
+ assertFalse(
+ COMPLEX_REPETITION
+ .test(List.of(Metric.YOTTA, Metric.MEGA, Metric.KILO)),
+ "\"kilomega\" allowed after yotta (should use \"giga\")");
+ assertFalse(COMPLEX_REPETITION.test(List.of(Metric.YOTTA, Metric.MILLI)),
+ "Complex repetition does not factor direction of prefixes");
+ }
+
+ /**
+ * Tests the {@code NO_REPETITION} rule.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testNoRepetition() {
+ assertTrue(NO_REPETITION.test(List.of(Metric.KILO)));
+ assertFalse(NO_REPETITION.test(List.of(Metric.KILO, Metric.YOTTA)));
+ assertTrue(NO_REPETITION.test(List.of(Metric.MILLI)));
+ assertTrue(NO_REPETITION.test(List.of()), "Empty list yields false");
+ }
+
+ /**
+ * Tests the {@code NO_RESTRICTION} rule.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testNoRestriction() {
+ assertTrue(NO_RESTRICTION.test(List.of(Metric.KILO)));
+ assertTrue(NO_RESTRICTION.test(List.of(Metric.KILO, Metric.YOTTA)));
+ assertTrue(NO_RESTRICTION.test(List.of(Metric.MILLI)));
+ assertTrue(NO_RESTRICTION.test(List.of()));
+ }
+
+ /**
+ * Ensures that the complex repetition rule allows valid prefix lists.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testValidComplexRepetition() {
+ // simple situations
+ assertTrue(COMPLEX_REPETITION.test(List.of(Metric.KILO)),
+ "Single prefix not allowed");
+ assertTrue(COMPLEX_REPETITION.test(List.of()), "No prefixes not allowed");
+
+ // valid complex repetition
+ assertTrue(COMPLEX_REPETITION.test(List.of(Metric.YOTTA, Metric.KILO)),
+ "Valid complex repetition (kiloyotta) not allowed");
+ assertTrue(COMPLEX_REPETITION.test(List.of(Metric.KILO, Metric.DEKA)),
+ "Valid complex repetition (dekakilo) not allowed");
+ assertTrue(
+ COMPLEX_REPETITION
+ .test(List.of(Metric.YOTTA, Metric.KILO, Metric.DEKA)),
+ "Valid complex repetition (dekakiloyotta) not allowed");
+ assertTrue(
+ COMPLEX_REPETITION.test(List.of(Metric.YOTTA, Metric.YOTTA,
+ Metric.KILO, Metric.DEKA)),
+ "Valid complex repetition (dekakiloyottayotta) not allowed");
+
+ // valid with negative prefixes
+ assertTrue(COMPLEX_REPETITION.test(List.of(Metric.YOCTO, Metric.MILLI)),
+ "Valid complex repetition (milliyocto) not allowed");
+ assertTrue(COMPLEX_REPETITION.test(List.of(Metric.MILLI, Metric.CENTI)),
+ "Valid complex repetition (centimilli) not allowed");
+ assertTrue(
+ COMPLEX_REPETITION
+ .test(List.of(Metric.YOCTO, Metric.MILLI, Metric.CENTI)),
+ "Valid complex repetition (centimilliyocto) not allowed");
+ }
+}
diff --git a/src/test/java/sevenUnitsGUI/PrefixSearchTest.java b/src/test/java/sevenUnitsGUI/PrefixSearchTest.java
new file mode 100644
index 0000000..ca238fe
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/PrefixSearchTest.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static sevenUnitsGUI.PrefixSearchRule.COMMON_PREFIXES;
+import static sevenUnitsGUI.PrefixSearchRule.NO_PREFIXES;
+import static sevenUnitsGUI.PrefixSearchRule.getCoherentOnlyRule;
+import static sevenUnitsGUI.PrefixSearchRule.getUniversalRule;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import sevenUnits.unit.BritishImperial;
+import sevenUnits.unit.LinearUnit;
+import sevenUnits.unit.Metric;
+
+/**
+ * Tests for {@link PrefixSearchRule}
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+class PrefixSearchTest {
+ /**
+ * A method that creates duplicate copies of the common prefix rule.
+ */
+ private static final PrefixSearchRule getCommonRuleCopy() {
+ return getCoherentOnlyRule(Set.of(Metric.KILO, Metric.MILLI));
+ }
+
+ /**
+ * Test method for
+ * {@link sevenUnitsGUI.PrefixSearchRule#apply(java.util.Map.Entry)}, for a
+ * coherent unit and {@link PrefixSearchRule#COMMON_PREFIXES}.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ final void testCoherentPrefixSearch() {
+ final var expected = Map.of("metre", Metric.METRE, "kilometre",
+ Metric.KILOMETRE, "millimetre", Metric.MILLIMETRE);
+ final var actual = COMMON_PREFIXES
+ .apply(Map.entry("metre", Metric.METRE));
+
+ assertEquals(expected, actual,
+ "Prefixes not correctly applied to coherent unit.");
+ }
+
+ /**
+ * Test method for
+ * {@link sevenUnitsGUI.PrefixSearchRule#equals(java.lang.Object)}.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ final void testEquals() {
+ assertEquals(getCommonRuleCopy(), getCommonRuleCopy(),
+ "equals considers something other than prefixes/rule");
+
+ assertNotEquals(getCoherentOnlyRule(Set.of()), getUniversalRule(Set.of()),
+ "equals ignores rule");
+ }
+
+ /**
+ * Test method for {@link sevenUnitsGUI.PrefixSearchRule#getPrefixes()}.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ final void testGetPrefixes() {
+ assertEquals(Set.of(), NO_PREFIXES.getPrefixes());
+ assertEquals(Metric.ALL_PREFIXES,
+ PrefixSearchRule.ALL_METRIC_PREFIXES.getPrefixes());
+ }
+
+ /**
+ * Test method for {@link sevenUnitsGUI.PrefixSearchRule#hashCode()}.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ final void testHashCode() {
+ assertEquals(getCommonRuleCopy().hashCode(),
+ getCommonRuleCopy().hashCode());
+ }
+
+ /**
+ * Tests prefix searching for a non-coherent unit and
+ * {@link PrefixSearchRule#COMMON_PREFIXES}.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ final void testNonCoherentPrefixSearch() {
+ final var input = Map.entry("inch", BritishImperial.Length.INCH);
+ final var expected = Map.ofEntries(input);
+ final var actual = COMMON_PREFIXES.apply(input);
+
+ assertEquals(expected, actual, "Prefixes applied to non-coherent unit.");
+ }
+
+ /**
+ * Tests that {@link PrefixSearchRule#NO_PREFIXES} returns the original unit.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testNoPrefixes() {
+ for (final String name : Set.of("name1", "hello", "there")) {
+ for (final LinearUnit unit : Set.of(Metric.METRE, Metric.KILOMETRE,
+ BritishImperial.Length.INCH)) {
+ final var testEntry = Map.entry(name, unit);
+ final var expected = Map.ofEntries(testEntry);
+ final var actual = NO_PREFIXES.apply(testEntry);
+ assertEquals(expected, actual, () -> String
+ .format("NO_PREFIXES.apply(%s) != %s", testEntry, actual));
+ }
+ }
+ }
+
+ /**
+ * Test method for {@link sevenUnitsGUI.PrefixSearchRule#toString()}.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ final void testToString() {
+ assertEquals(
+ "Apply the following prefixes: [kilo (\u00D7 1000.0), milli (\u00D7 0.001)]",
+ COMMON_PREFIXES.toString());
+ }
+
+}
diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java
new file mode 100644
index 0000000..13d7986
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/PresenterTest.java
@@ -0,0 +1,423 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.RoundingMode;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import sevenUnits.unit.BaseDimension;
+import sevenUnits.unit.BritishImperial;
+import sevenUnits.unit.LinearUnit;
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.Unit;
+import sevenUnits.unit.UnitDatabase;
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
+import sevenUnits.utils.ObjectProduct;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * Various tests for the {@link Presenter}.
+ * <p>
+ * <em>Note: this test outputs a lot to the standard output, because creating a
+ * {@link UnitDatabase} and converting with a {@link ViewBot} both trigger
+ * println statements.</em>
+ *
+ * @author Adrien Hopkins
+ *
+ * @since v0.4.0
+ * @since 2022-02-10
+ */
+public final class PresenterTest {
+ private static final Path TEST_SETTINGS = Path.of("src", "test", "resources",
+ "test-settings.txt");
+ static final Set<Unit> testUnits = Set.of(Metric.METRE, Metric.KILOMETRE,
+ Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR);
+
+ static final Set<ObjectProduct<BaseDimension>> testDimensions = Set
+ .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY);
+
+ private static final Stream<Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>>> SEARCH_RULES = Stream
+ .of(PrefixSearchRule.NO_PREFIXES, PrefixSearchRule.COMMON_PREFIXES,
+ PrefixSearchRule.ALL_METRIC_PREFIXES);
+
+ /**
+ * @return rounding rules used by {@link #testRoundingRules}
+ * @since v0.4.0
+ * @since 2022-04-16
+ */
+ private static final Stream<Function<UncertainDouble, String>> getRoundingRules() {
+ final var SCIENTIFIC_ROUNDING = StandardDisplayRules.uncertaintyBased();
+ final var INTEGER_ROUNDING = StandardDisplayRules.fixedDecimals(0);
+ final var SIG_FIG_ROUNDING = StandardDisplayRules.fixedPrecision(4);
+
+ return Stream.of(SCIENTIFIC_ROUNDING, INTEGER_ROUNDING, SIG_FIG_ROUNDING);
+ }
+
+ private static final Stream<Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>>> getSearchRules() {
+ return SEARCH_RULES;
+ }
+
+ private static final Set<String> names(Set<? extends Nameable> units) {
+ return units.stream().map(Nameable::getName).collect(Collectors.toSet());
+ }
+
+ /**
+ * Test method for {@link Presenter#convertExpressions}
+ *
+ * @since v0.4.0
+ * @since 2022-02-12
+ */
+ @Test
+ void testConvertExpressions() {
+ // setup
+ final ViewBot viewBot = new ViewBot();
+ final Presenter presenter = new Presenter(viewBot);
+
+ viewBot.setFromExpression("10000.0 m");
+ viewBot.setToExpression("km");
+
+ // convert expression
+ presenter.convertExpressions();
+
+ // test result
+ final List<UnitConversionRecord> outputs = viewBot
+ .expressionConversionList();
+ assertEquals("10000.0 m = 10.00000 km",
+ outputs.get(outputs.size() - 1).toString());
+ }
+
+ /**
+ * Tests that unit-conversion Views can correctly convert units
+ *
+ * @since v0.4.0
+ * @since 2022-02-12
+ */
+ @Test
+ void testConvertUnits() {
+ // setup
+ final ViewBot viewBot = new ViewBot();
+ final Presenter presenter = new Presenter(viewBot);
+
+ viewBot.setFromUnitNames(names(testUnits));
+ viewBot.setToUnitNames(names(testUnits));
+ viewBot.setFromSelection("metre");
+ viewBot.setToSelection("kilometre");
+ viewBot.setInputValue("10000.0");
+
+ // convert units
+ presenter.convertUnits();
+
+ /*
+ * use result from system as expected - I'm not testing unit conversion
+ * here (that's for the backend tests), I'm just testing that it correctly
+ * calls the unit conversion system
+ */
+ final LinearUnitValue expectedInput = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.fromRoundedString("10000.0"));
+ final LinearUnitValue expectedOutput = expectedInput
+ .convertTo(Metric.KILOMETRE);
+ final UnitConversionRecord expectedUC = UnitConversionRecord.valueOf(
+ expectedInput.getUnit().getName(),
+ expectedOutput.getUnit().getName(), "10000.0",
+ expectedOutput.getValue().toString(false, RoundingMode.HALF_EVEN));
+ assertEquals(List.of(expectedUC), viewBot.unitConversionList());
+ }
+
+ /**
+ * Tests that duplicate units are successfully removed, if that is asked for
+ *
+ * @since v0.4.0
+ * @since 2022-04-16
+ */
+ @Test
+ void testDuplicateUnits() {
+ final var metre = Metric.METRE;
+ final var meter = Metric.METRE.withName(NameSymbol.of("meter", "m"));
+
+ // load 2 duplicate units
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ presenter.database.clear();
+ presenter.database.addUnit("metre", metre);
+ presenter.database.addUnit("meter", meter);
+ presenter.setOneWayConversionEnabled(false);
+ presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES);
+
+ // test that only one of them is included if duplicate units disabled
+ presenter.setShowDuplicates(false);
+ presenter.updateView();
+ assertEquals(1, viewBot.getFromUnitNames().size());
+ assertEquals(1, viewBot.getToUnitNames().size());
+
+ // test that both of them is included if duplicate units enabled
+ presenter.setShowDuplicates(true);
+ presenter.updateView();
+ assertEquals(2, viewBot.getFromUnitNames().size());
+ assertEquals(2, viewBot.getToUnitNames().size());
+ }
+
+ /**
+ * Tests that one-way conversion correctly filters From and To units
+ *
+ * @since v0.4.0
+ * @since 2022-04-16
+ */
+ @Test
+ void testOneWayConversion() {
+ // metre is metric, inch is non-metric, tempC is semi-metric
+ final var allNames = Set.of("metre", "inch", "tempC");
+ final var metricNames = Set.of("metre", "tempC");
+ final var nonMetricNames = Set.of("inch", "tempC");
+
+ // load view with one metric and one non-metric unit
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ presenter.database.clear();
+ presenter.database.addUnit("metre", Metric.METRE);
+ presenter.database.addUnit("inch", BritishImperial.Length.INCH);
+ presenter.database.addUnit("tempC", Metric.CELSIUS);
+ presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES);
+
+ // test that units are removed from each side when one-way conversion is
+ // enabled
+ presenter.setOneWayConversionEnabled(true);
+ assertEquals(nonMetricNames, viewBot.getFromUnitNames());
+ assertEquals(metricNames, viewBot.getToUnitNames());
+
+ // test that units are kept when one-way conversion is disabled
+ presenter.setOneWayConversionEnabled(false);
+ assertEquals(allNames, viewBot.getFromUnitNames());
+ assertEquals(allNames, viewBot.getToUnitNames());
+ }
+
+ /**
+ * Tests the prefix-viewing functionality.
+ *
+ * @since v0.4.0
+ * @since 2022-04-16
+ */
+ @Test
+ void testPrefixViewing() {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ viewBot.setViewablePrefixNames(Set.of("kilo", "milli"));
+ presenter.setNumberDisplayRule(UncertainDouble::toString);
+
+ // view prefix
+ viewBot.setViewedPrefixName("kilo");
+ presenter.prefixSelected(); // just in case
+
+ // get correct values
+ final var expectedNameSymbol = presenter.database.getPrefix("kilo")
+ .getNameSymbol();
+ final var expectedMultiplierString = String
+ .valueOf(Metric.KILO.getMultiplier());
+
+ // test that presenter's values are correct
+ final var prefixRecord = viewBot.prefixViewList().get(0);
+ assertEquals(expectedNameSymbol, prefixRecord.getNameSymbol());
+ assertEquals(expectedMultiplierString, prefixRecord.multiplierString());
+ }
+
+ /**
+ * Tests that rounding rules are used correctly.
+ *
+ * @since v0.4.0
+ * @since 2022-04-16
+ */
+ @ParameterizedTest
+ @MethodSource("getRoundingRules")
+ void testRoundingRules(Function<UncertainDouble, String> roundingRule) {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ presenter.setNumberDisplayRule(roundingRule);
+
+ // convert and round
+ viewBot.setInputValue("12345.6789");
+ viewBot.setFromSelection("metre");
+ viewBot.setToSelection("kilometre");
+ presenter.convertUnits();
+
+ // test the result of the rounding
+ final String expectedOutputString = roundingRule
+ .apply(UncertainDouble.fromRoundedString("12.3456789"));
+ final String actualOutputString = viewBot.unitConversionList().get(0)
+ .outputValueString();
+ assertEquals(expectedOutputString, actualOutputString);
+ }
+
+ /**
+ * Tests that the Presenter correctly applies search rules.
+ *
+ * @param searchRule search rule to test
+ * @since v0.4.0
+ * @since 2022-07-08
+ */
+ @ParameterizedTest
+ @MethodSource("getSearchRules")
+ void testSearchRules(
+ Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+
+ presenter.setSearchRule(searchRule);
+ presenter.setOneWayConversionEnabled(false);
+
+ presenter.database.clear();
+ presenter.database.addUnit("metre", Metric.METRE);
+ presenter.database.addUnit("inch", BritishImperial.Length.INCH);
+ presenter.updateView();
+
+ // create expected output based on rule
+ final Set<String> expectedOutput = new HashSet<>();
+ expectedOutput.addAll(searchRule
+ .apply(Map.entry("inch", BritishImperial.Length.INCH)).keySet());
+ expectedOutput.addAll(
+ searchRule.apply(Map.entry("metre", Metric.METRE)).keySet());
+ final Set<String> actualOutput = viewBot.getFromUnitNames();
+
+ // test output
+ assertEquals(expectedOutput, actualOutput);
+ }
+
+ /**
+ * Tests that settings can be saved to and loaded from a file.
+ *
+ * @since v0.4.0
+ * @since 2022-04-16
+ */
+ @Test
+ void testSettingsSaving() {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+
+ // set and save custom settings
+ presenter.setOneWayConversionEnabled(true);
+ presenter.setShowDuplicates(true);
+ presenter.setNumberDisplayRule(StandardDisplayRules.fixedPrecision(11));
+ presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.COMPLEX_REPETITION);
+ presenter.saveSettings(TEST_SETTINGS);
+
+ // overwrite custom settings
+ presenter.setOneWayConversionEnabled(false);
+ presenter.setShowDuplicates(false);
+ presenter.setNumberDisplayRule(StandardDisplayRules.uncertaintyBased());
+
+ // load settings & test that they're the same
+ presenter.loadSettings(TEST_SETTINGS);
+ assertTrue(presenter.oneWayConversionEnabled());
+ assertTrue(presenter.duplicatesShown());
+ assertEquals(StandardDisplayRules.fixedPrecision(11),
+ presenter.getNumberDisplayRule());
+ }
+
+ /**
+ * Ensures the Presenter generates the correct data upon a unit-viewing.
+ *
+ * @since v0.4.0
+ * @since 2022-04-16
+ */
+ @Test
+ void testUnitViewing() {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ viewBot.setViewableUnitNames(names(testUnits));
+
+ // view unit
+ viewBot.setViewedUnitName("metre");
+ presenter.unitNameSelected(); // just in case this isn't triggered
+ // automatically
+
+ // get correct values
+ final var expectedNameSymbol = presenter.database.getUnit("metre")
+ .getNameSymbol();
+ final var expectedDefinition = "(Base unit)";
+ final var expectedDimensionName = presenter
+ .getDimensionName(Metric.METRE.getDimension());
+ final var expectedUnitType = UnitType.METRIC;
+
+ // test for correctness
+ final var viewRecord = viewBot.unitViewList().get(0);
+ assertEquals(expectedNameSymbol, viewRecord.getNameSymbol());
+ assertEquals(expectedDefinition, viewRecord.definition());
+ assertEquals(expectedDimensionName, viewRecord.dimensionName());
+ assertEquals(expectedUnitType, viewRecord.unitType());
+ }
+
+ /**
+ * Test for {@link Presenter#updateView()}
+ *
+ * @since v0.4.0
+ * @since 2022-02-12
+ */
+ @Test
+ void testUpdateView() {
+ // setup
+ final ViewBot viewBot = new ViewBot();
+ final Presenter presenter = new Presenter(viewBot);
+ presenter.setOneWayConversionEnabled(false);
+ presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES);
+
+ // override default database units
+ presenter.database.clear();
+ for (final Unit unit : testUnits) {
+ presenter.database.addUnit(unit.getPrimaryName().orElseThrow(), unit);
+ }
+ for (final var dimension : testDimensions) {
+ presenter.database.addDimension(
+ dimension.getPrimaryName().orElseThrow(), dimension);
+ }
+
+ // set from and to units
+ viewBot.setFromUnitNames(names(testUnits));
+ viewBot.setToUnitNames(names(testUnits));
+ viewBot.setDimensionNames(names(testDimensions));
+ viewBot.setSelectedDimensionName(Metric.Dimensions.LENGTH.getName());
+
+ // filter to length units only, then get the filtered sets of units
+ presenter.updateView();
+ final Set<String> fromUnits = viewBot.getFromUnitNames();
+ final Set<String> toUnits = viewBot.getToUnitNames();
+
+ // test that fromUnits/toUnits is [METRE, KILOMETRE]
+ assertEquals(Set.of("metre", "kilometre"), fromUnits);
+ assertEquals(Set.of("metre", "kilometre"), toUnits);
+ }
+}
diff --git a/src/test/java/sevenUnitsGUI/RoundingTest.java b/src/test/java/sevenUnitsGUI/RoundingTest.java
new file mode 100644
index 0000000..ca1a272
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/RoundingTest.java
@@ -0,0 +1,287 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static sevenUnitsGUI.StandardDisplayRules.fixedDecimals;
+import static sevenUnitsGUI.StandardDisplayRules.fixedPrecision;
+import static sevenUnitsGUI.StandardDisplayRules.getStandardRule;
+import static sevenUnitsGUI.StandardDisplayRules.uncertaintyBased;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import sevenUnits.utils.UncertainDouble;
+import sevenUnitsGUI.StandardDisplayRules.FixedDecimals;
+import sevenUnitsGUI.StandardDisplayRules.FixedPrecision;
+import sevenUnitsGUI.StandardDisplayRules.UncertaintyBased;
+
+/**
+ * Tests that ensure the rounding rules work as intended.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+class RoundingTest {
+ // rounding rules to test
+ private static final FixedDecimals ZERO_DECIMALS = fixedDecimals(0);
+ private static final FixedDecimals TWO_DECIMALS = fixedDecimals(2);
+ private static final FixedDecimals SIX_DECIMALS = fixedDecimals(6);
+
+ private static final FixedPrecision ONE_SIG_FIG = fixedPrecision(1);
+ private static final FixedPrecision THREE_SIG_FIGS = fixedPrecision(3);
+ private static final FixedPrecision TWELVE_SIG_FIGS = fixedPrecision(12);
+
+ private static final UncertaintyBased UNCERTAINTY_BASED = uncertaintyBased();
+
+ // numbers to test rounding with
+ private static final UncertainDouble INPUT1 = UncertainDouble.of(12.3456789,
+ 0.0);
+ private static final UncertainDouble INPUT2 = UncertainDouble.of(300.9,
+ 0.005);
+ private static final UncertainDouble INPUT3 = UncertainDouble.of(12345432.1,
+ 0.0);
+ private static final UncertainDouble INPUT4 = UncertainDouble.of(0.00001234,
+ 0.000001);
+
+ /**
+ * @return arguments for
+ * {@link #testFixedDecimalRounding(UncertainDouble, String, String, String)}
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ private static final Stream<Arguments> fixedDecimalRoundingExamples() {
+ // input, zero decimal string, two decimal string, six decimal string
+ return Stream.of(Arguments.of(INPUT1, "12", "12.35", "12.345679"),
+ Arguments.of(INPUT2, "301", "300.90", "300.900000"),
+ Arguments.of(INPUT3, "12345432", "12345432.10", "12345432.100000"),
+ Arguments.of(INPUT4, "0", "0.00", "0.000012"));
+ }
+
+ /**
+ * @return arguments for
+ * {@link #testFixedPrecisionRounding(UncertainDouble, String, String, String)}
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ private static final Stream<Arguments> fixedPrecisionRoundingExamples() {
+ // input, one sig fig string, three s.f. string, six s.f. string
+ return Stream.of(Arguments.of(INPUT1, "1E+1", "12.3", "12.3456789000"),
+ Arguments.of(INPUT2, "3E+2", "301", "300.900000000"),
+ Arguments.of(INPUT3, "1E+7", "1.23E+7", "12345432.1000"),
+ Arguments.of(INPUT4, "0.00001", "0.0000123", "0.0000123400000000"));
+ }
+
+ /**
+ * @return arguments for
+ * {@link #testUncertaintyRounding(UncertainDouble, String)}
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ private static final Stream<Arguments> uncertaintyRoundingExamples() {
+ // input, uncertainty rounding string
+ return Stream.of(Arguments.of(INPUT1, "12.3456789"),
+ Arguments.of(INPUT2, "300.900"),
+ Arguments.of(INPUT3, "1.23454321E7"),
+ Arguments.of(INPUT4, "0.0000123"));
+ }
+
+ /**
+ * Test for {@link FixedDecimals#decimalPlaces()} and
+ * {@link FixedPrecision#significantFigures()}.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testDataMethods() {
+ // ensure # of decimal places can be accessed
+ assertEquals(0, ZERO_DECIMALS.decimalPlaces(),
+ "ZERO_DECIMALS has " + ZERO_DECIMALS.decimalPlaces() + " decimals");
+ assertEquals(2, TWO_DECIMALS.decimalPlaces(),
+ "TWO_DECIMALS has " + TWO_DECIMALS.decimalPlaces() + " decimals");
+ assertEquals(6, SIX_DECIMALS.decimalPlaces(),
+ "SIX_DECIMALS has " + SIX_DECIMALS.decimalPlaces() + " decimals");
+
+ // ensure # of sig figs can be accessed
+ assertEquals(1, ONE_SIG_FIG.significantFigures(), "ONE_SIG_FIG has "
+ + ONE_SIG_FIG.significantFigures() + " significant figures");
+ assertEquals(3, THREE_SIG_FIGS.significantFigures(), "THREE_SIG_FIGS has "
+ + THREE_SIG_FIGS.significantFigures() + " significant figures");
+ assertEquals(12, TWELVE_SIG_FIGS.significantFigures(),
+ "TWELVE_SIG_FIGS has " + TWELVE_SIG_FIGS.significantFigures()
+ + " significant figures");
+ }
+
+ /**
+ * Tests that the rounding methods' equals() methods work.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testEquals() {
+ // basic equals tests
+ assertTrue(ZERO_DECIMALS.equals(ZERO_DECIMALS),
+ "ZERO_DECIMALS does not equal itself");
+ assertFalse(TWO_DECIMALS.equals(SIX_DECIMALS),
+ "TWO_DECIMALS == SIX_DECIMALS");
+ assertTrue(Objects.equals(fixedDecimals(0), fixedDecimals(0)),
+ "FixedDecimals.equals() depends on something other than decimal places.");
+
+ assertTrue(ONE_SIG_FIG.equals(ONE_SIG_FIG),
+ "ONE_SIG_FIG does not equal itself");
+ assertFalse(THREE_SIG_FIGS.equals(TWELVE_SIG_FIGS),
+ "THREE_SIG_FIGS == TWELVE_SIG_FIGS");
+ assertTrue(Objects.equals(fixedPrecision(1), fixedPrecision(1)),
+ "FixedPrecision.equals() depends on something other than significant figures.");
+
+ // test that FixedDecimals is never equal to FixedPrecision
+ // this unlikely argument is the test - the equals should return false!
+ @SuppressWarnings("unlikely-arg-type")
+ final boolean differentRulesEqual = Objects.equals(fixedDecimals(4),
+ fixedPrecision(4));
+ assertFalse(differentRulesEqual, "fixedDecimals(4) == fixedPrecision(4)");
+ }
+
+ /**
+ * Ensures that fixed decimal rounding works as expected
+ *
+ * @param input number to test
+ * @param zeroDecimalString expected string for zero decimal places
+ * @param twoDecimalString expected string for two decimal places
+ * @param sixDecimalString expected string for six decimal places
+ * @since 2022-07-17
+ */
+ @ParameterizedTest
+ @MethodSource("fixedDecimalRoundingExamples")
+ void testFixedDecimalRounding(UncertainDouble input,
+ String zeroDecimalString, String twoDecimalString,
+ String sixDecimalString) {
+ // test the three rounding rules against the provided strings
+ assertEquals(zeroDecimalString, ZERO_DECIMALS.apply(input),
+ "ZERO_DECIMALS rounded " + input + " as "
+ + ZERO_DECIMALS.apply(input) + " (should be "
+ + zeroDecimalString + ")");
+ assertEquals(twoDecimalString, TWO_DECIMALS.apply(input),
+ "TWO_DECIMALS rounded " + input + " as " + TWO_DECIMALS.apply(input)
+ + " (should be " + twoDecimalString + ")");
+ assertEquals(sixDecimalString, SIX_DECIMALS.apply(input),
+ "TWO_DECIMALS rounded " + input + " as " + SIX_DECIMALS.apply(input)
+ + " (should be " + sixDecimalString + ")");
+ }
+
+ /**
+ * Ensures that fixed precision rounding works as expected
+ *
+ * @param input number to test
+ * @param oneSigFigString expected string for one significant figure
+ * @param threeSigFigString expected string for three significant figures
+ * @param twelveSigFigString expected string for twelve significant figures
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @ParameterizedTest
+ @MethodSource("fixedPrecisionRoundingExamples")
+ void testFixedPrecisionRounding(UncertainDouble input,
+ String oneSigFigString, String threeSigFigString,
+ String twelveSigFigString) {
+ // test the three rounding rules against the provided strings
+ assertEquals(oneSigFigString, ONE_SIG_FIG.apply(input),
+ "ONE_SIG_FIG rounded " + input + " as " + ONE_SIG_FIG.apply(input)
+ + " (should be " + oneSigFigString + ")");
+ assertEquals(threeSigFigString, THREE_SIG_FIGS.apply(input),
+ "THREE_SIG_FIGS rounded " + input + " as "
+ + THREE_SIG_FIGS.apply(input) + " (should be "
+ + threeSigFigString + ")");
+ assertEquals(twelveSigFigString, TWELVE_SIG_FIGS.apply(input),
+ "TWELVE_SIG_FIGS rounded " + input + " as "
+ + TWELVE_SIG_FIGS.apply(input) + " (should be "
+ + twelveSigFigString + ")");
+ }
+
+ /**
+ * Tests that {@link StandardDisplayRules#getStandardRule} gets rounding
+ * rules as intended.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testGetStandardRule() {
+ assertEquals(ZERO_DECIMALS, getStandardRule("Round to 0 decimal places"));
+ assertEquals(THREE_SIG_FIGS,
+ getStandardRule("Round to 3 significant figures"));
+ assertEquals(UNCERTAINTY_BASED,
+ getStandardRule("Uncertainty-Based Rounding"));
+
+ assertThrows(IllegalArgumentException.class,
+ () -> getStandardRule("Not a rounding rule"));
+ }
+
+ /**
+ * Tests that the rounding methods' equals() methods work.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testHashCode() {
+ assertEquals(ZERO_DECIMALS.hashCode(), ZERO_DECIMALS.hashCode());
+ assertEquals(ONE_SIG_FIG.hashCode(), ONE_SIG_FIG.hashCode());
+ assertEquals(UNCERTAINTY_BASED.hashCode(), UNCERTAINTY_BASED.hashCode());
+ }
+
+ /**
+ * Tests that the {@code toString()} methods of the three rounding rule
+ * classes work correctly.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testToString() {
+ assertEquals("Round to 0 decimal places", ZERO_DECIMALS.toString());
+ assertEquals("Round to 3 significant figures", THREE_SIG_FIGS.toString());
+ assertEquals("Uncertainty-Based Rounding", UNCERTAINTY_BASED.toString());
+ }
+
+ /**
+ * Tests that Uncertainty Rounding works as expected
+ *
+ * @param input input number
+ * @param output expected output string
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @ParameterizedTest
+ @MethodSource("uncertaintyRoundingExamples")
+ void testUncertaintyRounding(UncertainDouble input, String output) {
+ assertEquals(output, UNCERTAINTY_BASED.apply(input),
+ () -> String.format(
+ "Uncertainty Rounding rounded %s as %s (should be %s)", input,
+ UNCERTAINTY_BASED.apply(input), output));
+ }
+}
diff --git a/src/test/java/sevenUnitsGUI/TabbedViewTest.java b/src/test/java/sevenUnitsGUI/TabbedViewTest.java
new file mode 100644
index 0000000..00092a4
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/TabbedViewTest.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for the TabbedView
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+class TabbedViewTest {
+ /**
+ * @return a view with all settings set to standard values
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ private static final TabbedView setupView() {
+ final var view = new TabbedView();
+ final var presenter = view.getPresenter();
+
+ presenter.setNumberDisplayRule(StandardDisplayRules.uncertaintyBased());
+ presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_RESTRICTION);
+ presenter.setSearchRule(PrefixSearchRule.COMMON_PREFIXES);
+ presenter.setOneWayConversionEnabled(false);
+ presenter.setShowDuplicates(true);
+
+ return view;
+ }
+
+ /**
+ * Simulates an expression conversion operation, and ensures it works
+ * properly.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testExpressionConversion() {
+ final var view = setupView();
+
+ // prepare for unit conversion
+ view.masterPane.setSelectedIndex(1);
+ view.fromEntry.setText("250.0 inch");
+ view.toEntry.setText("metre");
+
+ view.convertExpressionButton.doClick();
+
+ // check result of conversion
+ assertEquals("250.0 inch = 6.350 metre", view.expressionOutput.getText());
+ }
+
+ /**
+ * Simulates a unit conversion operation, and ensures it works properly.
+ *
+ * @since v0.4.0
+ * @since 2022-07-17
+ */
+ @Test
+ void testUnitConversion() {
+ final var view = setupView();
+
+ // prepare for unit conversion
+ view.masterPane.setSelectedIndex(0);
+ view.dimensionSelector.setSelectedItem("Length");
+ view.fromSearch.getSearchList().setSelectedValue("inch", true);
+ view.toSearch.getSearchList().setSelectedValue("metre", true);
+ view.valueInput.setText("250.0");
+
+ view.convertUnitButton.doClick();
+
+ // check result of conversion
+ assertEquals("250.0 inch = 6.350 metre", view.unitOutput.getText());
+ }
+
+}