diff options
82 files changed, 4184 insertions, 3224 deletions
@@ -25,6 +25,6 @@ <attribute name="module" value="true"/> </attributes> </classpathentry> - <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/> <classpathentry kind="output" path="bin/default"/> </classpath> diff --git a/CHANGELOG.org b/CHANGELOG.org index 9696497..01b3b34 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,5 +1,23 @@ * Changelog All notable changes in this project will be shown in this file. +** v1.0.0 - [2025-06-15 Sun] +This version is the first one with a stable public API; as per Semantic Versioning all backwards incompatible changes will be put on a major version. +*** Added +- *Allowed conversion to a sum of units (e.g. 4/3 ft \rightarrow 1 ft + 4 in).* +- *Allowed exponents on units to be non-integer numbers.* + The resulting exponents are rounded to the nearest integer, and the user is warned if this rounding changes the value by more than normal floating-point error. +- *Added the ability to change the language of 7Units's UI.* + This does not affect the names of units, prefixes and dimensions. +- Added more information to the loading-success message, and added it to the about tab. +- Added the ability to not use the default data files. +- Added the ability to generate Javadoc automatically. +*** Changed +- *Errors in unit/dimension files are shown in popups, rather than crashing the program.* +- Significantly increased number and coverage of automated tests (~./gradlew test~). +*** Fixed +- Fixed encoding of \pm character in values with uncertainty. +- ExpressionParser uses the correct order internally. + /Note: this failure was only visible if you call the submethods; the public method fixed the problem on its own. This was fixed primarily to improve testing./ ** v0.5.0 - [2024-03-24 Sun] *** Added - *Added specifications for all types data files used by 7Units.* @@ -1,18 +1,35 @@ -* 7Units Version 0.5.0 - (this project uses Semantic Versioning) +* 7Units Version 1.0.0 +This project uses Semantic Versioning, and its public API is all public code in ~src/main/java~. + +[[http://unmaintained.tech/][http://unmaintained.tech/badge.svg]] ** 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. +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. + +7Units started as a high school project, but over my bachelor's degree I continued working on it from time to time to perfect it. - Check the [[./docs/manual.pdf][user manual]] for information on how to use the program. +Check the [[./docs/manual.pdf][user manual]] for information on how to use the program. ** Features - - Convert between units and between expressions of units - - Any linear or base units can use unit prefixes (including non-metric units!) - - Units and prefixes are defined in an editable data file, in a simple and intuitive format. - - Viewer and Prefix Viewer which allow you to search through all of the available units and prefixes and learn details about them. - - All of SI included in default text file - - Customizable rounding rules +- Convert between units and between expressions of units +- Any linear or base units can use unit prefixes (including non-metric units!) +- Units and prefixes are defined in an editable data file, in a simple and intuitive format. +- Viewer and Prefix Viewer which allow you to search through all of the available units and prefixes and learn details about them. +- All of SI included in default text file +- Customizable rounding rules +- Multiple languages, with ability to add additional custom language files ** Screenshots - #+CAPTION: The main conversion interface - [[./screenshots/main-interface-dimension-converter.png]] - #+CAPTION: A secondary, more complicated conversion option - [[./screenshots/main-interface-expression-converter.png]] +#+CAPTION: The main conversion interface +[[./screenshots/main-interface-dimension-converter.png]] +#+CAPTION: A secondary, more complicated conversion option +[[./screenshots/main-interface-expression-converter.png]] +** How to Use 7Units +7Units uses Gradle as its build system. + +- To build 7Units: ~./gradlew build~. + This creates a test report at ~build/reports/tests/test/index.html~ and a Jacoco code coverage report at ~build/reports/jacoco/test/html/index.html~. +- To run 7Units: ~./gradlew run~. +- To generate Javadoc: ~./gradlew javadoc~. + The Javadoc will be at ~build/docs/javadoc/index.html~. +- To create a JAR: ~./gradlew jar~. + The JAR will be at ~build/libs/sevenUnits.jar~. + +Instructions on using the program itself can be found at ~docs/manual.pdf~. diff --git a/build.gradle b/build.gradle index 8b90060..929f1c2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { - id "java" - id "application" - id "jacoco" + id "java" + id "application" + id "jacoco" } java { @@ -11,13 +11,13 @@ java { mainClassName = "sevenUnitsGUI.Main" repositories { - mavenCentral() + mavenCentral() } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' } jar { @@ -42,5 +42,12 @@ jacocoTestReport { dependsOn test } +javadoc { + destinationDir = file("${buildDir}/docs/javadoc") + // https://gist.github.com/claudioaltamura/aba1f6506a53b9f5499fd507abd572df + (options as StandardJavadocDocletOptions) + .tags("apiNote:a:API Note:", "implSpec:a:Implementation Requirements:", "implNote:a:Implementation Note:") +} + run { } diff --git a/docs/data_spec.org b/docs/data_spec.org index c75eaa7..d251230 100644 --- a/docs/data_spec.org +++ b/docs/data_spec.org @@ -1,6 +1,6 @@ #+TITLE: 7Units Datafile Specification -#+SUBTITLE: For Version 0.5.0 -#+DATE: 2024 March 23 +#+SUBTITLE: For Version 1.0.0 +#+DATE: 2025 June 1 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX: \newpage @@ -35,5 +35,9 @@ If a line's name part ends in the ASCII dash (~-~, 0x2D), it defines a prefix in Dimension files give names to unit dimensions, so they can be selected in the unit converter to determine which units are shown. Dimension files are similar to unit and prefix files, except that they use a different set of base /dimensions/ (defined with ! as usual), and that addition and subtraction are not supported in dimension expressions. * Metric Exception Files Metric exception files list exceptions to the One-Way Conversion rule. Units included in these files are always shown on both sides of the unit converter, even if One Way Conversion is enabled. They are just a list of units, one per line. Ignored lines and comments work the same way as in other data files. +* Locale Files +A locale file contains the data to translate 7Units into another language. Each line is in the form ~key=value~. A sample locale file, with all available keys, can be found in ~src/resources/locales/en.txt~. + +To translate 7Units, simply create another locale file in the ~locales~ subdirectory of your 7Units configuration directory. * Configuration Files A configuration file contains one line for each configuration setting, in the format ~key=value~. Check the user manual for the list of configurable setting keys. Ignored lines and comments work the same way as in other data files. diff --git a/docs/data_spec.pdf b/docs/data_spec.pdf Binary files differindex 35e01f8..cae6592 100644 --- a/docs/data_spec.pdf +++ b/docs/data_spec.pdf diff --git a/docs/data_spec.tex b/docs/data_spec.tex index 879d62c..4e2475d 100644 --- a/docs/data_spec.tex +++ b/docs/data_spec.tex @@ -1,4 +1,4 @@ -% Created 2024-03-24 Sun 13:16 +% Created 2025-06-01 Sun 19:42 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -13,15 +13,15 @@ \usepackage{capt-of} \usepackage{hyperref} \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} -\date{2024 March 23} +\date{2025 June 1} \title{7Units Datafile Specification\\\medskip -\large For Version 0.5.0} +\large For Version 1.0.0} \hypersetup{ pdfauthor={}, pdftitle={7Units Datafile Specification}, pdfkeywords={}, pdfsubject={}, - pdfcreator={Emacs 29.2 (Org mode 9.6.15)}, + pdfcreator={Emacs 29.3 (Org mode 9.6.15)}, pdflang={English}} \begin{document} @@ -30,7 +30,7 @@ \newpage \section{Unit and Prefix Files} -\label{sec:org3f0bfdf} +\label{sec:orgc529fa1} Unit and prefix files specify the units and prefixes that are available to 7Units. Their format is intended to be compatible with 7Units's inspiration, \href{https://www.gnu.org/software/units/}{GNU Units}. Each unit or prefix is specified by a line in the following format: @@ -40,7 +40,7 @@ Each unit or prefix is specified by a line in the following format: The name may not contain whitespace, but the definition can and often will. Lines can be separated by either a line feed (0x0A) or a carriage return followed by a line feed (0x0D 0x0A). \subsection{Ignored Lines \& Comments} -\label{sec:org7396c6b} +\label{sec:orga9c97a7} All of the following should be ignored: \begin{itemize} \item Any line containing only whitespace @@ -50,7 +50,7 @@ All of the following should be ignored: This allows unit and prefix files to be easily organized and divided. \subsection{Unit Expressions} -\label{sec:orga56737c} +\label{sec:orgebaa825} The definition part of a unit's line is a unit expression - the same sort of expression you would use in the complex unit converter. Expressions should be a standard mathematical expression, which can operate on either numbers or units. The following operators are supported: addition (+), subtraction (-), multiplication (\texttt{*} or no operator), division (\texttt{/} or \texttt{|}), exponentation (\texttt{\textasciicircum{}}; exponent must be a number). Brackets (\texttt{(} and \texttt{)}) may be used to change order of operations, but otherwise standard BEDMAS order is followed (exponentation first, then multiplication and division, then addition and subtraction), with two exceptions: if a number is multiplied by a unit using spaces, the multiplication will have precedence over division, and division with \texttt{|} has higher precedence than any other operator. For example, "2 m / 1 m" is equal to the dimensionless value 2, not 2 m\textsuperscript{2}. An example of a line defining a unit is: @@ -60,15 +60,20 @@ yard 9 dm + 1.4 cm + 4 mm^2 / 10 mm If the definition is an exclamation mark (\texttt{!}), this defines a base unit, which is expected to already be stored in the system. \subsection{Prefixes \& Prefix Expressions} -\label{sec:org28c98b9} +\label{sec:orgd0ef1d8} If a line's name part ends in the ASCII dash (\texttt{-}, 0x2D), it defines a prefix instead of a unit (this dash is not included in the prefix's name). Prefix expressions are like unit expressions, but the operands are prefixes and numbers, not units and numbers. Because prefixes do not have a dimension, there are no base prefixes - the exclamation mark is forbidden in prefix expressions. \section{Dimension Files} -\label{sec:org9530301} +\label{sec:orgdc0ddd9} Dimension files give names to unit dimensions, so they can be selected in the unit converter to determine which units are shown. Dimension files are similar to unit and prefix files, except that they use a different set of base \emph{dimensions} (defined with ! as usual), and that addition and subtraction are not supported in dimension expressions. \section{Metric Exception Files} -\label{sec:org79afe10} +\label{sec:org0587104} Metric exception files list exceptions to the One-Way Conversion rule. Units included in these files are always shown on both sides of the unit converter, even if One Way Conversion is enabled. They are just a list of units, one per line. Ignored lines and comments work the same way as in other data files. +\section{Locale Files} +\label{sec:orgdc95be9} +A locale file contains the data to translate 7Units into another language. Each line is in the form \texttt{key=value}. A sample locale file, with all available keys, can be found in \texttt{src/resources/locales/en.txt}. + +To translate 7Units, simply create another locale file in the \texttt{locales} subdirectory of your 7Units configuration directory. \section{Configuration Files} -\label{sec:orgf3f6b9a} +\label{sec:orgad1bd81} A configuration file contains one line for each configuration setting, in the format \texttt{key=value}. Check the user manual for the list of configurable setting keys. Ignored lines and comments work the same way as in other data files. \end{document} diff --git a/docs/design.org b/docs/design.org index 65ee651..3764adb 100644 --- a/docs/design.org +++ b/docs/design.org @@ -1,6 +1,6 @@ #+TITLE: 7Units Design Document -#+SUBTITLE: For version 0.5.0 -#+DATE: 2024 March 23 +#+SUBTITLE: For version 1.0.0 +#+DATE: 2025 June 1 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX_HEADER: \usepackage{xurl} #+LaTeX: \newpage @@ -66,7 +66,6 @@ - Note that any operations will return a unit without name(s) or a symbol. All unit classes have a ~withName~ method that returns a copy of them with different names and/or a different symbol (all of this info is contained in the ~NameSymbol~ class) There are a few more classes which play small roles in the unit system: - - Unitlike :: A class that is like a unit, but its "value" can be any class. The only use of this class right now is to implement ~MultiUnit~, a combination of units (like "foot + inch", commonly used in North America for measuring height); its "value" is a list of numbers. - FunctionalUnit :: A convenience class that implements the two conversion functions of ~Unit~ using ~DoubleUnaryOperator~ instances. This is used internally to implement degrees Celsius and Fahrenheit. There is also a version of this for ~Unitlike~, ~FunctionalUnitlike~. - UnitValue :: A value expressed as a certain unit (such as "7 inches"). This class is used by the simple unit converter to represent units. You can convert them between units. There are also versions of this for ~LinearUnit~ and ~Unitlike~. - Metric :: A static utility class with instances of all of the SI named units, the 9 base dimensions, SI prefixes, some common prefixed units like the kilometre, and a few non-SI units used commonly with them. diff --git a/docs/design.pdf b/docs/design.pdf Binary files differindex 00703d8..764bf3b 100644 --- a/docs/design.pdf +++ b/docs/design.pdf diff --git a/docs/design.tex b/docs/design.tex index c850412..9779a9e 100644 --- a/docs/design.tex +++ b/docs/design.tex @@ -1,4 +1,4 @@ -% Created 2024-03-24 Sun 13:15 +% Created 2025-06-01 Sun 19:45 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -14,15 +14,15 @@ \usepackage{hyperref} \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} \usepackage{xurl} -\date{2024 March 23} +\date{2025 June 1} \title{7Units Design Document\\\medskip -\large For version 0.5.0} +\large For version 1.0.0} \hypersetup{ pdfauthor={}, pdftitle={7Units Design Document}, pdfkeywords={}, pdfsubject={}, - pdfcreator={Emacs 29.2 (Org mode 9.6.15)}, + pdfcreator={Emacs 29.3 (Org mode 9.6.15)}, pdflang={English}} \begin{document} @@ -32,35 +32,35 @@ \newpage \section{Introduction} -\label{sec:orgdac8c13} +\label{sec:orge8f18c8} 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. \section{System Overview} -\label{sec:org2a8e77a} +\label{sec:org3dec32e} \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:org80ac84e} +\label{sec:org5f2f6d8} 7Units splits its code into three main packages: \begin{description} -\item[{\texttt{sevenUnits.unit}}] The \hyperref[sec:orgc2400d6]{unit system} +\item[{\texttt{sevenUnits.unit}}] The \hyperref[sec:orgdc040fb]{unit system} \item[{\texttt{sevenUnits.utils}}] Extra classes that aid the unit system. -\item[{\texttt{sevenUnitsGUI}}] The \hyperref[sec:org261b06e]{front end} code +\item[{\texttt{sevenUnitsGUI}}] The \hyperref[sec:org1879a96]{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:org5910307} +\label{sec:orgea198c1} \begin{description} -\item[{\hyperref[sec:org946a4e5]{sevenUnits.unit.Unit}}] The class representing a unit -\item[{\hyperref[sec:orgac71770]{sevenUnits.unit.UnitDatabase}}] A class that stores collections of units, prefixes and dimensions. -\item[{\hyperref[sec:org57b8a42]{sevenUnitsGUI.View}}] The class that handles interaction between the user and the program. -\item[{\hyperref[sec:orga668171]{sevenUnitsGUI.Presenter}}] The class that handles communication between the \texttt{View} and the unit system. +\item[{\hyperref[sec:org097d2bc]{sevenUnits.unit.Unit}}] The class representing a unit +\item[{\hyperref[sec:orgdd255ff]{sevenUnits.unit.UnitDatabase}}] A class that stores collections of units, prefixes and dimensions. +\item[{\hyperref[sec:org1047a59]{sevenUnitsGUI.View}}] The class that handles interaction between the user and the program. +\item[{\hyperref[sec:org17c3fce]{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:orgbbad9d5} +\label{sec:orgfe8e937} \begin{figure}[htbp] \centering \includegraphics[width=.9\linewidth]{./diagrams/convert-units.plantuml.png} @@ -75,7 +75,7 @@ \end{enumerate} \newpage \subsection{Process of Expression Conversion} -\label{sec:org52749d5} +\label{sec:orgbdb360e} The process of expression conversion is similar to that of unit conversion. \begin{figure}[htbp] \centering @@ -91,7 +91,7 @@ The process of expression conversion is similar to that of unit conversion. \end{enumerate} \newpage \section{Unit System Design} -\label{sec:orgc2400d6} +\label{sec:orgdc040fb} 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. @@ -102,11 +102,11 @@ Here is a class diagram of the system. Unimportant methods, methods inherited f \end{figure} \newpage \subsection{Dimensions} -\label{sec:orgb21aaed} -Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:org16bc96f]{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:orgd0c0232} +Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:org5bd9128]{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:org946a4e5} -Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:org16bc96f]{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:org097d2bc} +Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:org5bd9128]{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. @@ -123,7 +123,6 @@ However, most units are instances of \texttt{LinearUnit}, another subclass of \t There are a few more classes which play small roles in the unit system: \begin{description} -\item[{Unitlike}] A class that is like a unit, but its "value" can be any class. The only use of this class right now is to implement \texttt{MultiUnit}, a combination of units (like "foot + inch", commonly used in North America for measuring height); its "value" is a list of numbers. \item[{FunctionalUnit}] A convenience class that implements the two conversion functions of \texttt{Unit} using \texttt{DoubleUnaryOperator} instances. This is used internally to implement degrees Celsius and Fahrenheit. There is also a version of this for \texttt{Unitlike}, \texttt{FunctionalUnitlike}. \item[{UnitValue}] A value expressed as a certain unit (such as "7 inches"). This class is used by the simple unit converter to represent units. You can convert them between units. There are also versions of this for \texttt{LinearUnit} and \texttt{Unitlike}. \item[{Metric}] A static utility class with instances of all of the SI named units, the 9 base dimensions, SI prefixes, some common prefixed units like the kilometre, and a few non-SI units used commonly with them. @@ -131,20 +130,20 @@ 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:orgede1b85} +\label{sec:orge1a285c} 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:orgac71770} +\label{sec:orgdd255ff} 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:org02e3ff1} -Each \texttt{UnitDatabase} instance has four \hyperref[sec:org7296a14]{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:orgd4e45a3} +Each \texttt{UnitDatabase} instance has four \hyperref[sec:org6a2c74c]{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:org676148d} +\label{sec:org6cff6b9} 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}. @@ -152,10 +151,10 @@ Unit files contain data about units and prefixes. Each line contains the name o Dimension files are similar, only for dimensions instead of units and prefixes. \newpage \section{Front-End Design} -\label{sec:org261b06e} +\label{sec:org1879a96} 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:org57b8a42} +\label{sec:org1047a59} 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. @@ -169,10 +168,10 @@ There are currently two implementations of the \texttt{View}: \end{description} Both of these \texttt{View} implementations implement \texttt{UnitConversionView} and \texttt{ExpressionConversionView}. \subsection{The Presenter} -\label{sec:orga668171} +\label{sec:org17c3fce} 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:org81f6f8a} +\label{sec:org159ba12} 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. @@ -182,7 +181,7 @@ The \texttt{Presenter} has a set of function-object rules that determine some of 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:orga5b57ce} +\label{sec:org4d2c502} 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. @@ -195,15 +194,15 @@ The frontend has many miscellaneous utility classes. Many of them are package-p \end{description} \newpage \section{Utility Classes} -\label{sec:org3686e64} +\label{sec:org97c6f74} 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:org16bc96f} +\label{sec:org5bd9128} 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:org7296a14} +\label{sec:org6a2c74c} 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 /). @@ -224,13 +223,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:orgfd8c723} +\label{sec:orgfec3253} 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:org32d7d09} +\label{sec:orgc8ae9c7} 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/manual.org b/docs/manual.org index 3c6de1c..bc58ceb 100644 --- a/docs/manual.org +++ b/docs/manual.org @@ -1,78 +1,81 @@ #+TITLE: 7Units User Manual -#+SUBTITLE: For Version 0.5.0 -#+DATE: 2024 March 23 +#+SUBTITLE: For Version 1.0.0 +#+DATE: 2025 June 1 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX: \newpage * Introduction and Purpose - 7Units is a program that can be used to convert units. This document outlines how to use the program. +7Units is a program that can be used to convert units. This document outlines how to use the program. * 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+ required -# installation instructions go here - wait until git repository is fixed/set up +- 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+ required +- Gradle required +- To run the software, simply run './gradlew run' in the main directory. #+LaTeX: \newpage * How to Use 7Units ** Simple Unit Conversion - 1. Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure [[main-interface-dimension]]: - #+CAPTION: Taken in version 0.3.0 - #+ATTR_LaTeX: :height 250px - #+name: main-interface-dimension - [[../screenshots/main-interface-dimension-converter.png]] - 2. Use the dropdown box at the top to select what kind of unit to convert (length, mass, speed, etc.) - 3. Select the unit to convert /from/ on the left. - 4. Select the unit to convert /to/ on the right. - 5. Enter the value to convert in the box above the convert button. The program should look something like in figure [[sample-conversion-dimension]]: - #+CAPTION: This image, taken in version 0.3.0, shows the user about to convert 35 miles to kilometres. - #+attr_latex: :height 250px - #+name: sample-conversion-dimension - [[../screenshots/sample-conversion-dimension-converter.png]] - 6. Press the "Convert" button. The result will be shown below the "Convert" button. This is shown in figure [[sample-results-dimension]] - #+CAPTION: The result of the above conversion - #+attr_latex: :height 250px - #+name: sample-results-dimension - [[../screenshots/sample-conversion-results-dimension-converter.png]] +1. Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure [[main-interface-dimension]]: + #+CAPTION: Taken in version 0.3.0 + #+ATTR_LaTeX: :height 250px + #+name: main-interface-dimension + [[../screenshots/main-interface-dimension-converter.png]] +2. Use the dropdown box at the top to select what kind of unit to convert (length, mass, speed, etc.) +3. Select the unit to convert /from/ on the left. +4. Select the unit to convert /to/ on the right. +5. Enter the value to convert in the box above the convert button. The program should look something like in figure [[sample-conversion-dimension]]: + #+CAPTION: This image, taken in version 0.3.0, shows the user about to convert 35 miles to kilometres. + #+attr_latex: :height 250px + #+name: sample-conversion-dimension + [[../screenshots/sample-conversion-dimension-converter.png]] +6. Press the "Convert" button. The result will be shown below the "Convert" button. This is shown in figure [[sample-results-dimension]] + #+CAPTION: The result of the above conversion + #+attr_latex: :height 250px + #+name: sample-results-dimension + [[../screenshots/sample-conversion-results-dimension-converter.png]] ** Complex Unit Conversion - 1. Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure [[main-interface-expression]]: - #+CAPTION: Taken in version 0.3.0 - #+attr_latex: :height 250px - #+name: main-interface-expression - [[../screenshots/main-interface-expression-converter.png]] - 2. Enter a [[*Unit Expressions][unit expression]] in the From box. This can be something like "~7 km~" or "~6 ft - 2 in~" or "~3 kg m + 9 lb ft + (35 mm)^2 * (85 oz) / (20 in)~". - 3. Enter a unit name (or another unit expression) in the To box. - 4. 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). - #+CAPTION: A sample calculation. Divides ~100 km~ by ~35 km/h~ and converts the result to minutes. This could be used to calculate how long (in minutes) it takes to go 100 kilometres at a speed of 35 km/h. - #+attr_latex: :height 250px - #+name: sample-results-expression - [[../screenshots/sample-conversion-results-expression-converter.png]] +1. Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure [[main-interface-expression]]: + #+CAPTION: Taken in version 0.3.0 + #+attr_latex: :height 250px + #+name: main-interface-expression + [[../screenshots/main-interface-expression-converter.png]] +2. Enter a [[*Unit Expressions][unit expression]] in the From box. This can be something like "~7 km~" or "~6 ft - 2 in~" or "~3 kg m + 9 lb ft + (35 mm)^2 * (85 oz) / (20 in)~". +3. Enter a unit name (or another unit expression) in the To box. +4. 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). + #+CAPTION: A sample calculation. Divides ~100 km~ by ~35 km/h~ and converts the result to minutes. This could be used to calculate how long (in minutes) it takes to go 100 kilometres at a speed of 35 km/h. + #+attr_latex: :height 250px + #+name: sample-results-expression + [[../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.4.0 - #+ATTR_LaTeX: :height 250px - [[../screenshots/main-interface-settings.png]] +All settings can be accessed in the tab with the gear icon. +#+CAPTION: The settings menu, as of version 0.4.0 +#+ATTR_LaTeX: :height 250px +[[../screenshots/main-interface-settings.png]] ** Rounding Settings - These settings control how the output of a unit conversion is rounded. - - Fixed Precision :: Round to a fixed number of [[https://en.wikipedia.org/wiki/Significant_figures][significant digits]]. The number of significant digits is controlled by the precision slider below. - - Fixed Decimal Places :: Round to a fixed number of digits after the decimal point. The number of decimal places is also controlled by the precision slider below. - - Scientific Precision :: Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider. +These settings control how the output of a unit conversion is rounded. +- Fixed Precision :: Round to a fixed number of [[https://en.wikipedia.org/wiki/Significant_figures][significant digits]]. The number of significant digits is controlled by the precision slider below. +- Fixed Decimal Places :: Round to a fixed number of digits after the decimal point. The number of decimal places is also controlled by the precision slider below. +- Scientific Precision :: Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider. ** Prefix Repetition Settings - These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre) - - No Repetition :: Units may only have one prefix. - - No Restriction :: Units may have any number of prefixes. - - Complex Repetition :: A complex rule which makes it so that each power of 10 has one and only one prefix combination. Units may have the following prefixes: - - one of: centi, deci, deca, hecto - - one of: zepto, atto, femto, pico, nano, micro, milli, kilo, mega, giga, tera, peta, exa, zetta - - 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) +These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre) +- No Repetition :: Units may only have one prefix. +- No Restriction :: Units may have any number of prefixes. +- Complex Repetition :: A complex rule which makes it so that each power of 10 has one and only one prefix combination. Units may have the following prefixes: + - one of: centi, deci, deca, hecto + - one of: zepto, atto, femto, pico, nano, micro, milli, kilo, mega, giga, tera, peta, exa, zetta + - 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. +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. +- 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. +- Use Default Datafiles :: If unchecked, the default units, prefixes and diension names will not be loaded (except for SI base units and their dimensions). +- Locale :: Which language is used for the interface of 7Units. Custom locales can be added. ** Configuration File The settings are saved in a configuration file. On Windows, this is located at \\ ~%USERPROFILE%/AppData/Local/SevenUnits/config.txt~. On other operating systems, this is located at ~$HOME/.config/SevenUnits/config.txt~. The directory containing the ~SevenUnits~ directory can be overridden with the environment variables ~$LOCALAPPDATA~ on Windows or ~$XDG_CONFIG_HOME~ elsewhere. @@ -87,19 +90,22 @@ The possible setting names are: - ~include_duplicates~ :: Whether duplicate units should be shown; can be either ~true~ or ~false~. - ~search_prefix_rule~ :: The prefix search rule; can be ~NO_PREFIXES~, ~COMMON_PREFIXES~, \\ or ~ALL_METRIC_PREFIXES~. +- ~use_default_datafiles~ :: Whether default datafiles should be used; can be either ~true~ or ~false~. +- ~locale~ :: The name of the locale to use. + This is the name of a locale file, excluding the extension, in one of the directories specified in the data specification. For example, if this is 'en', 7Units will look for a file called 'en.txt' in either ~src/main/resources/locales/~ or ~[CONFIG]/locales/~. You can also use the special setting names ~custom_unit_file~, ~custom_dimension_file~ and ~custom_exception_file~ to add custom units, dimensions and metric exceptions to the system. These files use the same format as the standard files. These setting names can be used more than once to include multiple unit, dimension or exception files. * Appendices ** Unit Expressions - 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): - - Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent - - Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"). - You can also divide with ~|~ to create fractions. Using ~|~ instead of ~/~ gives the division a higher precedence than any other operator. For example, "2|5^2" evaluates to 4/25, not 2/25. - - Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. +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): +- Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent. +- Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"), and multiplication using spaces has higher precedence than division. Therefore, "2 m / 1 m" is the number 2, while "2 * m / 1 * m" is equal to 2 m^2. + You can also divide with ~|~ to create fractions. Using ~|~ instead of ~/~ gives the division a higher precedence than any other operator. For example, "2|5^2" evaluates to 4/25, not 2/25. +- Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. *You must use spaces between terms when adding or subtracting - "1 + 2" is valid, "1+2" is not. This only applies to addition and subtraction - other operators do not need spaces between them.* This is done to avoid complex rules when working with negative numbers and e-notation. - 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 \deg C + 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 \deg{}C ** Other Expressions - 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. +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. * Footnotes -[fn:1] 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. +[fn:1] 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. diff --git a/docs/manual.pdf b/docs/manual.pdf Binary files differindex 5fcc115..ca31104 100644 --- a/docs/manual.pdf +++ b/docs/manual.pdf diff --git a/docs/manual.tex b/docs/manual.tex index e16198f..8ab09d5 100644 --- a/docs/manual.tex +++ b/docs/manual.tex @@ -1,4 +1,4 @@ -% Created 2024-03-24 Sun 13:16 +% Created 2025-06-07 Sat 18:16 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -13,15 +13,15 @@ \usepackage{capt-of} \usepackage{hyperref} \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} -\date{2024 March 23} +\date{2025 June 1} \title{7Units User Manual\\\medskip -\large For Version 0.5.0} +\large For Version 1.0.0} \hypersetup{ pdfauthor={}, pdftitle={7Units User Manual}, pdfkeywords={}, pdfsubject={}, - pdfcreator={Emacs 29.2 (Org mode 9.6.15)}, + pdfcreator={Emacs 29.3 (Org mode 9.6.15)}, pdflang={English}} \begin{document} @@ -30,21 +30,22 @@ \newpage \section{Introduction and Purpose} -\label{sec:orgc09fcc7} +\label{sec:orgf5013f4} 7Units is a program that can be used to convert units. This document outlines how to use the program. \section{System Requirements} -\label{sec:orga902335} +\label{sec:org9c3bf6a} \begin{itemize} \item Works on all major operating systems \\[0pt] \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+ required +\item Gradle required +\item To run the software, simply run './gradlew run' in the main directory. \end{itemize} - \newpage \section{How to Use 7Units} -\label{sec:orgdec078f} +\label{sec:org6a030cf} \subsection{Simple Unit Conversion} -\label{sec:org785ebcb} +\label{sec:org4406ca2} \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] @@ -69,7 +70,7 @@ \end{figure} \end{enumerate} \subsection{Complex Unit Conversion} -\label{sec:org75a0192} +\label{sec:org721af5f} \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] @@ -77,7 +78,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:org3724d84]{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:orgfffe912]{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] @@ -87,7 +88,7 @@ \end{figure} \end{enumerate} \section{7Units Settings} -\label{sec:orgae2806f} +\label{sec:org437288e} All settings can be accessed in the tab with the gear icon. \begin{figure}[htbp] \centering @@ -95,7 +96,7 @@ All settings can be accessed in the tab with the gear icon. \caption{The settings menu, as of version 0.4.0} \end{figure} \subsection{Rounding Settings} -\label{sec:org6d3e49c} +\label{sec:orgc275b0f} 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. @@ -103,7 +104,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:org9aa98f8} +\label{sec:org1789e5a} 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. @@ -118,7 +119,7 @@ These settings control when you are allowed to repeat unit prefixes (e.g. kiloki \end{itemize} \end{description} \subsection{Search Settings} -\label{sec:org2745ba0} +\label{sec:orgfcf1ac1} 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. @@ -126,13 +127,15 @@ These settings control which prefixes are shown in the "Convert Units" tab. Onl \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:orgeabb2df} +\label{sec:org2a48ec0} \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. +\item[{Use Default Datafiles}] If unchecked, the default units, prefixes and diension names will not be loaded (except for SI base units and their dimensions). +\item[{Locale}] Which language is used for the interface of 7Units. Custom locales can be added. \end{description} \subsection{Configuration File} -\label{sec:org4cc2874} +\label{sec:orgd976cbf} The settings are saved in a configuration file. On Windows, this is located at \\[0pt] \texttt{\%USERPROFILE\%/AppData/Local/SevenUnits/config.txt}. On other operating systems, this is located at \texttt{\$HOME/.config/SevenUnits/config.txt}. The directory containing the \texttt{SevenUnits} directory can be overridden with the environment variables \texttt{\$LOCALAPPDATA} on Windows or \texttt{\$XDG\_CONFIG\_HOME} elsewhere. @@ -147,23 +150,26 @@ or \texttt{COMPLEX\_REPETITION}. \item[{\texttt{include\_duplicates}}] Whether duplicate units should be shown; can be either \texttt{true} or \texttt{false}. \item[{\texttt{search\_prefix\_rule}}] The prefix search rule; can be \texttt{NO\_PREFIXES}, \texttt{COMMON\_PREFIXES}, \\[0pt] or \texttt{ALL\_METRIC\_PREFIXES}. +\item[{\texttt{use\_default\_datafiles}}] Whether default datafiles should be used; can be either \texttt{true} or \texttt{false}. +\item[{\texttt{locale}}] The name of the locale to use. +This is the name of a locale file, excluding the extension, in one of the directories specified in the data specification. For example, if this is 'en', 7Units will look for a file called 'en.txt' in either \texttt{src/main/resources/locales/} or \texttt{[CONFIG]/locales/}. \end{description} You can also use the special setting names \texttt{custom\_unit\_file}, \texttt{custom\_dimension\_file} and \texttt{custom\_exception\_file} to add custom units, dimensions and metric exceptions to the system. These files use the same format as the standard files. These setting names can be used more than once to include multiple unit, dimension or exception files. \section{Appendices} -\label{sec:org60385a7} +\label{sec:orgec4e8a9} \subsection{Unit Expressions} -\label{sec:org3724d84} +\label{sec:orgfffe912} 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 -\item Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"). +\item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent. +\item Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"), and multiplication using spaces has higher precedence than division. Therefore, "2 m / 1 m" is the number 2, while "2 * m / 1 * m" is equal to 2 m\textsuperscript{2}. You can also divide with \texttt{|} to create fractions. Using \texttt{|} instead of \texttt{/} gives the division a higher precedence than any other operator. For example, "2|5\textsuperscript{2}" evaluates to 4/25, not 2/25. -\item Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. -\end{itemize} +\item Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. \textbf{You must use spaces between terms when adding or subtracting - "1 + 2" is valid, "1+2" is not. This only applies to addition and subtraction - other operators do not need spaces between them.} This is done to avoid complex rules when working with negative numbers and e-notation. -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 +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 +\end{itemize} \subsection{Other Expressions} -\label{sec:orgc72a672} +\label{sec:org5d2d129} 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/docs/roadmap.org b/docs/roadmap.org deleted file mode 100644 index 5a3888f..0000000 --- a/docs/roadmap.org +++ /dev/null @@ -1,22 +0,0 @@ -* Version 1.0.0 Roadmap -Here is a list of the unfinished requirements for version 1.0.0. When everything here is met, I intend to release version 1.0.0 and consider 7Units complete (for the most part). - -These requirements are subject to change. I intend to finish version 1.0.0 by [2025-04-27 Sun]. - -Feature Requirements: -- 7Units should be able to parse unit files from [[https://www.gnu.org/software/units/][GNU Units]], the program that inspired it. - (It should not be required to handle features that aren't in 7Units; those definitions should be ignored with a warning) -- 7Units's expression converter should support most or all of the conversion features supported by GNU Units: - - Converting to sums of units (it should also be possible to do this in the unit converter with preset combinations) - - Non-integer exponents - - (/Optional/) Inverse nonlinear conversion with the tilde prefix - - (/Optional/) Nonlinear units should be specifiable in unit files. - - /Any other feature not listed should be considered optional./ -- (/Optional/) It should be possible to add, edit and remove units and prefixes from the GUI unit and prefix viewers. - -Documentation/Testing Requirements: -- 7Units should be fully documented. -- 7Units should have automated testing with a code coverage of at least 2/3 (ideally at least 5/6). - -Other Requirements -- The public API of 7Units should be finalized and well-designed. diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 9c23c49..e74a99b 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021-2024 Adrien Hopkins + * Copyright (C) 2021-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -20,15 +20,14 @@ import sevenUnits.utils.SemanticVersionNumber; /** * Information about 7Units - * - * @since 0.3.1 + * * @since 2021-06-28 + * @since v0.3.1 */ public final class ProgramInfo { - - /** The version number (0.5.0) */ + /** The version number (1.0.0) */ public static final SemanticVersionNumber VERSION = SemanticVersionNumber - .stableVersion(0, 5, 0); + .stableVersion(1, 0, 0); private ProgramInfo() { // this class is only for static variables, you shouldn't be able to diff --git a/src/main/java/sevenUnits/package-info.java b/src/main/java/sevenUnits/package-info.java index 33b98fc..dc27e1f 100644 --- a/src/main/java/sevenUnits/package-info.java +++ b/src/main/java/sevenUnits/package-info.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019-2021 Adrien Hopkins + * Copyright (C) 2019-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ */ /** * A program that converts units. - * + * * @author Adrien Hopkins * @version v0.3.0 * @since 2019-01-25 diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java index 3f1f75f..11b822e 100644 --- a/src/main/java/sevenUnits/unit/BaseDimension.java +++ b/src/main/java/sevenUnits/unit/BaseDimension.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019, 2022 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -23,26 +23,26 @@ import sevenUnits.utils.Nameable; /** * A dimension that defines a {@code BaseUnit} - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public final class BaseDimension implements Nameable { /** * Gets a {@code BaseDimension} with the provided name and symbol. - * + * * @param name name of dimension * @param symbol symbol used for dimension * @return dimension * @since 2019-10-16 + * @since v0.3.0 */ public static BaseDimension valueOf(final String name, final String symbol) { return new BaseDimension(name, symbol); } - /** - * The name of the dimension. - */ + /** The name of the dimension. */ private final String name; /** * The symbol used by the dimension. Symbols should be short, generally one @@ -52,20 +52,19 @@ public final class BaseDimension implements Nameable { /** * Creates the {@code BaseDimension}. - * + * * @param name name of unit * @param symbol symbol of unit * @throws NullPointerException if any argument is null * @since 2019-10-16 + * @since v0.3.0 */ private BaseDimension(final String name, final String symbol) { this.name = Objects.requireNonNull(name, "name must not be null."); this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); } - /** - * @since v0.4.0 - */ + /** @since v0.4.0 */ @Override public NameSymbol getNameSymbol() { return NameSymbol.of(this.name, this.symbol); diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java index fe85a7b..13e76d9 100644 --- a/src/main/java/sevenUnits/unit/BaseUnit.java +++ b/src/main/java/sevenUnits/unit/BaseUnit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -28,19 +28,21 @@ import sevenUnits.utils.NameSymbol; * Note that BaseUnits <b>must</b> have names and symbols. This is because they * are used for toString code. Therefore, the Optionals provided by * {@link #getPrimaryName} and {@link #getSymbol} will always contain a value. - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public final class BaseUnit extends Unit { /** * Gets a base unit from the dimension it measures, its name and its symbol. - * + * * @param dimension dimension measured by this unit * @param name name of unit * @param symbol symbol of unit * @return base unit * @since 2019-10-16 + * @since v0.3.0 */ public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) { @@ -49,31 +51,32 @@ public final class BaseUnit extends Unit { /** * Gets a base unit from the dimension it measures, its name and its symbol. - * - * @param dimension dimension measured by this unit - * @param name name of unit - * @param symbol symbol of unit + * + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit + * @param otherNames other possible names of unit * @return base unit * @since 2019-10-21 + * @since v0.3.0 */ public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol, final Set<String> otherNames) { return new BaseUnit(dimension, name, symbol, otherNames); } - /** - * The dimension measured by this base unit. - */ + /** The dimension measured by this base unit. */ private final BaseDimension dimension; /** * Creates the {@code BaseUnit}. - * + * * @param dimension dimension of unit * @param primaryName name of unit * @param symbol symbol of unit * @throws NullPointerException if any argument is null * @since 2019-10-16 + * @since v0.3.0 */ private BaseUnit(final BaseDimension dimension, final String primaryName, final String symbol, final Set<String> otherNames) { @@ -86,9 +89,10 @@ public final class BaseUnit extends Unit { * Returns a {@code LinearUnit} with this unit as a base and a conversion * factor of 1. This operation must be done in order to allow units to be * created with operations. - * + * * @return this unit as a {@code LinearUnit} * @since 2019-10-16 + * @since v0.3.0 */ public LinearUnit asLinearUnit() { return LinearUnit.valueOf(this.getBase(), 1); @@ -107,8 +111,9 @@ public final class BaseUnit extends Unit { /** * @return dimension * @since 2019-10-16 + * @since v0.3.0 */ - public final BaseDimension getBaseDimension() { + public BaseDimension getBaseDimension() { return this.dimension; } diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java index 69a3c05..408e9e8 100644 --- a/src/main/java/sevenUnits/unit/BritishImperial.java +++ b/src/main/java/sevenUnits/unit/BritishImperial.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -20,16 +20,21 @@ import sevenUnits.utils.NameSymbol; /** * A static utility class that contains units in the British Imperial system. - * + * * @author Adrien Hopkins * @since 2019-10-21 + * @since v0.3.0 */ +// this class is just constants, most of which are obvious from the variable name +// so no need to check for missing values +@SuppressWarnings("javadoc") public final class BritishImperial { /** * Imperial units that measure area - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Area { public static final LinearUnit SQUARE_FOOT = Length.FOOT.toExponent(2); @@ -42,9 +47,10 @@ public final class BritishImperial { /** * Imperial units that measure length - * + * * @author Adrien Hopkins * @since 2019-10-28 + * @since v0.3.0 */ public static final class Length { /** @@ -55,11 +61,18 @@ public final class BritishImperial { public static final LinearUnit FOOT = YARD.dividedBy(3); public static final LinearUnit INCH = FOOT.dividedBy(12); public static final LinearUnit THOU = INCH.dividedBy(1000); + /** A chain, equal to 22 yards. */ public static final LinearUnit CHAIN = YARD.times(22); + /** A furlong, equal to 10 chains or 220 yards. */ public static final LinearUnit FURLONG = CHAIN.times(10); + /** A mile, equal to 8 furlongs or 1760 yards. */ public static final LinearUnit MILE = FURLONG.times(8); + /** A league, equal to 3 miles. */ public static final LinearUnit LEAGUE = MILE.times(3); + /** + * A nautical mile, around 1 arcminute around the Earth's circumference. + */ public static final LinearUnit NAUTICAL_MILE = Metric.METRE.times(1852); public static final LinearUnit CABLE = NAUTICAL_MILE.dividedBy(10); public static final LinearUnit FATHOM = CABLE.dividedBy(100); @@ -70,9 +83,10 @@ public final class BritishImperial { /** * British Imperial units that measure mass. - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Mass { public static final LinearUnit POUND = Metric.GRAM.times(453.59237); @@ -88,9 +102,10 @@ public final class BritishImperial { /** * British Imperial units that measure volume - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Volume { public static final LinearUnit FLUID_OUNCE = Metric.LITRE diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java index 6de446f..1d55b42 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnit.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,30 +24,33 @@ import sevenUnits.utils.ObjectProduct; /** * A unit that uses functional objects to convert to and from its base. - * + * * @author Adrien Hopkins * @since 2019-05-22 + * @since v0.3.0 */ final class FunctionalUnit extends Unit { /** * A function that accepts a value expressed in the unit's base and returns * that value expressed in this unit. - * + * * @since 2019-05-22 + * @since v0.3.0 */ private final DoubleUnaryOperator converterFrom; /** * A function that accepts a value expressed in the unit and returns that * value expressed in the unit's base. - * + * * @since 2019-05-22 + * @since v0.3.0 */ private final DoubleUnaryOperator converterTo; /** * Creates the {@code FunctionalUnit}. - * + * * @param base unit's base * @param converterFrom function that accepts a value expressed in the unit's * base and returns that value expressed in this unit. @@ -55,6 +58,7 @@ final class FunctionalUnit extends Unit { * and returns that value expressed in the unit's base. * @throws NullPointerException if any argument is null * @since 2019-05-22 + * @since v0.3.0 */ public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, @@ -68,14 +72,16 @@ final class FunctionalUnit extends Unit { /** * Creates the {@code FunctionalUnit}. - * + * * @param base unit's base * @param converterFrom function that accepts a value expressed in the unit's * base and returns that value expressed in this unit. * @param converterTo function that accepts a value expressed in the unit * and returns that value expressed in the unit's base. + * @param ns name and symbol of resulting unit * @throws NullPointerException if any argument is null * @since 2019-05-22 + * @since v0.3.0 */ public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, @@ -89,7 +95,7 @@ final class FunctionalUnit extends Unit { /** * {@inheritDoc} - * + * * Uses {@code converterFrom} to convert. */ @Override @@ -99,7 +105,7 @@ final class FunctionalUnit extends Unit { /** * {@inheritDoc} - * + * * Uses {@code converterTo} to convert. */ @Override diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java deleted file mode 100644 index e9b4d1f..0000000 --- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (C) 2020 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.unit; - -import java.util.function.DoubleFunction; -import java.util.function.ToDoubleFunction; - -import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.ObjectProduct; - -/** - * A unitlike form that converts using two conversion functions. - * - * @since 2020-09-07 - */ -final class FunctionalUnitlike<V> extends Unitlike<V> { - /** - * A function that accepts a value in the unitlike form's base and returns a - * value in the unitlike form. - * - * @since 2020-09-07 - */ - private final DoubleFunction<V> converterFrom; - - /** - * A function that accepts a value in the unitlike form and returns a value - * in the unitlike form's base. - */ - private final ToDoubleFunction<V> converterTo; - - /** - * Creates the {@code FunctionalUnitlike}. - * - * @param base unitlike form's base - * @param converterFrom function that accepts a value in the unitlike form's - * base and returns a value in the unitlike form. - * @param converterTo function that accepts a value in the unitlike form - * and returns a value in the unitlike form's base. - * @throws NullPointerException if any argument is null - * @since 2019-05-22 - */ - protected FunctionalUnitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns, - DoubleFunction<V> converterFrom, ToDoubleFunction<V> converterTo) { - super(unitBase, ns); - this.converterFrom = converterFrom; - this.converterTo = converterTo; - } - - @Override - protected V convertFromBase(double value) { - return this.converterFrom.apply(value); - } - - @Override - protected double convertToBase(V value) { - return this.converterTo.applyAsDouble(value); - } - -} diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 6489229..85f6dd9 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -26,19 +26,21 @@ import sevenUnits.utils.UncertainDouble; /** * A unit that can be expressed as a product of its base and a number. For * example, kilometres, inches and pounds. - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public final class LinearUnit extends Unit { /** * Gets a {@code LinearUnit} from a unit and a value. For example, converts * '59 °F' to a linear unit with the value of '288.15 K' - * + * * @param unit unit to convert * @param value value to convert * @return value expressed as a {@code LinearUnit} * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if unit is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value) { @@ -50,12 +52,13 @@ public final class LinearUnit extends Unit { /** * Gets a {@code LinearUnit} from a unit and a value. For example, converts * '59 °F' to a linear unit with the value of '288.15 K' - * + * * @param unit unit to convert * @param value value to convert * @param ns name(s) and symbol of unit * @return value expressed as a {@code LinearUnit} * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if unit or ns is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value, @@ -66,32 +69,26 @@ public final class LinearUnit extends Unit { } /** + * @param unit unit to get base version of * @return the base unit associated with {@code unit}, as a * {@code LinearUnit}. * @since 2020-10-02 + * @since v0.3.0 */ public static LinearUnit getBase(final Unit unit) { return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); } /** - * @return the base unit associated with {@code unitlike}, as a - * {@code LinearUnit}. - * @since 2020-10-02 - */ - public static LinearUnit getBase(final Unitlike<?> unit) { - return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); - } - - /** * Gets a {@code LinearUnit} from a unit base and a conversion factor. In * other words, gets the product of {@code unitBase} and * {@code conversionFactor}, expressed as a {@code LinearUnit}. - * + * * @param unitBase unit base to multiply by * @param conversionFactor number to multiply base by * @return product of base and conversion factor * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if unitBase is null */ public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, @@ -103,12 +100,13 @@ public final class LinearUnit extends Unit { * Gets a {@code LinearUnit} from a unit base and a conversion factor. In * other words, gets the product of {@code unitBase} and * {@code conversionFactor}, expressed as a {@code LinearUnit}. - * + * * @param unitBase unit base to multiply by * @param conversionFactor number to multiply base by * @param ns name(s) and symbol of unit * @return product of base and conversion factor * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if unitBase is null */ public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, @@ -118,21 +116,23 @@ public final class LinearUnit extends Unit { /** * The value of this unit as represented in its base form. Mathematically, - * + * * <pre> * this = conversionFactor * getBase() * </pre> - * + * * @since 2019-10-16 + * @since v0.3.0 */ private final double conversionFactor; /** * Creates the {@code LinearUnit}. - * + * * @param unitBase base of linear unit * @param conversionFactor conversion factor between base and unit * @since 2019-10-16 + * @since v0.3.0 */ private LinearUnit(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor, final NameSymbol ns) { @@ -142,7 +142,7 @@ public final class LinearUnit extends Unit { /** * {@inheritDoc} - * + * * Converts by dividing by {@code conversionFactor} */ @Override @@ -153,11 +153,12 @@ public final class LinearUnit extends Unit { /** * Converts an {@code UncertainDouble} value expressed in this unit to an * {@code UncertainValue} value expressed in {@code other}. - * + * * @param other unit to convert to * @param value value to convert * @return converted value * @since 2019-09-07 + * @since v0.3.0 * @throws IllegalArgumentException if {@code other} is incompatible for * conversion with this unit (as tested by * {@link Unit#canConvertTo}). @@ -169,15 +170,14 @@ public final class LinearUnit extends Unit { if (this.canConvertTo(other)) return value.timesExact( this.getConversionFactor() / other.getConversionFactor()); - else - throw new IllegalArgumentException( - String.format("Cannot convert from %s to %s.", this, other)); + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); } /** * {@inheritDoc} - * + * * Converts by multiplying by {@code conversionFactor} */ @Override @@ -187,8 +187,9 @@ public final class LinearUnit extends Unit { /** * Converts an {@code UncertainDouble} to the base unit. - * + * * @since 2020-09-07 + * @since v0.3.0 */ UncertainDouble convertToBase(final UncertainDouble value) { return value.timesExact(this.getConversionFactor()); @@ -196,7 +197,7 @@ public final class LinearUnit extends Unit { /** * Divides this unit by a scalar. - * + * * @param divisor scalar to divide by * @return quotient * @since 2018-12-23 @@ -208,7 +209,7 @@ public final class LinearUnit extends Unit { /** * Returns the quotient of this unit and another. - * + * * @param divisor unit to divide by * @return quotient of two units * @throws NullPointerException if {@code divisor} is null @@ -219,22 +220,35 @@ public final class LinearUnit extends Unit { Objects.requireNonNull(divisor, "other must not be null"); // divide the units - final ObjectProduct<BaseUnit> base = this.getBase() - .dividedBy(divisor.getBase()); + final var base = this.getBase().dividedBy(divisor.getBase()); return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); } /** * {@inheritDoc} - * + * * Uses the base and conversion factor of units to test for equality. */ @Override public boolean equals(final Object obj) { if (!(obj instanceof LinearUnit)) return false; - final LinearUnit other = (LinearUnit) obj; + final var other = (LinearUnit) obj; + return Objects.equals(this.getBase(), other.getBase()) + && Double.compare(this.getConversionFactor(), + other.getConversionFactor()) == 0; + } + + /** + * @param other unit to test equality with + * @return true iff this unit and other are equal, ignoring small differences + * caused by floating-point error. + * + * @apiNote This method is not transitive, so it cannot be used as an equals + * method. + */ + public boolean equalsApproximately(final LinearUnit other) { return Objects.equals(this.getBase(), other.getBase()) && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); @@ -243,6 +257,7 @@ public final class LinearUnit extends Unit { /** * @return conversion factor * @since 2019-10-16 + * @since v0.3.0 */ public double getConversionFactor() { return this.conversionFactor; @@ -250,13 +265,13 @@ public final class LinearUnit extends Unit { /** * {@inheritDoc} - * + * * Uses the base and conversion factor to compute a hash code. */ @Override public int hashCode() { return 31 * this.getBase().hashCode() - + DecimalComparison.hash(this.getConversionFactor()); + + Double.hashCode(this.getConversionFactor()); } /** @@ -264,6 +279,7 @@ public final class LinearUnit extends Unit { * is a {@code BaseUnit b} where * {@code b.asLinearUnit().equals(this)} returns {@code true}.) * @since 2019-10-16 + * @since v0.3.0 */ public boolean isBase() { return this.isCoherent() && this.getBase().isSingleObject(); @@ -272,6 +288,7 @@ public final class LinearUnit extends Unit { /** * @return whether this unit is coherent (i.e. has conversion factor 1) * @since 2019-10-16 + * @since v0.3.0 */ public boolean isCoherent() { return this.getConversionFactor() == 1; @@ -285,7 +302,7 @@ public final class LinearUnit extends Unit { * does not meet this condition, an {@code IllegalArgumentException} will be * thrown. * </p> - * + * * @param subtrahend unit to subtract * @return difference of units * @throws IllegalArgumentException if {@code subtrahend} is not compatible @@ -316,7 +333,7 @@ public final class LinearUnit extends Unit { * does not meet this condition, an {@code IllegalArgumentException} will be * thrown. * </p> - * + * * @param addend unit to add * @return sum of units * @throws IllegalArgumentException if {@code addend} is not compatible for @@ -341,7 +358,7 @@ public final class LinearUnit extends Unit { /** * Multiplies this unit by a scalar. - * + * * @param multiplier scalar to multiply by * @return product * @since 2018-12-23 @@ -353,7 +370,7 @@ public final class LinearUnit extends Unit { /** * Returns the product of this unit and another. - * + * * @param multiplier unit to multiply by * @return product of two units * @throws NullPointerException if {@code multiplier} is null @@ -364,8 +381,7 @@ public final class LinearUnit extends Unit { Objects.requireNonNull(multiplier, "other must not be null"); // multiply the units - final ObjectProduct<BaseUnit> base = this.getBase() - .times(multiplier.getBase()); + final var base = this.getBase().times(multiplier.getBase()); return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); } @@ -379,7 +395,7 @@ public final class LinearUnit extends Unit { /** * Returns this unit but to an exponent. - * + * * @param exponent exponent to exponentiate unit to * @return exponentiated unit * @since 2019-01-15 @@ -390,6 +406,21 @@ public final class LinearUnit extends Unit { Math.pow(this.conversionFactor, exponent)); } + /** + * Returns this unit to an exponent, rounding the resulting dimensions to the + * nearest integer. + * + * @param exponent exponent to raise unit to + * @return result of rounded exponentation + * @since 2024-08-22 + * @since v1.0.0 + * @see ObjectProduct#toExponentRounded + */ + public LinearUnit toExponentRounded(final double exponent) { + return valueOf(this.getBase().toExponentRounded(exponent), + Math.pow(this.conversionFactor, exponent)); + } + @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); @@ -404,7 +435,7 @@ public final class LinearUnit extends Unit { * have a symbol. <br> * This method ignores alternate names of both this unit and the provided * prefix. - * + * * @param prefix prefix to apply * @return unit with prefix * @since 2019-03-18 @@ -412,7 +443,7 @@ public final class LinearUnit extends Unit { * @throws NullPointerException if prefix is null */ public LinearUnit withPrefix(final UnitPrefix prefix) { - final LinearUnit unit = this.times(prefix.getMultiplier()); + final var unit = this.times(prefix.getMultiplier()); // create new name and symbol, if possible final String name; diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java index 3a9428b..ce60e3b 100644 --- a/src/main/java/sevenUnits/unit/LinearUnitValue.java +++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,33 +17,38 @@ package sevenUnits.unit; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; -import java.util.Optional; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; /** * A possibly uncertain value expressed in a linear unit. - * + * * Unless otherwise indicated, all methods in this class throw a * {@code NullPointerException} when an argument is null. - * + * * @author Adrien Hopkins * @since 2020-07-26 + * @since v0.3.0 */ public final class LinearUnitValue { + /** The value 1 as a LinearUnitValue. */ public static final LinearUnitValue ONE = getExact(Metric.ONE, 1); /** * Gets an exact {@code LinearUnitValue} - * + * * @param unit unit to express with * @param value value to express * @return exact {@code LinearUnitValue} instance * @since 2020-07-26 + * @since v0.3.0 */ - public static final LinearUnitValue getExact(final LinearUnit unit, + public static LinearUnitValue getExact(final LinearUnit unit, final double value) { return new LinearUnitValue( Objects.requireNonNull(unit, "unit must not be null"), @@ -52,14 +57,14 @@ public final class LinearUnitValue { /** * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param uncertainty absolute uncertainty of value + * + * @param unit unit to express with + * @param value value to express * @return uncertain {@code LinearUnitValue} instance * @since 2020-07-26 + * @since v0.3.0 */ - public static final LinearUnitValue of(final LinearUnit unit, + public static LinearUnitValue of(final LinearUnit unit, final UncertainDouble value) { return new LinearUnitValue( Objects.requireNonNull(unit, "unit must not be null"), @@ -74,6 +79,7 @@ public final class LinearUnitValue { * @param unit unit to express as * @param value value to express * @since 2020-07-26 + * @since v0.3.0 */ private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { this.unit = unit; @@ -84,8 +90,9 @@ public final class LinearUnitValue { * @return this value as a {@code UnitValue}. All uncertainty information is * removed from the returned value. * @since 2020-08-04 + * @since v0.3.0 */ - public final UnitValue asUnitValue() { + public UnitValue asUnitValue() { return UnitValue.of(this.unit, this.value.value()); } @@ -93,29 +100,68 @@ public final class LinearUnitValue { * @param other a {@code LinearUnit} * @return true iff this value can be represented with {@code other}. * @since 2020-07-26 + * @since v0.3.0 */ - public final boolean canConvertTo(final LinearUnit other) { + public boolean canConvertTo(final LinearUnit other) { return this.unit.canConvertTo(other); } /** * Returns a LinearUnitValue that represents the same value expressed in a * different unit - * + * * @param other new unit to express value in * @return value expressed in {@code other} * @since 2020-07-26 + * @since v0.3.0 */ - public final LinearUnitValue convertTo(final LinearUnit other) { + public LinearUnitValue convertTo(final LinearUnit other) { return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); } /** + * Convert a LinearUnitValue to a sum of multiple units, where all but the + * last have exact integer values. + * + * @param others units to convert to + * @return terms of the sum + * @throws IllegalArgumentException if no units are provided or units + * provided have incompatible bases + * @since 2024-08-15 + * @since v1.0.0 + */ + public List<LinearUnitValue> convertToMultiple( + final List<LinearUnit> others) { + if (others.size() < 1) + throw new IllegalArgumentException("Must have at least one unit"); + for (final LinearUnit unit : others) { + if (!Objects.equals(this.unit.getBase(), unit.getBase())) + throw new IllegalArgumentException( + "All provided units must have the same base as the value."); + } + + var remaining = this; + final List<LinearUnitValue> values = new ArrayList<>(others.size()); + for (final LinearUnit unit : others.subList(0, others.size() - 1)) { + final var remainingInUnit = remaining.convertTo(unit); + final var value = getExact(unit, + Math.floor(remainingInUnit.getValueExact() + 1e-12)); + values.add(value); + remaining = remaining.minus(value); + } + + final var lastValue = remaining.convertTo(others.get(others.size() - 1)); + values.add(lastValue); + return values; + } + + /** * Divides this value by a scalar - * + * * @param divisor value to divide by * @return multiplied value * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue dividedBy(final double divisor) { return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); @@ -123,10 +169,11 @@ public final class LinearUnitValue { /** * Divides this value by another value - * + * * @param divisor value to multiply by * @return quotient * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue dividedBy(final LinearUnitValue divisor) { return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), @@ -137,15 +184,16 @@ public final class LinearUnitValue { * Returns true if this and obj represent the same value, regardless of * whether or not they are expressed in the same unit. So (1000 m).equals(1 * km) returns true. - * + * * @since 2020-07-26 + * @since v0.3.0 * @see #equals(Object, boolean) */ @Override public boolean equals(final Object obj) { if (!(obj instanceof LinearUnitValue)) return false; - final LinearUnitValue other = (LinearUnitValue) obj; + final var other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) && this.unit.convertToBase(this.value) .equals(other.unit.convertToBase(other.value)); @@ -155,18 +203,22 @@ public final class LinearUnitValue { * Returns true if this and obj represent the same value, regardless of * whether or not they are expressed in the same unit. So (1000 m).equals(1 * km) returns true. - * <p> - * If avoidFPErrors is true, this method will attempt to avoid floating-point - * errors, at the cost of not always being transitive. - * + * + * @param obj object to test equality with + * @param avoidFPErrors if true, this method will attempt to avoid + * floating-point errors, at the cost of not always + * being transitive. + * @return true iff this and obj are equal + * * @since 2020-07-28 + * @since v0.3.0 */ public boolean equals(final Object obj, final boolean avoidFPErrors) { if (!avoidFPErrors) return this.equals(obj); if (!(obj instanceof LinearUnitValue)) return false; - final LinearUnitValue other = (LinearUnitValue) obj; + final var other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) && DecimalComparison.equals(this.unit.convertToBase(this.value), other.unit.convertToBase(other.value)); @@ -175,16 +227,17 @@ public final class LinearUnitValue { /** * @param other another {@code LinearUnitValue} * @return true iff this and other are within each other's uncertainty range - * + * * @since 2020-07-26 + * @since v0.3.0 */ public boolean equivalent(final LinearUnitValue other) { if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) return false; - final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); - final LinearUnitValue thisBase = this.convertTo(base); - final LinearUnitValue otherBase = other.convertTo(base); + final var base = LinearUnit.valueOf(this.unit.getBase(), 1); + final var thisBase = this.convertTo(base); + final var otherBase = other.convertTo(base); return thisBase.value.equivalent(otherBase.value); } @@ -192,24 +245,27 @@ public final class LinearUnitValue { /** * @return the unit * @since 2020-09-29 + * @since v0.3.0 */ - public final LinearUnit getUnit() { + public LinearUnit getUnit() { return this.unit; } /** * @return the value * @since 2020-09-29 + * @since v0.3.0 */ - public final UncertainDouble getValue() { + public UncertainDouble getValue() { return this.value; } /** * @return the exact value * @since 2020-09-07 + * @since v0.3.0 */ - public final double getValueExact() { + public double getValueExact() { return this.value.value(); } @@ -222,12 +278,13 @@ public final class LinearUnitValue { /** * Returns the difference of this value and another, expressed in this * value's unit - * + * * @param subtrahend value to subtract * @return difference of values * @throws IllegalArgumentException if {@code subtrahend} has a unit that is * not compatible for addition * @since 2020-07-26 + * @since v0.3.0 */ public LinearUnitValue minus(final LinearUnitValue subtrahend) { Objects.requireNonNull(subtrahend, "subtrahend may not be null"); @@ -237,19 +294,20 @@ public final class LinearUnitValue { "Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); - final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + final var otherConverted = subtrahend.convertTo(this.unit); return LinearUnitValue.of(this.unit, this.value.minus(otherConverted.value)); } /** * Returns the sum of this value and another, expressed in this value's unit - * + * * @param addend value to add * @return sum of values * @throws IllegalArgumentException if {@code addend} has a unit that is not * compatible for addition * @since 2020-07-26 + * @since v0.3.0 */ public LinearUnitValue plus(final LinearUnitValue addend) { Objects.requireNonNull(addend, "addend may not be null"); @@ -259,17 +317,18 @@ public final class LinearUnitValue { "Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); - final LinearUnitValue otherConverted = addend.convertTo(this.unit); + final var otherConverted = addend.convertTo(this.unit); return LinearUnitValue.of(this.unit, this.value.plus(otherConverted.value)); } /** * Multiplies this value by a scalar - * + * * @param multiplier value to multiply by * @return multiplied value * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue times(final double multiplier) { return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); @@ -277,10 +336,11 @@ public final class LinearUnitValue { /** * Multiplies this value by another value - * + * * @param multiplier value to multiply by * @return product * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue times(final LinearUnitValue multiplier) { return LinearUnitValue.of(this.unit.times(multiplier.unit), @@ -289,16 +349,32 @@ public final class LinearUnitValue { /** * Raises a value to an exponent - * + * * @param exponent exponent to raise to * @return result of exponentiation * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue toExponent(final int exponent) { return LinearUnitValue.of(this.unit.toExponent(exponent), this.value.toExponentExact(exponent)); } + /** + * Raises this value to an exponent, rounding all dimensions to integers. + * + * @param exponent exponent to raise this value to + * @return result of exponentation + * + * @since 2024-08-22 + * @since v1.0.0 + * @see ObjectProduct#toExponentRounded + */ + public LinearUnitValue toExponentRounded(final double exponent) { + return LinearUnitValue.of(this.unit.toExponentRounded(exponent), + this.value.toExponentExact(exponent)); + } + @Override public String toString() { return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN); @@ -313,23 +389,28 @@ public final class LinearUnitValue { * single numbers. * <p> * Non-exact values are rounded intelligently based on their uncertainty. - * + * + * @param showUncertainty whether to show the value's uncertainty + * @param roundingMode how to round numbers in this string + * @return string representing this value + * * @since 2020-07-26 + * @since v0.3.0 */ public String toString(final boolean showUncertainty, RoundingMode roundingMode) { - final Optional<String> primaryName = this.unit.getPrimaryName(); - final Optional<String> symbol = this.unit.getSymbol(); - final String chosenName = symbol.orElse(primaryName.orElse(null)); + final var primaryName = this.unit.getPrimaryName(); + final var symbol = this.unit.getSymbol(); + final var chosenName = symbol.orElse(primaryName.orElse(null)); - final UncertainDouble baseValue = this.unit.convertToBase(this.value); + final var baseValue = this.unit.convertToBase(this.value); // get rounded strings // if showUncertainty is true, add brackets around the string - final String valueString = (showUncertainty ? "(" : "") + final var valueString = (showUncertainty ? "(" : "") + this.value.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); - final String baseValueString = (showUncertainty ? "(" : "") + final var baseValueString = (showUncertainty ? "(" : "") + baseValue.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); @@ -338,7 +419,6 @@ public final class LinearUnitValue { return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase() .toString(unit -> unit.getSymbol().orElseThrow())); - else - return String.format("%s %s", valueString, chosenName); + return String.format("%s %s", valueString, chosenName); } } diff --git a/src/main/java/sevenUnits/unit/LoadingException.java b/src/main/java/sevenUnits/unit/LoadingException.java new file mode 100644 index 0000000..2a75c99 --- /dev/null +++ b/src/main/java/sevenUnits/unit/LoadingException.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2024, 2025 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnits.unit; + +import java.nio.file.Path; +import java.util.Optional; + +/** + * An exception that occurred when loading a file. This wrapper class adds more + * info about the error. + * + * @author Adrien Hopkins + * @since 2024-08-22 + * @since v1.0.0 + */ +public final class LoadingException extends RuntimeException { + /** The type of file that was being loaded. */ + public enum FileType { + @SuppressWarnings("javadoc") + UNIT, @SuppressWarnings("javadoc") + DIMENSION + } + + private static final long serialVersionUID = -8167971828216907607L; + + private final long lineNumber; + private final String line; + private final Optional<Path> file; + + private final FileType fileType; + + private final RuntimeException problem; + + /** + * Create a LoadingException from some information, without a file. + * + * @param lineNumber line number error happened on + * @param line text of invalid line + * @param fileType type of file + * @param problem problem, as an Exception + */ + public LoadingException(long lineNumber, String line, FileType fileType, + RuntimeException problem) { + super(problem); + this.lineNumber = lineNumber; + this.line = line; + this.file = Optional.empty(); + this.fileType = fileType; + this.problem = problem; + } + + /** + * Create a LoadingException from some information, with a file. + * + * @param lineNumber line number error happened on + * @param line text of invalid line + * @param file file error happened on + * @param fileType type of file + * @param problem problem, as an Exception + */ + public LoadingException(long lineNumber, String line, Path file, + FileType fileType, RuntimeException problem) { + super(problem); + this.lineNumber = lineNumber; + this.line = line; + this.file = Optional.of(file); + this.fileType = fileType; + this.problem = problem; + } + + /** @return the file this error happened in, if there is one */ + public Optional<Path> file() { + return this.file; + } + + /** @return type of file that this error happened in */ + public FileType fileType() { + return this.fileType; + } + + @Override + public String getMessage() { + return this.file + .map(f -> String.format( + "Error parsing line %d of %s file '%s' (\"%s\"): %s", + this.lineNumber, this.fileType.toString().toLowerCase(), f, + this.line, this.problem)) + .orElse(String.format( + "Error parsing line %d of %s stream (\"%s\"): %s", + this.lineNumber, this.fileType.toString().toLowerCase(), + this.line, this.problem)); + } + + /** @return text of line that caused this error */ + public String line() { + return this.line; + } + + /** @return number of line that caused this error */ + public long lineNumber() { + return this.lineNumber; + } + + /** @return the error, as an exception */ + public RuntimeException problem() { + return this.problem; + } +} diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 7841987..e712dc3 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,19 +24,23 @@ import sevenUnits.utils.ObjectProduct; /** * All of the units, prefixes and dimensions that are used by the SI, as well as * some outside the SI. - * + * * <p> * This class does not include prefixed units. To obtain prefixed units, use * {@link LinearUnit#withPrefix}: - * + * * <pre> * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO); * </pre> - * - * + * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ +// this class is just constants, most of which are obvious from the variable name +// so no need to check for missing values +@SuppressWarnings("javadoc") public final class Metric { /// dimensions used by SI units // base dimensions, as BaseDimensions @@ -67,8 +71,10 @@ public final class Metric { } /// base units of the SI - // suppressing warnings since these are the same object, but in a different - /// form (class) + // suppressing warnings since these are the same object, + // but in a different form (class) + // and because these objects are only used outside this class, + // where hiding is not a problem. @SuppressWarnings("hiding") public static final class BaseUnits { public static final BaseUnit METRE = BaseUnit @@ -101,9 +107,10 @@ public final class Metric { /** * Constants that relate to the SI or other systems. - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Constants { public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND) @@ -343,7 +350,7 @@ public final class Metric { pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np)) .withName(NameSymbol.of("neper", "Np")); public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(), - pr -> Math.log10(pr), dB -> Math.pow(10, dB)) + Math::log10, dB -> Math.pow(10, dB)) .withName(NameSymbol.of("bel", "B")); public static final Unit DECIBEL = Unit .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java deleted file mode 100644 index 950c547..0000000 --- a/src/main/java/sevenUnits/unit/MultiUnit.java +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Copyright (C) 2020 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.unit; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.ObjectProduct; - -/** - * A combination of units, like "5 foot + 7 inch". All but the last units should - * have a whole number value associated with them. - * - * @since 2020-10-02 - */ -public final class MultiUnit extends Unitlike<List<Double>> { - /** - * Creates a {@code MultiUnit} from its units. It will not have a name or - * symbol. - * - * @since 2020-10-03 - */ - public static final MultiUnit of(LinearUnit... units) { - return of(Arrays.asList(units)); - } - - /** - * Creates a {@code MultiUnit} from its units. It will not have a name or - * symbol. - * - * @since 2020-10-03 - */ - public static final MultiUnit of(List<LinearUnit> units) { - if (units.size() < 1) - throw new IllegalArgumentException("Must have at least one unit"); - final ObjectProduct<BaseUnit> unitBase = units.get(0).getBase(); - for (final LinearUnit unit : units) { - if (!unitBase.equals(unit.getBase())) - throw new IllegalArgumentException( - "All units must have the same base."); - } - return new MultiUnit(new ArrayList<>(units), unitBase, NameSymbol.EMPTY); - } - - /** - * The units that make up this value. - */ - private final List<LinearUnit> units; - - /** - * Creates a {@code MultiUnit}. - * - * @since 2020-10-03 - */ - private MultiUnit(List<LinearUnit> units, ObjectProduct<BaseUnit> unitBase, - NameSymbol ns) { - super(unitBase, ns); - this.units = units; - } - - @Override - protected List<Double> convertFromBase(double value) { - final List<Double> values = new ArrayList<>(this.units.size()); - double temp = value; - - for (final LinearUnit unit : this.units.subList(0, - this.units.size() - 1)) { - values.add(Math.floor(temp / unit.getConversionFactor())); - temp %= unit.getConversionFactor(); - } - - values.add(this.units.size() - 1, - this.units.get(this.units.size() - 1).convertFromBase(temp)); - - return values; - } - - /** - * Converts a value expressed in this unitlike form to a value expressed in - * {@code other}. - * - * @implSpec If conversion is possible, this implementation returns - * {@code other.convertFromBase(this.convertToBase(value))}. - * Therefore, overriding either of those methods will change the - * output of this method. - * - * @param other unit to convert to - * @param value value to convert - * @return converted value - * @since 2020-10-03 - * @throws IllegalArgumentException if {@code other} is incompatible for - * conversion with this unitlike form (as - * tested by {@link Unit#canConvertTo}). - * @throws NullPointerException if other is null - */ - public final <U extends Unitlike<V>, V> V convertTo(U other, - double... values) { - final List<Double> valueList = new ArrayList<>(values.length); - for (final double d : values) { - valueList.add(d); - } - - return this.convertTo(other, valueList); - } - - /** - * Converts a value expressed in this unitlike form to a value expressed in - * {@code other}. - * - * @implSpec If conversion is possible, this implementation returns - * {@code other.convertFromBase(this.convertToBase(value))}. - * Therefore, overriding either of those methods will change the - * output of this method. - * - * @param other unit to convert to - * @param value value to convert - * @return converted value - * @since 2020-10-03 - * @throws IllegalArgumentException if {@code other} is incompatible for - * conversion with this unitlike form (as - * tested by {@link Unit#canConvertTo}). - * @throws NullPointerException if other is null - */ - public final double convertTo(Unit other, double... values) { - final List<Double> valueList = new ArrayList<>(values.length); - for (final double d : values) { - valueList.add(d); - } - - return this.convertTo(other, valueList); - } - - @Override - protected double convertToBase(List<Double> value) { - if (value.size() != this.units.size()) - throw new IllegalArgumentException("Wrong number of values for " - + this.units.size() + "-unit MultiUnit."); - - double baseValue = 0; - for (int i = 0; i < this.units.size(); i++) { - baseValue += value.get(i) * this.units.get(i).getConversionFactor(); - } - return baseValue; - } -} diff --git a/src/main/java/sevenUnits/unit/USCustomary.java b/src/main/java/sevenUnits/unit/USCustomary.java index fce829e..ef12043 100644 --- a/src/main/java/sevenUnits/unit/USCustomary.java +++ b/src/main/java/sevenUnits/unit/USCustomary.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -18,16 +18,21 @@ package sevenUnits.unit; /** * A static utility class that contains units in the US Customary system. - * + * * @author Adrien Hopkins * @since 2019-10-21 + * @since v0.3.0 */ +// this class is just constants, most of which are obvious from the variable name +// so no need to check for missing values +@SuppressWarnings("javadoc") public final class USCustomary { /** * US Customary units that measure area - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Area { public static final LinearUnit SQUARE_SURVEY_FOOT = Length.SURVEY_FOOT @@ -43,9 +48,10 @@ public final class USCustomary { /** * US Customary units that measure length - * + * * @author Adrien Hopkins * @since 2019-10-28 + * @since v0.3.0 */ public static final class Length { public static final LinearUnit FOOT = BritishImperial.Length.FOOT; @@ -73,9 +79,10 @@ public final class USCustomary { /** * mass units - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Mass { public static final LinearUnit GRAIN = BritishImperial.Mass.GRAIN; @@ -93,9 +100,10 @@ public final class USCustomary { /** * Volume units - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Volume { public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 59e928a..40e6e0d 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -28,22 +28,23 @@ import sevenUnits.utils.ObjectProduct; /** * A unit that is composed of base units. - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public abstract class Unit implements Nameable { /** * Returns a unit from its base and the functions it uses to convert to and * from its base. - * + * * <p> * For example, to get a unit representing the degree Celsius, the following * code can be used: - * + * * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} * </p> - * + * * @param base unit's base * @param converterFrom function that accepts a value expressed in the unit's * base and returns that value expressed in this unit. @@ -51,6 +52,7 @@ public abstract class Unit implements Nameable { * and returns that value expressed in the unit's base. * @return a unit that uses the provided functions to convert. * @since 2019-05-22 + * @since v0.3.0 * @throws NullPointerException if any argument is null */ public static final Unit fromConversionFunctions( @@ -63,14 +65,14 @@ public abstract class Unit implements Nameable { /** * Returns a unit from its base and the functions it uses to convert to and * from its base. - * + * * <p> * For example, to get a unit representing the degree Celsius, the following * code can be used: - * + * * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} * </p> - * + * * @param base unit's base * @param converterFrom function that accepts a value expressed in the unit's * base and returns that value expressed in this unit. @@ -79,6 +81,7 @@ public abstract class Unit implements Nameable { * @param ns names and symbol of unit * @return a unit that uses the provided functions to convert. * @since 2019-05-22 + * @since v0.3.0 * @throws NullPointerException if any argument is null */ public static final Unit fromConversionFunctions( @@ -90,15 +93,17 @@ public abstract class Unit implements Nameable { /** * The combination of units that this unit is based on. - * + * * @since 2019-10-16 + * @since v0.3.0 */ private final ObjectProduct<BaseUnit> unitBase; /** * This unit's name(s) and symbol - * + * * @since 2020-09-07 + * @since v0.3.0 */ private final NameSymbol nameSymbol; @@ -106,28 +111,30 @@ public abstract class Unit implements Nameable { * Cache storing the result of getDimension() * * @since 2019-10-16 + * @since v0.3.0 */ private transient ObjectProduct<BaseDimension> dimension = null; /** * A constructor that constructs {@code BaseUnit} instances. - * + * * @since 2019-10-16 + * @since v0.3.0 */ Unit(final NameSymbol nameSymbol) { - if (this instanceof BaseUnit) { - this.unitBase = ObjectProduct.oneOf((BaseUnit) this); - } else + if (!(this instanceof BaseUnit)) throw new AssertionError(); + this.unitBase = ObjectProduct.oneOf((BaseUnit) this); this.nameSymbol = nameSymbol; } /** * Creates the {@code Unit}. - * + * * @param unitBase base of unit * @param ns names and symbol of unit * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if unitBase or ns is null */ protected Unit(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) { @@ -137,18 +144,9 @@ public abstract class Unit implements Nameable { } /** - * @return this unit as a {@link Unitlike} - * @since 2020-09-07 - */ - public final Unitlike<Double> asUnitlike() { - return Unitlike.fromConversionFunctions(this.getBase(), - this::convertFromBase, this::convertToBase, this.getNameSymbol()); - } - - /** * Checks if a value expressed in this unit can be converted to a value * expressed in {@code other} - * + * * @param other unit or unitlike form to test with * @return true if they are compatible * @since 2019-01-13 @@ -161,21 +159,6 @@ public abstract class Unit implements Nameable { } /** - * Checks if a value expressed in this unit can be converted to a value - * expressed in {@code other} - * - * @param other unit or unitlike form to test with - * @return true if they are compatible - * @since 2019-01-13 - * @since v0.1.0 - * @throws NullPointerException if other is null - */ - public final <W> boolean canConvertTo(final Unitlike<W> other) { - Objects.requireNonNull(other, "other must not be null."); - return Objects.equals(this.getBase(), other.getBase()); - } - - /** * Converts from a value expressed in this unit's base unit to a value * expressed in this unit. * <p> @@ -187,10 +170,10 @@ public abstract class Unit implements Nameable { * If this unit <i>is</i> a base unit, this method should return * {@code value}. * </p> - * + * * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. - * + * * @param value value expressed in <b>base</b> unit * @return value expressed in <b>this</b> unit * @since 2018-12-22 @@ -201,16 +184,17 @@ public abstract class Unit implements Nameable { /** * Converts a value expressed in this unit to a value expressed in * {@code other}. - * + * * @implSpec If unit conversion is possible, this implementation returns * {@code other.convertFromBase(this.convertToBase(value))}. * Therefore, overriding either of those methods will change the * output of this method. - * + * * @param other unit to convert to * @param value value to convert * @return converted value * @since 2019-05-22 + * @since v0.3.0 * @throws IllegalArgumentException if {@code other} is incompatible for * conversion with this unit (as tested by * {@link Unit#canConvertTo}). @@ -220,37 +204,8 @@ public abstract class Unit implements Nameable { Objects.requireNonNull(other, "other must not be null."); if (this.canConvertTo(other)) return other.convertFromBase(this.convertToBase(value)); - else - throw new IllegalArgumentException( - String.format("Cannot convert from %s to %s.", this, other)); - } - - /** - * Converts a value expressed in this unit to a value expressed in - * {@code other}. - * - * @implSpec If conversion is possible, this implementation returns - * {@code other.convertFromBase(this.convertToBase(value))}. - * Therefore, overriding either of those methods will change the - * output of this method. - * - * @param other unitlike form to convert to - * @param value value to convert - * @param <W> type of value to convert to - * @return converted value - * @since 2020-09-07 - * @throws IllegalArgumentException if {@code other} is incompatible for - * conversion with this unit (as tested by - * {@link Unit#canConvertTo}). - * @throws NullPointerException if other is null - */ - public final <W> W convertTo(final Unitlike<W> other, final double value) { - Objects.requireNonNull(other, "other must not be null."); - if (this.canConvertTo(other)) - return other.convertFromBase(this.convertToBase(value)); - else - throw new IllegalArgumentException( - String.format("Cannot convert from %s to %s.", this, other)); + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); } /** @@ -265,10 +220,10 @@ public abstract class Unit implements Nameable { * If this unit <i>is</i> a base unit, this method should return * {@code value}. * </p> - * + * * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. - * + * * @param value value expressed in <b>this</b> unit * @return value expressed in <b>base</b> unit * @since 2018-12-22 @@ -292,7 +247,7 @@ public abstract class Unit implements Nameable { */ public final ObjectProduct<BaseDimension> getDimension() { if (this.dimension == null) { - final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); + final var mapping = this.unitBase.exponentMap(); final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); for (final BaseUnit key : mapping.keySet()) { @@ -307,6 +262,7 @@ public abstract class Unit implements Nameable { /** * @return the nameSymbol * @since 2020-09-07 + * @since v0.3.0 */ @Override public final NameSymbol getNameSymbol() { @@ -314,7 +270,7 @@ public abstract class Unit implements Nameable { } /** - * Returns true iff this unit is metric. + * Determines whether this unit is metric. * <p> * "Metric" is defined by three conditions: * <ul> @@ -329,14 +285,17 @@ public abstract class Unit implements Nameable { * <p> * All SI units (as designated by the BIPM) except the degree Celsius are * considered "metric" by this definition. - * + * + * @return true iff this unit is metric. + * * @since 2020-08-27 + * @since v0.3.0 */ public final boolean isMetric() { // first condition - check that it is a linear unit if (!(this instanceof LinearUnit)) return false; - final LinearUnit linear = (LinearUnit) this; + final var linear = (LinearUnit) this; // second condition - check that for (final BaseUnit b : linear.getBase().getBaseSet()) { @@ -352,18 +311,18 @@ public abstract class Unit implements Nameable { /** * @return a string representing this unit's definition * @since 2022-03-10 + * @since v0.3.0 */ public String toDefinitionString() { if (!this.unitBase.getNameSymbol().isEmpty()) return "derived from " + this.unitBase.getName(); - else - return "derived from " - + this.getBase().toString(BaseUnit::getShortName); + return "derived from " + this.getBase().toString(BaseUnit::getShortName); } /** * @return a string containing both this unit's name and its definition * @since 2022-03-10 + * @since v0.3.0 */ public final String toFullString() { return this.toString() + " (" + this.toDefinitionString() + ")"; @@ -375,14 +334,14 @@ public abstract class Unit implements Nameable { && this.nameSymbol.getSymbol().isPresent()) return this.nameSymbol.getPrimaryName().orElseThrow() + " (" + this.nameSymbol.getSymbol().orElseThrow() + ")"; - else - return this.getName(); + return this.getName(); } /** * @param ns name(s) and symbol to use * @return a copy of this unit with provided name(s) and symbol * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if ns is null */ public Unit withName(final NameSymbol ns) { diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 0120067..36c225f 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2024 Adrien Hopkins + * Copyright (C) 2018-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -40,11 +40,9 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; -import java.util.regex.Matcher; import java.util.regex.Pattern; import sevenUnits.utils.ConditionalExistenceCollections; -import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; @@ -52,7 +50,7 @@ import sevenUnits.utils.UncertainDouble; /** * A database of units, prefixes and dimensions, and their names. - * + * * @author Adrien Hopkins * @since 2019-01-07 * @since v0.1.0 @@ -89,7 +87,7 @@ public final class UnitDatabase { * Because of ambiguities between prefixes (i.e. kilokilo = mega), * {@link #containsValue} and {@link #values()} currently ignore prefixes. * </p> - * + * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 @@ -97,7 +95,7 @@ public final class UnitDatabase { private static final class PrefixedUnitMap implements Map<String, Unit> { /** * The class used for entry sets. - * + * * <p> * If the map that created this set is infinite in size (has at least one * unit and at least one prefix), this set is infinite as well. If this @@ -105,7 +103,7 @@ public final class UnitDatabase { * {@code IllegalStateException} instead of creating an infinite-sized * array. * </p> - * + * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 @@ -114,7 +112,7 @@ public final class UnitDatabase { extends AbstractSet<Map.Entry<String, Unit>> { /** * The entry for this set. - * + * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 @@ -126,7 +124,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitEntry}. - * + * * @param key key * @param value value * @since 2019-04-14 @@ -139,6 +137,7 @@ public final class UnitDatabase { /** * @since 2019-05-03 + * @since v0.3.0 */ @Override public boolean equals(final Object o) { @@ -161,6 +160,7 @@ public final class UnitDatabase { /** * @since 2019-05-03 + * @since v0.3.0 */ @Override public int hashCode() { @@ -180,8 +180,9 @@ public final class UnitDatabase { * string is the string representation of the key, then the equals * ({@code =}) character, then the string representation of the * value. - * + * * @since 2019-05-03 + * @since v0.3.0 */ @Override public String toString() { @@ -192,7 +193,7 @@ public final class UnitDatabase { /** * An iterator that iterates over the units of a * {@code PrefixedUnitNameSet}. - * + * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 @@ -212,7 +213,9 @@ public final class UnitDatabase { /** * Creates the * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * + * + * @param map map to base iterator on + * * @since 2019-04-14 * @since v0.2.0 */ @@ -228,7 +231,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); + final var unitName = new StringBuilder(); for (final int i : this.prefixCoordinates) { unitName.append(this.prefixNames.get(i)); } @@ -241,18 +244,15 @@ public final class UnitDatabase { public boolean hasNext() { if (this.unitNames.isEmpty()) return false; - else { - if (this.prefixNames.isEmpty()) - return this.prefixCoordinates.isEmpty() - && this.unitNamePosition < this.unitNames.size(); - else - return true; - } + if (this.prefixNames.isEmpty()) + return this.prefixCoordinates.isEmpty() + && this.unitNamePosition < this.unitNames.size(); + return true; } /** * Changes this iterator's position to the next available one. - * + * * @since 2019-04-14 * @since v0.2.0 */ @@ -268,7 +268,7 @@ public final class UnitDatabase { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; + var i = this.prefixCoordinates.size() - 1; this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); @@ -294,7 +294,7 @@ public final class UnitDatabase { @Override public Entry<String, Unit> next() { // get next element - final Entry<String, Unit> nextEntry = this.peek(); + final var nextEntry = this.peek(); // iterate to next position this.incrementPosition(); @@ -306,6 +306,7 @@ public final class UnitDatabase { * @return the next element in the iterator, without iterating over * it * @since 2019-05-03 + * @since v0.3.0 */ private Entry<String, Unit> peek() { if (!this.hasNext()) @@ -322,7 +323,7 @@ public final class UnitDatabase { } } - final String nextName = this.getCurrentUnitName(); + final var nextName = this.getCurrentUnitName(); return new PrefixedUnitEntry(nextName, this.map.get(nextName)); } @@ -330,8 +331,9 @@ public final class UnitDatabase { /** * Returns a string representation of the object. The exact details * of the representation are unspecified and subject to change. - * + * * @since 2019-05-03 + * @since v0.3.0 */ @Override public String toString() { @@ -346,7 +348,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitNameSet}. - * + * * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 @@ -383,7 +385,7 @@ public final class UnitDatabase { // This is OK because I'm in a try-catch block, catching the // exact exception that would be thrown. @SuppressWarnings("unchecked") - final Entry<String, Unit> tempEntry = (Entry<String, Unit>) o; + final var tempEntry = (Entry<String, Unit>) o; entry = tempEntry; } catch (final ClassCastException e) { throw new IllegalArgumentException( @@ -441,55 +443,45 @@ public final class UnitDatabase { public int size() { if (this.map.units.isEmpty()) return 0; - else { - if (this.map.prefixes.isEmpty()) - return this.map.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + // infinite set + return Integer.MAX_VALUE; } - /** - * @throws IllegalStateException if the set is infinite in size - */ + /** @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - /** - * @throws IllegalStateException if the set is infinite in size - */ + /** @throws IllegalStateException if the set is infinite in size */ @Override public <T> T[] toArray(final T[] a) { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(a); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); - else - return String.format( - "Infinite set of name-unit entries created from units %s and prefixes %s", - this.map.units, this.map.prefixes); + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); } } /** * The class used for unit name sets. - * + * * <p> * If the map that created this set is infinite in size (has at least one * unit and at least one prefix), this set is infinite as well. If this @@ -497,7 +489,7 @@ public final class UnitDatabase { * {@code IllegalStateException} instead of creating an infinite-sized * array. * </p> - * + * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 @@ -507,7 +499,7 @@ public final class UnitDatabase { /** * An iterator that iterates over the units of a * {@code PrefixedUnitNameSet}. - * + * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 @@ -527,7 +519,9 @@ public final class UnitDatabase { /** * Creates the * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * + * + * @param map map to base itorator on + * * @since 2019-04-14 * @since v0.2.0 */ @@ -543,7 +537,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); + final var unitName = new StringBuilder(); for (final int i : this.prefixCoordinates) { unitName.append(this.prefixNames.get(i)); } @@ -556,18 +550,15 @@ public final class UnitDatabase { public boolean hasNext() { if (this.unitNames.isEmpty()) return false; - else { - if (this.prefixNames.isEmpty()) - return this.prefixCoordinates.isEmpty() - && this.unitNamePosition < this.unitNames.size(); - else - return true; - } + if (this.prefixNames.isEmpty()) + return this.prefixCoordinates.isEmpty() + && this.unitNamePosition < this.unitNames.size(); + return true; } /** * Changes this iterator's position to the next available one. - * + * * @since 2019-04-14 * @since v0.2.0 */ @@ -583,7 +574,7 @@ public final class UnitDatabase { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; + var i = this.prefixCoordinates.size() - 1; this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); @@ -608,7 +599,7 @@ public final class UnitDatabase { @Override public String next() { - final String nextName = this.peek(); + final var nextName = this.peek(); this.incrementPosition(); @@ -619,6 +610,7 @@ public final class UnitDatabase { * @return the next element in the iterator, without iterating over * it * @since 2019-05-03 + * @since v0.3.0 */ private String peek() { if (!this.hasNext()) @@ -640,8 +632,9 @@ public final class UnitDatabase { /** * Returns a string representation of the object. The exact details * of the representation are unspecified and subject to change. - * + * * @since 2019-05-03 + * @since v0.3.0 */ @Override public String toString() { @@ -656,7 +649,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitNameSet}. - * + * * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 @@ -734,56 +727,46 @@ public final class UnitDatabase { public int size() { if (this.map.units.isEmpty()) return 0; - else { - if (this.map.prefixes.isEmpty()) - return this.map.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + // infinite set + return Integer.MAX_VALUE; } - /** - * @throws IllegalStateException if the set is infinite in size - */ + /** @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - /** - * @throws IllegalStateException if the set is infinite in size - */ + /** @throws IllegalStateException if the set is infinite in size */ @Override public <T> T[] toArray(final T[] a) { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(a); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); - else - return String.format( - "Infinite set of name-unit entries created from units %s and prefixes %s", - this.map.units, this.map.prefixes); + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); } } /** * The units stored in this collection, without prefixes. - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -791,7 +774,7 @@ public final class UnitDatabase { /** * The available prefixes for use. - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -804,7 +787,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitMap}. - * + * * @param units map mapping unit names to units * @param prefixes map mapping prefix names to prefixes * @since 2019-04-13 @@ -855,11 +838,11 @@ public final class UnitDatabase { if (!(key instanceof String)) throw new IllegalArgumentException( "Attempted to test for a unit using a non-string name."); - final String unitName = (String) key; + final var unitName = (String) key; // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; - int longestLength = 0; + var longestLength = 0; for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: @@ -871,7 +854,7 @@ public final class UnitDatabase { // linear units can have prefixes) if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); + final var rest = unitName.substring(prefixName.length()); if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; @@ -885,7 +868,7 @@ public final class UnitDatabase { /** * {@inheritDoc} - * + * * <p> * Because of ambiguities between prefixes (i.e. kilokilo = mega), this * method only tests for prefixless units. @@ -914,11 +897,11 @@ public final class UnitDatabase { if (!(key instanceof String)) throw new IllegalArgumentException( "Attempted to obtain a unit using a non-string name."); - final String unitName = (String) key; + final var unitName = (String) key; // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; - int longestLength = 0; + var longestLength = 0; for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: @@ -930,7 +913,7 @@ public final class UnitDatabase { // linear units can have prefixes) if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); + final var rest = unitName.substring(prefixName.length()); if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; @@ -942,16 +925,14 @@ public final class UnitDatabase { // if none found, returns null if (longestPrefix == null) return null; - else { - // get necessary data - final String rest = unitName.substring(longestLength); - // this cast will not fail because I verified that it would work - // before selecting this prefix - final LinearUnit unit = (LinearUnit) this.get(rest); - final UnitPrefix prefix = this.prefixes.get(longestPrefix); - - return unit.withPrefix(prefix); - } + // get necessary data + final var rest = unitName.substring(longestLength); + // this cast will not fail because I verified that it would work + // before selecting this prefix + final var unit = (LinearUnit) this.get(rest); + final var prefix = this.prefixes.get(longestPrefix); + + return unit.withPrefix(prefix); } @Override @@ -1028,28 +1009,24 @@ public final class UnitDatabase { public int size() { if (this.units.isEmpty()) return 0; - else { - if (this.prefixes.isEmpty()) - return this.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } + if (this.prefixes.isEmpty()) + return this.units.size(); + // infinite set + return Integer.MAX_VALUE; } @Override public String toString() { if (this.units.isEmpty() || this.prefixes.isEmpty()) return new HashMap<>(this).toString(); - else - return String.format( - "Infinite map of name-unit entries created from units %s and prefixes %s", - this.units, this.prefixes); + return String.format( + "Infinite map of name-unit entries created from units %s and prefixes %s", + this.units, this.prefixes); } /** * {@inheritDoc} - * + * * <p> * Because of ambiguities between prefixes (i.e. kilokilo = mega), this * method ignores prefixes. @@ -1066,34 +1043,6 @@ public final class UnitDatabase { } /** - * Replacements done to *all* expression types - */ - private static final Map<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>(); - - // add data to expression replacements - static { - // add spaces around operators - for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) { - EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator), - " " + operator + " "); - } - - // replace multiple spaces with a single space - EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " "); - // place brackets around any expression of the form "number unit", with or - // without the space - EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer - + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers - // after it - + "\\s*" // optional space(s) - + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters - + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters - + "(?!-?\\d)" // no number directly afterwards (avoids matching - // "1e3") - ), "\\($1 $2\\)"); - } - - /** * A regular expression that separates names and expressions in unit files. */ private static final Pattern NAME_EXPRESSION = Pattern @@ -1108,66 +1057,76 @@ public final class UnitDatabase { /** * The exponent operator - * + * * @param base base of exponentiation * @param exponentUnit exponent * @return result * @since 2019-04-10 * @since v0.2.0 */ - private static final LinearUnit exponentiateUnits(final LinearUnit base, + private static LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) { - // exponent function - first check if o2 is a number, - if (exponentUnit.getBase().equals(Metric.ONE.getBase())) { - // then check if it is an integer, - final double exponent = exponentUnit.getConversionFactor(); - if (DecimalComparison.equals(exponent % 1, 0)) - // then exponentiate - return base.toExponent((int) (exponent + 0.5)); - else - // not an integer - throw new UnsupportedOperationException( - "Decimal exponents are currently not supported."); - } else - // not a number - throw new IllegalArgumentException("Exponents must be numbers."); + if (!exponentUnit.getBase().equals(Metric.ONE.getBase())) + throw new IllegalArgumentException(String.format( + "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.", + base, exponentUnit)); + + final var exponent = exponentUnit.getConversionFactor(); + return base.toExponentRounded(exponent); } /** * The exponent operator - * + * * @param base base of exponentiation * @param exponentUnit exponent * @return result * @since 2020-08-04 + * @since v0.3.0 */ - private static final LinearUnitValue exponentiateUnitValues( + private static LinearUnitValue exponentiateUnitValues( final LinearUnitValue base, final LinearUnitValue exponentValue) { - // exponent function - first check if o2 is a number, - if (exponentValue.canConvertTo(Metric.ONE)) { - // then check if it is an integer, - final double exponent = exponentValue.getValueExact(); - if (DecimalComparison.equals(exponent % 1, 0)) - // then exponentiate - return base.toExponent((int) (exponent + 0.5)); - else - // not an integer - throw new UnsupportedOperationException( - "Decimal exponents are currently not supported."); - } else - // not a number - throw new IllegalArgumentException("Exponents must be numbers."); + if (!exponentValue.canConvertTo(Metric.ONE)) + throw new IllegalArgumentException(String.format( + "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.", + base, exponentValue)); + + final var exponent = exponentValue.getValueExact(); + return base.toExponentRounded(exponent); + } + + /** + * Formats an expression so it can be parsed by the expression parser. + * + * Specifically, puts spaces around all operators so they can be parsed as + * words. + * + * @param expression expression to format + * @return formatted expression + * @since 2025-06-07 + * @since v1.0.0 + */ + static String formatExpression(String expression) { + var modifiedExpression = expression; + for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) { + modifiedExpression = modifiedExpression.replaceAll(operator, + " " + operator + " "); + } + + modifiedExpression = modifiedExpression.replaceAll("\\s+", " "); + return modifiedExpression; } /** * @return true if entry represents a removable duplicate entry of map. * @since 2021-05-22 + * @since v0.3.0 */ static <T> boolean isRemovableDuplicate(Map<String, T> map, Entry<String, T> entry) { for (final Entry<String, T> e : map.entrySet()) { - final String name = e.getKey(); - final T value = e.getValue(); + final var name = e.getKey(); + final var value = e.getValue(); if (lengthFirstComparator.compare(entry.getKey(), name) < 0 && Objects.equals(map.get(entry.getKey()), value)) return true; @@ -1177,7 +1136,7 @@ public final class UnitDatabase { /** * The units in this system, excluding prefixes. - * + * * @since 2019-01-07 * @since v0.1.0 */ @@ -1185,7 +1144,7 @@ public final class UnitDatabase { /** * The unit prefixes in this system. - * + * * @since 2019-01-14 * @since v0.1.0 */ @@ -1193,7 +1152,7 @@ public final class UnitDatabase { /** * The dimensions in this system. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -1201,13 +1160,21 @@ public final class UnitDatabase { /** * A map mapping strings to units (including prefixes) - * + * * @since 2019-04-13 * @since v0.2.0 */ private final Map<String, Unit> units; /** + * A map mapping strings to unit sets + * + * @since 2024-08-16 + * @since v1.0.0 + */ + private final Map<String, List<LinearUnit>> unitSets; + + /** * The rule that specifies when prefix repetition is allowed. It takes in one * argument: a list of the prefixes being applied to the unit * <p> @@ -1221,71 +1188,72 @@ public final class UnitDatabase { /** * A parser that can parse unit expressions. - * + * * @since 2019-03-22 * @since v0.2.0 */ private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>( - this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) - .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) - .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); + this::getLinearUnit).addBinaryOperator("+", LinearUnit::plus, 0) + .addBinaryOperator("-", LinearUnit::minus, 0) + .addBinaryOperator("*", LinearUnit::times, 1) + .addBinaryOperator("space_times", LinearUnit::times, 2) + .addSpaceFunction("space_times") + .addBinaryOperator("/", LinearUnit::dividedBy, 1) + .addBinaryOperator("|", LinearUnit::dividedBy, 4) + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 3).build(); /** * A parser that can parse unit value expressions. - * + * * @since 2020-08-04 + * @since v0.3.0 */ private final ExpressionParser<LinearUnitValue> unitValueExpressionParser = new ExpressionParser.Builder<>( this::getLinearUnitValue) - .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) - .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) - .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) + .addBinaryOperator("+", LinearUnitValue::plus, 0) + .addBinaryOperator("-", LinearUnitValue::minus, 0) + .addBinaryOperator("*", LinearUnitValue::times, 1) + .addBinaryOperator("space_times", LinearUnitValue::times, 2) + .addSpaceFunction("space_times") + .addBinaryOperator("/", LinearUnitValue::dividedBy, 1) + .addBinaryOperator("|", LinearUnitValue::dividedBy, 4) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 3) .build(); /** * A parser that can parse unit prefix expressions - * + * * @since 2019-04-13 * @since v0.2.0 */ private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>( - this::getPrefix).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) - .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) - .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), - 2) + this::getPrefix).addBinaryOperator("+", UnitPrefix::plus, 0) + .addBinaryOperator("-", UnitPrefix::minus, 0) + .addBinaryOperator("*", UnitPrefix::times, 1).addSpaceFunction("*") + .addBinaryOperator("/", UnitPrefix::dividedBy, 1) + .addBinaryOperator("|", UnitPrefix::dividedBy, 3).addBinaryOperator( + "^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 2) .build(); /** * A parser that can parse unit dimension expressions. - * + * * @since 2019-04-13 * @since v0.2.0 */ private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + this::getDimension).addBinaryOperator("*", ObjectProduct::times, 0) .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 2) + .addBinaryOperator("/", ObjectProduct::dividedBy, 0) + .addBinaryOperator("|", ObjectProduct::dividedBy, 2) .addNumericOperator("^", (o1, o2) -> { - int exponent = (int) Math.round(o2.value()); + final var exponent = (int) Math.round(o2.value()); return o1.toExponent(exponent); }, 1).build(); /** * Creates the {@code UnitsDatabase}. - * + * * @since 2019-01-10 * @since v0.1.0 */ @@ -1295,10 +1263,11 @@ public final class UnitDatabase { /** * Creates the {@code UnitsDatabase} - * + * * @param prefixRepetitionRule the rule that determines when prefix * repetition is allowed * @since 2020-08-26 + * @since v0.3.0 */ public UnitDatabase(Predicate<List<UnitPrefix>> prefixRepetitionRule) { this.prefixlessUnits = new HashMap<>(); @@ -1309,11 +1278,12 @@ public final class UnitDatabase { new PrefixedUnitMap(this.prefixlessUnits, this.prefixes), entry -> this.prefixRepetitionRule .test(this.getPrefixesFromName(entry.getKey()))); + this.unitSets = new HashMap<>(); } /** * Adds a unit dimension to the database. - * + * * @param name dimension's name * @param dimension dimension to add * @throws NullPointerException if name or dimension is null @@ -1324,14 +1294,14 @@ public final class UnitDatabase { final ObjectProduct<BaseDimension> dimension) { Objects.requireNonNull(name, "name may not be null"); Objects.requireNonNull(dimension, "dimension may not be null"); - final ObjectProduct<BaseDimension> namedDimension = dimension + final var namedDimension = dimension .withName(dimension.getNameSymbol().withExtraName(name)); this.dimensions.put(name, namedDimension); } /** * Adds to the list from a line in a unit dimension file. - * + * * @param line line to look at * @param lineCounter number of line, for error messages * @since 2019-04-10 @@ -1349,13 +1319,13 @@ public final class UnitDatabase { } // divide line into name and expression - final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + final var lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) throw new IllegalArgumentException(String.format( "Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.", lineCounter)); - final String name = lineMatcher.group(1); - final String expression = lineMatcher.group(2); + final var name = lineMatcher.group(1); + final var expression = lineMatcher.group(2); // if (name.endsWith(" ")) { // System.err.printf("Warning - line %d's dimension name ends in a space", @@ -1369,22 +1339,13 @@ public final class UnitDatabase { throw new IllegalArgumentException(String.format( "! used but no dimension found (line %d).", lineCounter)); } else { - // it's a unit, get the unit - final ObjectProduct<BaseDimension> dimension; - try { - dimension = this.getDimensionFromExpression(expression); - } catch (final IllegalArgumentException | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - this.addDimension(name, dimension); + this.addDimension(name, this.getDimensionFromExpression(expression)); } } /** * Adds a unit prefix to the database. - * + * * @param name prefix's name * @param prefix prefix to add * @throws NullPointerException if name or prefix is null @@ -1401,7 +1362,7 @@ public final class UnitDatabase { /** * Adds a unit to the database. - * + * * @param name unit's name * @param unit unit to add * @throws NullPointerException if unit is null @@ -1418,7 +1379,7 @@ public final class UnitDatabase { /** * Adds to the list from a line in a unit file. - * + * * @param line line to look at * @param lineCounter number of line, for error messages * @since 2019-04-10 @@ -1436,14 +1397,14 @@ public final class UnitDatabase { } // divide line into name and expression - final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + final var lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) throw new IllegalArgumentException(String.format( "Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.", lineCounter)); - final String name = lineMatcher.group(1); + final var name = lineMatcher.group(1); - final String expression = lineMatcher.group(2); + final var expression = lineMatcher.group(2); // this code should never occur // if (name.endsWith(" ")) { @@ -1457,47 +1418,55 @@ public final class UnitDatabase { if (!this.containsUnitName(name)) throw new IllegalArgumentException(String .format("! used but no unit found (line %d).", lineCounter)); + } else if (name.endsWith("-")) { + final var prefixName = name.substring(0, name.length() - 1); + this.addPrefix(prefixName, this.getPrefixFromExpression(expression)); + } else if (expression.contains(";")) { + // it's a multi-unit + this.addUnitSet(name, this.getUnitSetFromExpression(expression)); } else { - if (name.endsWith("-")) { - final UnitPrefix prefix; - try { - prefix = this.getPrefixFromExpression(expression); - } catch (final IllegalArgumentException - | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - final String prefixName = name.substring(0, name.length() - 1); - this.addPrefix(prefixName, prefix); - } else { - // it's a unit, get the unit - final Unit unit; - try { - unit = this.getUnitFromExpression(expression); - } catch (final IllegalArgumentException - | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - this.addUnit(name, unit); - } + // it's a unit, get the unit + this.addUnit(name, this.getUnitFromExpression(expression)); } } /** - * Removes all units, prefixes and dimensions from this database. - * + * Add a unit set to the database. + * + * @param name name of unit set + * @param value unit set to add + * @since 2024-08-16 + * @since v1.0.0 + */ + public void addUnitSet(String name, List<LinearUnit> value) { + if (value.isEmpty()) + throw new IllegalArgumentException("Unit sets must not be empty."); + for (final LinearUnit unit : value.subList(1, value.size())) { + if (!Objects.equals(unit.getDimension(), value.get(0).getDimension())) + throw new IllegalArgumentException( + "Unit sets must be all the same dimension, " + value + + " is not."); + } + + this.unitSets.put(name, value); + } + + /** + * Removes all units, unit sets, prefixes and dimensions from this database. + * * @since 2022-02-26 + * @since v0.4.0 */ public void clear() { this.dimensions.clear(); this.prefixes.clear(); this.prefixlessUnits.clear(); + this.unitSets.clear(); } /** * Tests if the database has a unit dimension with this name. - * + * * @param name name to test * @return if database contains name * @since 2019-03-14 @@ -1509,7 +1478,7 @@ public final class UnitDatabase { /** * Tests if the database has a unit prefix with this name. - * + * * @param name name to test * @return if database contains name * @since 2019-01-13 @@ -1522,7 +1491,7 @@ public final class UnitDatabase { /** * Tests if the database has a unit with this name, taking prefixes into * consideration - * + * * @param name name to test * @return if database contains name * @since 2019-01-13 @@ -1533,6 +1502,19 @@ public final class UnitDatabase { } /** + * Returns true iff there is a unit set with this name. + * + * @param name name to check for + * @return true iff there is a unit set with this name + * + * @since 2024-08-16 + * @since v1.0.0 + */ + public boolean containsUnitSetName(String name) { + return this.unitSets.containsKey(name); + } + + /** * @return a map mapping dimension names to dimensions * @since 2019-04-13 * @since v0.2.0 @@ -1544,10 +1526,11 @@ public final class UnitDatabase { /** * Evaluates a unit expression, following the same rules as * {@link #getUnitFromExpression}. - * + * * @param expression expression to parse * @return {@code LinearUnitValue} representing value of expression * @since 2020-08-04 + * @since v0.3.0 */ public LinearUnitValue evaluateUnitExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); @@ -1556,37 +1539,13 @@ public final class UnitDatabase { if (this.containsUnitName(expression)) return this.getLinearUnitValue(expression); - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); - modifiedExpression = modifiedExpression.replaceAll("-", " - "); - - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - // FIXME the previous operaton also breaks stuff like "1e-5" - for (int i = 0; i < modifiedExpression.length(); i++) { - if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '|', '^') - .contains(modifiedExpression.charAt(i - 2)))) { - // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) - + modifiedExpression.substring(i + 2); - } - } - - return this.unitValueExpressionParser.parseExpression(modifiedExpression); + return this.unitValueExpressionParser + .parseExpression(formatExpression(expression)); } /** * Gets a unit dimension from the database using its name. - * + * * @param name dimension's name * @return dimension * @since 2019-03-14 @@ -1594,12 +1553,11 @@ public final class UnitDatabase { */ public ObjectProduct<BaseDimension> getDimension(final String name) { Objects.requireNonNull(name, "name must not be null."); - final ObjectProduct<BaseDimension> dimension = this.dimensions.get(name); + final var dimension = this.dimensions.get(name); if (dimension == null) throw new NoSuchElementException( "No dimension with name \"" + name + "\"."); - else - return dimension; + return dimension; } /** @@ -1614,8 +1572,9 @@ public final class UnitDatabase { * multiplication)</li> * <li>The operator '^' which exponentiates. Exponents must be integers.</li> * </ul> - * + * * @param expression expression to parse + * @return parsed unit dimension * @throws IllegalArgumentException if the expression cannot be parsed * @throws NullPointerException if expression is null * @since 2019-04-13 @@ -1629,23 +1588,14 @@ public final class UnitDatabase { if (this.containsDimensionName(expression)) return this.getDimension(expression); - // force operators to have spaces - String modifiedExpression = expression; - - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - return this.unitDimensionParser.parseExpression(modifiedExpression); + return this.unitDimensionParser + .parseExpression(formatExpression(expression)); } /** * Gets a unit. If it is linear, cast it to a LinearUnit and return it. * Otherwise, throw an {@code IllegalArgumentException}. - * + * * @param name unit's name * @return unit * @since 2019-03-22 @@ -1662,29 +1612,28 @@ public final class UnitDatabase { "Format nonlinear units like: unit(value)."); // solve the function - final Unit unit = this.getUnit(parts.get(0)); - final double value = Double.parseDouble( + final var unit = this.getUnit(parts.get(0)); + final var value = Double.parseDouble( parts.get(1).substring(0, parts.get(1).length() - 1)); return LinearUnit.fromUnitValue(unit, value); - } else { - // get a linear unit - final Unit unit = this.getUnit(name); - - if (unit instanceof LinearUnit) - return (LinearUnit) unit; - else - throw new IllegalArgumentException( - String.format("%s is not a linear unit.", name)); } + // get a linear unit + final var unit = this.getUnit(name); + + if (unit instanceof LinearUnit) + return (LinearUnit) unit; + throw new IllegalArgumentException( + String.format("%s is not a linear unit.", name)); } /** * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be * converted to their base units. - * + * * @param name name of unit * @return {@code LinearUnitValue} instance * @since 2020-08-04 + * @since v0.3.0 */ LinearUnitValue getLinearUnitValue(final String name) { try { @@ -1698,7 +1647,7 @@ public final class UnitDatabase { /** * Gets a unit prefix from the database from its name - * + * * @param name prefix's name * @return prefix * @since 2019-01-10 @@ -1708,30 +1657,30 @@ public final class UnitDatabase { try { return UnitPrefix.valueOf(Double.parseDouble(name)); } catch (final NumberFormatException e) { - final UnitPrefix prefix = this.prefixes.get(name); + final var prefix = this.prefixes.get(name); if (prefix == null) throw new NoSuchElementException( "No prefix with name \"" + name + "\"."); - else - return prefix; + return prefix; } } /** * Gets all of the prefixes that are on a unit name, in application order. - * + * * @param unitName name of unit * @return prefixes * @since 2020-08-26 + * @since v0.3.0 */ List<UnitPrefix> getPrefixesFromName(final String unitName) { final List<UnitPrefix> prefixes = new ArrayList<>(); - String name = unitName; + var name = unitName; while (!this.prefixlessUnits.containsKey(name)) { // find the longest prefix String longestPrefixName = null; - int longestLength = name.length(); + var longestLength = name.length(); while (longestPrefixName == null) { longestLength--; @@ -1744,7 +1693,7 @@ public final class UnitDatabase { } // longest prefix found! - final UnitPrefix prefix = this.getPrefix(longestPrefixName); + final var prefix = this.getPrefix(longestPrefixName); prefixes.add(0, prefix); name = name.substring(longestLength); } @@ -1757,7 +1706,7 @@ public final class UnitDatabase { * Currently, prefix expressions are much simpler than unit expressions: They * are either a number or the name of another prefix * </p> - * + * * @param expression expression to input * @return prefix * @throws IllegalArgumentException if expression cannot be parsed @@ -1772,30 +1721,22 @@ public final class UnitDatabase { if (this.containsUnitName(expression)) return this.getPrefix(expression); - // force operators to have spaces - String modifiedExpression = expression; - - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - return this.prefixExpressionParser.parseExpression(modifiedExpression); + return this.prefixExpressionParser + .parseExpression(formatExpression(expression)); } /** * @return the prefixRepetitionRule * @since 2020-08-26 + * @since v0.3.0 */ - public final Predicate<List<UnitPrefix>> getPrefixRepetitionRule() { + public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() { return this.prefixRepetitionRule; } /** * Gets a unit from the database from its name, looking for prefixes. - * + * * @param name unit's name * @return unit * @since 2019-01-10 @@ -1803,27 +1744,28 @@ public final class UnitDatabase { */ public Unit getUnit(final String name) { try { - final double value = Double.parseDouble(name); + final var value = Double.parseDouble(name); return Metric.ONE.times(value); } catch (final NumberFormatException e) { - final Unit unit = this.units.get(name); + final var unit = this.units.get(name); if (unit == null) throw new NoSuchElementException("No unit " + name); - else if (unit.getPrimaryName().isEmpty()) + if (unit.getPrimaryName().isEmpty()) return unit.withName(NameSymbol.ofName(name)); - else if (!unit.getPrimaryName().get().equals(name)) { + if (!unit.getPrimaryName().get().equals(name)) { final Set<String> otherNames = new HashSet<>(unit.getOtherNames()); otherNames.add(unit.getPrimaryName().get()); return unit.withName(NameSymbol.ofNullable(name, unit.getSymbol().orElse(null), otherNames)); - } else if (!unit.getOtherNames().contains(name)) { + } + if (!unit.getOtherNames().contains(name)) { final Set<String> otherNames = new HashSet<>(unit.getOtherNames()); otherNames.add(name); return unit.withName( NameSymbol.ofNullable(unit.getPrimaryName().orElse(null), unit.getSymbol().orElse(null), otherNames)); - } else - return unit; + } + return unit; } } @@ -1842,8 +1784,9 @@ public final class UnitDatabase { * <li>A number which is multiplied or divided</li> * </ul> * This method only works with linear units. - * + * * @param expression expression to parse + * @return parsed unit * @throws IllegalArgumentException if the expression cannot be parsed * @throws NullPointerException if expression is null * @since 2019-01-07 @@ -1856,31 +1799,51 @@ public final class UnitDatabase { if (this.containsUnitName(expression)) return this.getUnit(expression); - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); - modifiedExpression = modifiedExpression.replaceAll("-", " - "); + return this.unitExpressionParser + .parseExpression(formatExpression(expression)); + } - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } + /** + * Get a unit set from its name, throwing a {@link NoSuchElementException} if + * there is none. + * + * @param name name of unit set + * @return unit set with that name + * + * @since 2024-08-16 + * @since v1.0.0 + */ + public List<LinearUnit> getUnitSet(String name) { + final var unitSet = this.unitSets.get(name); + if (unitSet == null) + throw new NoSuchElementException("No unit set with name " + name); + return unitSet; + } - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - for (int i = 0; i < modifiedExpression.length(); i++) { - if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '|', '^') - .contains(modifiedExpression.charAt(i - 2)))) { - // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) - + modifiedExpression.substring(i + 2); - } - } + /** + * Parses a semicolon-separated expression to get the unit set being used. + * + * @since 2024-08-22 + * @since v1.0.0 + */ + List<LinearUnit> getUnitSetFromExpression(String expression) { + final var parts = expression.split(";"); + final List<LinearUnit> units = new ArrayList<>(parts.length); + for (final String unitName : parts) { + final var unit = this.getUnitFromExpression(unitName.trim()); - return this.unitExpressionParser.parseExpression(modifiedExpression); + if (!(unit instanceof LinearUnit)) + throw new IllegalArgumentException(String.format( + "Unit '%s' is in a unit-set expression, but is not linear.", + unitName)); + if (units.size() > 0 && !unit.canConvertTo(units.get(0))) + throw new IllegalArgumentException(String.format( + "Units in expression '%s' have different dimensions.", + expression)); + + units.add((LinearUnit) unit); + } + return units; } /** @@ -1901,26 +1864,32 @@ public final class UnitDatabase { * no unit is found, an IllegalArgumentException is thrown. This is used to * define initial units and ensure that the database contains them.</li> * </ul> - * + * * @param file file to read - * @throws IllegalArgumentException if the file cannot be parsed, found or - * read - * @throws NullPointerException if file is null + * @throws NullPointerException if file is null + * @return list of errors that happened when loading file * @since 2019-01-13 * @since v0.1.0 */ - public void loadDimensionFile(final Path file) { + public List<LoadingException> loadDimensionFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); + final List<LoadingException> errors = new ArrayList<>(); try { - long lineCounter = 0; + var lineCounter = 0L; for (final String line : Files.readAllLines(file)) { - this.addDimensionFromLine(line, ++lineCounter); + try { + this.addDimensionFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, file, + LoadingException.FileType.DIMENSION, e)); + } } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); } catch (final IOException e) { throw new IllegalArgumentException("Could not read file " + file, e); } + return errors; } /** @@ -1928,15 +1897,26 @@ public final class UnitDatabase { * {@link #loadDimensionFile}. * * @param stream stream to load from + * @return list of all errors that happened loading the stream * @since 2021-03-27 + * @since v0.3.0 */ - public void loadDimensionsFromStream(final InputStream stream) { - try (final Scanner scanner = new Scanner(stream)) { - long lineCounter = 0; + public List<LoadingException> loadDimensionsFromStream( + final InputStream stream) { + final List<LoadingException> errors = new ArrayList<>(); + try (final var scanner = new Scanner(stream)) { + var lineCounter = 0L; while (scanner.hasNextLine()) { - this.addDimensionFromLine(scanner.nextLine(), ++lineCounter); + final var line = scanner.nextLine(); + try { + this.addDimensionFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, + LoadingException.FileType.DIMENSION, e)); + } } } + return errors; } /** @@ -1956,26 +1936,32 @@ public final class UnitDatabase { * no unit is found, an IllegalArgumentException is thrown. This is used to * define initial units and ensure that the database contains them.</li> * </ul> - * + * * @param file file to read - * @throws IllegalArgumentException if the file cannot be parsed, found or - * read - * @throws NullPointerException if file is null + * @throws NullPointerException if file is null + * @return list of errors that happened when loading file * @since 2019-01-13 * @since v0.1.0 */ - public void loadUnitsFile(final Path file) { + public List<LoadingException> loadUnitsFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); + final List<LoadingException> errors = new ArrayList<>(); try { - long lineCounter = 0; + var lineCounter = 0L; for (final String line : Files.readAllLines(file)) { - this.addUnitOrPrefixFromLine(line, ++lineCounter); + try { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, file, + LoadingException.FileType.UNIT, e)); + } } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); } catch (final IOException e) { throw new IllegalArgumentException("Could not read file " + file, e); } + return errors; } /** @@ -1983,15 +1969,25 @@ public final class UnitDatabase { * {@link #loadUnitsFile}. * * @param stream stream to load from + * @return list of all errors that happened loading the stream * @since 2021-03-27 + * @since v0.3.0 */ - public void loadUnitsFromStream(InputStream stream) { - try (final Scanner scanner = new Scanner(stream)) { - long lineCounter = 0; + public List<LoadingException> loadUnitsFromStream(InputStream stream) { + final List<LoadingException> errors = new ArrayList<>(); + try (final var scanner = new Scanner(stream)) { + var lineCounter = 0L; while (scanner.hasNextLine()) { - this.addUnitOrPrefixFromLine(scanner.nextLine(), ++lineCounter); + final var line = scanner.nextLine(); + try { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, + LoadingException.FileType.UNIT, e)); + } } } + return errors; } /** @@ -2003,17 +1999,17 @@ public final class UnitDatabase { public Map<String, UnitPrefix> prefixMap(boolean includeDuplicates) { if (includeDuplicates) return Collections.unmodifiableMap(this.prefixes); - else - return Collections.unmodifiableMap(ConditionalExistenceCollections - .conditionalExistenceMap(this.prefixes, - entry -> !isRemovableDuplicate(this.prefixes, entry))); + return Collections.unmodifiableMap(ConditionalExistenceCollections + .conditionalExistenceMap(this.prefixes, + entry -> !isRemovableDuplicate(this.prefixes, entry))); } /** * @param prefixRepetitionRule the prefixRepetitionRule to set * @since 2020-08-26 + * @since v0.3.0 */ - public final void setPrefixRepetitionRule( + public void setPrefixRepetitionRule( Predicate<List<UnitPrefix>> prefixRepetitionRule) { this.prefixRepetitionRule = prefixRepetitionRule; } @@ -2041,18 +2037,18 @@ public final class UnitDatabase { * <p> * Specifically, the operations that will throw an IllegalStateException if * the map is infinite in size are: + * </p> * <ul> * <li>{@code unitMap.entrySet().toArray()} (either overloading)</li> * <li>{@code unitMap.keySet().toArray()} (either overloading)</li> * </ul> - * </p> * <p> * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's * {@link PrefixedUnitMap#containsValue containsValue} and * {@link PrefixedUnitMap#values() values()} methods currently ignore * prefixes. * </p> - * + * * @return a map mapping unit names to units, including prefixed names * @since 2019-04-13 * @since v0.2.0 @@ -2073,10 +2069,17 @@ public final class UnitDatabase { public Map<String, Unit> unitMapPrefixless(boolean includeDuplicates) { if (includeDuplicates) return Collections.unmodifiableMap(this.prefixlessUnits); - else - return Collections.unmodifiableMap(ConditionalExistenceCollections - .conditionalExistenceMap(this.prefixlessUnits, - entry -> !isRemovableDuplicate(this.prefixlessUnits, - entry))); + return Collections.unmodifiableMap(ConditionalExistenceCollections + .conditionalExistenceMap(this.prefixlessUnits, + entry -> !isRemovableDuplicate(this.prefixlessUnits, entry))); + } + + /** + * @return an unmodifiable map mapping names to unit sets + * @since 2024-08-16 + * @since v1.0.0 + */ + public Map<String, List<LinearUnit>> unitSetMap() { + return Collections.unmodifiableMap(this.unitSets); } } diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index 9035969..af106b9 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -25,17 +25,19 @@ import sevenUnits.utils.Nameable; /** * A prefix that can be applied to a {@code LinearUnit} to multiply it by some * value - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public final class UnitPrefix implements Nameable { /** * Gets a {@code UnitPrefix} from a multiplier - * + * * @param multiplier multiplier of prefix * @return prefix * @since 2019-10-16 + * @since v0.3.0 */ public static UnitPrefix valueOf(final double multiplier) { return new UnitPrefix(multiplier, NameSymbol.EMPTY); @@ -43,11 +45,12 @@ public final class UnitPrefix implements Nameable { /** * Gets a {@code UnitPrefix} from a multiplier and a name - * + * * @param multiplier multiplier of prefix * @param ns name(s) and symbol of prefix * @return prefix * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if ns is null */ public static UnitPrefix valueOf(final double multiplier, @@ -58,21 +61,23 @@ public final class UnitPrefix implements Nameable { /** * This prefix's name(s) and symbol. - * + * * @since 2022-04-16 + * @since v0.4.0 */ private final NameSymbol nameSymbol; /** * The number that this prefix multiplies units by - * + * * @since 2019-10-16 + * @since v0.3.0 */ private final double multiplier; /** * Creates the {@code DefaultUnitPrefix}. - * + * * @param multiplier * @since 2019-01-14 * @since v0.2.0 @@ -84,10 +89,11 @@ public final class UnitPrefix implements Nameable { /** * Divides this prefix by a scalar - * + * * @param divisor number to divide by * @return quotient of prefix and scalar * @since 2019-10-16 + * @since v0.3.0 */ public UnitPrefix dividedBy(final double divisor) { return valueOf(this.getMultiplier() / divisor); @@ -95,7 +101,7 @@ public final class UnitPrefix implements Nameable { /** * Divides this prefix by {@code other}. - * + * * @param other prefix to divide by * @return quotient of prefixes * @since 2019-04-13 @@ -107,18 +113,32 @@ public final class UnitPrefix implements Nameable { /** * {@inheritDoc} - * + * * Uses the prefix's multiplier to determine equality. */ @Override public boolean equals(final Object obj) { if (this == obj) return true; - if (obj == null) + if ((obj == null) || !(obj instanceof UnitPrefix)) return false; - if (!(obj instanceof UnitPrefix)) + final var other = (UnitPrefix) obj; + return Double.compare(this.getMultiplier(), other.getMultiplier()) == 0; + } + + /** + * @param other prefix to compare to + * @return true iff this prefix and other are equal, ignoring small + * differences caused by floating-point error. + * + * @apiNote This method is not transitive, so it cannot be used as an equals + * method. + */ + public boolean equalsApproximately(final UnitPrefix other) { + if (this == other) + return true; + if (other == null) return false; - final UnitPrefix other = (UnitPrefix) obj; return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); } @@ -126,6 +146,7 @@ public final class UnitPrefix implements Nameable { /** * @return prefix's multiplier * @since 2019-11-26 + * @since v0.3.0 */ public double getMultiplier() { return this.multiplier; @@ -138,46 +159,55 @@ public final class UnitPrefix implements Nameable { /** * {@inheritDoc} - * + * * Uses the prefix's multiplier to determine a hash code. */ @Override public int hashCode() { - return DecimalComparison.hash(this.getMultiplier()); + return Double.hashCode(this.getMultiplier()); } /** - * Multiplies this prefix by a scalar - * - * @param multiplicand number to multiply by - * @return product of prefix and scalar - * @since 2019-10-16 + * Subtracts {@code other} from this prefix and returns the result. + * + * @param other prefix to subtract + * @return difference of prefixes + * + * @since 2024-03-03 + * @since v0.5.0 */ - public UnitPrefix times(final double multiplicand) { - return valueOf(this.getMultiplier() * multiplicand); + public UnitPrefix minus(final UnitPrefix other) { + return valueOf(this.getMultiplier() - other.getMultiplier()); } /** * Adds {@code other} to this prefix and returns the result. - * + * + * @param other prefix to add + * @return sum of prefixes + * * @since 2024-03-03 + * @since v0.5.0 */ public UnitPrefix plus(final UnitPrefix other) { return valueOf(this.getMultiplier() + other.getMultiplier()); } /** - * Subtracts {@code other} from this prefix and returns the result. - * - * @since 2024-03-03 + * Multiplies this prefix by a scalar + * + * @param multiplicand number to multiply by + * @return product of prefix and scalar + * @since 2019-10-16 + * @since v0.3.0 */ - public UnitPrefix minus(final UnitPrefix other) { - return valueOf(this.getMultiplier() - other.getMultiplier()); + public UnitPrefix times(final double multiplicand) { + return valueOf(this.getMultiplier() * multiplicand); } /** * Multiplies this prefix by {@code other}. - * + * * @param other prefix to multiply by * @return product of prefixes * @since 2019-04-13 @@ -189,7 +219,7 @@ public final class UnitPrefix implements Nameable { /** * Raises this prefix to an exponent. - * + * * @param exponent exponent to raise to * @return result of exponentiation. * @since 2019-04-13 @@ -199,25 +229,23 @@ public final class UnitPrefix implements Nameable { return valueOf(Math.pow(this.getMultiplier(), exponent)); } - /** - * @return a string describing the prefix and its multiplier - */ + /** @return a string describing the prefix and its multiplier */ @Override public String toString() { if (this.getPrimaryName().isPresent()) return String.format("%s (\u00D7 %s)", this.getPrimaryName().get(), this.multiplier); - else if (this.getSymbol().isPresent()) + if (this.getSymbol().isPresent()) return String.format("%s (\u00D7 %s)", this.getSymbol().get(), this.multiplier); - else - return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); + return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); } /** * @param ns name(s) and symbol to use * @return copy of this prefix with provided name(s) and symbol * @since 2019-11-26 + * @since v0.3.0 * @throws NullPointerException if ns is null */ public UnitPrefix withName(final NameSymbol ns) { diff --git a/src/main/java/sevenUnits/unit/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java index 9a87288..b195f13 100644 --- a/src/main/java/sevenUnits/unit/UnitType.java +++ b/src/main/java/sevenUnits/unit/UnitType.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -30,9 +30,15 @@ import java.util.function.Predicate; * </ul> * * @since 2022-04-10 + * @since v0.4.0 */ public enum UnitType { - METRIC, SEMI_METRIC, NON_METRIC; + /** Units that pass {@link Unit#isMetric} */ + METRIC, + /** certain exceptions like the degree Celsius */ + SEMI_METRIC, + /** Non-metric, non-excepted units */ + NON_METRIC; /** * Determines which type a unit is. The type will be: @@ -46,13 +52,13 @@ public enum UnitType { * @param isSemiMetric predicate to determine if a unit is semi-metric * @return type of unit * @since 2022-04-18 + * @since v0.4.0 */ public static final UnitType getType(Unit u, Predicate<Unit> isSemiMetric) { if (isSemiMetric.test(u)) return SEMI_METRIC; - else if (u.isMetric()) + if (u.isMetric()) return METRIC; - else - return NON_METRIC; + return NON_METRIC; } } diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java index 2d01831..e24b6e2 100644 --- a/src/main/java/sevenUnits/unit/UnitValue.java +++ b/src/main/java/sevenUnits/unit/UnitValue.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,23 +17,23 @@ package sevenUnits.unit; import java.util.Objects; -import java.util.Optional; import sevenUnits.utils.NameSymbol; /** * A value expressed in a unit. - * + * * Unless otherwise indicated, all methods in this class throw a * {@code NullPointerException} when an argument is null. - * + * * @author Adrien Hopkins * @since 2020-07-26 + * @since v0.3.0 */ public final class UnitValue { /** * Creates a {@code UnitValue} from a unit and the associated value. - * + * * @param unit unit to use * @param value value to use * @return {@code UnitValue} instance @@ -56,67 +56,52 @@ public final class UnitValue { } /** + * @param other unit to try to convert to * @return true if this value can be converted to {@code other}. * @since 2020-10-01 + * @since v0.3.0 */ - public final boolean canConvertTo(Unit other) { - return this.unit.canConvertTo(other); - } - - /** - * @return true if this value can be converted to {@code other}. - * @since 2020-10-01 - */ - public final <W> boolean canConvertTo(Unitlike<W> other) { + public boolean canConvertTo(Unit other) { return this.unit.canConvertTo(other); } /** - * Returns a UnitlikeValue that represents the same value expressed in a - * different unitlike form. - * - * @param other new unit to express value in - * @return value expressed in {@code other} - */ - public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo( - U other) { - return UnitlikeValue.of(other, - this.unit.convertTo(other, this.getValue())); - } - - /** * Returns a UnitValue that represents the same value expressed in a * different unit - * + * * @param other new unit to express value in * @return value expressed in {@code other} */ - public final UnitValue convertTo(Unit other) { + public UnitValue convertTo(Unit other) { return UnitValue.of(other, this.getUnit().convertTo(other, this.getValue())); } /** - * Returns this unit value represented as a {@code LinearUnitValue} with this + * Returns this unit value represented as a {@link LinearUnitValue} with this * unit's base unit as the base. * * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not * needed. + * @return this unit as a {@link LinearUnitValue} * @since 2020-09-29 + * @since v0.3.0 */ - public final LinearUnitValue convertToBase(NameSymbol ns) { - final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns); + public LinearUnitValue convertToBase(NameSymbol ns) { + final var base = LinearUnit.getBase(this.unit).withName(ns); return this.convertToLinear(base); } /** + * @param newUnit unit to use for this value * @return a {@code LinearUnitValue} that is equivalent to this value. It * will have zero uncertainty. * @since 2020-09-29 + * @since v0.3.0 */ - public final LinearUnitValue convertToLinear(LinearUnit other) { - return LinearUnitValue.getExact(other, - this.getUnit().convertTo(other, this.getValue())); + public LinearUnitValue convertToLinear(LinearUnit newUnit) { + return LinearUnitValue.getExact(newUnit, + this.getUnit().convertTo(newUnit, this.getValue())); } /** @@ -128,7 +113,7 @@ public final class UnitValue { public boolean equals(Object obj) { if (!(obj instanceof UnitValue)) return false; - final UnitValue other = (UnitValue) obj; + final var other = (UnitValue) obj; return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) && Double.doubleToLongBits( this.getUnit().convertToBase(this.getValue())) == Double @@ -139,16 +124,18 @@ public final class UnitValue { /** * @return the unit * @since 2020-09-29 + * @since v0.3.0 */ - public final Unit getUnit() { + public Unit getUnit() { return this.unit; } /** * @return the value * @since 2020-09-29 + * @since v0.3.0 */ - public final double getValue() { + public double getValue() { return this.value; } @@ -160,16 +147,15 @@ public final class UnitValue { @Override public String toString() { - final Optional<String> primaryName = this.getUnit().getPrimaryName(); - final Optional<String> symbol = this.getUnit().getSymbol(); + final var primaryName = this.getUnit().getPrimaryName(); + final var symbol = this.getUnit().getSymbol(); if (primaryName.isEmpty() && symbol.isEmpty()) { - final double baseValue = this.getUnit().convertToBase(this.getValue()); + final var baseValue = this.getUnit().convertToBase(this.getValue()); return String.format("%s unnamed unit (= %s %s)", this.getValue(), baseValue, this.getUnit().getBase() .toString(unit -> unit.getSymbol().orElseThrow())); - } else { - final String unitName = symbol.orElse(primaryName.get()); - return this.getValue() + " " + unitName; } + final var unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; } } diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java deleted file mode 100644 index fef424e..0000000 --- a/src/main/java/sevenUnits/unit/Unitlike.java +++ /dev/null @@ -1,262 +0,0 @@ -/** - * Copyright (C) 2020 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.unit; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.DoubleFunction; -import java.util.function.ToDoubleFunction; - -import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.Nameable; -import sevenUnits.utils.ObjectProduct; - -/** - * An object that can convert a value between multiple forms (instances of the - * object); like a unit but the "converted value" can be any type. - * - * @since 2020-09-07 - */ -public abstract class Unitlike<V> implements Nameable { - /** - * Returns a unitlike form from its base and the functions it uses to convert - * to and from its base. - * - * @param base unitlike form's base - * @param converterFrom function that accepts a value expressed in the - * unitlike form's base and returns that value expressed - * in this unitlike form. - * @param converterTo function that accepts a value expressed in the - * unitlike form and returns that value expressed in the - * unit's base. - * @return a unitlike form that uses the provided functions to convert. - * @since 2020-09-07 - * @throws NullPointerException if any argument is null - */ - public static final <W> Unitlike<W> fromConversionFunctions( - final ObjectProduct<BaseUnit> base, - final DoubleFunction<W> converterFrom, - final ToDoubleFunction<W> converterTo) { - return new FunctionalUnitlike<>(base, NameSymbol.EMPTY, converterFrom, - converterTo); - } - - /** - * Returns a unitlike form from its base and the functions it uses to convert - * to and from its base. - * - * @param base unitlike form's base - * @param converterFrom function that accepts a value expressed in the - * unitlike form's base and returns that value expressed - * in this unitlike form. - * @param converterTo function that accepts a value expressed in the - * unitlike form and returns that value expressed in the - * unit's base. - * @param ns names and symbol of unit - * @return a unitlike form that uses the provided functions to convert. - * @since 2020-09-07 - * @throws NullPointerException if any argument is null - */ - public static final <W> Unitlike<W> fromConversionFunctions( - final ObjectProduct<BaseUnit> base, - final DoubleFunction<W> converterFrom, - final ToDoubleFunction<W> converterTo, final NameSymbol ns) { - return new FunctionalUnitlike<>(base, ns, converterFrom, converterTo); - } - - /** - * The combination of units that this unit is based on. - * - * @since 2019-10-16 - */ - private final ObjectProduct<BaseUnit> unitBase; - - /** - * This unit's name(s) and symbol - * - * @since 2020-09-07 - */ - private final NameSymbol nameSymbol; - - /** - * Cache storing the result of getDimension() - * - * @since 2019-10-16 - */ - private transient ObjectProduct<BaseDimension> dimension = null; - - /** - * @param unitBase - * @since 2020-09-07 - */ - protected Unitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) { - this.unitBase = Objects.requireNonNull(unitBase, - "unitBase may not be null"); - this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); - } - - /** - * Checks if a value expressed in this unitlike form can be converted to a - * value expressed in {@code other} - * - * @param other unit or unitlike form to test with - * @return true if they are compatible - * @since 2019-01-13 - * @since v0.1.0 - * @throws NullPointerException if other is null - */ - public final boolean canConvertTo(final Unit other) { - Objects.requireNonNull(other, "other must not be null."); - return Objects.equals(this.getBase(), other.getBase()); - } - - /** - * Checks if a value expressed in this unitlike form can be converted to a - * value expressed in {@code other} - * - * @param other unit or unitlike form to test with - * @return true if they are compatible - * @since 2019-01-13 - * @since v0.1.0 - * @throws NullPointerException if other is null - */ - public final <W> boolean canConvertTo(final Unitlike<W> other) { - Objects.requireNonNull(other, "other must not be null."); - return Objects.equals(this.getBase(), other.getBase()); - } - - protected abstract V convertFromBase(double value); - - /** - * Converts a value expressed in this unitlike form to a value expressed in - * {@code other}. - * - * @implSpec If conversion is possible, this implementation returns - * {@code other.convertFromBase(this.convertToBase(value))}. - * Therefore, overriding either of those methods will change the - * output of this method. - * - * @param other unit to convert to - * @param value value to convert - * @return converted value - * @since 2019-05-22 - * @throws IllegalArgumentException if {@code other} is incompatible for - * conversion with this unitlike form (as - * tested by {@link Unit#canConvertTo}). - * @throws NullPointerException if other is null - */ - public final double convertTo(final Unit other, final V value) { - Objects.requireNonNull(other, "other must not be null."); - if (this.canConvertTo(other)) - return other.convertFromBase(this.convertToBase(value)); - else - throw new IllegalArgumentException( - String.format("Cannot convert from %s to %s.", this, other)); - } - - /** - * Converts a value expressed in this unitlike form to a value expressed in - * {@code other}. - * - * @implSpec If conversion is possible, this implementation returns - * {@code other.convertFromBase(this.convertToBase(value))}. - * Therefore, overriding either of those methods will change the - * output of this method. - * - * @param other unitlike form to convert to - * @param value value to convert - * @param <W> type of value to convert to - * @return converted value - * @since 2020-09-07 - * @throws IllegalArgumentException if {@code other} is incompatible for - * conversion with this unitlike form (as - * tested by {@link Unit#canConvertTo}). - * @throws NullPointerException if other is null - */ - public final <W> W convertTo(final Unitlike<W> other, final V value) { - Objects.requireNonNull(other, "other must not be null."); - if (this.canConvertTo(other)) - return other.convertFromBase(this.convertToBase(value)); - else - throw new IllegalArgumentException( - String.format("Cannot convert from %s to %s.", this, other)); - } - - protected abstract double convertToBase(V value); - - /** - * @return combination of units that this unit is based on - * @since 2018-12-22 - * @since v0.1.0 - */ - public final ObjectProduct<BaseUnit> getBase() { - return this.unitBase; - } - - /** - * @return dimension measured by this unit - * @since 2018-12-22 - * @since v0.1.0 - */ - public final ObjectProduct<BaseDimension> getDimension() { - if (this.dimension == null) { - final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); - final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); - - for (final BaseUnit key : mapping.keySet()) { - dimensionMap.put(key.getBaseDimension(), mapping.get(key)); - } - - this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); - } - return this.dimension; - } - - /** - * @return the nameSymbol - * @since 2020-09-07 - */ - @Override - public final NameSymbol getNameSymbol() { - return this.nameSymbol; - } - - @Override - public String toString() { - return this.getPrimaryName().orElse("Unnamed unitlike form") - + (this.getSymbol().isPresent() - ? String.format(" (%s)", this.getSymbol().get()) - : "") - + ", derived from " - + this.getBase().toString(u -> u.getSymbol().get()) - + (this.getOtherNames().isEmpty() ? "" - : ", also called " + String.join(", ", this.getOtherNames())); - } - - /** - * @param ns name(s) and symbol to use - * @return a copy of this unitlike form with provided name(s) and symbol - * @since 2020-09-07 - * @throws NullPointerException if ns is null - */ - public Unitlike<V> withName(final NameSymbol ns) { - return fromConversionFunctions(this.getBase(), this::convertFromBase, - this::convertToBase, - Objects.requireNonNull(ns, "ns must not be null.")); - } -} diff --git a/src/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java deleted file mode 100644 index ad0d1ea..0000000 --- a/src/main/java/sevenUnits/unit/UnitlikeValue.java +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (C) 2020 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.unit; - -import java.util.Optional; - -import sevenUnits.utils.NameSymbol; - -/** - * - * @since 2020-09-07 - */ -final class UnitlikeValue<T extends Unitlike<V>, V> { - /** - * Gets a {@code UnitlikeValue<V>}. - * - * @since 2020-10-02 - */ - public static <T extends Unitlike<V>, V> UnitlikeValue<T, V> of(T unitlike, - V value) { - return new UnitlikeValue<>(unitlike, value); - } - - private final T unitlike; - private final V value; - - /** - * @param unitlike - * @param value - * @since 2020-09-07 - */ - private UnitlikeValue(T unitlike, V value) { - this.unitlike = unitlike; - this.value = value; - } - - /** - * @return true if this value can be converted to {@code other}. - * @since 2020-10-01 - */ - public final boolean canConvertTo(Unit other) { - return this.unitlike.canConvertTo(other); - } - - /** - * @return true if this value can be converted to {@code other}. - * @since 2020-10-01 - */ - public final <W> boolean canConvertTo(Unitlike<W> other) { - return this.unitlike.canConvertTo(other); - } - - /** - * Returns a UnitlikeValue that represents the same value expressed in a - * different unitlike form. - * - * @param other new unit to express value in - * @return value expressed in {@code other} - */ - public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo( - U other) { - return UnitlikeValue.of(other, - this.unitlike.convertTo(other, this.getValue())); - } - - /** - * Returns a UnitValue that represents the same value expressed in a - * different unit - * - * @param other new unit to express value in - * @return value expressed in {@code other} - */ - public final UnitValue convertTo(Unit other) { - return UnitValue.of(other, - this.unitlike.convertTo(other, this.getValue())); - } - - /** - * Returns this unit value represented as a {@code LinearUnitValue} with this - * unit's base unit as the base. - * - * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not - * needed. - * @since 2020-09-29 - */ - public final LinearUnitValue convertToBase(NameSymbol ns) { - final LinearUnit base = LinearUnit.getBase(this.unitlike).withName(ns); - return this.convertToLinear(base); - } - - /** - * @return a {@code LinearUnitValue} that is equivalent to this value. It - * will have zero uncertainty. - * @since 2020-09-29 - */ - public final LinearUnitValue convertToLinear(LinearUnit other) { - return LinearUnitValue.getExact(other, - this.getUnitlike().convertTo(other, this.getValue())); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof UnitlikeValue)) - return false; - final UnitlikeValue<?, ?> other = (UnitlikeValue<?, ?>) obj; - if (this.getUnitlike() == null) { - if (other.getUnitlike() != null) - return false; - } else if (!this.getUnitlike().equals(other.getUnitlike())) - return false; - if (this.getValue() == null) { - if (other.getValue() != null) - return false; - } else if (!this.getValue().equals(other.getValue())) - return false; - return true; - } - - /** - * @return the unitlike - * @since 2020-09-29 - */ - public final Unitlike<V> getUnitlike() { - return this.unitlike; - } - - /** - * @return the value - * @since 2020-09-29 - */ - public final V getValue() { - return this.value; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + (this.getUnitlike() == null ? 0 : this.getUnitlike().hashCode()); - result = prime * result - + (this.getValue() == null ? 0 : this.getValue().hashCode()); - return result; - } - - @Override - public String toString() { - final Optional<String> primaryName = this.getUnitlike().getPrimaryName(); - final Optional<String> symbol = this.getUnitlike().getSymbol(); - if (primaryName.isEmpty() && symbol.isEmpty()) { - final double baseValue = this.getUnitlike() - .convertToBase(this.getValue()); - return String.format("%s unnamed unit (= %s %s)", this.getValue(), - baseValue, this.getUnitlike().getBase()); - } else { - final String unitName = symbol.orElse(primaryName.get()); - return this.getValue() + " " + unitName; - } - } -} diff --git a/src/main/java/sevenUnits/unit/package-info.java b/src/main/java/sevenUnits/unit/package-info.java index 6aedb9d..c650b58 100644 --- a/src/main/java/sevenUnits/unit/package-info.java +++ b/src/main/java/sevenUnits/unit/package-info.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ */ /** * Everything to do with the units that make up Unit Converter. - * + * * @author Adrien Hopkins * @since 2019-10-16 * @since v0.1.0 diff --git a/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java b/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java index b71a4e0..6244ee6 100644 --- a/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java +++ b/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -49,18 +49,19 @@ import java.util.function.Predicate; * Other than that, <i>the only difference between the provided collections and * the returned collections are that elements don't exist if they don't pass the * provided condition</i>. - * - * + * + * * @author Adrien Hopkins * @since 2019-10-17 + * @since v0.3.0 */ -// TODO add conditional existence Lists and Sorted/Navigable Sets/Maps public final class ConditionalExistenceCollections { /** * Elements in this collection only exist if they meet a condition. - * + * * @author Adrien Hopkins * @since 2019-10-17 + * @since v0.3.0 * @param <E> type of element in collection */ static final class ConditionalExistenceCollection<E> @@ -70,10 +71,11 @@ public final class ConditionalExistenceCollections { /** * Creates the {@code ConditionalExistenceCollection}. - * + * * @param collection * @param existenceCondition * @since 2019-10-17 + * @since v0.3.0 */ private ConditionalExistenceCollection(final Collection<E> collection, final Predicate<E> existenceCondition) { @@ -101,7 +103,7 @@ public final class ConditionalExistenceCollections { // instance of E // therefore this cast will always work @SuppressWarnings("unchecked") - final E e = (E) o; + final var e = (E) o; return this.existenceCondition.test(e); } @@ -116,7 +118,7 @@ public final class ConditionalExistenceCollections { public boolean remove(final Object o) { // remove() must be first in the && statement, otherwise it may not // execute - final boolean containedObject = this.contains(o); + final var containedObject = this.contains(o); return this.collection.remove(o) && containedObject; } @@ -147,9 +149,10 @@ public final class ConditionalExistenceCollections { /** * Elements in this wrapper iterator only exist if they pass a condition. - * + * * @author Adrien Hopkins * @since 2019-10-17 + * @since v0.3.0 * @param <E> type of elements in iterator */ static final class ConditionalExistenceIterator<E> implements Iterator<E> { @@ -160,10 +163,11 @@ public final class ConditionalExistenceCollections { /** * Creates the {@code ConditionalExistenceIterator}. - * + * * @param iterator * @param condition * @since 2019-10-17 + * @since v0.3.0 */ private ConditionalExistenceIterator(final Iterator<E> iterator, final Predicate<E> condition) { @@ -174,8 +178,9 @@ public final class ConditionalExistenceCollections { /** * Gets the next element, and sets nextElement and hasNext accordingly. - * + * * @since 2019-10-17 + * @since v0.3.0 */ private void getAndSetNextElement() { do { @@ -197,11 +202,11 @@ public final class ConditionalExistenceCollections { @Override public E next() { if (this.hasNext()) { - final E next = this.nextElement; + final var next = this.nextElement; this.getAndSetNextElement(); return next; - } else - throw new NoSuchElementException(); + } + throw new NoSuchElementException(); } @Override @@ -212,9 +217,10 @@ public final class ConditionalExistenceCollections { /** * Mappings in this map only exist if the entry passes some condition. - * + * * @author Adrien Hopkins * @since 2019-10-17 + * @since v0.3.0 * @param <K> key type * @param <V> value type */ @@ -224,10 +230,11 @@ public final class ConditionalExistenceCollections { /** * Creates the {@code ConditionalExistenceMap}. - * + * * @param map * @param entryExistenceCondition * @since 2019-10-17 + * @since v0.3.0 */ private ConditionalExistenceMap(final Map<K, V> map, final Predicate<Entry<K, V>> entryExistenceCondition) { @@ -243,10 +250,10 @@ public final class ConditionalExistenceCollections { // only instances of K have mappings in the backing map // since we know that key is a valid key, it must be an instance of K @SuppressWarnings("unchecked") - final K keyAsK = (K) key; + final var keyAsK = (K) key; // get and test entry - final V value = this.map.get(key); + final var value = this.map.get(key); final Entry<K, V> entry = new SimpleEntry<>(keyAsK, value); return this.entryExistenceCondition.test(entry); } @@ -262,7 +269,7 @@ public final class ConditionalExistenceCollections { return this.containsKey(key) ? this.map.get(key) : null; } - private final Entry<K, V> getEntry(K key) { + private Entry<K, V> getEntry(K key) { return new Entry<>() { @Override public K getKey() { @@ -289,7 +296,7 @@ public final class ConditionalExistenceCollections { @Override public V put(final K key, final V value) { - final V oldValue = this.map.put(key, value); + final var oldValue = this.map.put(key, value); // get and test entry final Entry<K, V> entry = new SimpleEntry<>(key, oldValue); @@ -298,7 +305,7 @@ public final class ConditionalExistenceCollections { @Override public V remove(final Object key) { - final V oldValue = this.map.remove(key); + final var oldValue = this.map.remove(key); return this.containsKey(key) ? oldValue : null; } @@ -311,9 +318,10 @@ public final class ConditionalExistenceCollections { /** * Elements in this set only exist if a certain condition is true. - * + * * @author Adrien Hopkins * @since 2019-10-17 + * @since v0.3.0 * @param <E> type of element in set */ static final class ConditionalExistenceSet<E> extends AbstractSet<E> { @@ -322,10 +330,11 @@ public final class ConditionalExistenceCollections { /** * Creates the {@code ConditionalNonexistenceSet}. - * + * * @param set set to use * @param existenceCondition condition where element exists * @since 2019-10-17 + * @since v0.3.0 */ private ConditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { @@ -359,7 +368,7 @@ public final class ConditionalExistenceCollections { // of E // therefore this cast will always work @SuppressWarnings("unchecked") - final E e = (E) o; + final var e = (E) o; return this.existenceCondition.test(e); } @@ -374,7 +383,7 @@ public final class ConditionalExistenceCollections { public boolean remove(final Object o) { // remove() must be first in the && statement, otherwise it may not // execute - final boolean containedObject = this.contains(o); + final var containedObject = this.contains(o); return this.set.remove(o) && containedObject; } @@ -405,14 +414,15 @@ public final class ConditionalExistenceCollections { /** * Elements in the returned wrapper collection are ignored if they don't pass * a condition. - * + * * @param <E> type of elements in collection * @param collection collection to wrap * @param existenceCondition elements only exist if this returns true * @return wrapper collection * @since 2019-10-17 + * @since v0.3.0 */ - public static final <E> Collection<E> conditionalExistenceCollection( + public static <E> Collection<E> conditionalExistenceCollection( final Collection<E> collection, final Predicate<E> existenceCondition) { return new ConditionalExistenceCollection<>(collection, @@ -422,14 +432,15 @@ public final class ConditionalExistenceCollections { /** * Elements in the returned wrapper iterator are ignored if they don't pass a * condition. - * + * * @param <E> type of elements in iterator * @param iterator iterator to wrap * @param existenceCondition elements only exist if this returns true * @return wrapper iterator * @since 2019-10-17 + * @since v0.3.0 */ - public static final <E> Iterator<E> conditionalExistenceIterator( + public static <E> Iterator<E> conditionalExistenceIterator( final Iterator<E> iterator, final Predicate<E> existenceCondition) { return new ConditionalExistenceIterator<>(iterator, existenceCondition); } @@ -437,16 +448,16 @@ public final class ConditionalExistenceCollections { /** * Mappings in the returned wrapper map are ignored if the corresponding * entry doesn't pass a condition - * + * * @param <K> type of key in map * @param <V> type of value in map * @param map map to wrap * @param entryExistenceCondition mappings only exist if this returns true * @return wrapper map * @since 2019-10-17 + * @since v0.3.0 */ - public static final <K, V> Map<K, V> conditionalExistenceMap( - final Map<K, V> map, + public static <K, V> Map<K, V> conditionalExistenceMap(final Map<K, V> map, final Predicate<Entry<K, V>> entryExistenceCondition) { return new ConditionalExistenceMap<>(map, entryExistenceCondition); } @@ -454,14 +465,15 @@ public final class ConditionalExistenceCollections { /** * Elements in the returned wrapper set are ignored if they don't pass a * condition. - * + * * @param <E> type of elements in set * @param set set to wrap * @param existenceCondition elements only exist if this returns true * @return wrapper set * @since 2019-10-17 + * @since v0.3.0 */ - public static final <E> Set<E> conditionalExistenceSet(final Set<E> set, + public static <E> Set<E> conditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { return new ConditionalExistenceSet<>(set, existenceCondition); } diff --git a/src/main/java/sevenUnits/utils/DecimalComparison.java b/src/main/java/sevenUnits/utils/DecimalComparison.java index 0515b6b..1366fd3 100644 --- a/src/main/java/sevenUnits/utils/DecimalComparison.java +++ b/src/main/java/sevenUnits/utils/DecimalComparison.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -20,7 +20,7 @@ import java.math.BigDecimal; /** * A class that contains methods to compare float and double values. - * + * * @author Adrien Hopkins * @since 2019-03-18 * @since v0.2.0 @@ -29,7 +29,7 @@ public final class DecimalComparison { /** * The value used for double comparison. If two double values are within this * value multiplied by the larger value, they are considered equal. - * + * * @since 2019-03-18 * @since v0.2.0 */ @@ -38,7 +38,7 @@ public final class DecimalComparison { /** * The value used for float comparison. If two float values are within this * value multiplied by the larger value, they are considered equal. - * + * * @since 2019-03-18 * @since v0.2.0 */ @@ -63,21 +63,20 @@ public final class DecimalComparison { * <li>Use {@link BigDecimal} instead of {@code double} (this will make a * violation of transitivity 100% impossible) * </ol> - * + * * @param a first value to test * @param b second value to test * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 - * @see #hashCode(double) */ - public static final boolean equals(final double a, final double b) { + public static boolean equals(final double a, final double b) { return DecimalComparison.equals(a, b, DOUBLE_EPSILON); } /** * Tests for double equality using a custom epsilon value. - * + * * <p> * <strong>WARNING: </strong>this method is not technically transitive. If a * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, @@ -94,7 +93,7 @@ public final class DecimalComparison { * <li>Use {@link BigDecimal} instead of {@code double} (this will make a * violation of transitivity 100% impossible) * </ol> - * + * * @param a first value to test * @param b second value to test * @param epsilon allowed difference @@ -102,14 +101,14 @@ public final class DecimalComparison { * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final double a, final double b, + public static boolean equals(final double a, final double b, final double epsilon) { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } /** * Tests for equality of float values using {@link #FLOAT_EPSILON}. - * + * * <p> * <strong>WARNING: </strong>this method is not technically transitive. If a * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, @@ -126,20 +125,20 @@ public final class DecimalComparison { * <li>Use {@link BigDecimal} instead of {@code float} (this will make a * violation of transitivity 100% impossible) * </ol> - * + * * @param a first value to test * @param b second value to test * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final float a, final float b) { + public static boolean equals(final float a, final float b) { return DecimalComparison.equals(a, b, FLOAT_EPSILON); } /** * Tests for float equality using a custom epsilon value. - * + * * <p> * <strong>WARNING: </strong>this method is not technically transitive. If a * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, @@ -156,7 +155,7 @@ public final class DecimalComparison { * <li>Use {@link BigDecimal} instead of {@code float} (this will make a * violation of transitivity 100% impossible) * </ol> - * + * * @param a first value to test * @param b second value to test * @param epsilon allowed difference @@ -164,7 +163,7 @@ public final class DecimalComparison { * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final float a, final float b, + public static boolean equals(final float a, final float b, final float epsilon) { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } @@ -189,14 +188,14 @@ public final class DecimalComparison { * <li>Use {@link BigDecimal} instead of {@code double} (this will make a * violation of transitivity 100% impossible) * </ol> - * + * * @param a first value to test * @param b second value to test * @return whether they are equal * @since 2020-09-07 - * @see #hashCode(double) + * @since v0.3.0 */ - public static final boolean equals(final UncertainDouble a, + public static boolean equals(final UncertainDouble a, final UncertainDouble b) { return DecimalComparison.equals(a.value(), b.value()) && DecimalComparison.equals(a.uncertainty(), b.uncertainty()); @@ -204,7 +203,7 @@ public final class DecimalComparison { /** * Tests for {@code UncertainDouble} equality using a custom epsilon value. - * + * * <p> * <strong>WARNING: </strong>this method is not technically transitive. If a * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, @@ -221,7 +220,7 @@ public final class DecimalComparison { * <li>Use {@link BigDecimal} instead of {@code double} (this will make a * violation of transitivity 100% impossible) * </ol> - * + * * @param a first value to test * @param b second value to test * @param epsilon allowed difference @@ -229,25 +228,13 @@ public final class DecimalComparison { * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final UncertainDouble a, + public static boolean equals(final UncertainDouble a, final UncertainDouble b, final double epsilon) { return DecimalComparison.equals(a.value(), b.value(), epsilon) && DecimalComparison.equals(a.uncertainty(), b.uncertainty(), epsilon); } - /** - * Takes the hash code of doubles. Values that are equal according to - * {@link #equals(double, double)} will have the same hash code. - * - * @param d double to hash - * @return hash code of double - * @since 2019-10-16 - */ - public static final int hash(final double d) { - return Float.hashCode((float) d); - } - // You may NOT get any DecimalComparison instances private DecimalComparison() { throw new AssertionError(); diff --git a/src/main/java/sevenUnits/utils/ExpressionParser.java b/src/main/java/sevenUnits/utils/ExpressionParser.java index e248ff0..03c763c 100644 --- a/src/main/java/sevenUnits/utils/ExpressionParser.java +++ b/src/main/java/sevenUnits/utils/ExpressionParser.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019, 2024 Adrien Hopkins + * Copyright (C) 2019, 2021, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -31,7 +31,7 @@ import java.util.function.UnaryOperator; /** * An object that can parse expressions with unary or binary operators. - * + * * @author Adrien Hopkins * @param <T> type of object that exists in parsed expressions * @since 2019-03-14 @@ -40,7 +40,7 @@ import java.util.function.UnaryOperator; public final class ExpressionParser<T> { /** * A builder that can create {@code ExpressionParser<T>} instances. - * + * * @author Adrien Hopkins * @param <T> type of object that exists in parsed expressions * @since 2019-03-17 @@ -51,7 +51,7 @@ public final class ExpressionParser<T> { * A function that obtains a parseable object from a string. For example, * an integer {@code ExpressionParser} would use * {@code Integer::parseInt}. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -59,7 +59,7 @@ public final class ExpressionParser<T> { /** * The function of the space as an operator (like 3 x y) - * + * * @since 2019-03-22 * @since v0.2.0 */ @@ -68,7 +68,7 @@ public final class ExpressionParser<T> { /** * A map mapping operator strings to operator functions, for unary * operators. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -77,7 +77,7 @@ public final class ExpressionParser<T> { /** * A map mapping operator strings to operator functions, for binary * operators. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -85,14 +85,15 @@ public final class ExpressionParser<T> { /** * A map mapping operator strings to numeric functions. - * + * * @since 2024-03-23 + * @since v0.5.0 */ private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators; /** * Creates the {@code Builder}. - * + * * @param objectObtainer a function that can turn strings into objects of * the type handled by the parser. * @throws NullPointerException if {@code objectObtainer} is null @@ -109,7 +110,7 @@ public final class ExpressionParser<T> { /** * Adds a binary operator to the builder. - * + * * @param text text used to reference the operator, like '+' * @param operator operator to add * @param priority operator's priority, which determines which operators @@ -142,7 +143,7 @@ public final class ExpressionParser<T> { /** * Adds a two-argument operator where the second operator is a number. * This is used for operations like vector scaling and exponentation. - * + * * @param text text used to reference the operator, like '^' * @param operator operator to add * @param priority operator's priority, which determines which operators @@ -172,7 +173,7 @@ public final class ExpressionParser<T> { /** * Adds a function for spaces. You must use the text of an existing binary * operator. - * + * * @param operator text of operator to use * @return this builder * @since 2019-03-22 @@ -191,7 +192,7 @@ public final class ExpressionParser<T> { /** * Adds a unary operator to the builder. - * + * * @param text text used to reference the operator, like '-' * @param operator operator to add * @param priority operator's priority, which determines which operators @@ -235,18 +236,18 @@ public final class ExpressionParser<T> { /** * A binary operator with a priority field that determines which operators * apply first. - * + * * @author Adrien Hopkins * @param <T> type of operand and result * @since 2019-03-17 * @since v0.2.0 */ - private static abstract class PriorityBinaryOperator<T> - implements BinaryOperator<T>, Comparable<PriorityBinaryOperator<T>> { + private static abstract class PriorityBiFunction<T, U, R> implements + BiFunction<T, U, R>, Comparable<PriorityBiFunction<T, U, R>> { /** * The operator's priority. Higher-priority operators are applied before * lower-priority operators - * + * * @since 2019-03-17 * @since v0.2.0 */ @@ -254,33 +255,32 @@ public final class ExpressionParser<T> { /** * Creates the {@code PriorityBinaryOperator}. - * + * * @param priority operator's priority * @since 2019-03-17 * @since v0.2.0 */ - public PriorityBinaryOperator(final int priority) { + public PriorityBiFunction(final int priority) { this.priority = priority; } /** * Compares this object to another by priority. - * + * * <p> * {@inheritDoc} * </p> - * + * * @since 2019-03-17 * @since v0.2.0 */ @Override - public int compareTo(final PriorityBinaryOperator<T> o) { + public int compareTo(final PriorityBiFunction<T, U, R> o) { if (this.priority < o.priority) return -1; - else if (this.priority > o.priority) + if (this.priority > o.priority) return 1; - else - return 0; + return 0; } /** @@ -296,18 +296,18 @@ public final class ExpressionParser<T> { /** * A binary operator with a priority field that determines which operators * apply first. - * + * * @author Adrien Hopkins * @param <T> type of operand and result * @since 2019-03-17 * @since v0.2.0 */ - private static abstract class PriorityBiFunction<T, U, R> implements - BiFunction<T, U, R>, Comparable<PriorityBiFunction<T, U, R>> { + private static abstract class PriorityBinaryOperator<T> + implements BinaryOperator<T>, Comparable<PriorityBinaryOperator<T>> { /** * The operator's priority. Higher-priority operators are applied before * lower-priority operators - * + * * @since 2019-03-17 * @since v0.2.0 */ @@ -315,33 +315,32 @@ public final class ExpressionParser<T> { /** * Creates the {@code PriorityBinaryOperator}. - * + * * @param priority operator's priority * @since 2019-03-17 * @since v0.2.0 */ - public PriorityBiFunction(final int priority) { + public PriorityBinaryOperator(final int priority) { this.priority = priority; } /** * Compares this object to another by priority. - * + * * <p> * {@inheritDoc} * </p> - * + * * @since 2019-03-17 * @since v0.2.0 */ @Override - public int compareTo(final PriorityBiFunction<T, U, R> o) { + public int compareTo(final PriorityBinaryOperator<T> o) { if (this.priority < o.priority) return -1; - else if (this.priority > o.priority) + if (this.priority > o.priority) return 1; - else - return 0; + return 0; } /** @@ -357,7 +356,7 @@ public final class ExpressionParser<T> { /** * A unary operator with a priority field that determines which operators * apply first. - * + * * @author Adrien Hopkins * @param <T> type of operand and result * @since 2019-03-17 @@ -368,7 +367,7 @@ public final class ExpressionParser<T> { /** * The operator's priority. Higher-priority operators are applied before * lower-priority operators - * + * * @since 2019-03-17 * @since v0.2.0 */ @@ -376,7 +375,7 @@ public final class ExpressionParser<T> { /** * Creates the {@code PriorityUnaryOperator}. - * + * * @param priority operator's priority * @since 2019-03-17 * @since v0.2.0 @@ -387,11 +386,11 @@ public final class ExpressionParser<T> { /** * Compares this object to another by priority. - * + * * <p> * {@inheritDoc} * </p> - * + * * @since 2019-03-17 * @since v0.2.0 */ @@ -399,10 +398,9 @@ public final class ExpressionParser<T> { public int compareTo(final PriorityUnaryOperator<T> o) { if (this.priority < o.priority) return -1; - else if (this.priority > o.priority) + if (this.priority > o.priority) return 1; - else - return 0; + return 0; } /** @@ -417,18 +415,18 @@ public final class ExpressionParser<T> { /** * The types of tokens that are available. - * + * * @author Adrien Hopkins * @since 2019-03-14 * @since v0.2.0 */ - private static enum TokenType { + private enum TokenType { OBJECT, UNARY_OPERATOR, BINARY_OPERATOR, NUMERIC_OPERATOR; } /** * The opening bracket. - * + * * @since 2019-03-22 * @since v0.2.0 */ @@ -436,7 +434,7 @@ public final class ExpressionParser<T> { /** * The closing bracket. - * + * * @since 2019-03-22 * @since v0.2.0 */ @@ -444,7 +442,7 @@ public final class ExpressionParser<T> { /** * Finds the other bracket in a pair of brackets, given the position of one. - * + * * @param string string that contains brackets * @param bracketPosition position of first bracket * @return position of matching bracket @@ -456,7 +454,7 @@ public final class ExpressionParser<T> { final int bracketPosition) { Objects.requireNonNull(string, "string must not be null."); - final char openingBracket = string.charAt(bracketPosition); + final var openingBracket = string.charAt(bracketPosition); // figure out what closing bracket to look for final char closingBracket; @@ -477,12 +475,12 @@ public final class ExpressionParser<T> { // level of brackets. every opening bracket increments this; every closing // bracket decrements it - int bracketLevel = 0; + var bracketLevel = 0; // iterate over the string to find the closing bracket - for (int currentPosition = bracketPosition; currentPosition < string + for (var currentPosition = bracketPosition; currentPosition < string .length(); currentPosition++) { - final char currentCharacter = string.charAt(currentPosition); + final var currentCharacter = string.charAt(currentPosition); if (currentCharacter == openingBracket) { bracketLevel++; @@ -499,7 +497,7 @@ public final class ExpressionParser<T> { /** * A function that obtains a parseable object from a string. For example, an * integer {@code ExpressionParser} would use {@code Integer::parseInt}. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -507,7 +505,7 @@ public final class ExpressionParser<T> { /** * A map mapping operator strings to operator functions, for unary operators. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -516,7 +514,7 @@ public final class ExpressionParser<T> { /** * A map mapping operator strings to operator functions, for binary * operators. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -524,14 +522,15 @@ public final class ExpressionParser<T> { /** * A map mapping operator strings to numeric functions. - * + * * @since 2024-03-23 + * @since v0.5.0 */ private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators; /** * The operator for space, or null if spaces have no function. - * + * * @since 2019-03-22 * @since v0.2.0 */ @@ -539,7 +538,7 @@ public final class ExpressionParser<T> { /** * Creates the {@code ExpressionParser}. - * + * * @param objectObtainer function to get objects from strings * @param unaryOperators unary operators available to the parser * @param binaryOperators binary operators available to the parser @@ -568,7 +567,7 @@ public final class ExpressionParser<T> { * {@code 2 * (3 + 4)}<br> * becomes<br> * {@code 2 3 4 + *}. - * + * * @param expression expression * @return expression in RPN * @throws IllegalArgumentException if expression is invalid (e.g. @@ -582,13 +581,13 @@ public final class ExpressionParser<T> { final List<String> components = new ArrayList<>(); // the part of the expression remaining to parse - String partialExpression = expression; + var partialExpression = expression; // find and deal with brackets while (partialExpression.indexOf(OPENING_BRACKET) != -1) { - final int openingBracketPosition = partialExpression + final var openingBracketPosition = partialExpression .indexOf(OPENING_BRACKET); - final int closingBracketPosition = findBracketPair(partialExpression, + final var closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition); // check for function @@ -596,7 +595,7 @@ public final class ExpressionParser<T> { && partialExpression.charAt(openingBracketPosition - 1) != ' ') { // function like sin(2) or tempF(32) // find the position of the last space - int spacePosition = openingBracketPosition; + var spacePosition = openingBracketPosition; while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') { spacePosition--; @@ -607,8 +606,6 @@ public final class ExpressionParser<T> { .substring(0, spacePosition + 1).split(" "))); components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1)); - partialExpression = partialExpression - .substring(closingBracketPosition + 1); } else { // normal brackets like (1 + 2) * (3 / 5) components.addAll(Arrays.asList(partialExpression @@ -616,9 +613,9 @@ public final class ExpressionParser<T> { components.add(this.convertExpressionToReversePolish( partialExpression.substring(openingBracketPosition + 1, closingBracketPosition))); - partialExpression = partialExpression - .substring(closingBracketPosition + 1); } + partialExpression = partialExpression + .substring(closingBracketPosition + 1); } // add everything else @@ -631,7 +628,7 @@ public final class ExpressionParser<T> { // deal with space multiplication (x y) if (this.spaceOperator != null) { - for (int i = 0; i < components.size() - 1; i++) { + for (var i = 0; i < components.size() - 1; i++) { if (this.getTokenType(components.get(i)) == TokenType.OBJECT && this .getTokenType(components.get(i + 1)) == TokenType.OBJECT) { components.add(++i, this.spaceOperator); @@ -641,7 +638,7 @@ public final class ExpressionParser<T> { // turn the expression into reverse Polish while (true) { - final int highestPriorityOperatorPosition = this + final var highestPriorityOperatorPosition = this .findHighestPriorityOperatorPosition(components); if (highestPriorityOperatorPosition == -1) { break; @@ -656,9 +653,9 @@ public final class ExpressionParser<T> { if (components.size() < 2) throw new IllegalArgumentException( "Invalid expression \"" + expression + "\""); - final String unaryOperator = components + final var unaryOperator = components .remove(highestPriorityOperatorPosition); - final String operand = components + final var operand = components .remove(highestPriorityOperatorPosition); components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator); @@ -668,14 +665,14 @@ public final class ExpressionParser<T> { if (components.size() < 3) throw new IllegalArgumentException( "Invalid expression \"" + expression + "\""); - final String binaryOperator = components + final var binaryOperator = components .remove(highestPriorityOperatorPosition); - final String operand1 = components + final var operand1 = components .remove(highestPriorityOperatorPosition - 1); - final String operand2 = components + final var operand2 = components .remove(highestPriorityOperatorPosition - 1); components.add(highestPriorityOperatorPosition - 1, - operand2 + " " + operand1 + " " + binaryOperator); + operand1 + " " + operand2 + " " + binaryOperator); break; default: throw new AssertionError("Expected operator, found non-operator."); @@ -684,20 +681,15 @@ public final class ExpressionParser<T> { // join all of the components together, then ensure there is only one // space in a row - String expressionRPN = String.join(" ", components).replaceAll(" +", " "); - - while (expressionRPN.charAt(0) == ' ') { - expressionRPN = expressionRPN.substring(1); - } - while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') { - expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1); - } - return expressionRPN; + if (components.size() != 1) + throw new IllegalArgumentException( + "Invalid expression \"" + expression + "\"."); + return components.get(0).replaceAll(" +", " ").trim(); } /** * Finds the position of the highest-priority operator in a list - * + * * @param components components to test * @param blacklist positions of operators that should be ignored * @return position of highest priority, or -1 if the list contains no @@ -710,19 +702,19 @@ public final class ExpressionParser<T> { final List<String> components) { Objects.requireNonNull(components, "components must not be null."); // find highest priority - int maxPriority = Integer.MIN_VALUE; - int maxPriorityPosition = -1; + var maxPriority = Integer.MIN_VALUE; + var maxPriorityPosition = -1; // go over components one by one // if it is an operator, test its priority to see if it's max // if it is, update maxPriority and maxPriorityPosition - for (int i = 0; i < components.size(); i++) { + for (var i = 0; i < components.size(); i++) { switch (this.getTokenType(components.get(i))) { case UNARY_OPERATOR: - final PriorityUnaryOperator<T> unaryOperator = this.unaryOperators + final var unaryOperator = this.unaryOperators .get(components.get(i)); - final int unaryPriority = unaryOperator.getPriority(); + final var unaryPriority = unaryOperator.getPriority(); if (unaryPriority > maxPriority) { maxPriority = unaryPriority; @@ -730,9 +722,9 @@ public final class ExpressionParser<T> { } break; case BINARY_OPERATOR: - final PriorityBinaryOperator<T> binaryOperator = this.binaryOperators + final var binaryOperator = this.binaryOperators .get(components.get(i)); - final int binaryPriority = binaryOperator.getPriority(); + final var binaryPriority = binaryOperator.getPriority(); if (binaryPriority > maxPriority) { maxPriority = binaryPriority; @@ -740,9 +732,9 @@ public final class ExpressionParser<T> { } break; case NUMERIC_OPERATOR: - final PriorityBiFunction<T, UncertainDouble, T> numericOperator = this.numericOperators + final var numericOperator = this.numericOperators .get(components.get(i)); - final int numericPriority = numericOperator.getPriority(); + final var numericPriority = numericOperator.getPriority(); if (numericPriority > maxPriority) { maxPriority = numericPriority; @@ -760,7 +752,7 @@ public final class ExpressionParser<T> { /** * Determines whether an inputted string is an object or an operator - * + * * @param token string to input * @return type of token it is * @throws NullPointerException if {@code expression} is null @@ -772,17 +764,16 @@ public final class ExpressionParser<T> { if (this.unaryOperators.containsKey(token)) return TokenType.UNARY_OPERATOR; - else if (this.binaryOperators.containsKey(token)) + if (this.binaryOperators.containsKey(token)) return TokenType.BINARY_OPERATOR; - else if (this.numericOperators.containsKey(token)) + if (this.numericOperators.containsKey(token)) return TokenType.NUMERIC_OPERATOR; - else - return TokenType.OBJECT; + return TokenType.OBJECT; } /** * Parses an expression. - * + * * @param expression expression to parse * @return result * @throws NullPointerException if {@code expression} is null @@ -796,7 +787,7 @@ public final class ExpressionParser<T> { /** * Parses an expression expressed in reverse Polish notation. - * + * * @param expression expression to parse * @return result * @throws NullPointerException if {@code expression} is null @@ -821,12 +812,12 @@ public final class ExpressionParser<T> { item, stack.size())); // get two arguments and operator, then apply! - final T o1 = stack.pop(); - final T o2 = stack.pop(); + final var o1 = stack.pop(); + final var o2 = stack.pop(); final BinaryOperator<T> binaryOperator = this.binaryOperators .get(item); - stack.push(binaryOperator.apply(o1, o2)); + stack.push(binaryOperator.apply(o2, o1)); break; case NUMERIC_OPERATOR: @@ -835,8 +826,8 @@ public final class ExpressionParser<T> { "Attempted to call binary operator %s with insufficient arguments.", item)); - final T ot = stack.pop(); - final UncertainDouble on = doubleStack.pop(); + final var ot = stack.pop(); + final var on = doubleStack.pop(); final BiFunction<T, UncertainDouble, T> op = this.numericOperators .get(item); stack.push(op.apply(ot, on)); @@ -850,14 +841,14 @@ public final class ExpressionParser<T> { // that's the only way to tell if an expression is a number or not. try { stack.push(this.objectObtainer.apply(item)); - } catch (Exception e) { + } catch (final Exception e) { try { doubleStack.push(UncertainDouble.fromString(item)); - } catch (IllegalArgumentException e2) { + } catch (final IllegalArgumentException e2) { try { doubleStack.push( UncertainDouble.of(Double.parseDouble(item), 0)); - } catch (NumberFormatException e3) { + } catch (final NumberFormatException e3) { throw e; } } @@ -871,7 +862,7 @@ public final class ExpressionParser<T> { item, stack.size())); // get one argument and operator, then apply! - final T o = stack.pop(); + final var o = stack.pop(); final UnaryOperator<T> unaryOperator = this.unaryOperators .get(item); @@ -889,7 +880,7 @@ public final class ExpressionParser<T> { if (stack.size() > 1) throw new IllegalStateException( "Computation ended up with more than one answer."); - else if (stack.size() == 0) + if (stack.size() == 0) throw new IllegalStateException( "Computation ended up without an answer."); return stack.pop(); diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 49c44fa..089978b 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,34 +19,35 @@ package sevenUnits.utils; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.Objects; import java.util.Optional; import java.util.Set; /** * A class that can be used to specify names and a symbol for a unit. - * + * * @author Adrien Hopkins * @since 2019-10-21 + * @since v0.3.0 */ public final class NameSymbol { + /** The {@code NameSymbol} with all fields empty. */ public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), Optional.empty(), new HashSet<>()); /** * Creates a {@code NameSymbol}, ensuring that if primaryName is null and * otherNames is not empty, one name is moved from otherNames to primaryName - * + * * Ensure that otherNames is not a copy of the inputted argument. */ - private static final NameSymbol create(final String name, - final String symbol, final Set<String> otherNames) { + private static NameSymbol create(final String name, final String symbol, + final Set<String> otherNames) { final Optional<String> primaryName; if (name == null && !otherNames.isEmpty()) { // get primary name and remove it from savedNames - final Iterator<String> it = otherNames.iterator(); + final var it = otherNames.iterator(); assert it.hasNext(); primaryName = Optional.of(it.next()); otherNames.remove(primaryName.get()); @@ -61,14 +62,15 @@ public final class NameSymbol { /** * Gets a {@code NameSymbol} with a primary name, a symbol and no other * names. - * + * * @param name name to use * @param symbol symbol to use * @return NameSymbol instance * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if name or symbol is null */ - public static final NameSymbol of(final String name, final String symbol) { + public static NameSymbol of(final String name, final String symbol) { return new NameSymbol(Optional.of(name), Optional.of(symbol), new HashSet<>()); } @@ -76,15 +78,16 @@ public final class NameSymbol { /** * Gets a {@code NameSymbol} with a primary name, a symbol and additional * names. - * + * * @param name name to use * @param symbol symbol to use * @param otherNames other names to use * @return NameSymbol instance * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if any argument is null */ - public static final NameSymbol of(final String name, final String symbol, + public static NameSymbol of(final String name, final String symbol, final Set<String> otherNames) { return new NameSymbol(Optional.of(name), Optional.of(symbol), new HashSet<>(Objects.requireNonNull(otherNames, @@ -94,12 +97,13 @@ public final class NameSymbol { /** * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional * names. - * + * * @param name name to use * @param symbol symbol to use * @param otherNames other names to use * @return NameSymbol instance * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if any argument is null */ public static final NameSymbol of(final String name, final String symbol, @@ -112,13 +116,14 @@ public final class NameSymbol { /** * Gets a {@code NameSymbol} with a primary name, no symbol, and no other * names. - * + * * @param name name to use * @return NameSymbol instance * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if name is null */ - public static final NameSymbol ofName(final String name) { + public static NameSymbol ofName(final String name) { return new NameSymbol(Optional.of(name), Optional.empty(), new HashSet<>()); } @@ -133,15 +138,16 @@ public final class NameSymbol { * If {@code name} is null and {@code otherNames} is not empty, a primary * name will be picked from {@code otherNames}. This name will not appear in * getOtherNames(). - * + * * @param name name to use * @param symbol symbol to use * @param otherNames other names to use * @return NameSymbol instance * @since 2019-11-26 + * @since v0.3.0 */ - public static final NameSymbol ofNullable(final String name, - final String symbol, final Set<String> otherNames) { + public static NameSymbol ofNullable(final String name, final String symbol, + final Set<String> otherNames) { return NameSymbol.create(name, symbol, otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); } @@ -156,12 +162,13 @@ public final class NameSymbol { * If {@code name} is null and {@code otherNames} is not empty, a primary * name will be picked from {@code otherNames}. This name will not appear in * getOtherNames(). - * + * * @param name name to use * @param symbol symbol to use * @param otherNames other names to use * @return NameSymbol instance * @since 2019-11-26 + * @since v0.3.0 */ public static final NameSymbol ofNullable(final String name, final String symbol, final String... otherNames) { @@ -171,13 +178,14 @@ public final class NameSymbol { /** * Gets a {@code NameSymbol} with a symbol and no names. - * + * * @param symbol symbol to use * @return NameSymbol instance * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if symbol is null */ - public static final NameSymbol ofSymbol(final String symbol) { + public static NameSymbol ofSymbol(final String symbol) { return new NameSymbol(Optional.empty(), Optional.of(symbol), new HashSet<>()); } @@ -189,21 +197,26 @@ public final class NameSymbol { /** * Creates the {@code NameSymbol}. - * + * * @param primaryName primary name of unit * @param symbol symbol used to represent unit * @param otherNames other names and/or spellings, should be a mutable copy * of the argument * @since 2019-10-21 + * @since v0.3.0 */ - private NameSymbol(final Optional<String> primaryName, - final Optional<String> symbol, final Set<String> otherNames) { + NameSymbol(final Optional<String> primaryName, final Optional<String> symbol, + final Set<String> otherNames) { this.primaryName = primaryName; this.symbol = symbol; - otherNames.remove(null); - this.otherNames = Collections.unmodifiableSet(otherNames); + if (otherNames != null) { + otherNames.remove(null); + this.otherNames = Collections.unmodifiableSet(otherNames); + } else { + this.otherNames = Set.of(); + } - if (this.primaryName.isEmpty()) { + if (this.primaryName == null || this.primaryName.isEmpty()) { assert this.otherNames.isEmpty(); } } @@ -214,7 +227,7 @@ public final class NameSymbol { return true; if (!(obj instanceof NameSymbol)) return false; - final NameSymbol other = (NameSymbol) obj; + final var other = (NameSymbol) obj; if (this.otherNames == null) { if (other.otherNames != null) return false; @@ -236,31 +249,34 @@ public final class NameSymbol { /** * @return otherNames * @since 2019-10-21 + * @since v0.3.0 */ - public final Set<String> getOtherNames() { + public Set<String> getOtherNames() { return this.otherNames; } /** * @return primaryName * @since 2019-10-21 + * @since v0.3.0 */ - public final Optional<String> getPrimaryName() { + public Optional<String> getPrimaryName() { return this.primaryName; } /** * @return symbol * @since 2019-10-21 + * @since v0.3.0 */ - public final Optional<String> getSymbol() { + public Optional<String> getSymbol() { return this.symbol; } @Override public int hashCode() { - final int prime = 31; - int result = 1; + final var prime = 31; + var result = 1; result = prime * result + (this.otherNames == null ? 0 : this.otherNames.hashCode()); result = prime * result @@ -270,10 +286,8 @@ public final class NameSymbol { return result; } - /** - * @return true iff this {@code NameSymbol} contains no names or symbols. - */ - public final boolean isEmpty() { + /** @return true iff this {@code NameSymbol} contains no names or symbols. */ + public boolean isEmpty() { // if primaryName is empty, otherNames must also be empty return this.primaryName.isEmpty() && this.symbol.isEmpty(); } @@ -282,11 +296,10 @@ public final class NameSymbol { public String toString() { if (this.isEmpty()) return "NameSymbol.EMPTY"; - else if (this.primaryName.isPresent() && this.symbol.isPresent()) + if (this.primaryName.isPresent() && this.symbol.isPresent()) return this.primaryName.orElseThrow() + " (" + this.symbol.orElseThrow() + ")"; - else - return this.primaryName.orElseGet(this.symbol::orElseThrow); + return this.primaryName.orElseGet(this.symbol::orElseThrow); } /** @@ -294,16 +307,19 @@ public final class NameSymbol { * extra name. If this {@code NameSymbol} has a primary name, the provided * name will become an other name, otherwise it will become the primary name. * - * @since v0.4.0 + * @param name additional name to add + * @return copy of this NameSymbol with the additional name + * * @since 2022-04-19 + * @since v0.4.0 */ - public final NameSymbol withExtraName(String name) { + public NameSymbol withExtraName(String name) { if (this.primaryName.isPresent()) { final var otherNames = new HashSet<>(this.otherNames); otherNames.add(name); return NameSymbol.ofNullable(this.primaryName.orElse(null), this.symbol.orElse(null), otherNames); - } else - return NameSymbol.ofNullable(name, this.symbol.orElse(null)); + } + return NameSymbol.ofNullable(name, this.symbol.orElse(null)); } }
\ No newline at end of file diff --git a/src/main/java/sevenUnits/utils/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java index 3959a64..166de55 100644 --- a/src/main/java/sevenUnits/utils/Nameable.java +++ b/src/main/java/sevenUnits/utils/Nameable.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 Adrien Hopkins + * Copyright (C) 2020, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,15 +24,17 @@ import java.util.Set; * and symbol data should be immutable. * * @since 2020-09-07 + * @since v0.3.0 */ public interface Nameable { /** * @return a name for the object - if there's a primary name, it's that, * otherwise the symbol, otherwise "Unnamed" * @since 2022-02-26 + * @since v0.4.0 */ default String getName() { - final NameSymbol ns = this.getNameSymbol(); + final var ns = this.getNameSymbol(); return ns.getPrimaryName().or(ns::getSymbol).orElse("Unnamed"); } @@ -40,12 +42,14 @@ public interface Nameable { * @return a {@code NameSymbol} that contains this object's primary name, * symbol and other names * @since 2020-09-07 + * @since v0.3.0 */ NameSymbol getNameSymbol(); /** * @return set of alternate names * @since 2020-09-07 + * @since v0.3.0 */ default Set<String> getOtherNames() { return this.getNameSymbol().getOtherNames(); @@ -54,6 +58,7 @@ public interface Nameable { /** * @return preferred name of object * @since 2020-09-07 + * @since v0.3.0 */ default Optional<String> getPrimaryName() { return this.getNameSymbol().getPrimaryName(); @@ -63,15 +68,17 @@ public interface Nameable { * @return a short name for the object - if there's a symbol, it's that, * otherwise the symbol, otherwise "Unnamed" * @since 2022-02-26 + * @since v0.4.0 */ default String getShortName() { - final NameSymbol ns = this.getNameSymbol(); + final var ns = this.getNameSymbol(); return ns.getSymbol().or(ns::getPrimaryName).orElse("Unnamed"); } /** * @return short symbol representing object * @since 2020-09-07 + * @since v0.3.0 */ default Optional<String> getSymbol() { return this.getNameSymbol().getSymbol(); diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 5a29d79..23fe41c 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2021, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -29,17 +29,26 @@ import java.util.function.Function; /** * An immutable product of multiple objects of a type, such as base units. The * objects can be multiplied and exponentiated. - * + * * @author Adrien Hopkins + * @param <T> type of object that is being multiplied * @since 2019-10-16 + * @since v0.3.0 */ public class ObjectProduct<T> implements Nameable { /** + * If {@link #toExponentRounded}'s rounding changes exponents by more than + * this value, a warning will be printed to standard error. + */ + private static final double ROUND_WARN_THRESHOLD = 1e-12; + + /** * Returns an empty ObjectProduct of a certain type - * + * * @param <T> type of objects that can be multiplied * @return empty product * @since 2019-10-16 + * @since v0.3.0 */ public static final <T> ObjectProduct<T> empty() { return new ObjectProduct<>(new HashMap<>()); @@ -47,11 +56,12 @@ public class ObjectProduct<T> implements Nameable { /** * Gets an {@code ObjectProduct} from an object-to-integer mapping - * + * * @param <T> type of object in product * @param map map mapping objects to exponents * @return object product * @since 2019-10-16 + * @since v0.3.0 */ public static final <T> ObjectProduct<T> fromExponentMapping( final Map<T, Integer> map) { @@ -61,10 +71,12 @@ public class ObjectProduct<T> implements Nameable { /** * Gets an ObjectProduct that has one of the inputted argument, and nothing * else. - * + * * @param object object that will be in the product + * @param <T> type of object contained in returned ObjectProduct * @return product * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if object is null */ public static final <T> ObjectProduct<T> oneOf(final T object) { @@ -77,21 +89,21 @@ public class ObjectProduct<T> implements Nameable { /** * The objects that make up the product, mapped to their exponents. This map * treats zero as null, and is immutable. - * + * * @since 2019-10-16 + * @since v0.3.0 */ final Map<T, Integer> exponents; - /** - * The object's name and symbol - */ + /** The object's name and symbol */ private final NameSymbol nameSymbol; /** * Creates a {@code ObjectProduct} without a name/symbol. - * + * * @param exponents objects that make up this product * @since 2019-10-16 + * @since v0.3.0 */ ObjectProduct(final Map<T, Integer> exponents) { this(exponents, NameSymbol.EMPTY); @@ -99,10 +111,11 @@ public class ObjectProduct<T> implements Nameable { /** * Creates the {@code ObjectProduct}. - * + * * @param exponents objects that make up this product * @param nameSymbol name and symbol of object product * @since 2019-10-16 + * @since v0.3.0 */ ObjectProduct(final Map<T, Integer> exponents, NameSymbol nameSymbol) { this.exponents = Collections.unmodifiableMap( @@ -117,6 +130,7 @@ public class ObjectProduct<T> implements Nameable { * @param other other product * @return quotient of two products * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if other is null */ public ObjectProduct<T> dividedBy(final ObjectProduct<T> other) { @@ -150,6 +164,7 @@ public class ObjectProduct<T> implements Nameable { /** * @return immutable map mapping objects to exponents * @since 2019-10-16 + * @since v0.3.0 */ public Map<T, Integer> exponentMap() { return this.exponents; @@ -177,7 +192,7 @@ public class ObjectProduct<T> implements Nameable { /** * Gets the exponent for a specific dimension. - * + * * @param dimension dimension to check * @return exponent for that dimension * @since 2018-12-12 @@ -201,10 +216,11 @@ public class ObjectProduct<T> implements Nameable { * @return true if this product is a single object, i.e. it has one exponent * of one and no other nonzero exponents * @since 2019-10-16 + * @since v0.3.0 */ public boolean isSingleObject() { - int oneCount = 0; - boolean twoOrMore = false; // has exponents of 2 or more + var oneCount = 0; + var twoOrMore = false; // has exponents of 2 or more for (final T b : this.getBaseSet()) { if (this.getExponent(b) == 1) { oneCount++; @@ -221,6 +237,7 @@ public class ObjectProduct<T> implements Nameable { * @param other other product * @return product of two products * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if other is null */ public ObjectProduct<T> times(final ObjectProduct<T> other) { @@ -242,10 +259,11 @@ public class ObjectProduct<T> implements Nameable { /** * Returns this product, but to an exponent - * + * * @param exponent exponent * @return result of exponentiation * @since 2019-10-16 + * @since v0.3.0 */ public ObjectProduct<T> toExponent(final int exponent) { final Map<T, Integer> map = new HashMap<>(this.exponents); @@ -256,12 +274,41 @@ public class ObjectProduct<T> implements Nameable { } /** + * Returns this product to an exponent, where every dimension is rounded to + * the nearest integer. + * + * This function will send a warning (via standard error) if the rounding + * significantly changes the value. + * + * @param exponent exponent to raise this product to + * @return result of exponentiation + * + * @since 2024-08-22 + * @since v0.3.0 + */ + public ObjectProduct<T> toExponentRounded(final double exponent) { + final Map<T, Integer> map = new HashMap<>(this.exponents); + for (final T key : this.exponents.keySet()) { + final var newExponent = this.getExponent(key) * exponent; + if (Math.abs( + newExponent - Math.round(newExponent)) > ROUND_WARN_THRESHOLD) { + System.err.printf( + "Exponent Rounding Warning: Dimension exponents must be integers, so %d ^ %g = %g was rounded to %d.\n", + this.getExponent(key), exponent, newExponent, + Math.round(newExponent)); + } + map.put(key, (int) Math.round(newExponent)); + } + return new ObjectProduct<>(map); + } + + /** * Converts this product to a string using the objects' * {@link Object#toString()} method (or {@link Nameable#getShortName} if * available). If objects have a long toString representation, it is * recommended to use {@link #toString(Function)} instead to shorten the * returned string. - * + * * <p> * {@inheritDoc} */ @@ -275,10 +322,11 @@ public class ObjectProduct<T> implements Nameable { /** * Converts this product to a string. The objects that make up this product * are represented by {@code objectToString} - * + * * @param objectToString function to convert objects to strings * @return string representation of product * @since 2019-10-16 + * @since v0.3.0 */ public String toString(final Function<T, String> objectToString) { final List<String> positiveStringComponents = new ArrayList<>(); @@ -298,18 +346,20 @@ public class ObjectProduct<T> implements Nameable { } } - final String positiveString = positiveStringComponents.isEmpty() ? "1" + final var positiveString = positiveStringComponents.isEmpty() ? "1" : String.join(" * ", positiveStringComponents); - final String negativeString = negativeStringComponents.isEmpty() ? "" + final var negativeString = negativeStringComponents.isEmpty() ? "" : " / " + String.join(" * ", negativeStringComponents); return positiveString + negativeString; } /** + * @param nameSymbol name to add to this product * @return named version of this {@code ObjectProduct}, using data from * {@code nameSymbol} * @since 2021-12-15 + * @since v0.3.0 */ public ObjectProduct<T> withName(NameSymbol nameSymbol) { return new ObjectProduct<>(this.exponents, nameSymbol); diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java index fc47baa..4bb7ce5 100644 --- a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java +++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -39,8 +38,8 @@ import java.util.regex.Pattern; * are made * </ol> * - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public final class SemanticVersionNumber implements Comparable<SemanticVersionNumber> { @@ -52,8 +51,8 @@ public final class SemanticVersionNumber * throw NullPointerExceptions, everything else throws * IllegalArgumentException. * - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public static final class Builder { private final int major; @@ -69,8 +68,8 @@ public final class SemanticVersionNumber * @param major major version number of final version * @param minor minor version number of final version * @param patch patch version number of final version - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ private Builder(int major, int minor, int patch) { this.major = major; @@ -82,8 +81,8 @@ public final class SemanticVersionNumber /** * @return version number created by this builder - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public SemanticVersionNumber build() { return new SemanticVersionNumber(this.major, this.minor, this.patch, @@ -95,8 +94,8 @@ public final class SemanticVersionNumber * * @param identifiers build metadata * @return this builder - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public Builder buildMetadata(List<String> identifiers) { Objects.requireNonNull(identifiers, "identifiers may not be null"); @@ -115,8 +114,8 @@ public final class SemanticVersionNumber * * @param identifiers build metadata * @return this builder - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public Builder buildMetadata(String... identifiers) { Objects.requireNonNull(identifiers, "identifiers may not be null"); @@ -136,7 +135,7 @@ public final class SemanticVersionNumber return true; if (!(obj instanceof Builder)) return false; - final Builder other = (Builder) obj; + final var other = (Builder) obj; return Objects.equals(this.buildMetadata, other.buildMetadata) && this.major == other.major && this.minor == other.minor && this.patch == other.patch && Objects.equals( @@ -154,8 +153,8 @@ public final class SemanticVersionNumber * * @param identifiers pre-release identifier(s) to add * @return this builder - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public Builder preRelease(int... identifiers) { Objects.requireNonNull(identifiers, "identifiers may not be null"); @@ -173,8 +172,8 @@ public final class SemanticVersionNumber * * @param identifiers pre-release identifier(s) to add * @return this builder - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public Builder preRelease(List<String> identifiers) { Objects.requireNonNull(identifiers, "identifiers may not be null"); @@ -193,8 +192,8 @@ public final class SemanticVersionNumber * * @param identifiers pre-release identifier(s) to add * @return this builder - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public Builder preRelease(String... identifiers) { Objects.requireNonNull(identifiers, "identifiers may not be null"); @@ -214,8 +213,8 @@ public final class SemanticVersionNumber * @param identifier1 first identifier * @param identifier2 second identifier * @return this builder - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public Builder preRelease(String identifier1, int identifier2) { Objects.requireNonNull(identifier1, "identifier1 may not be null"); @@ -249,12 +248,11 @@ public final class SemanticVersionNumber public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) { Objects.requireNonNull(o1, "o1 may not be null"); Objects.requireNonNull(o2, "o2 may not be null"); - final int naturalComparison = o1.compareTo(o2); + final var naturalComparison = o1.compareTo(o2); if (naturalComparison == 0) return SemanticVersionNumber.compareIdentifiers(o1.buildMetadata, o2.buildMetadata); - else - return naturalComparison; + return naturalComparison; }; }; @@ -280,11 +278,11 @@ public final class SemanticVersionNumber * @param patch patch version number of final version * @return version number builder * @throws IllegalArgumentException if any argument is negative - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ - public static final SemanticVersionNumber.Builder builder(int major, - int minor, int patch) { + public static SemanticVersionNumber.Builder builder(int major, int minor, + int patch) { if (major < 0) throw new IllegalArgumentException( "Major version must be non-negative."); @@ -304,60 +302,57 @@ public final class SemanticVersionNumber * @param b second list * @return result of comparison as in a comparator * @see Comparator - * @since v0.4.0 * @since 2022-02-20 + * @since v0.4.0 */ - private static final int compareIdentifiers(List<String> a, List<String> b) { + private static int compareIdentifiers(List<String> a, List<String> b) { // test pre-release size - final int aSize = a.size(); - final int bSize = b.size(); + final var aSize = a.size(); + final var bSize = b.size(); // no identifiers is greater than any identifiers if (aSize != 0 && bSize == 0) return -1; - else if (aSize == 0 && bSize != 0) + if (aSize == 0 && bSize != 0) return 1; // test identifiers one by one - for (int i = 0; i < Math.min(aSize, bSize); i++) { - final String aElement = a.get(i); - final String bElement = b.get(i); + for (var i = 0; i < Math.min(aSize, bSize); i++) { + final var aElement = a.get(i); + final var bElement = b.get(i); if (NUMERIC_IDENTIFER.matcher(aElement).matches()) { - if (NUMERIC_IDENTIFER.matcher(bElement).matches()) { - // both are numbers, compare them - final int aNumber = Integer.parseInt(aElement); - final int bNumber = Integer.parseInt(bElement); - - if (aNumber < bNumber) - return -1; - else if (aNumber > bNumber) - return 1; - } else + if (!NUMERIC_IDENTIFER.matcher(bElement).matches()) // aElement is a number and bElement is not a number // by the rules, a goes before b return -1; - } else { - if (NUMERIC_IDENTIFER.matcher(bElement).matches()) - // aElement is not a number but bElement is - // by the rules, a goes after b + // both are numbers, compare them + final var aNumber = Integer.parseInt(aElement); + final var bNumber = Integer.parseInt(bElement); + + if (aNumber < bNumber) + return -1; + if (aNumber > bNumber) return 1; - else { - // both are not numbers, compare them - final int comparison = aElement.compareTo(bElement); - if (comparison != 0) - return comparison; - } + } else if (NUMERIC_IDENTIFER.matcher(bElement).matches()) + // aElement is not a number but bElement is + // by the rules, a goes after b + return 1; + else { + // both are not numbers, compare them + final var comparison = aElement.compareTo(bElement); + if (comparison != 0) + return comparison; } } - - // we just tested the stuff that's in common, maybe someone has more if (aSize < bSize) return -1; - else if (aSize > bSize) + if (aSize > bSize) return 1; - else - return 0; + return 0; + + // we just tested the stuff that's in common, maybe someone has more + } /** @@ -365,23 +360,23 @@ public final class SemanticVersionNumber * * @param versionString string to parse * @return {@code SemanticVersionNumber} instance - * @since v0.4.0 * @since 2022-02-19 - * @see {@link #toString} + * @since v0.4.0 + * @see #toString */ - public static final SemanticVersionNumber fromString(String versionString) { + public static SemanticVersionNumber fromString(String versionString) { // parse & validate version string Objects.requireNonNull(versionString, "versionString may not be null"); - final Matcher m = VERSION_NUMBER.matcher(versionString); + final var m = VERSION_NUMBER.matcher(versionString); if (!m.matches()) throw new IllegalArgumentException( String.format("Provided string \"%s\" is not a version number", versionString)); // main parts - final int major = Integer.parseInt(m.group(1)); - final int minor = Integer.parseInt(m.group(2)); - final int patch = Integer.parseInt(m.group(3)); + final var major = Integer.parseInt(m.group(1)); + final var minor = Integer.parseInt(m.group(2)); + final var patch = Integer.parseInt(m.group(3)); // pre release final List<String> preRelease; @@ -406,13 +401,13 @@ public final class SemanticVersionNumber /** * Tests whether a string is a valid Semantic Version string - * + * * @param versionString string to test * @return true iff string is valid - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ - public static final boolean isValidVersionString(String versionString) { + public static boolean isValidVersionString(String versionString) { return VERSION_NUMBER.matcher(versionString).matches(); } @@ -429,10 +424,10 @@ public final class SemanticVersionNumber * @throws IllegalArgumentException if any argument is negative or if the * preReleaseType is null, empty or not * alphanumeric (0-9, A-Z, a-z, - only) - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ - public static final SemanticVersionNumber preRelease(int major, int minor, + public static SemanticVersionNumber preRelease(int major, int minor, int patch, String preReleaseType, int preReleaseNumber) { if (major < 0) throw new IllegalArgumentException( @@ -467,10 +462,10 @@ public final class SemanticVersionNumber * @param patch patch version number * @return {@code SemanticVersionNumber} instance * @throws IllegalArgumentException if any argument is negative - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ - public static final SemanticVersionNumber stableVersion(int major, int minor, + public static SemanticVersionNumber stableVersion(int major, int minor, int patch) { if (major < 0) throw new IllegalArgumentException( @@ -500,8 +495,8 @@ public final class SemanticVersionNumber * @param patch patch version number * @param preReleaseIdentifiers pre-release version data * @param buildMetadata build metadata - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ private SemanticVersionNumber(int major, int minor, int patch, List<String> preReleaseIdentifiers, List<String> buildMetadata) { @@ -514,8 +509,8 @@ public final class SemanticVersionNumber /** * @return build metadata (empty if there is none) - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public List<String> buildMetadata() { return Collections.unmodifiableList(this.buildMetadata); @@ -536,17 +531,17 @@ public final class SemanticVersionNumber // test the three big numbers in order first if (this.major < o.major) return -1; - else if (this.major > o.major) + if (this.major > o.major) return 1; if (this.minor < o.minor) return -1; - else if (this.minor > o.minor) + if (this.minor > o.minor) return 1; if (this.patch < o.patch) return -1; - else if (this.patch > o.patch) + if (this.patch > o.patch) return 1; // now we just compare pre-release identifiers @@ -569,7 +564,7 @@ public final class SemanticVersionNumber * </ul> * If this function returns <b>false</b>, you may have to change your code to * upgrade it to {@code other} - * + * * <p> * Two version numbers that are identical (ignoring build metadata) are * always compatible. Different version numbers are compatible as long as: @@ -585,8 +580,8 @@ public final class SemanticVersionNumber * @param other version to compare with * @return true if you can definitely upgrade to {@code other} without * changing code - * @since v0.4.0 * @since 2022-02-20 + * @since v0.4.0 */ public boolean compatibleWith(SemanticVersionNumber other) { Objects.requireNonNull(other, "other may not be null"); @@ -601,17 +596,14 @@ public final class SemanticVersionNumber return true; if (!(obj instanceof SemanticVersionNumber)) return false; - final SemanticVersionNumber other = (SemanticVersionNumber) obj; + final var other = (SemanticVersionNumber) obj; if (this.buildMetadata == null) { if (other.buildMetadata != null) return false; } else if (!this.buildMetadata.equals(other.buildMetadata)) return false; - if (this.major != other.major) - return false; - if (this.minor != other.minor) - return false; - if (this.patch != other.patch) + if ((this.major != other.major) || (this.minor != other.minor) + || (this.patch != other.patch)) return false; if (this.preReleaseIdentifiers == null) { if (other.preReleaseIdentifiers != null) @@ -624,8 +616,8 @@ public final class SemanticVersionNumber @Override public int hashCode() { - final int prime = 31; - int result = 1; + final var prime = 31; + var result = 1; result = prime * result + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode()); result = prime * result + this.major; @@ -639,8 +631,8 @@ public final class SemanticVersionNumber /** * @return true iff this version is stable (major version > 0 and not a * pre-release) - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public boolean isStable() { return this.major > 0 && this.preReleaseIdentifiers.isEmpty(); @@ -649,8 +641,8 @@ public final class SemanticVersionNumber /** * @return the MAJOR version number, incremented when you make backwards * incompatible API changes - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public int majorVersion() { return this.major; @@ -659,8 +651,8 @@ public final class SemanticVersionNumber /** * @return the MINOR version number, incremented when you add backwards * compatible functionality - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public int minorVersion() { return this.minor; @@ -669,8 +661,8 @@ public final class SemanticVersionNumber /** * @return the PATCH version number, incremented when you make backwards * compatible bug fixes - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public int patchVersion() { return this.patch; @@ -679,8 +671,8 @@ public final class SemanticVersionNumber /** * @return identifiers describing this pre-release (empty if not a * pre-release) - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public List<String> preReleaseIdentifiers() { return Collections.unmodifiableList(this.preReleaseIdentifiers); @@ -697,13 +689,13 @@ public final class SemanticVersionNumber * For example, the version with major number 3, minor number 2, patch number * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19" * has a string representation "3.2.1-alpha.1+2022-02-19". - * + * * @since v0.4.0 * @see <a href="https://semver.org">The official SemVer specification</a> */ @Override public String toString() { - String versionString = String.format("%d.%d.%d", this.major, this.minor, + var versionString = String.format("%d.%d.%d", this.major, this.minor, this.patch); if (!this.preReleaseIdentifiers.isEmpty()) { versionString += "-" + String.join(".", this.preReleaseIdentifiers); diff --git a/src/main/java/sevenUnits/utils/UncertainDouble.java b/src/main/java/sevenUnits/utils/UncertainDouble.java index 66d8103..24ada20 100644 --- a/src/main/java/sevenUnits/utils/UncertainDouble.java +++ b/src/main/java/sevenUnits/utils/UncertainDouble.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 Adrien Hopkins + * Copyright (C) 2020-2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,7 +19,6 @@ package sevenUnits.utils; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Objects; -import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -29,56 +28,59 @@ import java.util.regex.Pattern; * arguments is null. * * @since 2020-09-07 + * @since v0.3.0 */ public final class UncertainDouble implements Comparable<UncertainDouble> { - /** - * The exact value 0 - */ + /** The exact value 0 */ public static final UncertainDouble ZERO = UncertainDouble.of(0, 0); static final String NUMBER_REGEX = "(\\d+(?:[\\.,]\\d+))"; - /** - * A regular expression that can recognize toString forms - */ + /** A regular expression that can recognize toString forms */ static final Pattern TO_STRING = Pattern.compile(NUMBER_REGEX - // optional "� [number]" - + "(?:\\s*(?:�|\\+-)\\s*" + NUMBER_REGEX + ")?"); + // optional "± [number]" + + "(?:\\s*(?:±|\\+-)\\s*" + NUMBER_REGEX + ")?"); /** * Gets an UncertainDouble from a double string. The uncertainty of the * double will be one of the lowest decimal place of the number. For example, - * "12345.678" will become 12345.678 � 0.001. - * + * "12345.678" will become 12345.678 ± 0.001. + * + * @param s string to parse + * @return parsed {@code UncertainDouble} + * * @throws NumberFormatException if the argument is not a number * * @since 2022-04-18 + * @since v0.4.0 */ - public static final UncertainDouble fromRoundedString(String s) { - final BigDecimal value = new BigDecimal(s); - final double uncertainty = Math.pow(10, -value.scale()); + public static UncertainDouble fromRoundedString(String s) { + final var value = new BigDecimal(s); + final var uncertainty = Math.pow(10, -value.scale()); return UncertainDouble.of(value.doubleValue(), uncertainty); } /** - * Parses a string in the form of {@link UncertainDouble#toString(boolean)} - * and returns the corresponding {@code UncertainDouble} instance. + * Parses a string in the form of + * {@link UncertainDouble#toString(boolean, RoundingMode)} and returns the + * corresponding {@code UncertainDouble} instance. * <p> * This method allows some alternative forms of the string representation, - * such as using "+-" instead of "�". - * + * such as using "+-" instead of "±". + * * @param s string to parse * @return {@code UncertainDouble} instance * @throws IllegalArgumentException if the string is invalid * @since 2020-09-07 + * @since v0.3.0 */ - public static final UncertainDouble fromString(String s) { + public static UncertainDouble fromString(String s) { Objects.requireNonNull(s, "s may not be null"); - final Matcher matcher = TO_STRING.matcher(s); + final var matcher = TO_STRING.matcher(s); if (!matcher.matches()) throw new IllegalArgumentException( - "Could not parse stirng \"" + s + "\"."); + "Could not parse string \"" + s + "\"."); double value, uncertainty; try { @@ -88,7 +90,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { "String " + s + " not in correct format."); } - final String uncertaintyString = matcher.group(2); + final var uncertaintyString = matcher.group(2); if (uncertaintyString == null) { uncertainty = 0; } else { @@ -106,20 +108,32 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Gets an {@code UncertainDouble} from its value and <b>absolute</b> * uncertainty. - * + * + * @param value double's value + * @param uncertainty double's uncertainty (non-negative) + * @return {@code UncertainDouble} instance with these parameters + * * @since 2020-09-07 + * @since v0.3.0 */ - public static final UncertainDouble of(double value, double uncertainty) { + public static UncertainDouble of(double value, double uncertainty) { return new UncertainDouble(value, uncertainty); } /** * Gets an {@code UncertainDouble} from its value and <b>relative</b> * uncertainty. - * + * + * @param value double's value + * @param relativeUncertainty double's uncertainty (non-negative); the + * absolute uncertainty is equal to this value + * multiplied by {@code relativeUncertainty} + * @return {@code UncertainDouble} instance with these parameters + * * @since 2020-09-07 + * @since v0.3.0 */ - public static final UncertainDouble ofRelative(double value, + public static UncertainDouble ofRelative(double value, double relativeUncertainty) { return new UncertainDouble(value, value * relativeUncertainty); } @@ -132,6 +146,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { * @param value * @param uncertainty * @since 2020-09-07 + * @since v0.3.0 */ private UncertainDouble(double value, double uncertainty) { this.value = value; @@ -153,16 +168,20 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { * {@code false}. */ @Override - public final int compareTo(UncertainDouble o) { + public int compareTo(UncertainDouble o) { return Double.compare(this.value, o.value); } /** * Returns the quotient of {@code this} and {@code other}. - * + * + * @param other number to divide by + * @return quotient + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble dividedBy(UncertainDouble other) { + public UncertainDouble dividedBy(UncertainDouble other) { Objects.requireNonNull(other, "other may not be null"); return UncertainDouble.ofRelative(this.value / other.value, Math .hypot(this.relativeUncertainty(), other.relativeUncertainty())); @@ -170,23 +189,26 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Returns the quotient of {@code this} and the exact value {@code other}. - * + * + * @param other number to divide by + * @return quotient + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble dividedByExact(double other) { + public UncertainDouble dividedByExact(double other) { return UncertainDouble.of(this.value / other, this.uncertainty / other); } @Override - public final boolean equals(Object obj) { + public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof UncertainDouble)) return false; - final UncertainDouble other = (UncertainDouble) obj; - if (Double.compare(this.value, other.value) != 0) - return false; - if (Double.compare(this.uncertainty, other.uncertainty) != 0) + final var other = (UncertainDouble) obj; + if ((Double.compare(this.value, other.value) != 0) + || (Double.compare(this.uncertainty, other.uncertainty) != 0)) return false; return true; } @@ -196,8 +218,9 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { * @return true iff this and {@code other} are within each other's * uncertainty range. * @since 2020-09-07 + * @since v0.3.0 */ - public final boolean equivalent(UncertainDouble other) { + public boolean equivalent(UncertainDouble other) { Objects.requireNonNull(other, "other may not be null"); return Math.abs(this.value - other.value) <= Math.min(this.uncertainty, other.uncertainty); @@ -205,10 +228,11 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Gets the preferred scale for rounding a value for toString. - * + * * @since 2020-09-07 + * @since v0.3.0 */ - private final int getDisplayScale() { + private int getDisplayScale() { // round based on uncertainty // if uncertainty starts with 1 (ignoring zeroes and the decimal // point), rounds @@ -216,24 +240,23 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { // otherwise, rounds so that uncertainty has 1 significant digits. // the value is rounded to the same number of decimal places as the // uncertainty. - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + final var bigUncertainty = BigDecimal.valueOf(this.uncertainty); // the scale that will give the uncertainty two decimal places - final int twoDecimalPlacesScale = bigUncertainty.scale() + final var twoDecimalPlacesScale = bigUncertainty.scale() - bigUncertainty.precision() + 2; - final BigDecimal roundedUncertainty = bigUncertainty + final var roundedUncertainty = bigUncertainty .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN); if (roundedUncertainty.unscaledValue().intValue() >= 20) return twoDecimalPlacesScale - 1; // one decimal place - else - return twoDecimalPlacesScale; + return twoDecimalPlacesScale; } @Override - public final int hashCode() { - final int prime = 31; - int result = 1; + public int hashCode() { + final var prime = 31; + var result = 1; result = prime * result + Double.hashCode(this.value); result = prime * result + Double.hashCode(this.uncertainty); return result; @@ -241,19 +264,24 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * @return true iff the value has no uncertainty - * + * * @since 2020-09-07 + * @since v0.3.0 */ - public final boolean isExact() { + public boolean isExact() { return this.uncertainty == 0; } /** * Returns the difference of {@code this} and {@code other}. - * + * + * @param other number to subtract + * @return result of subtraction + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble minus(UncertainDouble other) { + public UncertainDouble minus(UncertainDouble other) { Objects.requireNonNull(other, "other may not be null"); return UncertainDouble.of(this.value - other.value, Math.hypot(this.uncertainty, other.uncertainty)); @@ -261,19 +289,27 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Returns the difference of {@code this} and the exact value {@code other}. - * + * + * @param other number to subtract + * @return result of subtraction + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble minusExact(double other) { + public UncertainDouble minusExact(double other) { return UncertainDouble.of(this.value - other, this.uncertainty); } /** * Returns the sum of {@code this} and {@code other}. - * + * + * @param other number to add + * @return result of addition + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble plus(UncertainDouble other) { + public UncertainDouble plus(UncertainDouble other) { Objects.requireNonNull(other, "other may not be null"); return UncertainDouble.of(this.value + other.value, Math.hypot(this.uncertainty, other.uncertainty)); @@ -281,27 +317,36 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Returns the sum of {@code this} and the exact value {@code other}. - * + * + * @param other number to add + * @return result of addition + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble plusExact(double other) { + public UncertainDouble plusExact(double other) { return UncertainDouble.of(this.value + other, this.uncertainty); } /** * @return relative uncertainty * @since 2020-09-07 + * @since v0.3.0 */ - public final double relativeUncertainty() { + public double relativeUncertainty() { return this.uncertainty / this.value; } /** * Returns the product of {@code this} and {@code other}. - * + * + * @param other number to multiply + * @return product + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble times(UncertainDouble other) { + public UncertainDouble times(UncertainDouble other) { Objects.requireNonNull(other, "other may not be null"); return UncertainDouble.ofRelative(this.value * other.value, Math .hypot(this.relativeUncertainty(), other.relativeUncertainty())); @@ -309,23 +354,31 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Returns the product of {@code this} and the exact value {@code other}. - * + * + * @param other number to multiply + * @return product + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble timesExact(double other) { + public UncertainDouble timesExact(double other) { return UncertainDouble.of(this.value * other, this.uncertainty * other); } /** * Returns the result of {@code this} raised to the exponent {@code other}. - * + * + * @param other exponent + * @return result of exponentation + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble toExponent(UncertainDouble other) { + public UncertainDouble toExponent(UncertainDouble other) { Objects.requireNonNull(other, "other may not be null"); - final double result = Math.pow(this.value, other.value); - final double relativeUncertainty = Math.hypot( + final var result = Math.pow(this.value, other.value); + final var relativeUncertainty = Math.hypot( other.value * this.relativeUncertainty(), Math.log(this.value) * other.uncertainty); @@ -335,10 +388,14 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Returns the result of {@code this} raised the exact exponent * {@code other}. - * + * + * @param other exponent + * @return result of exponentation + * * @since 2020-09-07 + * @since v0.3.0 */ - public final UncertainDouble toExponentExact(double other) { + public UncertainDouble toExponentExact(double other) { return UncertainDouble.ofRelative(Math.pow(this.value, other), this.relativeUncertainty() * other); } @@ -346,23 +403,24 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { /** * Returns a string representation of this {@code UncertainDouble}. * <p> - * This method returns the same value as {@link #toString(boolean)}, but - * {@code showUncertainty} is true if and only if the uncertainty is - * non-zero. - * + * This method returns the same value as + * {@link #toString(boolean, RoundingMode)}, but {@code showUncertainty} is + * true if and only if the uncertainty is non-zero. + * * <p> * Examples: - * + * * <pre> * UncertainDouble.of(3.27, 0.22).toString() = "3.3 � 0.2" * UncertainDouble.of(3.27, 0.13).toString() = "3.27 � 0.13" * UncertainDouble.of(-5.01, 0).toString() = "-5.01" * </pre> - * + * * @since 2020-09-07 + * @since v0.3.0 */ @Override - public final String toString() { + public String toString() { return this.toString(!this.isExact(), RoundingMode.HALF_EVEN); } @@ -370,7 +428,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { * Returns a string representation of this {@code UncertainDouble}. * <p> * If {@code showUncertainty} is true, the string will be of the form "VALUE - * � UNCERTAINTY", and if it is false the string will be of the form "VALUE" + * ± UNCERTAINTY", and if it is false the string will be of the form "VALUE" * <p> * VALUE represents a string representation of this {@code UncertainDouble}'s * value. If the uncertainty is non-zero, the string will be rounded to the @@ -382,20 +440,24 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { * digits otherwise it will be rounded to one significant digit. * <p> * Examples: - * + * * <pre> * UncertainDouble.of(3.27, 0.22).toString(false) = "3.3" - * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 � 0.2" + * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2" * UncertainDouble.of(3.27, 0.13).toString(false) = "3.27" - * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 � 0.13" + * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13" * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01" - * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 � 0.0" + * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0" * </pre> - * + * + * @param showUncertainty uncertainty is only shown if this parameter is true + * @param roundingMode how to round values + * @return string representation of this {@code UncertainDouble} + * * @since 2020-09-07 + * @since v0.3.0 */ - public final String toString(boolean showUncertainty, - RoundingMode roundingMode) { + public String toString(boolean showUncertainty, RoundingMode roundingMode) { String valueString, uncertaintyString; // generate the string representation of value and uncertainty @@ -405,36 +467,37 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { } else { // round the value and uncertainty according to getDisplayScale() - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + final var bigValue = BigDecimal.valueOf(this.value); + final var bigUncertainty = BigDecimal.valueOf(this.uncertainty); - final int displayScale = this.getDisplayScale(); - final BigDecimal roundedUncertainty = bigUncertainty - .setScale(displayScale, roundingMode); - final BigDecimal roundedValue = bigValue.setScale(displayScale, + final var displayScale = this.getDisplayScale(); + final var roundedUncertainty = bigUncertainty.setScale(displayScale, roundingMode); + final var roundedValue = bigValue.setScale(displayScale, roundingMode); valueString = roundedValue.toString(); uncertaintyString = roundedUncertainty.toString(); } - // return "value" or "value � uncertainty" depending on showUncertainty - return valueString + (showUncertainty ? " � " + uncertaintyString : ""); + // return "value" or "value ± uncertainty" depending on showUncertainty + return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); } /** * @return absolute uncertainty * @since 2020-09-07 + * @since v0.3.0 */ - public final double uncertainty() { + public double uncertainty() { return this.uncertainty; } /** * @return value without uncertainty * @since 2020-09-07 + * @since v0.3.0 */ - public final double value() { + public double value() { return this.value; } } diff --git a/src/main/java/sevenUnits/utils/package-info.java b/src/main/java/sevenUnits/utils/package-info.java index 350c62d..b600c17 100644 --- a/src/main/java/sevenUnits/utils/package-info.java +++ b/src/main/java/sevenUnits/utils/package-info.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2020 Adrien Hopkins + * Copyright (C) 2018-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,7 +17,7 @@ /** * Supplementary classes that are not related to units, but are necessary for * their function. - * + * * @author Adrien Hopkins * @since 2019-03-14 * @since v0.2.0 diff --git a/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java index 1fb2709..0e38c67 100644 --- a/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java +++ b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java @@ -1,5 +1,18 @@ /** - * @since 2020-08-26 + * Copyright (C) 2020, 2022, 2024, 2025 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package sevenUnitsGUI; @@ -13,14 +26,17 @@ import sevenUnits.unit.UnitPrefix; * A rule that specifies whether prefix repetition is allowed * * @since 2020-08-26 + * @since v0.3.0 */ public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { + /** Prefix repetition is never allowed; only one prefix may be used. */ NO_REPETITION { @Override public boolean test(List<UnitPrefix> prefixes) { return prefixes.size() <= 1; } }, + /** Prefix repetition is always allowed, without restrictions. */ NO_RESTRICTION { @Override public boolean test(List<UnitPrefix> prefixes) { @@ -40,7 +56,7 @@ public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { final boolean magnifying; if (prefixes.isEmpty()) return true; - else if (prefixes.get(0).getMultiplier() > 1) { + if (prefixes.get(0).getMultiplier() > 1) { magnifying = true; } else { magnifying = false; @@ -52,15 +68,14 @@ public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { if (!Metric.DECIMAL_PREFIXES.contains(prefixes.get(0))) return NO_REPETITION.test(prefixes); - int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + var part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, // 2=deka,hecto,deci,centi for (final UnitPrefix prefix : prefixes) { // check that the current prefix is metric and appropriately // magnifying/reducing - if (!Metric.DECIMAL_PREFIXES.contains(prefix)) - return false; - if (magnifying != prefix.getMultiplier() > 1) + if (!Metric.DECIMAL_PREFIXES.contains(prefix) + || (magnifying != prefix.getMultiplier() > 1)) return false; // check if the current prefix is correct diff --git a/src/main/java/sevenUnitsGUI/DelegateListModel.java b/src/main/java/sevenUnitsGUI/DelegateListModel.java index 798383b..da4f978 100644 --- a/src/main/java/sevenUnitsGUI/DelegateListModel.java +++ b/src/main/java/sevenUnitsGUI/DelegateListModel.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2022, 2024 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -31,7 +31,7 @@ import javax.swing.AbstractListModel; * the delegated list's methods because the delegate methods handle updating the * list. * </p> - * + * * @author Adrien Hopkins * @since 2019-01-14 * @since v0.1.0 @@ -46,7 +46,7 @@ final class DelegateListModel<E> extends AbstractListModel<E> /** * The list that this model is a delegate to. - * + * * @since 2019-01-14 * @since v0.1.0 */ @@ -54,8 +54,9 @@ final class DelegateListModel<E> extends AbstractListModel<E> /** * Creates an empty {@code DelegateListModel}. - * + * * @since 2019-04-13 + * @since v0.2.0 */ public DelegateListModel() { this(new ArrayList<>()); @@ -63,7 +64,7 @@ final class DelegateListModel<E> extends AbstractListModel<E> /** * Creates the {@code DelegateListModel}. - * + * * @param delegate list to delegate * @since 2019-01-14 * @since v0.1.0 @@ -74,8 +75,8 @@ final class DelegateListModel<E> extends AbstractListModel<E> @Override public boolean add(final E element) { - final int index = this.delegate.size(); - final boolean success = this.delegate.add(element); + final var index = this.delegate.size(); + final var success = this.delegate.add(element); this.fireIntervalAdded(this, index, index); return success; } @@ -88,7 +89,7 @@ final class DelegateListModel<E> extends AbstractListModel<E> @Override public boolean addAll(final Collection<? extends E> c) { - boolean changed = false; + var changed = false; for (final E e : c) { if (this.add(e)) { changed = true; @@ -108,7 +109,7 @@ final class DelegateListModel<E> extends AbstractListModel<E> @Override public void clear() { - final int oldSize = this.delegate.size(); + final var oldSize = this.delegate.size(); this.delegate.clear(); if (oldSize >= 1) { this.fireIntervalRemoved(this, 0, oldSize - 1); @@ -176,7 +177,7 @@ final class DelegateListModel<E> extends AbstractListModel<E> @Override public E remove(final int index) { - final E returnValue = this.delegate.get(index); + final var returnValue = this.delegate.get(index); this.delegate.remove(index); this.fireIntervalRemoved(this, index, index); return returnValue; @@ -184,15 +185,15 @@ final class DelegateListModel<E> extends AbstractListModel<E> @Override public boolean remove(final Object o) { - final int index = this.delegate.indexOf(o); - final boolean returnValue = this.delegate.remove(o); + final var index = this.delegate.indexOf(o); + final var returnValue = this.delegate.remove(o); this.fireIntervalRemoved(this, index, index); return returnValue; } @Override public boolean removeAll(final Collection<?> c) { - boolean changed = false; + var changed = false; for (final Object e : c) { if (this.remove(e)) { changed = true; @@ -203,15 +204,15 @@ final class DelegateListModel<E> extends AbstractListModel<E> @Override public boolean retainAll(final Collection<?> c) { - final int oldSize = this.size(); - final boolean returnValue = this.delegate.retainAll(c); + final var oldSize = this.size(); + final var returnValue = this.delegate.retainAll(c); this.fireIntervalRemoved(this, this.size(), oldSize - 1); return returnValue; } @Override public E set(final int index, final E element) { - final E returnValue = this.delegate.get(index); + final var returnValue = this.delegate.get(index); this.delegate.set(index, element); this.fireContentsChanged(this, index, index); return returnValue; diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java index 882c995..ce69365 100644 --- a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java +++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * Copyright (C) 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -18,32 +18,32 @@ package sevenUnitsGUI; /** * A View that can convert unit expressions - * + * * @author Adrien Hopkins - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ public interface ExpressionConversionView extends View { /** * @return unit expression to convert <em>from</em> - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ String getFromExpression(); /** * @return unit expression to convert <em>to</em> - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ String getToExpression(); /** * Shows the output of an expression conversion to the user. - * + * * @param uc unit conversion to show - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ void showExpressionConversionOutput(UnitConversionRecord uc); } diff --git a/src/main/java/sevenUnitsGUI/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java index 484a98f..ff942fb 100644 --- a/src/main/java/sevenUnitsGUI/FilterComparator.java +++ b/src/main/java/sevenUnitsGUI/FilterComparator.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -21,9 +21,9 @@ import java.util.Objects; /** * A comparator that compares strings using a filter. - * + * * @param <T> type of element being compared - * + * * @author Adrien Hopkins * @since 2019-01-15 * @since v0.1.0 @@ -31,21 +31,21 @@ import java.util.Objects; final class FilterComparator<T> implements Comparator<T> { /** * The filter that the comparator is filtered by. - * + * * @since 2019-01-15 * @since v0.1.0 */ private final String filter; /** * The comparator to use if the arguments are otherwise equal. - * + * * @since 2019-01-15 * @since v0.1.0 */ private final Comparator<T> comparator; /** * Whether or not the comparison is case-sensitive. - * + * * @since 2019-04-14 * @since v0.2.0 */ @@ -53,7 +53,7 @@ final class FilterComparator<T> implements Comparator<T> { /** * Creates the {@code FilterComparator}. - * + * * @param filter * @since 2019-01-15 * @since v0.1.0 @@ -64,7 +64,7 @@ final class FilterComparator<T> implements Comparator<T> { /** * Creates the {@code FilterComparator}. - * + * * @param filter string to filter by * @param comparator comparator to fall back to if all else fails, null is * compareTo. @@ -79,7 +79,7 @@ final class FilterComparator<T> implements Comparator<T> { /** * Creates the {@code FilterComparator}. - * + * * @param filter string to filter by * @param comparator comparator to fall back to if all else fails, null is * compareTo. @@ -118,19 +118,18 @@ final class FilterComparator<T> implements Comparator<T> { // elements that start with the filter always go first if (str0.startsWith(this.filter) && !str1.startsWith(this.filter)) return -1; - else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) + if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) return 1; // elements that contain the filter but don't start with them go next if (str0.contains(this.filter) && !str1.contains(this.filter)) return -1; - else if (!str0.contains(this.filter) && !str1.contains(this.filter)) + if (!str0.contains(this.filter) && !str1.contains(this.filter)) return 1; // other elements go last if (this.comparator == null) return str0.compareTo(str1); - else - return this.comparator.compare(arg0, arg1); + return this.comparator.compare(arg0, arg1); } } diff --git a/src/main/java/sevenUnitsGUI/GridBagBuilder.java b/src/main/java/sevenUnitsGUI/GridBagBuilder.java index fdbaee7..a9fede3 100644 --- a/src/main/java/sevenUnitsGUI/GridBagBuilder.java +++ b/src/main/java/sevenUnitsGUI/GridBagBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -21,7 +21,7 @@ import java.awt.Insets; /** * A builder for Java's {@link java.awt.GridBagConstraints} class. - * + * * @author Adrien Hopkins * @since 2018-11-30 * @since v0.1.0 @@ -40,7 +40,7 @@ final class GridBagBuilder { * <p> * The default value is <code>RELATIVE</code>. <code>gridx</code> should be a * non-negative value. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#gridy @@ -58,7 +58,7 @@ final class GridBagBuilder { * <p> * The default value is <code>RELATIVE</code>. <code>gridy</code> should be a * non-negative value. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#gridx @@ -76,7 +76,7 @@ final class GridBagBuilder { * from <code>gridx</code> to the next to the last one in its row. * <p> * <code>gridwidth</code> should be non-negative and the default value is 1. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#gridheight @@ -96,7 +96,7 @@ final class GridBagBuilder { * <p> * <code>gridheight</code> should be a non-negative value and the default * value is 1. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#gridwidth @@ -119,7 +119,7 @@ final class GridBagBuilder { * <p> * The default value of this field is <code>0</code>. <code>weightx</code> * should be a non-negative value. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#weighty @@ -142,7 +142,7 @@ final class GridBagBuilder { * <p> * The default value of this field is <code>0</code>. <code>weighty</code> * should be a non-negative value. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#weightx @@ -173,7 +173,7 @@ final class GridBagBuilder { * <code>BELOW_BASELINE</code>, <code>BELOW_BASELINE_LEADING</code>, and * <code>BELOW_BASELINE_TRAILING</code>. The default value is * <code>CENTER</code>. - * + * * @serial * @see #clone() * @see java.awt.ComponentOrientation @@ -199,7 +199,7 @@ final class GridBagBuilder { * </ul> * <p> * The default value is <code>NONE</code>. - * + * * @serial * @see #clone() */ @@ -212,7 +212,7 @@ final class GridBagBuilder { * amount of space between the component and the edges of its display area. * <p> * The default value is <code>new Insets(0, 0, 0, 0)</code>. - * + * * @serial * @see #clone() */ @@ -226,7 +226,7 @@ final class GridBagBuilder { * is at least its minimum width plus <code>ipadx</code> pixels. * <p> * The default value is <code>0</code>. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#ipady @@ -241,7 +241,7 @@ final class GridBagBuilder { * least its minimum height plus <code>ipady</code> pixels. * <p> * The default value is 0. - * + * * @serial * @see #clone() * @see java.awt.GridBagConstraints#ipadx @@ -292,7 +292,6 @@ final class GridBagBuilder { final int gridheight, final double weightx, final double weighty, final int anchor, final int fill, final Insets insets, final int ipadx, final int ipady) { - super(); this.gridx = gridx; this.gridy = gridy; this.gridwidth = gridwidth; @@ -418,6 +417,7 @@ final class GridBagBuilder { /** * @param anchor anchor to set + * @return this, so that you can chain methods * @since 2018-11-30 * @since v0.1.0 */ @@ -428,6 +428,7 @@ final class GridBagBuilder { /** * @param fill fill to set + * @return this, so that you can chain methods * @since 2018-11-30 * @since v0.1.0 */ @@ -438,6 +439,7 @@ final class GridBagBuilder { /** * @param insets insets to set + * @return this, so that you can chain methods * @since 2018-11-30 * @since v0.1.0 */ @@ -448,6 +450,7 @@ final class GridBagBuilder { /** * @param ipadx ipadx to set + * @return this, so that you can chain methods * @since 2018-11-30 * @since v0.1.0 */ @@ -458,6 +461,7 @@ final class GridBagBuilder { /** * @param ipady ipady to set + * @return this, so that you can chain methods * @since 2018-11-30 * @since v0.1.0 */ @@ -468,6 +472,7 @@ final class GridBagBuilder { /** * @param weightx weightx to set + * @return this, so that you can chain methods * @since 2018-11-30 * @since v0.1.0 */ @@ -478,6 +483,7 @@ final class GridBagBuilder { /** * @param weighty weighty to set + * @return this, so that you can chain methods * @since 2018-11-30 * @since v0.1.0 */ diff --git a/src/main/java/sevenUnitsGUI/Main.java b/src/main/java/sevenUnitsGUI/Main.java index ff61b3b..3ff2fd9 100644 --- a/src/main/java/sevenUnitsGUI/Main.java +++ b/src/main/java/sevenUnitsGUI/Main.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,8 +19,8 @@ package sevenUnitsGUI; /** * The main code for the 7Units GUI * - * @since v0.4.0 * @since 2022-04-19 + * @since v0.4.0 */ public final class Main { @@ -28,8 +28,8 @@ public final class Main { * The main method that starts 7Units * * @param args commandline arguments - * @since v0.4.0 * @since 2022-04-19 + * @since v0.4.0 */ public static void main(String[] args) { View.createTabbedView(); diff --git a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java index 69f09e6..73d12bc 100644 --- a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java +++ b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -34,8 +34,8 @@ import sevenUnits.unit.UnitPrefix; * A search rule that applies a certain set of prefixes to a unit. It always * includes the original unit in the output map. * - * @since v0.4.0 * @since 2022-07-06 + * @since v0.4.0 */ public final class PrefixSearchRule implements Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> { @@ -70,10 +70,10 @@ public final class PrefixSearchRule implements * * @param prefixes prefixes to apply * @return prefix rule - * @since v0.4.0 * @since 2022-07-06 + * @since v0.4.0 */ - public static final PrefixSearchRule getCoherentOnlyRule( + public static PrefixSearchRule getCoherentOnlyRule( Set<UnitPrefix> prefixes) { return new PrefixSearchRule(prefixes, u -> u.isCoherent() && !u.getName().equals("kilogram")); @@ -84,30 +84,25 @@ public final class PrefixSearchRule implements * * @param prefixes prefixes to apply * @return prefix rule - * @since v0.4.0 * @since 2022-07-06 + * @since v0.4.0 */ - public static final PrefixSearchRule getUniversalRule( - Set<UnitPrefix> prefixes) { + public static PrefixSearchRule getUniversalRule(Set<UnitPrefix> prefixes) { return new PrefixSearchRule(prefixes, u -> true); } - /** - * The set of prefixes that will be applied to the unit. - */ + /** The set of prefixes that will be applied to the unit. */ private final Set<UnitPrefix> prefixes; - /** - * Determines which units are given prefixes. - */ + /** Determines which units are given prefixes. */ private final Predicate<LinearUnit> prefixableUnitRule; /** * @param prefixes prefixes to add to units * @param prefixableUnitRule function that determines which units get * prefixes - * @since v0.4.0 * @since 2022-07-06 + * @since v0.4.0 */ public PrefixSearchRule(Set<UnitPrefix> prefixes, Predicate<LinearUnit> prefixableUnitRule) { @@ -118,8 +113,8 @@ public final class PrefixSearchRule implements @Override public Map<String, LinearUnit> apply(Entry<String, LinearUnit> t) { final Map<String, LinearUnit> outputUnits = new HashMap<>(); - final String originalName = t.getKey(); - final LinearUnit originalUnit = t.getValue(); + final var originalName = t.getKey(); + final var originalUnit = t.getValue(); outputUnits.put(originalName, originalUnit); if (this.prefixableUnitRule.test(originalUnit)) { for (final UnitPrefix prefix : this.prefixes) { @@ -136,15 +131,15 @@ public final class PrefixSearchRule implements return true; if (!(obj instanceof PrefixSearchRule)) return false; - final PrefixSearchRule other = (PrefixSearchRule) obj; + final var other = (PrefixSearchRule) obj; return Objects.equals(this.prefixableUnitRule, other.prefixableUnitRule) && Objects.equals(this.prefixes, other.prefixes); } /** * @return rule that determines which units get prefixes - * @since v0.4.0 * @since 2022-07-09 + * @since v0.4.0 */ public Predicate<LinearUnit> getPrefixableUnitRule() { return this.prefixableUnitRule; @@ -152,8 +147,8 @@ public final class PrefixSearchRule implements /** * @return the prefixes that are applied by this rule - * @since v0.4.0 * @since 2022-07-06 + * @since v0.4.0 */ public Set<UnitPrefix> getPrefixes() { return this.prefixes; diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index eba8438..d258e1f 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021-2022 Adrien Hopkins + * Copyright (C) 2021-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,12 +16,12 @@ */ package sevenUnitsGUI; -import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -40,12 +40,14 @@ import sevenUnits.unit.BaseUnit; import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; +import sevenUnits.unit.LoadingException; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; import sevenUnits.unit.UnitType; import sevenUnits.unit.UnitValue; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -55,9 +57,10 @@ import sevenUnitsGUI.StandardDisplayRules.UncertaintyBased; /** * An object that handles interactions between the view and the backend code - * + * * @author Adrien Hopkins * @since 2021-12-15 + * @since v0.4.0 */ public final class Presenter { /** @@ -72,33 +75,25 @@ public final class Presenter { private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; /** The default place where exceptions are stored. */ private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; + /** A Predicate that returns true iff the argument is a full base unit */ + private static final Predicate<Unit> IS_FULL_BASE = unit -> unit instanceof LinearUnit + && ((LinearUnit) unit).isBase(); + /** + * The default locale, used in two situations: + * <ul> + * <li>If no text is available in your locale, uses text from this locale. + * <li>Users are initialized with this locale. + * </ul> + */ + static final String DEFAULT_LOCALE = "en"; - private static final Path userConfigDir() { - if (System.getProperty("os.name").startsWith("Windows")) { - final String envFolder = System.getenv("LOCALAPPDATA"); - if (envFolder == null || "".equals(envFolder)) { - return Path.of(System.getenv("USERPROFILE"), "AppData", "Local"); - } else { - return Path.of(envFolder); - } - } else { - final String envFolder = System.getenv("XDG_CONFIG_HOME"); - if (envFolder == null || "".equals(envFolder)) { - return Path.of(System.getenv("HOME"), ".config"); - } else { - return Path.of(envFolder); - } - } - } - - /** Gets a Path from a pathname in the config file. */ - private static Path pathFromConfig(String pathname) { - return CONFIG_FILE.getParent().resolve(pathname); - } + private static final List<String> LOCAL_LOCALES = List.of("en", "fr"); + private static final Path USER_LOCALES_DIR = userConfigDir() + .resolve("SevenUnits").resolve("locales"); /** * Adds default units and dimensions to a database. - * + * * @param database database to add to * @since 2019-04-14 * @since v0.2.0 @@ -125,14 +120,32 @@ public final class Presenter { database.addDimension("Temperature", Metric.Dimensions.TEMPERATURE); } + private static String displayRuleToString( + Function<UncertainDouble, String> numberDisplayRule) { + if (numberDisplayRule instanceof FixedDecimals) + return String.format("FIXED_DECIMALS %d", + ((FixedDecimals) numberDisplayRule).decimalPlaces()); + if (numberDisplayRule instanceof FixedPrecision) + return String.format("FIXED_PRECISION %d", + ((FixedPrecision) numberDisplayRule).significantFigures()); + if (numberDisplayRule instanceof UncertaintyBased) + return "UNCERTAINTY_BASED"; + return numberDisplayRule.toString(); + } + /** - * @return text in About file - * @since 2022-02-19 + * Determines where to wrap {@code toWrap} with a max line length of + * {@code maxLineLength}. If no good spot is found, returns -1. + * + * @since 2024-08-22 + * @since v1.0.0 */ - public static final String getAboutText() { - return Presenter.getLinesFromResource("/about.txt").stream() - .map(Presenter::withoutComments).collect(Collectors.joining("\n")) - .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString()); + private static int findLineSplit(String toWrap, int maxLineLength) { + for (var i = maxLineLength - 1; i >= 0; i--) { + if (Character.isWhitespace(toWrap.charAt(i))) + return i; + } + return -1; } /** @@ -142,12 +155,13 @@ public final class Presenter { * @param filename filename to get resource from * @return contents of file * @since 2021-03-27 + * @since v0.3.0 */ - private static final List<String> getLinesFromResource(String filename) { + private static List<String> getLinesFromResource(String filename) { final List<String> lines = new ArrayList<>(); - try (InputStream stream = inputStream(filename); - Scanner scanner = new Scanner(stream)) { + try (var stream = inputStream(filename); + var scanner = new Scanner(stream)) { while (scanner.hasNextLine()) { lines.add(scanner.nextLine()); } @@ -165,16 +179,59 @@ public final class Presenter { * @param filepath file to use as resource * @return obtained Path * @since 2021-03-27 + * @since v0.3.0 */ - private static final InputStream inputStream(String filepath) { + private static InputStream inputStream(String filepath) { return Presenter.class.getResourceAsStream(filepath); } /** + * Convert a linear unit value to a string, where the number is rounded to + * the nearest integer. + * + * @since 2024-08-16 + * @since v1.0.0 + */ + private static String linearUnitValueIntToString(LinearUnitValue uv) { + return Long.toString(Math.round(uv.getValueExact())) + " " + uv.getUnit(); + } + + private static Map.Entry<String, String> parseSettingLine(String line) { + final var equalsIndex = line.indexOf('='); + if (equalsIndex == -1) + throw new IllegalStateException( + "Settings file is malformed at line: " + line); + + final var param = line.substring(0, equalsIndex); + final var value = line.substring(equalsIndex + 1); + + return Map.entry(param, value); + } + + /** Gets a Path from a pathname in the config file. */ + private static Path pathFromConfig(String pathname) { + return CONFIG_FILE.getParent().resolve(pathname); + } + + // ====== SETTINGS ====== + + private static String searchRuleToString( + Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) { + if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) + return "NO_PREFIXES"; + if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) + return "COMMON_PREFIXES"; + if (PrefixSearchRule.ALL_METRIC_PREFIXES.equals(searchRule)) + return "ALL_METRIC_PREFIXES"; + return searchRule.toString(); + } + + /** * @return true iff a and b have any elements in common * @since 2022-04-19 + * @since v0.4.0 */ - private static final boolean sharesAnyElements(Set<?> a, Set<?> b) { + private static boolean sharesAnyElements(Set<?> a, Set<?> b) { for (final Object e : a) { if (b.contains(e)) return true; @@ -182,20 +239,55 @@ public final class Presenter { return false; } + private static Path userConfigDir() { + if (System.getProperty("os.name").startsWith("Windows")) { + final var envFolder = System.getenv("LOCALAPPDATA"); + if (envFolder == null || "".equals(envFolder)) + return Path.of(System.getenv("USERPROFILE"), "AppData", "Local"); + return Path.of(envFolder); + } + final var envFolder = System.getenv("XDG_CONFIG_HOME"); + if (envFolder == null || "".equals(envFolder)) + return Path.of(System.getenv("HOME"), ".config"); + return Path.of(envFolder); + } + /** * @return {@code line} with any comments removed. * @since 2021-03-13 + * @since v0.3.0 */ - private static final String withoutComments(String line) { - final int index = line.indexOf('#'); + private static String withoutComments(String line) { + final var index = line.indexOf('#'); return index == -1 ? line : line.substring(0, index); } - // ====== SETTINGS ====== - /** - * The view that this presenter communicates with + * Wraps a string, ensuring no line is longer than {@code maxLineLength}. + * + * @since 2024-08-22 + * @since v1.0.0 */ + private static String wrapString(String toWrap, int maxLineLength) { + final var wrapped = new StringBuilder(toWrap.length()); + var remaining = toWrap; + while (remaining.length() > maxLineLength) { + final var spot = findLineSplit(toWrap, maxLineLength); + if (spot == -1) { + wrapped.append(remaining.substring(0, maxLineLength)); + wrapped.append("-\n"); + remaining = remaining.substring(maxLineLength).stripLeading(); + } else { + wrapped.append(remaining.substring(0, spot)); + wrapped.append("\n"); + remaining = remaining.substring(spot + 1).stripLeading(); + } + } + wrapped.append(remaining); + return wrapped.toString(); + } + + /** The view that this presenter communicates with */ private final View view; /** @@ -238,6 +330,12 @@ public final class Presenter { */ private final Set<String> metricExceptions; + /** maps locale names (e.g. 'en') to key-text maps */ + final Map<String, Map<String, String>> locales; + + /** name of locale in locales to use */ + String userLocale; + /** * If this is true, views that show units as a list will have metric units * removed from the From unit list and imperial/USC units removed from the To @@ -252,65 +350,61 @@ public final class Presenter { private boolean showDuplicates = false; /** + * The default unit, prefix, dimension and exception data will only be loaded + * if this variable is true. + */ + private boolean useDefaultDatafiles = true; + + /** Custom unit datafiles that will be loaded by {@link #reloadData} */ + private final Set<Path> customUnitFiles = new HashSet<>(); + /** Custom dimension datafiles that will be loaded by {@link #reloadData} */ + private final Set<Path> customDimensionFiles = new HashSet<>(); + /** Custom exception datafiles that will be loaded by {@link #reloadData} */ + private final Set<Path> customExceptionFiles = new HashSet<>(); + + /** * Creates a Presenter - * + * * @param view the view that this presenter communicates with * @since 2021-12-15 + * @since v0.4.0 */ public Presenter(View view) { this.view = view; this.database = new UnitDatabase(); - addDefaults(this.database); + this.metricExceptions = new HashSet<>(); - // load units and prefixes - try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { - this.database.loadUnitsFromStream(units); - } catch (final IOException e) { - throw new AssertionError("Loading of unitsfile.txt failed.", e); - } - - // load dimensions - try (final InputStream dimensions = inputStream( - DEFAULT_DIMENSIONS_FILEPATH)) { - this.database.loadDimensionsFromStream(dimensions); - } catch (final IOException e) { - throw new AssertionError("Loading of dimensionfile.txt failed.", e); - } - - // load metric exceptions - try { - this.metricExceptions = new HashSet<>(); - try (InputStream exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH); - Scanner scanner = new Scanner(exceptions)) { - while (scanner.hasNextLine()) { - final String line = Presenter - .withoutComments(scanner.nextLine()); - if (!line.isBlank()) { - this.metricExceptions.add(line); - } - } - } - } catch (final IOException e) { - throw new AssertionError("Loading of metric_exceptions.txt failed.", - e); - } + this.locales = this.loadLocales(); + this.userLocale = DEFAULT_LOCALE; // set default settings temporarily if (Files.exists(CONFIG_FILE)) { this.loadSettings(CONFIG_FILE); } - // a Predicate that returns true iff the argument is a full base unit - final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit - && ((LinearUnit) unit).isBase(); + this.reloadData(); // print out unit counts - System.out.printf( - "Successfully loaded %d units with %d unit names (%d base units).%n", - this.database.unitMapPrefixless(false).size(), - this.database.unitMapPrefixless(true).size(), - this.database.unitMapPrefixless(false).values().stream() - .filter(isFullBase).count()); + System.out.println(this.loadStatMsg()); + } + + private void addLocaleFile(Map<String, Map<String, String>> locales, + Path file) throws IOException { + final Map<String, String> locale = new HashMap<>(); + final var fileName = file.getName(file.getNameCount() - 1).toString(); + final var localeName = fileName.substring(0, fileName.length() - 4); + try (var lines = Files.lines(file)) { + lines.forEach(line -> this.addLocaleLine(locale, line)); + } + locales.put(localeName, locale); + } + + private void addLocaleLine(Map<String, String> locale, String line) { + final var parts = line.split("=", 2); + if (parts.length < 2) + return; + + locale.put(parts[0], parts[1]); } /** @@ -319,201 +413,387 @@ public final class Presenter { * @param e entry * @return stream of entries, ready for flat-mapping * @since 2022-07-06 + * @since v0.4.0 */ - private final Stream<Map.Entry<String, Unit>> applySearchRule( + private Stream<Map.Entry<String, Unit>> applySearchRule( Map.Entry<String, Unit> e) { - final Unit u = e.getValue(); + final var u = e.getValue(); if (u instanceof LinearUnit) { - final String name = e.getKey(); + final var name = e.getKey(); final Map.Entry<String, LinearUnit> linearEntry = Map.entry(name, (LinearUnit) u); return this.searchRule.apply(linearEntry).entrySet().stream().map( entry -> Map.entry(entry.getKey(), (Unit) entry.getValue())); - } else - return Stream.of(e); + } + return Stream.of(e); } /** * Converts from the view's input expression to its output expression. * Displays an error message if any of the required fields are invalid. - * + * * @throws UnsupportedOperationException if the view does not support * expression-based conversion (does * not implement * {@link ExpressionConversionView}) * @since 2021-12-15 + * @since v0.4.0 */ public void convertExpressions() { - if (this.view instanceof ExpressionConversionView) { - final ExpressionConversionView xcview = (ExpressionConversionView) this.view; + if (!(this.view instanceof ExpressionConversionView)) + throw new UnsupportedOperationException( + "This function can only be called when the view is an ExpressionConversionView"); + final var xcview = (ExpressionConversionView) this.view; - final String fromExpression = xcview.getFromExpression(); - final String toExpression = xcview.getToExpression(); + final var fromExpression = xcview.getFromExpression(); + final var toExpression = xcview.getToExpression(); - // expressions must not be empty - if (fromExpression.isEmpty()) { - this.view.showErrorMessage("Parse Error", - "Please enter a unit expression in the From: box."); - return; - } - if (toExpression.isEmpty()) { - this.view.showErrorMessage("Parse Error", - "Please enter a unit expression in the To: box."); - return; - } + // expressions must not be empty + if (fromExpression.isEmpty()) { + this.view.showErrorMessage("Parse Error", + "Please enter a unit expression in the From: box."); + return; + } + if (toExpression.isEmpty()) { + this.view.showErrorMessage("Parse Error", + "Please enter a unit expression in the To: box."); + return; + } - // evaluate expressions - final LinearUnitValue from; - final Unit to; - try { - from = this.database.evaluateUnitExpression(fromExpression); - } catch (final IllegalArgumentException | NoSuchElementException e) { - this.view.showErrorMessage("Parse Error", - "Could not recognize text in From entry: " + e.getMessage()); - return; - } + final Optional<UnitConversionRecord> uc; + if (this.database.containsUnitSetName(toExpression)) { + uc = this.convertExpressionToNamedMultiUnit(fromExpression, + toExpression); + } else if (toExpression.contains(";")) { + final var toExpressions = toExpression.split(";"); + uc = this.convertExpressionToMultiUnit(fromExpression, toExpressions); + } else { + uc = this.convertExpressionToExpression(fromExpression, toExpression); + } + + uc.ifPresent(xcview::showExpressionConversionOutput); + } + + /** + * Converts a unit expression to another expression. + * + * If an error happened, it is shown to the view and Optional.empty() is + * returned. + * + * @since 2024-08-15 + * @since v1.0.0 + */ + private Optional<UnitConversionRecord> convertExpressionToExpression( + String fromExpression, String toExpression) { + // evaluate expressions + final LinearUnitValue from; + final Unit to; + try { + from = this.database.evaluateUnitExpression(fromExpression); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorMessage("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); + return Optional.empty(); + } + try { + to = this.database.getUnitFromExpression(toExpression); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorMessage("Parse Error", + "Could not recognize text in To entry: " + e.getMessage()); + return Optional.empty(); + } + + // convert and show output + if (!from.getUnit().canConvertTo(to)) { + this.view.showErrorMessage("Conversion Error", + "Cannot convert between \"" + fromExpression + "\" and \"" + + toExpression + "\"."); + return Optional.empty(); + } + final UncertainDouble uncertainValue; + + // uncertainty is meaningless for non-linear units, so we will have + // to erase uncertainty information for them + if (to instanceof LinearUnit) { + final var toLinear = (LinearUnit) to; + uncertainValue = from.convertTo(toLinear).getValue(); + } else { + final var value = from.asUnitValue().convertTo(to).getValue(); + uncertainValue = UncertainDouble.of(value, 0); + } + + final var uc = UnitConversionRecord.valueOf(fromExpression, toExpression, + "", this.numberDisplayRule.apply(uncertainValue)); + return Optional.of(uc); + + } + + /** + * Convert an expression to a MultiUnit. If an error happened, it is shown to + * the view and Optional.empty() is returned. + * + * @since 2024-08-15 + * @since v1.0.0 + */ + private Optional<UnitConversionRecord> convertExpressionToMultiUnit( + String fromExpression, String[] toExpressions) { + final LinearUnitValue from; + try { + from = this.database.evaluateUnitExpression(fromExpression); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorMessage("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); + return Optional.empty(); + } + + final List<LinearUnit> toUnits = new ArrayList<>(toExpressions.length); + for (final String toExpression : toExpressions) { try { - to = this.database.getUnitFromExpression(toExpression); + final var toI = this.database + .getUnitFromExpression(toExpression.trim()); + if (!(toI instanceof LinearUnit)) { + this.view.showErrorMessage("Unit Type Error", + "Units separated by ';' must be linear; " + toI + + " is not."); + return Optional.empty(); + } + toUnits.add((LinearUnit) toI); } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorMessage("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); - return; + return Optional.empty(); } + } + + final List<LinearUnitValue> toValues; + try { + toValues = from.convertToMultiple(toUnits); + } catch (final IllegalArgumentException e) { + this.view.showErrorMessage("Unit Error", + "Invalid units separated by ';': " + e.getMessage()); + return Optional.empty(); + } - // convert and show output - if (from.getUnit().canConvertTo(to)) { - final UncertainDouble uncertainValue; + final var toExpression = this.linearUnitValueSumToString(toValues); + return Optional.of( + UnitConversionRecord.valueOf(fromExpression, toExpression, "", "")); + } - // uncertainty is meaningless for non-linear units, so we will have - // to erase uncertainty information for them - if (to instanceof LinearUnit) { - final var toLinear = (LinearUnit) to; - uncertainValue = from.convertTo(toLinear).getValue(); - } else { - final double value = from.asUnitValue().convertTo(to).getValue(); - uncertainValue = UncertainDouble.of(value, 0); - } + /** + * Convert an expression to a MultiUnit with a name from the database. If an + * error happened, it is shown to the view and Optional.empty() is returned. + * + * @since 2024-08-15 + * @since v1.0.0 + */ + private Optional<UnitConversionRecord> convertExpressionToNamedMultiUnit( + String fromExpression, String toName) { + final LinearUnitValue from; + try { + from = this.database.evaluateUnitExpression(fromExpression); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorMessage("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); + return Optional.empty(); + } - final UnitConversionRecord uc = UnitConversionRecord.valueOf( - fromExpression, toExpression, "", - this.numberDisplayRule.apply(uncertainValue)); - xcview.showExpressionConversionOutput(uc); - } else { - this.view.showErrorMessage("Conversion Error", - "Cannot convert between \"" + fromExpression + "\" and \"" - + toExpression + "\"."); - } + final var toUnits = this.database.getUnitSet(toName); + final List<LinearUnitValue> toValues; + try { + toValues = from.convertToMultiple(toUnits); + } catch (final IllegalArgumentException e) { + this.view.showErrorMessage("Unit Error", + "Invalid units separated by ';': " + e.getMessage()); + return Optional.empty(); + } - } else - throw new UnsupportedOperationException( - "This function can only be called when the view is an ExpressionConversionView"); + final var toExpression = this.linearUnitValueSumToString(toValues); + return Optional.of( + UnitConversionRecord.valueOf(fromExpression, toExpression, "", "")); } /** * Converts from the view's input unit to its output unit. Displays an error * message if any of the required fields are invalid. - * + * * @throws UnsupportedOperationException if the view does not support * unit-based conversion (does not * implement * {@link UnitConversionView}) * @since 2021-12-15 + * @since v0.4.0 */ public void convertUnits() { - if (this.view instanceof UnitConversionView) { - final UnitConversionView ucview = (UnitConversionView) this.view; + if (!(this.view instanceof UnitConversionView)) + throw new UnsupportedOperationException( + "This function can only be called when the view is a UnitConversionView."); + final var ucview = (UnitConversionView) this.view; + + final var fromUnitOptional = ucview.getFromSelection(); + final var toUnitOptional = ucview.getToSelection(); + final var inputValueString = ucview.getInputValue(); + + // extract values from optionals + final String fromUnitString, toUnitString; + if (!fromUnitOptional.isPresent()) { + this.view.showErrorMessage("Unit Selection Error", + "Please specify a From unit"); + return; + } + fromUnitString = fromUnitOptional.orElseThrow(); + if (!toUnitOptional.isPresent()) { + this.view.showErrorMessage("Unit Selection Error", + "Please specify a To unit"); + return; + } + toUnitString = toUnitOptional.orElseThrow(); - final Optional<String> fromUnitOptional = ucview.getFromSelection(); - final Optional<String> toUnitOptional = ucview.getToSelection(); - final String inputValueString = ucview.getInputValue(); + // convert strings to data, checking if anything is invalid + final Unit fromUnit; + final UncertainDouble uncertainValue; - // extract values from optionals - final String fromUnitString, toUnitString; - if (fromUnitOptional.isPresent()) { - fromUnitString = fromUnitOptional.orElseThrow(); - } else { - this.view.showErrorMessage("Unit Selection Error", - "Please specify a From unit"); - return; - } - if (toUnitOptional.isPresent()) { - toUnitString = toUnitOptional.orElseThrow(); - } else { - this.view.showErrorMessage("Unit Selection Error", - "Please specify a To unit"); - return; - } - - // convert strings to data, checking if anything is invalid - final Unit fromUnit, toUnit; - final UncertainDouble uncertainValue; - - if (this.database.containsUnitName(fromUnitString)) { - fromUnit = this.database.getUnit(fromUnitString); - } else - throw this.viewError("Nonexistent From unit: %s", fromUnitString); - if (this.database.containsUnitName(toUnitString)) { - toUnit = this.database.getUnit(toUnitString); - } else - throw this.viewError("Nonexistent To unit: %s", toUnitString); - try { - uncertainValue = UncertainDouble - .fromRoundedString(inputValueString); - } catch (final NumberFormatException e) { - this.view.showErrorMessage("Value Error", - "Invalid value " + inputValueString); - return; - } + if (!this.database.containsUnitName(fromUnitString)) + throw this.viewError("Nonexistent From unit: %s", fromUnitString); + fromUnit = this.database.getUnit(fromUnitString); + try { + uncertainValue = UncertainDouble.fromRoundedString(inputValueString); + } catch (final NumberFormatException e) { + this.view.showErrorMessage("Value Error", + "Invalid value " + inputValueString); + return; + } + if (this.database.containsUnitName(toUnitString)) { + final var toUnit = this.database.getUnit(toUnitString); + ucview.showUnitConversionOutput( + this.convertUnitToUnit(fromUnitString, toUnitString, + inputValueString, fromUnit, toUnit, uncertainValue)); + } else if (this.database.containsUnitSetName(toUnitString)) { + final var toMulti = this.database.getUnitSet(toUnitString); + ucview.showUnitConversionOutput(this.convertUnitToMulti(fromUnitString, + inputValueString, fromUnit, toMulti, uncertainValue)); + } else + throw this.viewError("Nonexistent To unit: %s", toUnitString); + } + private UnitConversionRecord convertUnitToMulti(String fromUnitString, + String inputValueString, Unit fromUnit, List<LinearUnit> toMulti, + UncertainDouble uncertainValue) { + for (final LinearUnit toUnit : toMulti) { if (!fromUnit.canConvertTo(toUnit)) throw this.viewError("Could not convert between %s and %s", fromUnit, toUnit); + } - // convert - we will need to erase uncertainty for non-linear units, so - // we need to treat linear and non-linear units differently - final String outputValueString; - if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) { - final LinearUnit fromLinear = (LinearUnit) fromUnit; - final LinearUnit toLinear = (LinearUnit) toUnit; - final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear, - uncertainValue); - final LinearUnitValue converted = initialValue.convertTo(toLinear); - - outputValueString = this.numberDisplayRule - .apply(converted.getValue()); - } else { - final UnitValue initialValue = UnitValue.of(fromUnit, - uncertainValue.value()); - final UnitValue converted = initialValue.convertTo(toUnit); + final LinearUnitValue initValue; + if (fromUnit instanceof LinearUnit) { + final var fromLinear = (LinearUnit) fromUnit; + initValue = LinearUnitValue.of(fromLinear, uncertainValue); + } else { + initValue = UnitValue.of(fromUnit, uncertainValue.value()) + .convertToBase(NameSymbol.EMPTY); + } - outputValueString = this.numberDisplayRule - .apply(UncertainDouble.of(converted.getValue(), 0)); - } + final var converted = initValue.convertToMultiple(toMulti); + final var toExpression = this.linearUnitValueSumToString(converted); + return UnitConversionRecord.valueOf(fromUnitString, toExpression, + inputValueString, ""); - ucview.showUnitConversionOutput( - UnitConversionRecord.valueOf(fromUnitString, toUnitString, - inputValueString, outputValueString)); - } else - throw new UnsupportedOperationException( - "This function can only be called when the view is a UnitConversionView."); + } + + private UnitConversionRecord convertUnitToUnit(String fromUnitString, + String toUnitString, String inputValueString, Unit fromUnit, + Unit toUnit, UncertainDouble uncertainValue) { + if (!fromUnit.canConvertTo(toUnit)) + throw this.viewError("Could not convert between %s and %s", fromUnit, + toUnit); + + // convert - we will need to erase uncertainty for non-linear units, so + // we need to treat linear and non-linear units differently + final String outputValueString; + if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) { + final var fromLinear = (LinearUnit) fromUnit; + final var toLinear = (LinearUnit) toUnit; + final var initialValue = LinearUnitValue.of(fromLinear, + uncertainValue); + final var converted = initialValue.convertTo(toLinear); + + outputValueString = this.numberDisplayRule.apply(converted.getValue()); + } else { + final var initialValue = UnitValue.of(fromUnit, + uncertainValue.value()); + final var converted = initialValue.convertTo(toUnit); + + outputValueString = this.numberDisplayRule + .apply(UncertainDouble.of(converted.getValue(), 0)); + } + + return UnitConversionRecord.valueOf(fromUnitString, toUnitString, + inputValueString, outputValueString); } /** * @return true iff duplicate units are shown in unit lists * @since 2022-03-30 + * @since v0.4.0 */ public boolean duplicatesShown() { return this.showDuplicates; } + private String formatAboutText(Stream<String> rawLines) { + return rawLines.map(Presenter::withoutComments) + .collect(Collectors.joining("\n")) + .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString()) + .replaceAll("\\[LOADSTATS\\]", wrapString(this.loadStatMsg(), 72)); + } + + /** + * @return text in About file + * @since 2022-02-19 + * @since v0.4.0 + */ + public String getAboutText() { + final var customFilepath = Presenter + .pathFromConfig("about/" + this.userLocale + ".txt"); + if (Files.exists(customFilepath)) { + try (var lines = Files.lines(customFilepath)) { + return this.formatAboutText(lines); + } catch (final IOException e) { + final var filename = String.format("/about/%s.txt", + this.userLocale); + return this.formatAboutText( + Presenter.getLinesFromResource(filename).stream()); + } + } + if (LOCAL_LOCALES.contains(this.userLocale)) { + final var filename = String.format("/about/%s.txt", this.userLocale); + return this.formatAboutText( + Presenter.getLinesFromResource(filename).stream()); + } + final var filename = String.format("/about/%s.txt", DEFAULT_LOCALE); + return this + .formatAboutText(Presenter.getLinesFromResource(filename).stream()); + + } + + /** + * @return set of all locales available to select + * @since 2025-02-21 + * @since v1.0.0 + */ + public Set<String> getAvailableLocales() { + return this.locales.keySet(); + } + /** * Gets a name for this dimension using the database * * @param dimension dimension to name * @return name of dimension * @since 2022-04-16 + * @since v0.4.0 */ - final String getDimensionName(ObjectProduct<BaseDimension> dimension) { + String getDimensionName(ObjectProduct<BaseDimension> dimension) { // find this dimension in the database and get its name // if it isn't there, use the dimension's toString instead return this.database.dimensionMap().values().stream() @@ -522,9 +802,24 @@ public final class Presenter { } /** + * Gets the correct text for a provided ID. If this text is available in the + * user's locale, that text is provided. Otherwise, text is taken from the + * system default locale {@link #DEFAULT_LOCALE}. + * + * @param textID ID of text to get (used in locale files) + * @return text to be displayed + */ + public String getLocalizedText(String textID) { + final var userLocale = this.locales.get(this.userLocale); + final var defaultLocale = this.locales.get(DEFAULT_LOCALE); + return userLocale.getOrDefault(textID, defaultLocale.get(textID)); + } + + /** * @return the rule that is used by this presenter to convert numbers into * strings * @since 2022-04-10 + * @since v0.4.0 */ public Function<UncertainDouble, String> getNumberDisplayRule() { return this.numberDisplayRule; @@ -534,6 +829,7 @@ public final class Presenter { * @return the rule that is used by this presenter to convert strings into * numbers * @since 2022-04-10 + * @since v0.4.0 */ @SuppressWarnings("unused") // not implemented yet private Function<String, UncertainDouble> getNumberParsingRule() { @@ -543,6 +839,7 @@ public final class Presenter { /** * @return the rule that determines whether a set of prefixes is valid * @since 2022-04-19 + * @since v0.4.0 */ public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() { return this.prefixRepetitionRule; @@ -551,6 +848,7 @@ public final class Presenter { /** * @return the rule that determines which units are prefixed * @since 2022-07-08 + * @since v0.4.0 */ public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getSearchRule() { return this.searchRule; @@ -559,6 +857,7 @@ public final class Presenter { /** * @return a search rule that shows all single prefixes * @since 2022-07-08 + * @since v0.4.0 */ public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getUniversalSearchRule() { return PrefixSearchRule.getCoherentOnlyRule( @@ -566,19 +865,49 @@ public final class Presenter { } /** + * @return user's selected locale + * @since 2025-02-21 + * @since v1.0.0 + */ + public String getUserLocale() { + return this.userLocale; + } + + /** * @return the view associated with this presenter * @since 2022-04-19 + * @since v0.4.0 */ public View getView() { return this.view; } /** + * Accepts a list of errors. If that list is non-empty, prints an error + * message and alerts the user. + * + * @since 2024-08-22 + * @since v1.0.0 + */ + private void handleLoadErrors(List<LoadingException> errors) { + if (!errors.isEmpty()) { + final var errorMessage = String.format( + "%d error(s) happened while loading file:\n%s\n", errors.size(), + errors.stream().map(Throwable::getMessage) + .collect(Collectors.joining("\n"))); + System.err.print(errorMessage); + this.view.showErrorMessage(errors.size() + "Loading Error(s)", + errorMessage); + } + } + + /** * @return whether or not the provided unit is semi-metric (i.e. an * exception) * @since 2022-04-16 + * @since v0.4.0 */ - final boolean isSemiMetric(Unit u) { + boolean isSemiMetric(Unit u) { // determine if u is an exception final var primaryName = u.getPrimaryName(); final var symbol = u.getSymbol(); @@ -590,27 +919,127 @@ public final class Presenter { } /** + * Convert a list of LinearUnitValues that you would get from a unit-set + * conversion to a string. All but the last have their numbers rendered as + * integers, since they are always integers. The last one follows the usual + * number display rule. + * + * @since 2024-08-16 + * @since v1.0.0 + */ + private String linearUnitValueSumToString(List<LinearUnitValue> values) { + final var integerPart = values.subList(0, values.size() - 1).stream() + .map(Presenter::linearUnitValueIntToString) + .collect(Collectors.joining(" + ")); + final var last = values.get(values.size() - 1); + return integerPart + " + " + this.numberDisplayRule.apply(last.getValue()) + + " " + last.getUnit(); + } + + /** Load units, prefixes and dimensions from the default files. */ + private void loadDefaultData() { + // load units and prefixes + try (final var units = inputStream(DEFAULT_UNITS_FILEPATH)) { + this.handleLoadErrors(this.database.loadUnitsFromStream(units)); + } catch (final IOException e) { + throw new AssertionError("Loading of unitsfile.txt failed.", e); + } + + // load dimensions + try (final var dimensions = inputStream(DEFAULT_DIMENSIONS_FILEPATH)) { + this.handleLoadErrors( + this.database.loadDimensionsFromStream(dimensions)); + } catch (final IOException e) { + throw new AssertionError("Loading of dimensionfile.txt failed.", e); + } + + // load metric exceptions + try { + try (var exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH); + var scanner = new Scanner(exceptions)) { + while (scanner.hasNextLine()) { + final var line = Presenter.withoutComments(scanner.nextLine()); + if (!line.isBlank()) { + this.metricExceptions.add(line); + } + } + } + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } + } + + private void loadExceptionFile(Path exceptionFile) { + try (var lines = Files.lines(exceptionFile)) { + lines.map(Presenter::withoutComments) + .forEach(this.metricExceptions::add); + } catch (final IOException e) { + this.view.showErrorMessage("File Load Error", + "Error loading configured metric exception file \"" + + exceptionFile + "\": " + e.getLocalizedMessage()); + } + } + + /** + * Loads all available locales, including custom ones, into a map. + * + * @return map containing locales + */ + private Map<String, Map<String, String>> loadLocales() { + final Map<String, Map<String, String>> locales = new HashMap<>(); + for (final String localeName : LOCAL_LOCALES) { + final Map<String, String> locale = new HashMap<>(); + final var filename = "/locales/" + localeName + ".txt"; + getLinesFromResource(filename) + .forEach(line -> this.addLocaleLine(locale, line)); + locales.put(localeName, locale); + } + + if (Files.exists(USER_LOCALES_DIR)) { + try (var files = Files.list(USER_LOCALES_DIR)) { + files.forEach(localeFile -> { + try { + this.addLocaleFile(locales, localeFile); + } catch (final IOException e) { + e.printStackTrace(); + } + }); + } catch (final IOException e) { + e.printStackTrace(); + } + } + return locales; + } + + /** * Loads settings from the user's settings file and applies them to the * presenter. * * @param settingsFile file settings should be loaded from * @since 2021-12-15 + * @since v0.4.0 */ void loadSettings(Path settingsFile) { - for (Map.Entry<String, String> setting : settingsFromFile(settingsFile)) { - final String value = setting.getValue(); + this.customDimensionFiles.clear(); + this.customExceptionFiles.clear(); + this.customUnitFiles.clear(); + + for (final Map.Entry<String, String> setting : this + .settingsFromFile(settingsFile)) { + final var value = setting.getValue(); switch (setting.getKey()) { // set manually to avoid the unnecessary saving of the non-manual // methods case "custom_dimension_file": - this.database.loadDimensionFile(pathFromConfig(value)); + this.customDimensionFiles.add(pathFromConfig(value)); break; case "custom_exception_file": - this.loadExceptionFile(pathFromConfig(value)); + this.customExceptionFiles.add(pathFromConfig(value)); break; case "custom_unit_file": - this.database.loadUnitsFile(pathFromConfig(value)); + this.customUnitFiles.add(pathFromConfig(value)); break; case "number_display_rule": this.setDisplayRuleFromString(value); @@ -621,14 +1050,28 @@ public final class Presenter { this.database.setPrefixRepetitionRule(this.prefixRepetitionRule); break; case "one_way": - this.oneWayConversionEnabled = Boolean.valueOf(value); + this.oneWayConversionEnabled = Boolean.parseBoolean(value); break; case "include_duplicates": - this.showDuplicates = Boolean.valueOf(value); + this.showDuplicates = Boolean.parseBoolean(value); break; case "search_prefix_rule": this.setSearchRuleFromString(value); break; + case "use_default_datafiles": + this.useDefaultDatafiles = Boolean.parseBoolean(value); + break; + case "locale": + if (this.locales.containsKey(value)) { + this.userLocale = value; + } else { + System.err.printf("Warning: unrecognized locale \"%s\".%n", + value); + this.view.showErrorMessage("Unrecognized Locale", + "Could not find locale \"" + value + + "\", resetting to default."); + } + break; default: System.err.printf("Warning: unrecognized setting \"%s\".%n", setting.getKey()); @@ -641,86 +1084,38 @@ public final class Presenter { } } - private List<Map.Entry<String, String>> settingsFromFile(Path settingsFile) { - try (Stream<String> lines = Files.lines(settingsFile)) { - return lines.map(Presenter::withoutComments) - .filter(line -> !line.isBlank()).map(Presenter::parseSettingLine) - .toList(); - } catch (final IOException e) { - this.view.showErrorMessage("Settings Loading Error", - "Error loading settings file. Using default settings."); - return null; - } - } - - private static Map.Entry<String, String> parseSettingLine(String line) { - final int equalsIndex = line.indexOf('='); - if (equalsIndex == -1) - throw new IllegalStateException( - "Settings file is malformed at line: " + line); - - final String param = line.substring(0, equalsIndex); - final String value = line.substring(equalsIndex + 1); - - return Map.entry(param, value); - } - - private void setSearchRuleFromString(String ruleString) { - switch (ruleString) { - case "NO_PREFIXES": - this.searchRule = PrefixSearchRule.NO_PREFIXES; - break; - case "COMMON_PREFIXES": - this.searchRule = PrefixSearchRule.COMMON_PREFIXES; - break; - case "ALL_METRIC_PREFIXES": - this.searchRule = PrefixSearchRule.ALL_METRIC_PREFIXES; - break; - default: - System.err.printf( - "Warning: unrecognized value for search_prefix_rule: %s\n", - ruleString); - } - } - - private void setDisplayRuleFromString(String ruleString) { - String[] tokens = ruleString.split(" "); - switch (tokens[0]) { - case "FIXED_DECIMALS": - final int decimals = Integer.parseInt(tokens[1]); - this.numberDisplayRule = StandardDisplayRules.fixedDecimals(decimals); - break; - case "FIXED_PRECISION": - final int sigDigs = Integer.parseInt(tokens[1]); - this.numberDisplayRule = StandardDisplayRules.fixedPrecision(sigDigs); - break; - case "UNCERTAINTY_BASED": - this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); - break; - default: - this.numberDisplayRule = StandardDisplayRules - .getStandardRule(ruleString); - break; - } - } - - private void loadExceptionFile(Path exceptionFile) { - try (Stream<String> lines = Files.lines(exceptionFile)) { - lines.map(Presenter::withoutComments) - .forEach(this.metricExceptions::add); - } catch (IOException e) { - this.view.showErrorMessage("File Load Error", - "Error loading configured metric exception file \"" - + exceptionFile + "\": " + e.getLocalizedMessage()); - } + /** + * @return a message showing how much stuff has been loaded + * @since 2024-08-22 + * @since v1.0.0 + */ + private String loadStatMsg() { + return this.getLocalizedText("load_stat_msg") + .replace("[u]", + Integer.toString( + this.database.unitMapPrefixless(false).size())) + .replace("[un]", + Integer + .toString(this.database.unitMapPrefixless(true).size())) + .replace("[b]", + Long.toString(this.database.unitMapPrefixless(false).values() + .stream().filter(IS_FULL_BASE).count())) + .replace("[p]", + Integer.toString(this.database.prefixMap(false).size())) + .replace("[pn]", + Integer.toString(this.database.prefixMap(true).size())) + .replace("[s]", Integer.toString(this.database.unitSetMap().size())) + .replace("[d]", + Integer.toString(this.database.dimensionMap().size())); } /** * @return true iff the One-Way Conversion feature is available (views that * show units as a list will have metric units removed from the From * unit list and imperial/USC units removed from the To unit list) - * + * * @since 2022-03-30 + * @since v0.4.0 */ public boolean oneWayConversionEnabled() { return this.oneWayConversionEnabled; @@ -730,22 +1125,23 @@ public final class Presenter { * Completes creation of the presenter. This part of the initialization * depends on the view's functions, so it cannot be run if the components * they depend on are not created yet. - * + * * @since 2022-02-26 + * @since v0.4.0 */ public void postViewInitialize() { // unit conversion specific stuff if (this.view instanceof UnitConversionView) { - final UnitConversionView ucview = (UnitConversionView) this.view; + final var ucview = (UnitConversionView) this.view; ucview.setDimensionNames(this.database.dimensionMap().keySet()); } this.updateView(); + this.view.updateText(); } void prefixSelected() { - final Optional<String> selectedPrefixName = this.view - .getViewedPrefixName(); + final var selectedPrefixName = this.view.getViewedPrefixName(); final Optional<UnitPrefix> selectedPrefix = selectedPrefixName .map(name -> this.database.containsPrefixName(name) ? this.database.getPrefix(name) @@ -755,19 +1151,37 @@ public final class Presenter { String.valueOf(prefix.getMultiplier()))); } + /** Clears then reloads all unit, prefix, dimension and exception data. */ + public void reloadData() { + this.database.clear(); + this.metricExceptions.clear(); + addDefaults(this.database); + + if (this.useDefaultDatafiles) { + this.loadDefaultData(); + } + + this.customUnitFiles.forEach( + path -> this.handleLoadErrors(this.database.loadUnitsFile(path))); + this.customDimensionFiles.forEach(path -> this + .handleLoadErrors(this.database.loadDimensionFile(path))); + this.customExceptionFiles.forEach(this::loadExceptionFile); + } + /** * Saves the presenter's current settings to the config file, creating it if * it doesn't exist. - * + * * @return false iff the presenter could not write to the file * @since 2022-04-19 + * @since v0.4.0 */ public boolean saveSettings() { - final Path configDir = CONFIG_FILE.getParent(); + final var configDir = CONFIG_FILE.getParent(); if (!Files.exists(configDir)) { try { Files.createDirectories(configDir); - } catch (IOException e) { + } catch (final IOException e) { return false; } } @@ -775,64 +1189,32 @@ public final class Presenter { return this.writeSettings(CONFIG_FILE); } - /** - * Saves the presenter's settings to the user settings file. - * - * @param settingsFile file settings should be saved to - * @since 2021-12-15 - */ - boolean writeSettings(Path settingsFile) { - try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) { - writer.write(String.format("number_display_rule=%s\n", - displayRuleToString(this.numberDisplayRule))); - writer.write( - String.format("prefix_rule=%s\n", this.prefixRepetitionRule)); - writer.write( - String.format("one_way=%s\n", this.oneWayConversionEnabled)); - writer.write( - String.format("include_duplicates=%s\n", this.showDuplicates)); - writer.write(String.format("search_prefix_rule=%s\n", - searchRuleToString(this.searchRule))); - return true; - } catch (final IOException e) { - e.printStackTrace(); - this.view.showErrorMessage("I/O Error", - "Error occurred while saving settings: " - + e.getLocalizedMessage()); - return false; + private void setDisplayRuleFromString(String ruleString) { + final var tokens = ruleString.split(" "); + switch (tokens[0]) { + case "FIXED_DECIMALS": + final var decimals = Integer.parseInt(tokens[1]); + this.numberDisplayRule = StandardDisplayRules.fixedDecimals(decimals); + break; + case "FIXED_PRECISION": + final var sigDigs = Integer.parseInt(tokens[1]); + this.numberDisplayRule = StandardDisplayRules.fixedPrecision(sigDigs); + break; + case "UNCERTAINTY_BASED": + this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); + break; + default: + this.numberDisplayRule = StandardDisplayRules + .getStandardRule(ruleString); + break; } } - private static String searchRuleToString( - Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) { - if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) { - return "NO_PREFIXES"; - } else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) { - return "COMMON_PREFIXES"; - } else if (PrefixSearchRule.ALL_METRIC_PREFIXES.equals(searchRule)) { - return "ALL_METRIC_PREFIXES"; - } else - return searchRule.toString(); - } - - private static String displayRuleToString( - Function<UncertainDouble, String> numberDisplayRule) { - if (numberDisplayRule instanceof FixedDecimals) { - return String.format("FIXED_DECIMALS %d", - ((FixedDecimals) numberDisplayRule).decimalPlaces()); - } else if (numberDisplayRule instanceof FixedPrecision) { - return String.format("FIXED_PRECISION %d", - ((FixedPrecision) numberDisplayRule).significantFigures()); - } else if (numberDisplayRule instanceof UncertaintyBased) { - return "UNCERTAINTY_BASED"; - } else - return numberDisplayRule.toString(); - } - /** * @param numberDisplayRule the new rule that will be used by this presenter * to convert numbers into strings * @since 2022-04-10 + * @since v0.4.0 */ public void setNumberDisplayRule( Function<UncertainDouble, String> numberDisplayRule) { @@ -843,6 +1225,7 @@ public final class Presenter { * @param numberParsingRule the new rule that will be used by this presenter * to convert strings into numbers * @since 2022-04-10 + * @since v0.4.0 */ @SuppressWarnings("unused") // not implemented yet private void setNumberParsingRule( @@ -854,7 +1237,8 @@ public final class Presenter { * @param oneWayConversionEnabled whether not one-way conversion should be * enabled * @since 2022-03-30 - * @see {@link #isOneWayConversionEnabled} + * @since v0.4.0 + * @see #oneWayConversionEnabled */ public void setOneWayConversionEnabled(boolean oneWayConversionEnabled) { this.oneWayConversionEnabled = oneWayConversionEnabled; @@ -865,6 +1249,7 @@ public final class Presenter { * @param prefixRepetitionRule the rule that determines whether a set of * prefixes is valid * @since 2022-04-19 + * @since v0.4.0 */ public void setPrefixRepetitionRule( Predicate<List<UnitPrefix>> prefixRepetitionRule) { @@ -878,30 +1263,86 @@ public final class Presenter { * unit (including the unit itself) that should be * searchable. * @since 2022-07-08 + * @since v0.4.0 */ public void setSearchRule( Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) { this.searchRule = searchRule; } + private void setSearchRuleFromString(String ruleString) { + switch (ruleString) { + case "NO_PREFIXES": + this.searchRule = PrefixSearchRule.NO_PREFIXES; + break; + case "COMMON_PREFIXES": + this.searchRule = PrefixSearchRule.COMMON_PREFIXES; + break; + case "ALL_METRIC_PREFIXES": + this.searchRule = PrefixSearchRule.ALL_METRIC_PREFIXES; + break; + default: + System.err.printf( + "Warning: unrecognized value for search_prefix_rule: %s\n", + ruleString); + } + } + /** * @param showDuplicateUnits whether or not duplicate units should be shown * @since 2022-03-30 + * @since v0.4.0 */ public void setShowDuplicates(boolean showDuplicateUnits) { this.showDuplicates = showDuplicateUnits; this.updateView(); } + private List<Map.Entry<String, String>> settingsFromFile(Path settingsFile) { + try (var lines = Files.lines(settingsFile)) { + return lines.map(Presenter::withoutComments) + .filter(line -> !line.isBlank()).map(Presenter::parseSettingLine) + .toList(); + } catch (final IOException e) { + this.view.showErrorMessage("Settings Loading Error", + "Error loading settings file. Using default settings."); + return null; + } + } + + /** + * Sets whether or not the default datafiles will be loaded. This method + * automatically updates the view's units. + * + * @param useDefaultDatafiles whether or not default datafiles should be + * loaded + */ + public void setUseDefaultDatafiles(boolean useDefaultDatafiles) { + this.useDefaultDatafiles = useDefaultDatafiles; + this.reloadData(); + this.updateView(); + } + + /** + * Sets the user's locale, updating the view. + * + * @param userLocale locale to use + */ + public void setUserLocale(String userLocale) { + this.userLocale = userLocale; + this.view.updateText(); + } + /** * Shows a unit in the unit viewer * * @param u unit to show * @since 2022-04-16 + * @since v0.4.0 */ - private final void showUnit(Unit u) { + private void showUnit(Unit u) { final var nameSymbol = u.getNameSymbol(); - final boolean isBase = u instanceof BaseUnit + final var isBase = u instanceof BaseUnit || u instanceof LinearUnit && ((LinearUnit) u).isBase(); final var definition = isBase ? "(Base unit)" : u.toDefinitionString(); final var dimensionString = this.getDimensionName(u.getDimension()); @@ -912,12 +1353,13 @@ public final class Presenter { /** * Runs whenever a unit name is selected in the unit viewer. Gets the * description of a unit and displays it. - * + * * @since 2022-04-10 + * @since v0.4.0 */ void unitNameSelected() { // get selected unit, if it's there and valid - final Optional<String> selectedUnitName = this.view.getViewedUnitName(); + final var selectedUnitName = this.view.getViewedUnitName(); final Optional<Unit> selectedUnit = selectedUnitName .map(unitName -> this.database.containsUnitName(unitName) ? this.database.getUnit(unitName) @@ -927,12 +1369,13 @@ public final class Presenter { /** * Updates the view's From and To units, if it has some - * + * * @since 2021-12-15 + * @since v0.4.0 */ public void updateView() { if (this.view instanceof UnitConversionView) { - final UnitConversionView ucview = (UnitConversionView) this.view; + final var ucview = (UnitConversionView) this.view; final var selectedDimensionName = ucview.getSelectedDimensionName(); // load units & prefixes into viewers @@ -946,6 +1389,7 @@ public final class Presenter { .entrySet().stream(); var toUnits = this.database.unitMapPrefixless(this.showDuplicates) .entrySet().stream(); + var unitSets = this.database.unitSetMap().entrySet().stream(); // filter by dimension, if one is selected if (selectedDimensionName.isPresent()) { @@ -955,6 +1399,8 @@ public final class Presenter { u -> viewDimension.equals(u.getValue().getDimension())); toUnits = toUnits.filter( u -> viewDimension.equals(u.getValue().getDimension())); + unitSets = unitSets.filter(us -> viewDimension + .equals(us.getValue().get(0).getDimension())); } // filter by unit type, if desired @@ -963,25 +1409,70 @@ public final class Presenter { this::isSemiMetric) != UnitType.METRIC); toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(), this::isSemiMetric) != UnitType.NON_METRIC); + // unit sets are never considered metric + unitSets = unitSets + .filter(us -> this.metricExceptions.contains(us.getKey())); } // set unit names ucview.setFromUnitNames(fromUnits.flatMap(this::applySearchRule) .map(Map.Entry::getKey).collect(Collectors.toSet())); - ucview.setToUnitNames(toUnits.flatMap(this::applySearchRule) - .map(Map.Entry::getKey).collect(Collectors.toSet())); + final var toUnitNames = toUnits.flatMap(this::applySearchRule) + .map(Map.Entry::getKey); + final var unitSetNames = unitSets.map(Map.Entry::getKey); + final var toNames = Stream.concat(toUnitNames, unitSetNames) + .collect(Collectors.toSet()); + ucview.setToUnitNames(toNames); } } + /** @return true iff the default datafiles are being used */ + public boolean usingDefaultDatafiles() { + return this.useDefaultDatafiles; + } + /** * @param message message to add * @param args string formatting arguments for message * @return AssertionError stating that an error has happened in the view's * code * @since 2022-04-09 + * @since v0.4.0 */ private AssertionError viewError(String message, Object... args) { return new AssertionError("View Programming Error (from " + this.view + "): " + String.format(message, args)); } + + /** + * Saves the presenter's settings to the user settings file. + * + * @param settingsFile file settings should be saved to + * @since 2021-12-15 + * @since v0.4.0 + */ + boolean writeSettings(Path settingsFile) { + try (var writer = Files.newBufferedWriter(settingsFile)) { + writer.write(String.format("number_display_rule=%s\n", + displayRuleToString(this.numberDisplayRule))); + writer.write( + String.format("prefix_rule=%s\n", this.prefixRepetitionRule)); + writer.write( + String.format("one_way=%s\n", this.oneWayConversionEnabled)); + writer.write( + String.format("include_duplicates=%s\n", this.showDuplicates)); + writer.write(String.format("search_prefix_rule=%s\n", + searchRuleToString(this.searchRule))); + writer.write(String.format("use_default_datafiles=%s\n", + this.useDefaultDatafiles)); + writer.write(String.format("locale=%s\n", this.userLocale)); + return true; + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorMessage("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + return false; + } + } } diff --git a/src/main/java/sevenUnitsGUI/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java index 8fba459..96f71de 100644 --- a/src/main/java/sevenUnitsGUI/SearchBoxList.java +++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -49,7 +49,7 @@ final class SearchBoxList<E> extends JPanel { /** * The text to place in an empty search box. - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -57,7 +57,7 @@ final class SearchBoxList<E> extends JPanel { /** * The color to use for an empty foreground. - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -82,8 +82,9 @@ final class SearchBoxList<E> extends JPanel { /** * Creates an empty SearchBoxList - * + * * @since 2022-02-19 + * @since v0.4.0 */ public SearchBoxList() { this(List.of(), null, false); @@ -91,9 +92,10 @@ final class SearchBoxList<E> extends JPanel { /** * Creates the {@code SearchBoxList}. - * + * * @param itemsToFilter items to put in the list * @since 2019-04-14 + * @since v0.2.0 */ public SearchBoxList(final Collection<E> itemsToFilter) { this(itemsToFilter, null, false); @@ -101,12 +103,12 @@ final class SearchBoxList<E> extends JPanel { /** * Creates the {@code SearchBoxList}. - * + * * @param itemsToFilter items to put in the list * @param defaultOrdering default ordering of items after filtration * (null=Comparable) * @param caseSensitive whether or not the filtration is case-sensitive - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -147,7 +149,7 @@ final class SearchBoxList<E> extends JPanel { /** * Adds an additional filter for searching. - * + * * @param filter filter to add. * @since 2019-04-13 * @since v0.2.0 @@ -158,7 +160,7 @@ final class SearchBoxList<E> extends JPanel { /** * Resets the search filter. - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -170,6 +172,7 @@ final class SearchBoxList<E> extends JPanel { * @return items available in search list, including items that are hidden by * the search filter * @since 2022-03-30 + * @since v0.4.0 */ public Collection<E> getItems() { return Collections.unmodifiableCollection(this.itemsToFilter); @@ -180,7 +183,7 @@ final class SearchBoxList<E> extends JPanel { * @since 2019-04-14 * @since v0.2.0 */ - public final JTextField getSearchBox() { + public JTextField getSearchBox() { return this.searchBox; } @@ -194,9 +197,8 @@ final class SearchBoxList<E> extends JPanel { private Predicate<E> getSearchFilter(final String searchText) { if (this.caseSensitive) return item -> item.toString().contains(searchText); - else - return item -> item.toString().toLowerCase() - .contains(searchText.toLowerCase()); + return item -> item.toString().toLowerCase() + .contains(searchText.toLowerCase()); } /** @@ -204,7 +206,7 @@ final class SearchBoxList<E> extends JPanel { * @since 2019-04-14 * @since v0.2.0 */ - public final JList<E> getSearchList() { + public JList<E> getSearchList() { return this.searchItems; } @@ -228,16 +230,16 @@ final class SearchBoxList<E> extends JPanel { /** * Re-applies the filters. - * + * * @since 2019-04-13 * @since v0.2.0 */ public void reapplyFilter() { - final String searchText = this.searchBoxEmpty ? "" + final var searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator<E> comparator = new FilterComparator<>(searchText, + final var comparator = new FilterComparator<>(searchText, this.defaultOrdering, this.caseSensitive); - final Predicate<E> searchFilter = this.getSearchFilter(searchText); + final var searchFilter = this.getSearchFilter(searchText); this.listModel.clear(); this.itemsToFilter.forEach(item -> { @@ -255,7 +257,7 @@ final class SearchBoxList<E> extends JPanel { /** * Runs whenever the search box gains focus. - * + * * @param e focus event * @since 2019-04-13 * @since v0.2.0 @@ -270,7 +272,7 @@ final class SearchBoxList<E> extends JPanel { /** * Runs whenever the search box loses focus. - * + * * @param e focus event * @since 2019-04-13 * @since v0.2.0 @@ -288,7 +290,7 @@ final class SearchBoxList<E> extends JPanel { * <p> * Reapplies the search filter, and custom filters. * </p> - * + * * @since 2019-04-14 * @since v0.2.0 */ @@ -296,11 +298,11 @@ final class SearchBoxList<E> extends JPanel { if (this.searchBoxFocused) { this.searchBoxEmpty = this.searchBox.getText().equals(""); } - final String searchText = this.searchBoxEmpty ? "" + final var searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator<E> comparator = new FilterComparator<>(searchText, + final var comparator = new FilterComparator<>(searchText, this.defaultOrdering, this.caseSensitive); - final Predicate<E> searchFilter = this.getSearchFilter(searchText); + final var searchFilter = this.getSearchFilter(searchText); // initialize list with items that match the filter then sort this.listModel.clear(); @@ -323,6 +325,7 @@ final class SearchBoxList<E> extends JPanel { * * @param newItems new items to put in list * @since 2021-05-22 + * @since v0.3.0 */ public void setItems(Collection<? extends E> newItems) { this.itemsToFilter.clear(); @@ -332,8 +335,9 @@ final class SearchBoxList<E> extends JPanel { /** * Manually updates the search box's item list. - * + * * @since 2020-08-27 + * @since v0.3.0 */ public void updateList() { this.searchBoxTextChanged(); diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java index d00263b..16d31ae 100644 --- a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -28,28 +28,28 @@ import sevenUnits.utils.UncertainDouble; * A static utility class that can be used to make display rules for the * presenter. * - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ public final class StandardDisplayRules { /** * A rule that rounds to a fixed number of decimal places. * - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ public static final class FixedDecimals implements Function<UncertainDouble, String> { + /** Regular expression used for converting this to a string. */ public static final Pattern TO_STRING_PATTERN = Pattern .compile("Round to (\\d+) decimal places"); - /** - * The number of places to round to. - */ + /** The number of places to round to. */ private final int decimalPlaces; /** * @param decimalPlaces * @since 2022-04-18 + * @since v0.4.0 */ private FixedDecimals(int decimalPlaces) { this.decimalPlaces = decimalPlaces; @@ -65,6 +65,7 @@ public final class StandardDisplayRules { /** * @return the number of decimal places this rule rounds to * @since 2022-04-18 + * @since v0.4.0 */ public int decimalPlaces() { return this.decimalPlaces; @@ -76,7 +77,7 @@ public final class StandardDisplayRules { return true; if (!(obj instanceof FixedDecimals)) return false; - final FixedDecimals other = (FixedDecimals) obj; + final var other = (FixedDecimals) obj; if (this.decimalPlaces != other.decimalPlaces) return false; return true; @@ -96,22 +97,22 @@ public final class StandardDisplayRules { /** * A rule that rounds to a fixed number of significant digits. * - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ public static final class FixedPrecision implements Function<UncertainDouble, String> { + /** Regular expression used for converting this to a string. */ public static final Pattern TO_STRING_PATTERN = Pattern .compile("Round to (\\d+) significant figures"); - /** - * The number of significant figures to round to. - */ + /** The number of significant figures to round to. */ private final MathContext mathContext; /** * @param significantFigures * @since 2022-04-18 + * @since v0.4.0 */ private FixedPrecision(int significantFigures) { this.mathContext = new MathContext(significantFigures, @@ -130,7 +131,7 @@ public final class StandardDisplayRules { return true; if (!(obj instanceof FixedPrecision)) return false; - final FixedPrecision other = (FixedPrecision) obj; + final var other = (FixedPrecision) obj; if (this.mathContext == null) { if (other.mathContext != null) return false; @@ -148,6 +149,7 @@ public final class StandardDisplayRules { /** * @return the number of significant figures this rule rounds to * @since 2022-04-18 + * @since v0.4.0 */ public int significantFigures() { return this.mathContext.getPrecision(); @@ -165,8 +167,8 @@ public final class StandardDisplayRules { * This means the output will have around as many significant figures as the * input. * - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ public static final class UncertaintyBased implements Function<UncertainDouble, String> { @@ -193,10 +195,10 @@ public final class StandardDisplayRules { /** * @param decimalPlaces decimal places to round to * @return a rounding rule that rounds to fixed number of decimal places - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ - public static final FixedDecimals fixedDecimals(int decimalPlaces) { + public static FixedDecimals fixedDecimals(int decimalPlaces) { return new FixedDecimals(decimalPlaces); } @@ -204,10 +206,10 @@ public final class StandardDisplayRules { * @param significantFigures significant figures to round to * @return a rounding rule that rounds to a fixed number of significant * figures - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ - public static final FixedPrecision fixedPrecision(int significantFigures) { + public static FixedPrecision fixedPrecision(int significantFigures) { return new FixedPrecision(significantFigures); } @@ -218,10 +220,10 @@ public final class StandardDisplayRules { * @return display rule * @throws IllegalArgumentException if the provided string is not that of a * standard rule. - * @since v0.4.0 * @since 2021-12-24 + * @since v0.4.0 */ - public static final Function<UncertainDouble, String> getStandardRule( + public static Function<UncertainDouble, String> getStandardRule( String ruleToString) { if (UNCERTAINTY_BASED_ROUNDING_RULE.toString().equals(ruleToString)) return UNCERTAINTY_BASED_ROUNDING_RULE; @@ -230,13 +232,13 @@ public final class StandardDisplayRules { final var placesMatch = FixedDecimals.TO_STRING_PATTERN .matcher(ruleToString); if (placesMatch.matches()) - return new FixedDecimals(Integer.valueOf(placesMatch.group(1))); + return new FixedDecimals(Integer.parseInt(placesMatch.group(1))); // test if it is a fixed-sig-fig rule final var sigFigMatch = FixedPrecision.TO_STRING_PATTERN .matcher(ruleToString); if (sigFigMatch.matches()) - return new FixedPrecision(Integer.valueOf(sigFigMatch.group(1))); + return new FixedPrecision(Integer.parseInt(sigFigMatch.group(1))); throw new IllegalArgumentException( "Provided string does not match any given rules."); @@ -244,10 +246,10 @@ public final class StandardDisplayRules { /** * @return an UncertainDouble-based rounding rule - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ - public static final UncertaintyBased uncertaintyBased() { + public static UncertaintyBased uncertaintyBased() { return UNCERTAINTY_BASED_ROUNDING_RULE; } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 997acc3..8be58f5 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,13 +24,16 @@ import java.awt.event.ItemEvent; import java.awt.event.KeyEvent; import java.util.AbstractSet; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import javax.swing.BorderFactory; @@ -64,8 +67,8 @@ import sevenUnits.utils.UncertainDouble; /** * A View that separates its functions into multiple tabs * - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** @@ -73,8 +76,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { * * @param <E> type of item in list * - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ private static final class JComboBoxItemSet<E> extends AbstractSet<E> { private final JComboBox<E> comboBox; @@ -82,6 +85,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** * @param comboBox combo box to get items from * @since 2022-02-19 + * @since v0.4.0 */ public JComboBoxItemSet(JComboBox<E> comboBox) { this.comboBox = comboBox; @@ -101,9 +105,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { public E next() { if (this.hasNext()) return JComboBoxItemSet.this.comboBox.getItemAt(this.index++); - else - throw new NoSuchElementException( - "Iterator has finished iteration"); + throw new NoSuchElementException( + "Iterator has finished iteration"); } }; } @@ -119,10 +122,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { * The standard types of rounding, corresponding to the options on the * TabbedView's settings panel. * - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ - private static enum StandardRoundingType { + private enum StandardRoundingType { /** * Rounds to a fixed number of significant digits. Precision is used, * representing the number of significant digits to round to. @@ -144,8 +147,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { * Creates a TabbedView. * * @param args command line arguments - * @since v0.4.0 * @since 2022-02-19 + * @since v0.4.0 */ public static void main(String[] args) { // This view doesn't need to do anything, the side effects of creating it @@ -195,15 +198,19 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; - // SETTINGS STUFF + // INFO & SETTINGS STUFF + final JTextArea infoTextArea; + private final JComboBox<String> localeSelector; private StandardRoundingType roundingType; private int precision; + private final Map<String, Consumer<String>> localizedTextSetters; + /** * Creates the view and makes it visible to the user - * - * @since v0.4.0 + * * @since 2022-02-19 + * @since v0.4.0 */ public TabbedView() { // enable system look and feel @@ -218,21 +225,25 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // initialize important components this.presenter = new Presenter(this); - this.frame = new JFrame("7Units " + ProgramInfo.VERSION); + this.frame = new JFrame("7Units (Unlocalized)"); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // master components (those that contain everything else within them) this.masterPane = new JTabbedPane(); this.frame.add(this.masterPane); + this.localizedTextSetters = new HashMap<>(); + // ============ UNIT CONVERSION TAB ============ - final JPanel convertUnitPanel = new JPanel(); + final var convertUnitPanel = new JPanel(); this.masterPane.addTab("Convert Units", convertUnitPanel); + this.localizedTextSetters.put("tv.convert_units.title", + txt -> this.masterPane.setTitleAt(0, txt)); this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); convertUnitPanel.setLayout(new BorderLayout()); { // panel for input part - final JPanel inputPanel = new JPanel(); + final var inputPanel = new JPanel(); convertUnitPanel.add(inputPanel, BorderLayout.CENTER); inputPanel.setLayout(new GridLayout(1, 3)); inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6)); @@ -240,7 +251,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.fromSearch = new SearchBoxList<>(); inputPanel.add(this.fromSearch); - final JPanel inBetweenPanel = new JPanel(); + final var inBetweenPanel = new JPanel(); inputPanel.add(inBetweenPanel); inBetweenPanel.setLayout(new BorderLayout()); @@ -249,7 +260,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.dimensionSelector .addItemListener(e -> this.presenter.updateView()); - final JLabel arrowLabel = new JLabel("-->"); + final var arrowLabel = new JLabel("-->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); arrowLabel.setHorizontalAlignment(SwingConstants.CENTER); @@ -258,12 +269,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } { // panel for submit and output, and also value entry - final JPanel outputPanel = new JPanel(); + final var outputPanel = new JPanel(); convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); outputPanel.setLayout(new BorderLayout()); outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6)); - final JLabel valuePrompt = new JLabel("Value to convert: "); + final var valuePrompt = new JLabel(); + this.localizedTextSetters.put("tv.convert_units.value_prompt", + valuePrompt::setText); outputPanel.add(valuePrompt, BorderLayout.LINE_START); this.valueInput = new JTextField(); @@ -271,6 +284,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // conversion button this.convertUnitButton = new JButton("Convert"); + this.localizedTextSetters.put("tv.convert_units.convert_btn", + this.convertUnitButton::setText); outputPanel.add(this.convertUnitButton, BorderLayout.LINE_END); this.convertUnitButton .addActionListener(e -> this.presenter.convertUnits()); @@ -283,23 +298,31 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } // ============ EXPRESSION CONVERSION TAB ============ - final JPanel convertExpressionPanel = new JPanel(); + final var convertExpressionPanel = new JPanel(); this.masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); + this.localizedTextSetters.put("tv.convert_expressions.title", + txt -> this.masterPane.setTitleAt(1, txt)); this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); convertExpressionPanel.setLayout(new GridLayout(4, 1)); // from and to expressions this.fromEntry = new JTextField(); convertExpressionPanel.add(this.fromEntry); - this.fromEntry.setBorder(BorderFactory.createTitledBorder("From")); + this.localizedTextSetters.put("tv.convert_expressions.from", + txt -> this.fromEntry + .setBorder(BorderFactory.createTitledBorder(txt))); this.toEntry = new JTextField(); convertExpressionPanel.add(this.toEntry); - this.toEntry.setBorder(BorderFactory.createTitledBorder("To")); + this.localizedTextSetters.put("tv.convert_expressions.to", + txt -> this.toEntry + .setBorder(BorderFactory.createTitledBorder(txt))); // button to convert - this.convertExpressionButton = new JButton("Convert"); + this.convertExpressionButton = new JButton(); + this.localizedTextSetters.put("tv.convert_expressions.convert_btn", + this.convertExpressionButton::setText); convertExpressionPanel.add(this.convertExpressionButton); this.convertExpressionButton @@ -309,13 +332,16 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // output of conversion this.expressionOutput = new JTextArea(2, 32); convertExpressionPanel.add(this.expressionOutput); - this.expressionOutput - .setBorder(BorderFactory.createTitledBorder("Output")); + this.localizedTextSetters.put("tv.convert_expressions.output", + txt -> this.expressionOutput + .setBorder(BorderFactory.createTitledBorder(txt))); this.expressionOutput.setEditable(false); // =========== UNIT VIEWER =========== - final JPanel unitLookupPanel = new JPanel(); + final var unitLookupPanel = new JPanel(); this.masterPane.addTab("Unit Viewer", unitLookupPanel); + this.localizedTextSetters.put("tv.unit_viewer.title", + txt -> this.masterPane.setTitleAt(2, txt)); this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); unitLookupPanel.setLayout(new GridLayout()); @@ -331,8 +357,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.unitTextBox.setLineWrap(true); // ============ PREFIX VIEWER ============= - final JPanel prefixLookupPanel = new JPanel(); + final var prefixLookupPanel = new JPanel(); this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); + this.localizedTextSetters.put("tv.prefix_viewer.title", + txt -> this.masterPane.setTitleAt(3, txt)); this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); prefixLookupPanel.setLayout(new GridLayout(1, 2)); @@ -349,23 +377,24 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // ============ INFO PANEL ============ - final JPanel infoPanel = new JPanel(); + final var infoPanel = new JPanel(); this.masterPane.addTab("\uD83D\uDEC8", // info (i) character new JScrollPane(infoPanel)); - final JTextArea infoTextArea = new JTextArea(); - infoTextArea.setEditable(false); - infoTextArea.setOpaque(false); - infoPanel.add(infoTextArea); - infoTextArea.setText(Presenter.getAboutText()); + this.infoTextArea = new JTextArea(); + this.infoTextArea.setEditable(false); + this.infoTextArea.setOpaque(false); + infoPanel.add(this.infoTextArea); // ============ SETTINGS PANEL ============ + this.localeSelector = new JComboBox<>(); this.masterPane.addTab("\u2699", new JScrollPane(this.createSettingsPanel())); this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); // ============ FINALIZE CREATION OF VIEW ============ this.presenter.postViewInitialize(); + this.updateText(); this.frame.pack(); this.frame.setVisible(true); } @@ -375,40 +404,46 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { * code more organized, as this function is massive!) * * @since 2022-02-19 + * @since v0.4.0 */ private JPanel createSettingsPanel() { - final JPanel settingsPanel = new JPanel(); + final var settingsPanel = new JPanel(); settingsPanel .setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); // ============ ROUNDING SETTINGS ============ { - final JPanel roundingPanel = new JPanel(); + final var roundingPanel = new JPanel(); settingsPanel.add(roundingPanel); - roundingPanel.setBorder(new TitledBorder("Rounding Settings")); + this.localizedTextSetters.put("tv.settings.rounding.title", + txt -> roundingPanel.setBorder(new TitledBorder(txt))); roundingPanel.setLayout(new GridBagLayout()); // rounding rule selection - final ButtonGroup roundingRuleButtons = new ButtonGroup(); + final var roundingRuleButtons = new ButtonGroup(); this.roundingType = this.getPresenterRoundingType() .orElseThrow(() -> new AssertionError( "Presenter loaded non-standard rounding rule")); this.precision = this.getPresenterPrecision().orElse(6); - final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); + final var roundingRuleLabel = new JLabel(); + this.localizedTextSetters.put("tv.settings.rounding.rule", + roundingRuleLabel::setText); roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); // sigDigSlider needs to be first so that the rounding-type buttons can // show and hide it - final JLabel sliderLabel = new JLabel("Precision:"); + final var sliderLabel = new JLabel(); + this.localizedTextSetters.put("tv.settings.rounding.precision", + sliderLabel::setText); sliderLabel.setVisible( this.roundingType != StandardRoundingType.UNCERTAINTY); roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) .setAnchor(GridBagConstraints.LINE_START).build()); - final JSlider sigDigSlider = new JSlider(0, 12); + final var sigDigSlider = new JSlider(0, 12); roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) .setAnchor(GridBagConstraints.LINE_START).build()); @@ -428,8 +463,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { }); // significant digit rounding - final JRadioButton fixedPrecision = new JRadioButton( - "Fixed Precision"); + final var fixedPrecision = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.rounding.fixed_sigfig", + fixedPrecision::setText); if (this.roundingType == StandardRoundingType.SIGNIFICANT_DIGITS) { fixedPrecision.setSelected(true); } @@ -444,8 +480,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { .setAnchor(GridBagConstraints.LINE_START).build()); // decimal place rounding - final JRadioButton fixedDecimals = new JRadioButton( - "Fixed Decimal Places"); + final var fixedDecimals = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.rounding.fixed_places", + fixedDecimals::setText); if (this.roundingType == StandardRoundingType.DECIMAL_PLACES) { fixedDecimals.setSelected(true); } @@ -460,8 +497,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { .setAnchor(GridBagConstraints.LINE_START).build()); // scientific rounding - final JRadioButton relativePrecision = new JRadioButton( - "Uncertainty-Based Rounding"); + final var relativePrecision = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.rounding.uncertainty", + relativePrecision::setText); if (this.roundingType == StandardRoundingType.UNCERTAINTY) { relativePrecision.setSelected(true); } @@ -478,10 +516,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // ============ PREFIX REPETITION SETTINGS ============ { - final JPanel prefixRepetitionPanel = new JPanel(); + final var prefixRepetitionPanel = new JPanel(); settingsPanel.add(prefixRepetitionPanel); - prefixRepetitionPanel - .setBorder(new TitledBorder("Prefix Repetition Settings")); + this.localizedTextSetters.put("tv.settings.repetition.title", + txt -> prefixRepetitionPanel.setBorder(new TitledBorder(txt))); prefixRepetitionPanel.setLayout(new GridBagLayout()); final var prefixRule = this.getPresenterPrefixRule() @@ -489,9 +527,11 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { "Presenter loaded non-standard prefix rule")); // prefix rules - final ButtonGroup prefixRuleButtons = new ButtonGroup(); + final var prefixRuleButtons = new ButtonGroup(); - final JRadioButton noRepetition = new JRadioButton("No Repetition"); + final var noRepetition = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.repetition.no", + noRepetition::setText); if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { noRepetition.setSelected(true); } @@ -504,7 +544,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton noRestriction = new JRadioButton("No Restriction"); + final var noRestriction = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.repetition.any", + noRestriction::setText); if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { noRestriction.setSelected(true); } @@ -517,8 +559,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton customRepetition = new JRadioButton( - "Complex Repetition"); + final var customRepetition = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.repetition.complex", + customRepetition::setText); if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { customRepetition.setSelected(true); } @@ -534,18 +577,20 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // ============ SEARCH SETTINGS ============ { - final JPanel searchingPanel = new JPanel(); + final var searchingPanel = new JPanel(); settingsPanel.add(searchingPanel); - searchingPanel.setBorder(new TitledBorder("Search Settings")); + this.localizedTextSetters.put("tv.settings.search.title", + txt -> searchingPanel.setBorder(new TitledBorder(txt))); searchingPanel.setLayout(new GridBagLayout()); // searching rules - final ButtonGroup searchRuleButtons = new ButtonGroup(); + final var searchRuleButtons = new ButtonGroup(); final var searchRule = this.presenter.getSearchRule(); - final JRadioButton noPrefixes = new JRadioButton( - "Never Include Prefixed Units"); + final var noPrefixes = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.search.no_prefixes", + noPrefixes::setText); noPrefixes.addActionListener(e -> { this.presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES); this.presenter.updateView(); @@ -555,8 +600,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton commonPrefixes = new JRadioButton( - "Include Common Prefixes"); + final var commonPrefixes = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.search.common_prefixes", + commonPrefixes::setText); commonPrefixes.addActionListener(e -> { this.presenter.setSearchRule(PrefixSearchRule.COMMON_PREFIXES); this.presenter.updateView(); @@ -566,8 +612,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { searchingPanel.add(commonPrefixes, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton alwaysInclude = new JRadioButton( - "Include All Single Prefixes"); + final var alwaysInclude = new JRadioButton(); + this.localizedTextSetters.put("tv.settings.search.all_prefixes", + alwaysInclude::setText); alwaysInclude.addActionListener(e -> { this.presenter .setSearchRule(this.presenter.getUniversalSearchRule()); @@ -592,34 +639,66 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // ============ OTHER SETTINGS ============ { - final JPanel miscPanel = new JPanel(); + final var miscPanel = new JPanel(); settingsPanel.add(miscPanel); miscPanel.setLayout(new GridBagLayout()); - final JCheckBox oneWay = new JCheckBox("Convert One Way Only"); + final var oneWay = new JCheckBox(); + this.localizedTextSetters.put("tv.settings.oneway", oneWay::setText); oneWay.setSelected(this.presenter.oneWayConversionEnabled()); oneWay.addItemListener(e -> { this.presenter.setOneWayConversionEnabled( e.getStateChange() == ItemEvent.SELECTED); this.presenter.saveSettings(); }); - miscPanel.add(oneWay, new GridBagBuilder(0, 0) + miscPanel.add(oneWay, new GridBagBuilder(0, 0, 2, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - final JCheckBox showAllVariations = new JCheckBox( - "Show Duplicate Units & Prefixes"); + final var showAllVariations = new JCheckBox(); + this.localizedTextSetters.put("tv.settings.show_duplicate", + showAllVariations::setText); showAllVariations.setSelected(this.presenter.duplicatesShown()); showAllVariations.addItemListener(e -> { this.presenter .setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED); this.presenter.saveSettings(); }); - miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) + miscPanel.add(showAllVariations, new GridBagBuilder(0, 1, 2, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final var useDefaultFiles = new JCheckBox(); + this.localizedTextSetters.put("tv.settings.use_default_files", + useDefaultFiles::setText); + useDefaultFiles.setSelected(this.presenter.usingDefaultDatafiles()); + useDefaultFiles.addItemListener(e -> { + this.presenter.setUseDefaultDatafiles( + e.getStateChange() == ItemEvent.SELECTED); + this.presenter.saveSettings(); + }); + miscPanel.add(useDefaultFiles, new GridBagBuilder(0, 2, 2, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - final JButton unitFileButton = new JButton("Manage Unit Data Files"); + final var localeLabel = new JLabel(); + this.localizedTextSetters.put("tv.settings.locale", + localeLabel::setText); + miscPanel.add(localeLabel, new GridBagBuilder(0, 3, 1, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + this.presenter.getAvailableLocales().stream().sorted() + .forEachOrdered(this.localeSelector::addItem); + this.localeSelector.setSelectedItem(this.presenter.getUserLocale()); + this.localeSelector.addItemListener(e -> { + this.presenter.setUserLocale((String) e.getItem()); + this.presenter.saveSettings(); + }); + miscPanel.add(localeSelector, new GridBagBuilder(1, 3, 1, 1) + .setAnchor(GridBagConstraints.LINE_END).build()); + + final var unitFileButton = new JButton(); + this.localizedTextSetters.put("tv.settings.unitfiles.button", + unitFileButton::setText); unitFileButton.setEnabled(false); - miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) + miscPanel.add(unitFileButton, new GridBagBuilder(0, 4, 2, 1) .setAnchor(GridBagConstraints.LINE_START).build()); } @@ -662,8 +741,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** * @return the precision of the presenter's rounding rule, if that is * meaningful - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ private OptionalInt getPresenterPrecision() { final var presenterRule = this.presenter.getNumberDisplayRule(); @@ -671,18 +750,17 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return OptionalInt .of(((StandardDisplayRules.FixedDecimals) presenterRule) .decimalPlaces()); - else if (presenterRule instanceof StandardDisplayRules.FixedPrecision) + if (presenterRule instanceof StandardDisplayRules.FixedPrecision) return OptionalInt .of(((StandardDisplayRules.FixedPrecision) presenterRule) .significantFigures()); - else - return OptionalInt.empty(); + return OptionalInt.empty(); } /** * @return presenter's prefix repetition rule - * @since v0.4.0 * @since 2022-04-19 + * @since v0.4.0 */ private Optional<DefaultPrefixRepetitionRule> getPresenterPrefixRule() { final var prefixRule = this.presenter.getPrefixRepetitionRule(); @@ -694,25 +772,24 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** * Determines which rounding type the presenter is currently using, if any. * - * @since v0.4.0 * @since 2022-04-18 + * @since v0.4.0 */ private Optional<StandardRoundingType> getPresenterRoundingType() { final var presenterRule = this.presenter.getNumberDisplayRule(); if (Objects.equals(presenterRule, StandardDisplayRules.uncertaintyBased())) return Optional.of(StandardRoundingType.UNCERTAINTY); - else if (presenterRule instanceof StandardDisplayRules.FixedDecimals) + if (presenterRule instanceof StandardDisplayRules.FixedDecimals) return Optional.of(StandardRoundingType.DECIMAL_PLACES); - else if (presenterRule instanceof StandardDisplayRules.FixedPrecision) + if (presenterRule instanceof StandardDisplayRules.FixedPrecision) return Optional.of(StandardRoundingType.SIGNIFICANT_DIGITS); - else - return Optional.empty(); + return Optional.empty(); } @Override public Optional<String> getSelectedDimensionName() { - final String selectedItem = (String) this.dimensionSelector + final var selectedItem = (String) this.dimensionSelector .getSelectedItem(); return Optional.ofNullable(selectedItem); } @@ -780,8 +857,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public void showExpressionConversionOutput(UnitConversionRecord uc) { - this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(), - uc.outputValueString(), uc.toName())); + this.expressionOutput.setText(uc.toString()); } @Override @@ -806,9 +882,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** * Sets the presenter's rounding rule to the one specified by the current * settings - * - * @since v0.4.0 + * * @since 2022-04-18 + * @since v0.4.0 */ private void updatePresenterRoundingRule() { final Function<UncertainDouble, String> roundingRule; @@ -828,4 +904,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.presenter.setNumberDisplayRule(roundingRule); this.presenter.saveSettings(); } + + @Override + public void updateText() { + this.frame.setTitle(this.presenter.getLocalizedText("tv.title") + .replace("[v]", ProgramInfo.VERSION.toString())); + this.infoTextArea.setText(this.presenter.getAboutText()); + this.localizedTextSetters.forEach( + (id, action) -> action.accept(this.presenter.getLocalizedText(id))); + } } diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java index 43a62e6..3c2bb6c 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,18 +24,18 @@ import sevenUnits.unit.UnitValue; /** * A record of a conversion between units or expressions * - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public final class UnitConversionRecord { /** * Gets a {@code UnitConversionRecord} from two linear unit values * - * @param input input unit & value - * @param output output unit & value + * @param input input unit & value + * @param output output unit & value * @return unit conversion record - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public static UnitConversionRecord fromLinearValues(LinearUnitValue input, LinearUnitValue output) { @@ -48,11 +48,11 @@ public final class UnitConversionRecord { /** * Gets a {@code UnitConversionRecord} from two unit values * - * @param input input unit & value - * @param output output unit & value + * @param input input unit & value + * @param output output unit & value * @return unit conversion record - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public static UnitConversionRecord fromValues(UnitValue input, UnitValue output) { @@ -70,8 +70,8 @@ public final class UnitConversionRecord { * @param inputValueString string representing input value * @param outputValueString string representing output value * @return unit conversion record - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public static UnitConversionRecord valueOf(String fromName, String toName, String inputValueString, String outputValueString) { @@ -79,13 +79,9 @@ public final class UnitConversionRecord { outputValueString); } - /** - * The name of the unit or expression that was converted from - */ + /** The name of the unit or expression that was converted from */ private final String fromName; - /** - * The name of the unit or expression that was converted to - */ + /** The name of the unit or expression that was converted to */ private final String toName; /** @@ -106,6 +102,7 @@ public final class UnitConversionRecord { * @param inputValueString string representing input value * @param outputValueString string representing output value * @since 2022-04-09 + * @since v0.4.0 */ private UnitConversionRecord(String fromName, String toName, String inputValueString, String outputValueString) { @@ -121,7 +118,7 @@ public final class UnitConversionRecord { return true; if (!(obj instanceof UnitConversionRecord)) return false; - final UnitConversionRecord other = (UnitConversionRecord) obj; + final var other = (UnitConversionRecord) obj; if (this.fromName == null) { if (other.fromName != null) return false; @@ -147,8 +144,8 @@ public final class UnitConversionRecord { /** * @return name of unit or expression that was converted from - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public String fromName() { return this.fromName; @@ -156,8 +153,8 @@ public final class UnitConversionRecord { @Override public int hashCode() { - final int prime = 31; - int result = 1; + final var prime = 31; + var result = 1; result = prime * result + (this.fromName == null ? 0 : this.fromName.hashCode()); result = prime * result + (this.inputValueString == null ? 0 @@ -171,8 +168,8 @@ public final class UnitConversionRecord { /** * @return string representing input value - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public String inputValueString() { return this.inputValueString; @@ -180,8 +177,8 @@ public final class UnitConversionRecord { /** * @return string representing output value - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public String outputValueString() { return this.outputValueString; @@ -189,8 +186,8 @@ public final class UnitConversionRecord { /** * @return name of unit or expression that was converted to - * @since v0.4.0 * @since 2022-04-09 + * @since v0.4.0 */ public String toName() { return this.toName; @@ -198,9 +195,9 @@ public final class UnitConversionRecord { @Override public String toString() { - final String inputString = this.inputValueString.isBlank() ? this.fromName + final var inputString = this.inputValueString.isBlank() ? this.fromName : this.inputValueString + " " + this.fromName; - final String outputString = this.outputValueString.isBlank() ? this.toName + final var outputString = this.outputValueString.isBlank() ? this.toName : this.outputValueString + " " + this.toName; return inputString + " = " + outputString; } diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index b9077f7..fa3a388 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021-2022 Adrien Hopkins + * Copyright (C) 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -21,59 +21,59 @@ import java.util.Set; /** * A View that supports single unit-based conversion - * + * * @author Adrien Hopkins - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ public interface UnitConversionView extends View { /** * @return dimensions available for filtering - * @since v0.4.0 * @since 2022-01-29 + * @since v0.4.0 */ Set<String> getDimensionNames(); /** * @return name of unit to convert <em>from</em> - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ Optional<String> getFromSelection(); /** * @return list of names of units available to convert from - * @since v0.4.0 * @since 2022-03-30 + * @since v0.4.0 */ Set<String> getFromUnitNames(); /** * @return value to convert between the units (specifically, the numeric * string provided by the user) - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ String getInputValue(); /** * @return selected dimension - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ Optional<String> getSelectedDimensionName(); /** * @return name of unit to convert <em>to</em> - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ Optional<String> getToSelection(); /** * @return list of names of units available to convert to - * @since v0.4.0 * @since 2022-03-30 + * @since v0.4.0 */ Set<String> getToUnitNames(); @@ -81,8 +81,8 @@ public interface UnitConversionView extends View { * Sets the available dimensions for filtering. * * @param dimensionNames names of dimensions to use - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ void setDimensionNames(Set<String> dimensionNames); @@ -92,8 +92,8 @@ public interface UnitConversionView extends View { * that allow the user to select units from a list. * * @param unitNames names of units to convert from - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ void setFromUnitNames(Set<String> unitNames); @@ -103,18 +103,17 @@ public interface UnitConversionView extends View { * that allow the user to select units from a list. * * @param unitNames names of units to convert to - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ void setToUnitNames(Set<String> unitNames); /** * Shows the output of a unit conversion. - * - * @param input input unit & value (obtained from this view) - * @param output output unit & value - * @since v0.4.0 + * + * @param uc record of unit conversion * @since 2021-12-24 + * @since v0.4.0 */ void showUnitConversionOutput(UnitConversionRecord uc); } diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java index 7dd0c44..0adeb3a 100644 --- a/src/main/java/sevenUnitsGUI/View.java +++ b/src/main/java/sevenUnitsGUI/View.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021-2022 Adrien Hopkins + * Copyright (C) 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,16 +24,16 @@ import sevenUnits.utils.NameSymbol; /** * An object that controls user interaction with 7Units - * + * * @author Adrien Hopkins - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ public interface View { /** * @return a new tabbed view - * @since v0.4.0 * @since 2022-04-19 + * @since v0.4.0 */ static View createTabbedView() { return new TabbedView(); @@ -41,22 +41,22 @@ public interface View { /** * @return the presenter associated with this view - * @since v0.4.0 * @since 2022-04-19 + * @since v0.4.0 */ Presenter getPresenter(); /** * @return name of prefix currently being viewed - * @since v0.4.0 * @since 2022-04-10 + * @since v0.4.0 */ Optional<String> getViewedPrefixName(); /** * @return name of unit currently being viewed - * @since v0.4.0 * @since 2022-04-10 + * @since v0.4.0 */ Optional<String> getViewedUnitName(); @@ -65,8 +65,8 @@ public interface View { * viewer * * @param prefixNames prefix names to view - * @since v0.4.0 * @since 2022-04-10 + * @since v0.4.0 */ void setViewablePrefixNames(Set<String> prefixNames); @@ -74,8 +74,8 @@ public interface View { * Sets the list of units that are available to be viewed in a unit viewer * * @param unitNames unit names to view - * @since v0.4.0 * @since 2022-04-10 + * @since v0.4.0 */ void setViewableUnitNames(Set<String> unitNames); @@ -85,8 +85,8 @@ public interface View { * @param title title of error message; on any view that uses an error * dialog, this should be the title of the error dialog. * @param message error message - * @since v0.4.0 * @since 2021-12-15 + * @since v0.4.0 */ void showErrorMessage(String title, String message); @@ -95,8 +95,8 @@ public interface View { * * @param name name(s) and symbol of prefix * @param multiplierString string representation of prefix multiplier - * @since v0.4.0 * @since 2022-04-10 + * @since v0.4.0 */ void showPrefix(NameSymbol name, String multiplierString); @@ -107,9 +107,16 @@ public interface View { * @param definition unit's definition string * @param dimensionName name of unit's dimension * @param type type of unit (metric/semi-metric/non-metric) - * @since v0.4.0 * @since 2022-04-10 + * @since v0.4.0 */ void showUnit(NameSymbol name, String definition, String dimensionName, UnitType type); + + /** + * Updates the view's text to reflect the presenter's locale. + * + * This method <b>must not</b> call {@link Presenter#setUserLocale(String)}. + */ + void updateText(); } diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index e6593fb..750e2d9 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -30,10 +30,10 @@ import sevenUnits.utils.Nameable; /** * A class that simulates a View (supports both unit and expression conversion) * for testing. Getters and setters work as expected. - * + * * @author Adrien Hopkins - * @since v0.4.0 * @since 2022-01-29 + * @since v0.4.0 */ public final class ViewBot implements UnitConversionView, ExpressionConversionView { @@ -42,6 +42,7 @@ public final class ViewBot * {@link View#showPrefix(NameSymbol, String)}, for testing. * * @since 2022-04-16 + * @since v0.4.0 */ public static final class PrefixViewingRecord implements Nameable { private final NameSymbol nameSymbol; @@ -51,6 +52,7 @@ public final class ViewBot * @param nameSymbol * @param multiplierString * @since 2022-04-16 + * @since v0.4.0 */ public PrefixViewingRecord(NameSymbol nameSymbol, String multiplierString) { @@ -64,7 +66,7 @@ public final class ViewBot return true; if (!(obj instanceof PrefixViewingRecord)) return false; - final PrefixViewingRecord other = (PrefixViewingRecord) obj; + final var other = (PrefixViewingRecord) obj; return Objects.equals(this.multiplierString, other.multiplierString) && Objects.equals(this.nameSymbol, other.nameSymbol); } @@ -79,17 +81,19 @@ public final class ViewBot return Objects.hash(this.multiplierString, this.nameSymbol); } + /** @return A string representation of the prefix multiplier. */ public String multiplierString() { return this.multiplierString; } + /** @return A {@code NameSymbol} describing the prefix. */ public NameSymbol nameSymbol() { return this.nameSymbol; } @Override public String toString() { - final StringBuilder builder = new StringBuilder(); + final var builder = new StringBuilder(); builder.append("PrefixViewingRecord [nameSymbol="); builder.append(this.nameSymbol); builder.append(", multiplierString="); @@ -104,6 +108,7 @@ public final class ViewBot * {@link View#showUnit(NameSymbol, String, String, UnitType)}, for testing. * * @since 2022-04-16 + * @since v0.4.0 */ public static final class UnitViewingRecord implements Nameable { private final NameSymbol nameSymbol; @@ -112,7 +117,12 @@ public final class ViewBot private final UnitType unitType; /** + * @param nameSymbol name(s) and symbol of unit + * @param definition unit's definition string + * @param dimensionName name of unit's dimension + * @param unitType type of unit (metric/semi-metric/non-metric) * @since 2022-04-16 + * @since v0.4.0 */ public UnitViewingRecord(NameSymbol nameSymbol, String definition, String dimensionName, UnitType unitType) { @@ -125,6 +135,7 @@ public final class ViewBot /** * @return the definition * @since 2022-04-16 + * @since v0.4.0 */ public String definition() { return this.definition; @@ -133,6 +144,7 @@ public final class ViewBot /** * @return the dimensionName * @since 2022-04-16 + * @since v0.4.0 */ public String dimensionName() { return this.dimensionName; @@ -144,7 +156,7 @@ public final class ViewBot return true; if (!(obj instanceof UnitViewingRecord)) return false; - final UnitViewingRecord other = (UnitViewingRecord) obj; + final var other = (UnitViewingRecord) obj; return Objects.equals(this.definition, other.definition) && Objects.equals(this.dimensionName, other.dimensionName) && Objects.equals(this.nameSymbol, other.nameSymbol) @@ -154,6 +166,7 @@ public final class ViewBot /** * @return the nameSymbol * @since 2022-04-16 + * @since v0.4.0 */ @Override public NameSymbol getNameSymbol() { @@ -166,13 +179,14 @@ public final class ViewBot this.nameSymbol, this.unitType); } + /** @return name(s) and symbol of unit */ public NameSymbol nameSymbol() { return this.nameSymbol; } @Override public String toString() { - final StringBuilder builder = new StringBuilder(); + final var builder = new StringBuilder(); builder.append("UnitViewingRecord [nameSymbol="); builder.append(this.nameSymbol); builder.append(", definition="); @@ -188,6 +202,7 @@ public final class ViewBot /** * @return the unitType * @since 2022-04-16 + * @since v0.4.0 */ public UnitType unitType() { return this.unitType; @@ -236,6 +251,7 @@ public final class ViewBot * Creates a new {@code ViewBot} with a new presenter. * * @since 2022-01-29 + * @since v0.4.0 */ public ViewBot() { this.presenter = new Presenter(this); @@ -249,6 +265,7 @@ public final class ViewBot /** * @return list of records of expression conversions done by this bot * @since 2022-04-09 + * @since v0.4.0 */ public List<UnitConversionRecord> expressionConversionList() { return Collections.unmodifiableList(this.expressionConversions); @@ -257,6 +274,7 @@ public final class ViewBot /** * @return the available dimensions * @since 2022-01-29 + * @since v0.4.0 */ @Override public Set<String> getDimensionNames() { @@ -276,6 +294,7 @@ public final class ViewBot /** * @return the units available for selection in From * @since 2022-01-29 + * @since v0.4.0 */ @Override public Set<String> getFromUnitNames() { @@ -290,6 +309,7 @@ public final class ViewBot /** * @return the presenter associated with tihs view * @since 2022-01-29 + * @since v0.4.0 */ @Override public Presenter getPresenter() { @@ -314,6 +334,7 @@ public final class ViewBot /** * @return the units available for selection in To * @since 2022-01-29 + * @since v0.4.0 */ @Override public Set<String> getToUnitNames() { @@ -333,6 +354,7 @@ public final class ViewBot /** * @return list of records of this viewBot's prefix views * @since 2022-04-16 + * @since v0.4.0 */ public List<PrefixViewingRecord> prefixViewList() { return Collections.unmodifiableList(this.prefixViewingRecords); @@ -350,6 +372,7 @@ public final class ViewBot * @param fromExpression the expression to convert from * @throws NullPointerException if {@code fromExpression} is null * @since 2022-01-29 + * @since v0.4.0 */ public void setFromExpression(String fromExpression) { this.fromExpression = Objects.requireNonNull(fromExpression, @@ -359,6 +382,7 @@ public final class ViewBot /** * @param fromSelection the fromSelection to set * @since 2022-01-29 + * @since v0.4.0 */ public void setFromSelection(Optional<String> fromSelection) { this.fromSelection = Objects.requireNonNull(fromSelection, @@ -368,6 +392,7 @@ public final class ViewBot /** * @param fromSelection the fromSelection to set * @since 2022-02-10 + * @since v0.4.0 */ public void setFromSelection(String fromSelection) { this.setFromSelection(Optional.of(fromSelection)); @@ -381,20 +406,27 @@ public final class ViewBot /** * @param inputValue the inputValue to set * @since 2022-01-29 + * @since v0.4.0 */ public void setInputValue(String inputValue) { this.inputValue = inputValue; } /** - * @param selectedDimension the selectedDimension to set + * @param selectedDimensionName the selectedDimensionName to set * @since 2022-01-29 + * @since v0.4.0 */ public void setSelectedDimensionName( Optional<String> selectedDimensionName) { this.selectedDimensionName = selectedDimensionName; } + /** + * Sets the view's selected dimension + * + * @param selectedDimensionName name of dimension to select (string) + */ public void setSelectedDimensionName(String selectedDimensionName) { this.setSelectedDimensionName(Optional.of(selectedDimensionName)); } @@ -405,6 +437,7 @@ public final class ViewBot * @param toExpression the expression to convert to * @throws NullPointerException if {@code toExpression} is null * @since 2022-01-29 + * @since v0.4.0 */ public void setToExpression(String toExpression) { this.toExpression = Objects.requireNonNull(toExpression, @@ -412,14 +445,16 @@ public final class ViewBot } /** - * @param toSelection the toSelection to set + * @param toSelection unit set in the 'To' selection * @since 2022-01-29 + * @since v0.4.0 */ public void setToSelection(Optional<String> toSelection) { this.toSelection = Objects.requireNonNull(toSelection, "toSelection cannot be null."); } + /** @param toSelection unit set in the 'To' selection */ public void setToSelection(String toSelection) { this.setToSelection(Optional.of(toSelection)); } @@ -439,18 +474,28 @@ public final class ViewBot // do nothing, ViewBot supports selecting any unit } + /** @param viewedPrefixName name of prefix being used */ public void setViewedPrefixName(Optional<String> viewedPrefixName) { this.prefixViewerSelection = viewedPrefixName; } + /** + * @param viewedPrefixName name of prefix being used (may not be null) + * @throws NullPointerException if {@code viewedPrefixName} is null + */ public void setViewedPrefixName(String viewedPrefixName) { this.setViewedPrefixName(Optional.of(viewedPrefixName)); } + /** @param viewedUnitName name of unit being used */ public void setViewedUnitName(Optional<String> viewedUnitName) { this.unitViewerSelection = viewedUnitName; } + /** + * @param viewedUnitName name of unit being used (may not be null) + * @throws NullPointerException if {@code viewedUnitName} is null + */ public void setViewedUnitName(String viewedUnitName) { this.setViewedUnitName(Optional.of(viewedUnitName)); } @@ -493,6 +538,7 @@ public final class ViewBot /** * @return list of records of every unit conversion made by this bot * @since 2022-04-09 + * @since v0.4.0 */ public List<UnitConversionRecord> unitConversionList() { return Collections.unmodifiableList(this.unitConversions); @@ -501,8 +547,14 @@ public final class ViewBot /** * @return list of records of unit viewings made by this bot * @since 2022-04-16 + * @since v0.4.0 */ public List<UnitViewingRecord> unitViewList() { return Collections.unmodifiableList(this.unitViewingRecords); } + + @Override + public void updateText() { + // do nothing, since ViewBot is not localized + } } diff --git a/src/main/java/sevenUnitsGUI/package-info.java b/src/main/java/sevenUnitsGUI/package-info.java index cff1ded..9432960 100644 --- a/src/main/java/sevenUnitsGUI/package-info.java +++ b/src/main/java/sevenUnitsGUI/package-info.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * Copyright (C) 2021-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,8 +16,9 @@ */ /** * The MVP GUI of SevenUnits - * + * * @author Adrien Hopkins * @since 2021-12-15 + * @since v0.4.0 */ package sevenUnitsGUI;
\ No newline at end of file diff --git a/src/main/resources/about.txt b/src/main/resources/about/en.txt index 782b422..37986e5 100644 --- a/src/main/resources/about.txt +++ b/src/main/resources/about/en.txt @@ -4,14 +4,18 @@ About 7Units Version [VERSION] inspired by GNU Units (https://www.gnu.org/software/units/). You can use it to simply convert units, but you can also use it like a calculator, computing and converting expressions -like "10 m/s + (25^2 - 5^2) mi/hr". +like "10 m/s + (25^2 - 5^2) mi/h". This software was written by Adrien Hopkins <adrien.p.hopkins@gmail.com>. +Unit/Prefix/Dimension Statistics: + +[LOADSTATS] + Copyright Notice: -Unit Converter Copyright (C) 2018-2024 Adrien Hopkins +Unit Converter Copyright (C) 2018-2025 Adrien Hopkins This program comes with ABSOLUTELY NO WARRANTY; for details read the LICENSE file, section 15 diff --git a/src/main/resources/about/fr.txt b/src/main/resources/about/fr.txt new file mode 100644 index 0000000..fc7ed83 --- /dev/null +++ b/src/main/resources/about/fr.txt @@ -0,0 +1,24 @@ +À propos de 7Unités version [VERSION] + +7Unités est une programme pour convertir les unités avec plusieurs fonctions, +inspiré par GNU Units (https://www.gnu.org/software/units/). +Vous pouvez l’utiliser pour convertir des unités, mais vous pouvez aussi +l’utiliser comme calculatrice, computer et convertir des expressions +comme "10 m/s + (25^2 - 5^2) mi/h". + +Ce logiciel est par Adrien Hopkins <adrien.p.hopkins@gmail.com>. + +Statistiques d’unités, préfixes et dimensions: + +[LOADSTATS] + +Copyright Notice: + +Unit Converter Copyright (C) 2018-2025 Adrien Hopkins +This program comes with ABSOLUTELY NO WARRANTY; +for details read the LICENSE file, section 15 + +This is free software, and you are welcome to redistribute +it under certain conditions; for details go to +<https://www.gnu.org/licenses/quick-guide-gplv3.html> +or read the LICENSE file. diff --git a/src/main/resources/locales/en.txt b/src/main/resources/locales/en.txt new file mode 100644 index 0000000..666e363 --- /dev/null +++ b/src/main/resources/locales/en.txt @@ -0,0 +1,31 @@ +tv.title=7Units [v] +tv.convert_units.title=Convert Units +tv.convert_units.value_prompt=Value to convert: +tv.convert_units.convert_btn=Convert +tv.convert_expressions.title=Convert Unit Expressions +tv.convert_expressions.from=From +tv.convert_expressions.to=To +tv.convert_expressions.convert_btn=Convert +tv.convert_expressions.output=Output +tv.unit_viewer.title=Unit Viewer +tv.prefix_viewer.title=Prefix Viewer +tv.settings.rounding.title=Rounding Settings +tv.settings.rounding.rule=Rounding Rule: +tv.settings.rounding.precision=Precision: +tv.settings.rounding.fixed_sigfig=Fixed Precision +tv.settings.rounding.fixed_places=Fixed Decimal Places +tv.settings.rounding.uncertainty=Uncertainty-based Rounding +tv.settings.repetition.title=Prefix Repetition Settings +tv.settings.repetition.no=No Repetition +tv.settings.repetition.any=No Restriction +tv.settings.repetition.complex=Complex Repetition +tv.settings.search.title=Search Settings +tv.settings.search.no_prefixes=Never Include Prefixed Units +tv.settings.search.common_prefixes=Include Common Prefixes +tv.settings.search.all_prefixes=Include All Single Prefixes +tv.settings.oneway=Convert One Way Only +tv.settings.show_duplicate=Show Duplicate Units & Prefixes +tv.settings.use_default_files=Use Default Datafiles +tv.settings.locale=🌐 Locale: +tv.settings.unitfiles.button=Manage Unit Data Files +load_stat_msg=Successfully loaded [u] unique units with [un] names ([b] base units), [p] unique prefixes with [pn] names, [s] unit sets, and [d] named dimensions. diff --git a/src/main/resources/locales/fr.txt b/src/main/resources/locales/fr.txt new file mode 100644 index 0000000..3fef030 --- /dev/null +++ b/src/main/resources/locales/fr.txt @@ -0,0 +1,31 @@ +tv.title=7Unités [v] +tv.convert_units.title=Convertir Unités +tv.convert_units.value_prompt=Valueur à convertir: +tv.convert_units.convert_btn=Convertir +tv.convert_expressions.title=Convertir Expressions +tv.convert_expressions.from=De +tv.convert_expressions.to=À +tv.convert_expressions.convert_btn=Convertir +tv.convert_expressions.output=Résultat +tv.unit_viewer.title=Unités +tv.prefix_viewer.title=Préfixes +tv.settings.rounding.title=Préférences d’Arrondissement +tv.settings.rounding.rule=Règle d’Arrondissement +tv.settings.rounding.precision=Précision: +tv.settings.rounding.fixed_sigfig=Precision fixe +tv.settings.rounding.fixed_places=Chiffres fixe +tv.settings.rounding.uncertainty=Arrondissement d’Incertitude +tv.settings.repetition.title=Préférences de répetition de préfixes +tv.settings.repetition.no=Non répetition +tv.settings.repetition.any=Tout répetition +tv.settings.repetition.complex=Répetition Complexe +tv.settings.search.title=Préférences de Recherche +tv.settings.search.no_prefixes=Jamais inclure unités préfixés +tv.settings.search.common_prefixes=Inclure préfixes fréquents +tv.settings.search.all_prefixes=Inclure tous préfixes seuls +tv.settings.oneway=Convertir Seulement en un Direction +tv.settings.show_duplicate=Montrer unités et préfixes doubles +tv.settings.use_default_files=Utilise donées par défaut +tv.settings.locale=🌐 Locale: +tv.settings.unitfiles.button=Gérer donées d’unités +load_stat_msg=Chargé [u] unités uniques avec [un] noms ([b] unités bases), [p] préfixes uniques avec [pn] noms, [s] collections d’unités, et [d] dimensions nomées. diff --git a/src/main/resources/metric_exceptions.txt b/src/main/resources/metric_exceptions.txt index 73748c0..433dbb5 100644 --- a/src/main/resources/metric_exceptions.txt +++ b/src/main/resources/metric_exceptions.txt @@ -10,6 +10,7 @@ min minute h hour +hms d day wk diff --git a/src/main/resources/unitsfile.txt b/src/main/resources/unitsfile.txt index 543c821..17fe98a 100644 --- a/src/main/resources/unitsfile.txt +++ b/src/main/resources/unitsfile.txt @@ -155,6 +155,7 @@ minute 60 second min minute hour 3600 second h hour +hms hour; minute; second day 86400 second d day week 7 day @@ -164,6 +165,7 @@ gregorianyear 365.2425 day gregorianmonth gregorianyear / 12 # Other non-SI "metric" units +are 100 m^2 litre 0.001 m^3 liter litre l litre @@ -187,8 +189,16 @@ inch foot / 12 in inch yard 3 foot yd yard +chain 66 ft +ch chain +furlong 10 chain mile 1760 yard mi mile +ftin ft; in +ydftin yd; ft; in + +# Imperial area units +acre chain * furlong # Compressed notation kph km / hour diff --git a/src/test/java/sevenUnits/unit/MultiUnitTest.java b/src/test/java/sevenUnits/unit/MultiUnitTest.java deleted file mode 100644 index 949a1f1..0000000 --- a/src/test/java/sevenUnits/unit/MultiUnitTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (C) 2020 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.unit; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; - -import org.junit.jupiter.api.Test; - -/** - * Tests related to the {@code MultiUnit}. - * - * @since 2020-10-03 - */ -class MultiUnitTest { - - /** - * Ensures that the {@code MultiUnit} can convert properly. - */ - @Test - final void testConvert() { - final Random rng = ThreadLocalRandom.current(); - final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, - BritishImperial.Length.INCH); - - assertEquals(1702.0, - footInch.convertTo(Metric.METRE.withPrefix(Metric.MILLI), - Arrays.asList(5.0, 7.0)), - 1.0); - - for (int i = 0; i < 1000; i++) { - final double feet = rng.nextInt(1000); - final double inches = rng.nextDouble() * 12; - final double millimetres = feet * 304.8 + inches * 25.4; - - final List<Double> feetAndInches = Metric.METRE - .withPrefix(Metric.MILLI).convertTo(footInch, millimetres); - assertEquals(feet, feetAndInches.get(0), 1e-10); - assertEquals(inches, feetAndInches.get(1), 1e-10); - } - } - - /** - * Test method for {@link sevenUnits.unit.MultiUnit#convertFromBase(double)}. - */ - @Test - final void testConvertFromBase() { - final Random rng = ThreadLocalRandom.current(); - final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, - BritishImperial.Length.INCH); - - // 1.7 m =~ 5' + 7" - final List<Double> values = footInch.convertFromBase(1.7018); - - assertEquals(5, values.get(0)); - assertEquals(7, values.get(1), 1e-12); - - for (int i = 0; i < 1000; i++) { - final double feet = rng.nextInt(1000); - final double inches = rng.nextDouble() * 12; - final double metres = feet * 0.3048 + inches * 0.0254; - - final List<Double> feetAndInches = footInch.convertFromBase(metres); - assertEquals(feet, feetAndInches.get(0), 1e-10); - assertEquals(inches, feetAndInches.get(1), 1e-10); - } - } - - /** - * Test method for - * {@link sevenUnits.unit.MultiUnit#convertToBase(java.util.List)}. - */ - @Test - final void testConvertToBase() { - final Random rng = ThreadLocalRandom.current(); - final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, - BritishImperial.Length.INCH); - - // 1.7 m =~ 5' + 7" - assertEquals(1.7018, footInch.convertToBase(Arrays.asList(5.0, 7.0)), - 1e-12); - - for (int i = 0; i < 1000; i++) { - final double feet = rng.nextInt(1000); - final double inches = rng.nextDouble() * 12; - final double metres = feet * 0.3048 + inches * 0.0254; - - assertEquals(metres, - footInch.convertToBase(Arrays.asList(feet, inches)), 1e-12); - } - } -} diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java index 9d650f0..3d6d663 100644 --- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java +++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -23,20 +23,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; -import java.io.InputStream; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import sevenUnits.utils.NameSymbol; @@ -45,7 +46,7 @@ import sevenUnits.utils.UncertainDouble; /** * A test for the {@link UnitDatabase} class. This is NOT part of this program's * public API. - * + * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 @@ -57,8 +58,8 @@ class UnitDatabaseTest { private V value; /** - * * @since 2021-10-07 + * @since v0.3.2 */ public SimpleEntry(K key, V value) { this.key = key; @@ -94,7 +95,7 @@ class UnitDatabaseTest { @Override public V setValue(V value) { - final V oldValue = this.value; + final var oldValue = this.value; this.value = value; return oldValue; } @@ -135,6 +136,7 @@ class UnitDatabaseTest { * @param value value in entry * @return entry * @since 2021-10-07 + * @since v0.3.2 */ private static <K, V> Map.Entry<K, V> entry(K key, V value) { return new SimpleEntry<>(key, value); @@ -146,14 +148,18 @@ class UnitDatabaseTest { * * @param loadTo database to load to * @param path path of file to load + * @return exceptions returned by file loading * @since 2021-10-04 + * @since v0.3.2 */ - private static void loadDimensionFile(UnitDatabase loadTo, String path) { - try (final InputStream testFile = UnitDatabaseTest.class + private static List<LoadingException> loadDimensionFile(UnitDatabase loadTo, + String path) { + try (final var testFile = UnitDatabaseTest.class .getResourceAsStream(path)) { - loadTo.loadDimensionsFromStream(testFile); + return loadTo.loadDimensionsFromStream(testFile); } catch (final IOException e) { fail(e.getClass() + " occurred upon loading file \"" + path + "\"."); + return Collections.emptyList(); } } @@ -163,26 +169,89 @@ class UnitDatabaseTest { * * @param loadTo database to load to * @param path path of file to load + * @return exceptions returned by file loading * @since 2021-09-22 + * @since v0.3.2 */ - private static void loadUnitsFile(UnitDatabase loadTo, String path) { - try (final InputStream testFile = UnitDatabaseTest.class + private static List<LoadingException> loadUnitsFile(UnitDatabase loadTo, + String path) { + try (final var testFile = UnitDatabaseTest.class .getResourceAsStream(path)) { - loadTo.loadUnitsFromStream(testFile); + return loadTo.loadUnitsFromStream(testFile); } catch (final IOException e) { fail(e.getClass() + " occurred upon loading file \"" + path + "\"."); + return Collections.emptyList(); } } + private static final Stream<Arguments> testEvaluateExpressionInvalid() { + return Stream.of(Arguments.of("K^K"), Arguments.of("1 + K")); + } + + private static final Stream<Arguments> testEvaluateExpressionValid() { + final var uncertainTwoThirds = UncertainDouble.of(2.0, 1.0) + .dividedBy(UncertainDouble.of(3.0, 1.0)); + return Stream.of( + Arguments.of("J + (2 * 3) J + (20 / 4) J", + LinearUnitValue.of(J, + UncertainDouble.of(12, Math.sqrt(14.625)))), + Arguments.of("J + 2 * 3 * J + 20 / 4 * J", + LinearUnitValue.of(J, + UncertainDouble.of(12, Math.sqrt(14.625)))), + Arguments.of("J - -1 * J", + LinearUnitValue.of(J, UncertainDouble.of(2, 1))), + Arguments.of("K^2", + LinearUnitValue.of(K.times(K), UncertainDouble.of(1, 0))), + Arguments.of("2 J / 3 J", + LinearUnitValue.of(J.dividedBy(J), uncertainTwoThirds))); + } + + private static final Stream<Arguments> testFormatExpression() { + return Stream.of(Arguments.of("1*2", "1 * 2"), + Arguments.of("1/2", "1 / 2"), Arguments.of("1|2", "1 | 2"), + Arguments.of("1^2", "1 ^ 2"), Arguments.of("1 * 2", "1 * 2"), + Arguments.of("+1", "+1"), Arguments.of("-1", "-1"), + Arguments.of("1.1e+5", "1.1e+5"), + Arguments.of("1.25e-5", "1.25e-5")); + } + + /** + * Tests expressions that are valid to the parser, but semantically invalid + * (e.g. adding different dimensions) + * + * @param expression expression to test - should throw + * {@link IllegalArgumentException} + */ + @ParameterizedTest + @MethodSource + public void testEvaluateExpressionInvalid(String expression) { + final var database = new UnitDatabase(); + + database.addUnit("J", J); + database.addUnit("K", K); + + database.addPrefix("A", A); + database.addPrefix("B", B); + database.addPrefix("C", C); + + assertThrows(IllegalArgumentException.class, + () -> database.getUnitFromExpression(expression)); + assertThrows(IllegalArgumentException.class, + () -> database.evaluateUnitExpression(expression)); + } + /** * A test for the {@link UnitDatabase#evaluateUnitExpression(String)} * function. Simple because the expression parser has its own test. - * + * * @since 2021-09-27 + * @since v0.3.2 */ - @Test - public void testEvaluateExpression() { - final UnitDatabase database = new UnitDatabase(); + @ParameterizedTest + @MethodSource + public void testEvaluateExpressionValid(String expression, + LinearUnitValue expected) { + final var database = new UnitDatabase(); database.addUnit("J", J); database.addUnit("K", K); @@ -191,27 +260,30 @@ class UnitDatabaseTest { database.addPrefix("B", B); database.addPrefix("C", C); - final LinearUnitValue expected = LinearUnitValue.of(J, - UncertainDouble.of(12, Math.sqrt(14.625))); - // note: units are exact, each number has an uncertainty of 1 - final LinearUnitValue actual = database - .evaluateUnitExpression("J + (2 * 3) J + (20 / 4) J"); + final var actual = database.evaluateUnitExpression(expression); assertEquals(expected, actual); - // check that negation works properly - assertEquals(2, - database.evaluateUnitExpression("J - -1 * J").getValueExact()); + final var expectedU = expected.getUnit().times(expected.getValueExact()); + final var actualU = database.getUnitFromExpression(expression); + assertEquals(expectedU, actualU); + } + + @ParameterizedTest + @MethodSource + public void testFormatExpression(String expression, String expected) { + assertEquals(expected, UnitDatabase.formatExpression(expression)); } /** * Test for {@link UnitDatabase#getUnit}, {@link UnitDatabase#getLinearUnit} * and {@link UnitDatabase#getLinearUnitValue}. - * + * * @since 2021-10-07 + * @since v0.3.2 */ @Test public void testGetUnit() { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("m", Metric.METRE); database.addUnit("meter", Metric.METRE); @@ -239,14 +311,15 @@ class UnitDatabaseTest { /** * Confirms that operations that shouldn't function for infinite databases * throw an {@code IllegalStateException}. - * + * * @since 2019-05-03 + * @since v0.3.0 */ // @Test // @Timeout(value = 1, unit = TimeUnit.SECONDS) public void testInfiniteSetExceptions() { // load units - final UnitDatabase infiniteDatabase = new UnitDatabase(); + final var infiniteDatabase = new UnitDatabase(); infiniteDatabase.addUnit("J", J); infiniteDatabase.addUnit("K", K); @@ -255,9 +328,8 @@ class UnitDatabaseTest { infiniteDatabase.addPrefix("B", B); infiniteDatabase.addPrefix("C", C); - final Set<Entry<String, Unit>> entrySet = infiniteDatabase.unitMap() - .entrySet(); - final Set<String> keySet = infiniteDatabase.unitMap().keySet(); + final var entrySet = infiniteDatabase.unitMap().entrySet(); + final var keySet = infiniteDatabase.unitMap().keySet(); assertThrows(IllegalStateException.class, () -> entrySet.toArray()); assertThrows(IllegalStateException.class, () -> keySet.toArray()); } @@ -267,18 +339,20 @@ class UnitDatabaseTest { * * @param num which file to test * @since 2021-10-04 + * @since v0.3.2 */ @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) public void testLoadingInvalidDimensionFile(int num) { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addDimension("LENGTH", Metric.Dimensions.LENGTH); database.addDimension("MASS", Metric.Dimensions.MASS); database.addDimension("TIME", Metric.Dimensions.TIME); - final String filename = String.format("/test-dimensionfile-invalid%d.txt", + final var filename = String.format("/test-dimensionfile-invalid%d.txt", num); - final RuntimeException e = assertThrows(RuntimeException.class, - () -> loadDimensionFile(database, filename)); + final var errs = loadDimensionFile(database, filename); + assertFalse(errs.isEmpty(), "no error from invalid file " + filename); + final var e = errs.get(0).problem(); assertTrue(e instanceof IllegalArgumentException || e instanceof NoSuchElementException); } @@ -288,27 +362,29 @@ class UnitDatabaseTest { * * @param num which file to test * @since 2021-09-27 + * @since v0.3.2 */ @ParameterizedTest @ValueSource(ints = { 1, 2, 3, 4, 5 }) public void testLoadingInvalidUnitFile(int num) { - final UnitDatabase database = new UnitDatabase(); - final String filename = String.format("/test-unitsfile-invalid%d.txt", - num); - final RuntimeException e = assertThrows(RuntimeException.class, - () -> loadUnitsFile(database, filename)); + final var database = new UnitDatabase(); + final var filename = String.format("/test-unitsfile-invalid%d.txt", num); + final var errs = loadUnitsFile(database, filename); + assertFalse(errs.isEmpty(), "no error from invalid file " + filename); + final var e = errs.get(0).problem(); assertTrue(e instanceof IllegalArgumentException || e instanceof NoSuchElementException); } /** * Tests loading a valid dimension-file with some derived dimensions. - * + * * @since 2021-10-04 + * @since v0.3.2 */ @Test public void testLoadingValidDimensions() { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addDimension("LENGTH", Metric.Dimensions.LENGTH); database.addDimension("MASS", Metric.Dimensions.MASS); database.addDimension("TIME", Metric.Dimensions.TIME); @@ -321,27 +397,33 @@ class UnitDatabaseTest { /** * Tests loading a valid unitfile with some prefixes and no units. - * + * * @since 2021-09-22 + * @since v0.3.2 */ @Test public void testLoadingValidPrefixes() { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); loadUnitsFile(database, "/test-unitsfile-valid2.txt"); assertEquals(7, database.getPrefix("A").getMultiplier()); assertEquals(11, database.getPrefix("B").getMultiplier()); assertEquals(13, database.getPrefix("C").getMultiplier()); + + // test invalid prefixes + assertThrows(NoSuchElementException.class, + () -> database.getPrefix("N/A")); } /** * Tests loading a valid unitfile with some units and preloaded prefixes - * + * * @since 2021-09-22 + * @since v0.3.2 */ @Test public void testLoadingValidUnits() { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("U", U); database.addUnit("V", V); @@ -357,21 +439,21 @@ class UnitDatabaseTest { final Unit expected1 = ((LinearUnit) U).withPrefix(A).withPrefix(B) .withPrefix(C); - final Unit actual1 = database.getUnit("test1"); + final var actual1 = database.getUnit("test1"); assertEquals(expected1, actual1); final Unit expected2 = ((LinearUnit) W).withPrefix(B) .times(((LinearUnit) V).withPrefix(C)); - final Unit actual2 = database.getUnit("test2"); + final var actual2 = database.getUnit("test2"); assertEquals(expected2, actual2); final Unit expected3 = ((LinearUnit) U) .times(A.getMultiplier() + C.getMultiplier() - B.getMultiplier()); - final Unit actual3 = database.getUnit("test3"); + final var actual3 = database.getUnit("test3"); assertEquals(expected3, actual3); - final UnitValue expected4 = UnitValue.of(U, 1); - final UnitValue actual4 = database + final var expected4 = UnitValue.of(U, 1); + final var actual4 = database .evaluateUnitExpression("-5 * U + -3 * U + 12 * U - 3 * U") .asUnitValue(); assertEquals(expected4, actual4); @@ -382,21 +464,21 @@ class UnitDatabaseTest { /** * Tests the iterator of the prefixless unit map. These tests are simple, as * the unit map iterator is simple. - * + * * @since 2021-10-07 + * @since v0.3.2 */ @Test public void testPrefixedUnitMapIterator() { - final UnitDatabase database1 = new UnitDatabase(); + final var database1 = new UnitDatabase(); database1.addUnit("U", U); database1.addUnit("V", V); database1.addUnit("W", W); - final Map<String, Unit> map1 = database1.unitMap(); - final Iterator<String> keyIterator1 = map1.keySet().iterator(); - final Iterator<Map.Entry<String, Unit>> entryIterator1 = map1.entrySet() - .iterator(); + final var map1 = database1.unitMap(); + final var keyIterator1 = map1.keySet().iterator(); + final var entryIterator1 = map1.entrySet().iterator(); final Set<String> expectedKeys = Set.of("U", "V", "W"); final Set<String> actualKeys = new HashSet<>(); @@ -418,13 +500,13 @@ class UnitDatabaseTest { /** * Test that prefixes correctly apply to units. - * + * * @since 2019-04-14 * @since v0.2.0 */ @Test public void testPrefixes() { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("U", U); database.addUnit("V", V); @@ -439,29 +521,28 @@ class UnitDatabaseTest { assertEquals(expected, database.getPrefixesFromName("ABCU")); // get the product - final Unit abcuNonlinear = database.getUnit("ABCU"); + final var abcuNonlinear = database.getUnit("ABCU"); assert abcuNonlinear instanceof LinearUnit; - final LinearUnit abcu = (LinearUnit) abcuNonlinear; + final var abcu = (LinearUnit) abcuNonlinear; assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15); } /** * Tests the functionnalites of the prefixless unit map. - * + * * <p> * The map should be an auto-updating view of the units in the database. * </p> - * + * * @since 2019-04-14 * @since v0.2.0 */ @Test public void testPrefixlessUnitMap() { - final UnitDatabase database = new UnitDatabase(); - final Map<String, Unit> prefixlessUnits = database - .unitMapPrefixless(true); + final var database = new UnitDatabase(); + final var prefixlessUnits = database.unitMapPrefixless(true); database.addUnit("U", U); database.addUnit("V", V); @@ -478,13 +559,13 @@ class UnitDatabaseTest { /** * Tests that the database correctly stores and retrieves units, ignoring * prefixes. - * + * * @since 2019-04-14 * @since v0.2.0 */ @Test public void testPrefixlessUnits() { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("U", U); database.addUnit("V", V); @@ -517,7 +598,7 @@ class UnitDatabaseTest { @Test public void testToString() { - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("J", J); database.addUnit("K", J); @@ -538,14 +619,14 @@ class UnitDatabaseTest { /** * Test that unit expressions return the correct value. - * + * * @since 2019-04-14 * @since v0.2.0 */ @Test public void testUnitExpressions() { // load units - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("U", U); database.addUnit("V", V); @@ -560,13 +641,13 @@ class UnitDatabaseTest { // first test - test prefixes and operations final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C) .withPrefix(C); - final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W"); + final var actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W"); assertEquals(expected1, actual1); // second test - test addition and subtraction final Unit expected2 = J.times(58); - final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej"); + final var actual2 = database.getUnitFromExpression("2 fj + 6 ej"); assertEquals(expected2, actual2); @@ -579,14 +660,14 @@ class UnitDatabaseTest { /** * Tests both the unit name iterator and the name-unit entry iterator - * + * * @since 2019-04-14 * @since v0.2.0 */ @Test public void testUnitIterator() { // load units - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("J", J); database.addUnit("K", K); @@ -595,19 +676,17 @@ class UnitDatabaseTest { database.addPrefix("B", B); database.addPrefix("C", C); - final int NUM_UNITS = database.unitMapPrefixless(true).size(); - final int NUM_PREFIXES = database.prefixMap(true).size(); + final var NUM_UNITS = database.unitMapPrefixless(true).size(); + final var NUM_PREFIXES = database.prefixMap(true).size(); - final Iterator<String> nameIterator = database.unitMap().keySet() - .iterator(); - final Iterator<Entry<String, Unit>> entryIterator = database.unitMap() - .entrySet().iterator(); + final var nameIterator = database.unitMap().keySet().iterator(); + final var entryIterator = database.unitMap().entrySet().iterator(); - int expectedLength = 1; - int unitsWithThisLengthSoFar = 0; + var expectedLength = 1; + var unitsWithThisLengthSoFar = 0; // loop 1000 times - for (int i = 0; i < 1000; i++) { + for (var i = 0; i < 1000; i++) { // expected length of next if (unitsWithThisLengthSoFar >= NUM_UNITS * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { @@ -616,9 +695,9 @@ class UnitDatabaseTest { } // test that stuff is valid - final String nextName = nameIterator.next(); - final Unit nextUnit = database.getUnit(nextName); - final Entry<String, Unit> nextEntry = entryIterator.next(); + final var nextName = nameIterator.next(); + final var nextUnit = database.getUnit(nextName); + final var nextEntry = entryIterator.next(); assertEquals(expectedLength, nextName.length()); assertEquals(nextName, nextEntry.getKey()); @@ -628,13 +707,13 @@ class UnitDatabaseTest { } // test toString for consistency - final String entryIteratorString = entryIterator.toString(); - for (int i = 0; i < 3; i++) { + final var entryIteratorString = entryIterator.toString(); + for (var i = 0; i < 3; i++) { assertEquals(entryIteratorString, entryIterator.toString()); } - final String nameIteratorString = nameIterator.toString(); - for (int i = 0; i < 3; i++) { + final var nameIteratorString = nameIterator.toString(); + for (var i = 0; i < 3; i++) { assertEquals(nameIteratorString, nameIterator.toString()); } } @@ -646,14 +725,14 @@ class UnitDatabaseTest { * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this * case, "AB-C-U" is the correct choice. * </p> - * + * * @since 2019-04-14 * @since v0.2.0 */ @Test public void testUnitPrefixCombinations() { // load units - final UnitDatabase database = new UnitDatabase(); + final var database = new UnitDatabase(); database.addUnit("J", J); @@ -665,7 +744,7 @@ class UnitDatabaseTest { // test 1 - AB-C-J vs A-BC-J vs A-B-C-J final Unit expected1 = J.withPrefix(AB).withPrefix(C); - final Unit actual1 = database.getUnit("ABCJ"); + final var actual1 = database.getUnit("ABCJ"); assertEquals(expected1, actual1); @@ -674,8 +753,61 @@ class UnitDatabaseTest { database.addPrefix("ABC", UnitPrefix.valueOf(17)); final Unit expected2 = J.times(17); - final Unit actual2 = database.getUnit("ABCJ"); + final var actual2 = database.getUnit("ABCJ"); assertEquals(expected2, actual2); } + + /** + * Tests the ability to create, read, and delete unit sets. + * + * @since 2025-04-30 + * @since v1.0.0 + */ + @Test + void testUnitSetsInvalid() { + final List<LinearUnit> units = List.of(Metric.SECOND, + BritishImperial.Length.INCH); + + final var database = new UnitDatabase(); + + assertThrows(IllegalArgumentException.class, + () -> database.addUnitSet("badtest", units)); + assertThrows(NoSuchElementException.class, + () -> database.getUnitSet("badtest")); + + database.addUnit("ft", BritishImperial.Length.FOOT); + database.addUnit("s", Metric.SECOND); + database.addUnit("dC", Metric.CELSIUS); + database.addUnit("K", Metric.KELVIN); + + assertThrows(IllegalArgumentException.class, + () -> database.getUnitSetFromExpression("K; dC"), + "getUnitSetFromExpression allowed nonlinear unit."); + assertThrows(IllegalArgumentException.class, + () -> database.getUnitSetFromExpression("ft; s"), + "getUnitSetFromExpression allowed units of different dimension."); + } + + /** + * Tests the ability to create, read, and delete unit sets. + * + * @since 2025-04-30 + * @since v1.0.0 + */ + @Test + void testUnitSetsValid() { + final List<LinearUnit> units = List.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH); + + final var database = new UnitDatabase(); + + database.addUnitSet("ftintest", units); + assertEquals(units, database.getUnitSet("ftintest")); + + database.addUnit("ft", BritishImperial.Length.FOOT); + database.addUnit("in", BritishImperial.Length.INCH); + assertEquals(units, database.getUnitSetFromExpression("ft; in")); + } + } diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java index c93043b..fb21723 100644 --- a/src/test/java/sevenUnits/unit/UnitTest.java +++ b/src/test/java/sevenUnits/unit/UnitTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.math.RoundingMode; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @@ -29,12 +28,11 @@ import org.junit.jupiter.api.Test; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.UncertainDouble; /** * Testing the various Unit classes. This is NOT part of this program's public * API. - * + * * @author Adrien Hopkins * @since 2018-12-22 * @since v0.1.0 @@ -45,21 +43,25 @@ class UnitTest { @Test public void testAdditionAndSubtraction() { - final LinearUnit inch = Metric.METRE.times(0.0254) + final var inch = Metric.METRE.times(0.0254) .withName(NameSymbol.of("inch", "in")); - final LinearUnit foot = Metric.METRE.times(0.3048) + final var foot = Metric.METRE.times(0.3048) .withName(NameSymbol.of("foot", "ft")); - assertEquals(inch.plus(foot), Metric.METRE.times(0.3302)); - assertEquals(foot.minus(inch), Metric.METRE.times(0.2794)); + assertTrue( + inch.plus(foot).equalsApproximately(Metric.METRE.times(0.3302)), + String.format("Expected: %s, Actual: %s", inch.plus(foot), + Metric.METRE.times(0.3302))); + assertTrue( + foot.minus(inch).equalsApproximately(Metric.METRE.times(0.2794)), + String.format("Expected: %s, Actual: %s", foot.minus(inch), + Metric.METRE.times(0.2794))); // test with LinearUnitValue - final LinearUnitValue value1 = LinearUnitValue.getExact(Metric.METRE, 15); - final LinearUnitValue value2 = LinearUnitValue.getExact(foot, 120); - final LinearUnitValue value3 = LinearUnitValue.getExact(Metric.METRE, - 0.5); - final LinearUnitValue value4 = LinearUnitValue.getExact(Metric.KILOGRAM, - 60); + final var value1 = LinearUnitValue.getExact(Metric.METRE, 15); + final var value2 = LinearUnitValue.getExact(foot, 120); + final var value3 = LinearUnitValue.getExact(Metric.METRE, 0.5); + final var value4 = LinearUnitValue.getExact(Metric.KILOGRAM, 60); // make sure addition is done correctly assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001); @@ -81,24 +83,24 @@ class UnitTest { @Test public void testConversion() { - final LinearUnit metre = Metric.METRE; + final var metre = Metric.METRE; final Unit inch = metre.times(0.0254); - final UnitValue value = UnitValue.of(inch, 75); + final var value = UnitValue.of(inch, 75); assertEquals(1.9, inch.convertTo(metre, 75), 0.01); assertEquals(1.9, value.convertTo(metre).getValue(), 0.01); // try random stuff - for (int i = 0; i < 1000; i++) { + for (var i = 0; i < 1000; i++) { // initiate random values - final double conversionFactor = UnitTest.rng.nextDouble() * 1000000; - final double testValue = UnitTest.rng.nextDouble() * 1000000; - final double expected = testValue * conversionFactor; + final var conversionFactor = UnitTest.rng.nextDouble() * 1000000; + final var testValue = UnitTest.rng.nextDouble() * 1000000; + final var expected = testValue * conversionFactor; // test final Unit unit = Metric.METRE.times(conversionFactor); - final double actual = unit.convertToBase(testValue); + final var actual = unit.convertToBase(testValue); assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON); @@ -107,7 +109,7 @@ class UnitTest { @Test public void testEquals() { - final LinearUnit metre = Metric.METRE; + final var metre = Metric.METRE; final Unit meter = Metric.BaseUnits.METRE.asLinearUnit(); assertEquals(metre, meter); @@ -127,19 +129,19 @@ class UnitTest { @Test public void testMultiplicationAndDivision() { // test unit-times-unit multiplication - final LinearUnit generatedJoule = Metric.KILOGRAM + final var generatedJoule = Metric.KILOGRAM .times(Metric.METRE.toExponent(2)) .dividedBy(Metric.SECOND.toExponent(2)); - final LinearUnit actualJoule = Metric.JOULE; + final var actualJoule = Metric.JOULE; assertEquals(generatedJoule, actualJoule); // test multiplication by conversion factors - final LinearUnit kilometre = Metric.METRE.times(1000); - final LinearUnit hour = Metric.SECOND.times(3600); - final LinearUnit generatedKPH = kilometre.dividedBy(hour); + final var kilometre = Metric.METRE.times(1000); + final var hour = Metric.SECOND.times(3600); + final var generatedKPH = kilometre.dividedBy(hour); - final LinearUnit actualKPH = Metric.METRE.dividedBy(Metric.SECOND) + final var actualKPH = Metric.METRE.dividedBy(Metric.SECOND) .dividedBy(3.6); assertEquals(generatedKPH, actualKPH); @@ -147,82 +149,9 @@ class UnitTest { @Test public void testPrefixes() { - final LinearUnit generatedKilometre = Metric.METRE - .withPrefix(Metric.KILO); - final LinearUnit actualKilometre = Metric.METRE.times(1000); + final var generatedKilometre = Metric.METRE.withPrefix(Metric.KILO); + final var actualKilometre = Metric.METRE.times(1000); assertEquals(generatedKilometre, actualKilometre); } - - /** - * Tests converting an uncertain LinearUnitValue to a string. - * - * @since 2021-11-04 - */ - @Test - public void testValueToString1() { - final LinearUnitValue value = LinearUnitValue.of(Metric.METRE, - UncertainDouble.of(10, 0.24)); - - assertEquals("(10.0 � 0.2) m", value.toString()); - assertEquals("(10.0 � 0.2) m", - value.toString(true, RoundingMode.HALF_EVEN)); - assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN)); - } - - /** - * Tests converting a certain LinearUnitValue to a string. - * - * @since 2021-11-04 - */ - @Test - public void testValueToString2() { - final LinearUnitValue value = LinearUnitValue.of(Metric.METRE, - UncertainDouble.of(10, 0)); - - assertEquals("10.0 m", value.toString()); - assertEquals("(10.0 � 0.0) m", - value.toString(true, RoundingMode.HALF_EVEN)); - assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN)); - } - - /** - * Tests converting an unnamed LinearUnitValue to a string. - * - * @since 2021-11-04 - */ - @Test - public void testValueToString3() { - final LinearUnitValue value = LinearUnitValue.of( - Metric.METRE.withName(NameSymbol.EMPTY), - UncertainDouble.of(10, 0.24)); - - assertEquals("10.0 unnamed unit (= 10.0 m)", - value.toString(false, RoundingMode.HALF_EVEN)); - } - - /** - * Tests converting a named UnitValue to a string. - * - * @since 2021-11-04 - */ - @Test - public void testValueToString4() { - final UnitValue value = UnitValue.of(BritishImperial.FAHRENHEIT, 80); - - assertEquals("80.0 \u00B0F", value.toString()); - } - - /** - * Tests converting an unnamed UnitValue to a string. - * - * @since 2021-11-04 - */ - @Test - public void testValueToString5() { - final UnitValue value = UnitValue - .of(USCustomary.FAHRENHEIT.withName(NameSymbol.EMPTY), 50); - - assertEquals("50.0 unnamed unit (= 283.15 K)", value.toString()); - } } diff --git a/src/test/java/sevenUnits/unit/UnitValueTest.java b/src/test/java/sevenUnits/unit/UnitValueTest.java new file mode 100644 index 0000000..3679703 --- /dev/null +++ b/src/test/java/sevenUnits/unit/UnitValueTest.java @@ -0,0 +1,240 @@ +/** + * Copyright (C) 2021, 2025 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnits.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.RoundingMode; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.UncertainDouble; + +/** + * Tests for the UnitValue and LinearUnitValue classes + * + * @since v1.0.0 + */ +public final class UnitValueTest { + private static Stream<Arguments> testConvertToMultiple() { + return Stream.of( + Arguments.of( + LinearUnitValue.getExact(BritishImperial.Length.INCH, 64), + List.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH), + List.of(5.0, 4.0)), + Arguments.of(LinearUnitValue.getExact(Metric.SECOND, 44102.5), + List.of(Metric.HOUR, Metric.MINUTE, Metric.SECOND), + List.of(12.0, 15.0, 2.5))); + } + + @ParameterizedTest + @MethodSource + public void testConvertToMultiple(LinearUnitValue valFrom, + List<LinearUnit> others, List<Double> expectedValues) { + final var actual = valFrom.convertToMultiple(others); + assertEquals(others.size(), expectedValues.size(), String.format( + "%s converted to %s had the wrong number of resulting units (expected: %d, actual: %d).", + valFrom, others, others.size(), actual.size())); + + for (var i = 0; i < others.size(); i++) { + final var expectedValue = LinearUnitValue.getExact(others.get(i), + expectedValues.get(i)); + assertTrue(expectedValue.equivalent(actual.get(i)), + String.format("Value %d: expected %s, actual %s.", i, + expectedValue, actual.get(i))); + } + } + + @Test + public void testConvertToMultipleErrors() { + assertThrows(IllegalArgumentException.class, + () -> LinearUnitValue.ONE.convertToMultiple(List.of())); + assertThrows(IllegalArgumentException.class, + () -> LinearUnitValue.ONE.convertToMultiple(List.of(Metric.METRE))); + } + + // I am intentionally testing whether or not equals gracefully returns false + // when it gets an argument of the wrong type + @SuppressWarnings("unlikely-arg-type") + @Test + public void testEqualsHashCode() { + final var v1 = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(1.0, 0.01)); + final var v2 = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(1.0, 0.01)); + final var v3 = LinearUnitValue.of(Metric.MILLIMETRE, + UncertainDouble.of(1000.0, 10.0)); + final var v4 = LinearUnitValue.of(Metric.MILLIMETRE, + UncertainDouble.of(1000.0, 20.0)); + + assertTrue(Objects.equals(v1, v2), + "Two identical values are not equal()"); + assertTrue(Objects.equals(v1.hashCode(), v2.hashCode()), + "Two identical values have different hash codes."); + assertTrue(Objects.equals(v1, v3), + "Two values with the same value but different units are not equal()"); + assertTrue(Objects.equals(v1.hashCode(), v3.hashCode()), + "Two values with the same value but different units have different hash codes."); + assertFalse(Objects.equals(v1, v4), + "Two values with the same value but different uncertainties are equal()"); + assertFalse(Objects.equals(v1, "Hello")); + assertFalse(v1.equals("Hello", true)); + } + + @Test + public void testEqualsNoFP() { + final var v1 = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(1.0, 0.01)); + final var v2 = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(1.0, 0.01)); + final var v3 = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(1.0 + 5e-16, 0.01)); + assertTrue(v1.equals(v2, false)); + assertFalse(v1.equals(v3, false)); + assertTrue(v1.equals(v3, true)); + } + + @Test + public void testOperations() { + final var v1 = LinearUnitValue.getExact(Metric.METRE, 2.0); + final var v2 = LinearUnitValue.getExact(Metric.METRE, 4.0); + + assertEquals(v2, v1.times(2)); + assertEquals(v1, v2.dividedBy(2)); + assertEquals(LinearUnitValue.getExact(Metric.SQUARE_METRE, 8.0), + v1.times(v2)); + assertEquals(LinearUnitValue.getExact(Metric.ONE, 0.5), v1.dividedBy(v2)); + assertEquals(LinearUnitValue.getExact(Metric.SQUARE_METRE, 4.0), + v1.toExponent(2)); + assertEquals(LinearUnitValue.getExact(Metric.SQUARE_METRE, 4.0), + v1.toExponentRounded(2.0)); + } + + @Test + public void testValueEquivalent() { + final var v1 = LinearUnitValue.of(BritishImperial.Length.INCH, + UncertainDouble.of(1.0, 0.0)); + final var v2 = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(0.0255, 0.0)); + assertFalse(v1.equivalent(v2)); + + final var v3 = LinearUnitValue.of(BritishImperial.Length.INCH, + UncertainDouble.of(1.0, 1.0)); + final var v4 = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(0.0255, 0.025)); + assertTrue(v3.equivalent(v4)); + } + + @Test + public void testValueEquivalentErrors() { + final var v1 = LinearUnitValue.of(BritishImperial.Length.INCH, + UncertainDouble.of(1.0, 0.0)); + final var v2 = LinearUnitValue.of(BritishImperial.CALORIE, + UncertainDouble.of(1.0, 0.0)); + assertFalse(v1.equivalent(v2)); + + assertFalse(v1.equivalent(null)); + } + + /** + * Tests converting an uncertain LinearUnitValue to a string. + * + * @since 2021-11-04 + * @since v0.3.2 + */ + @Test + public void testValueToString1() { + final var value = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(10, 0.24)); + + assertEquals("(10.0 ± 0.2) m", value.toString()); + assertEquals("(10.0 ± 0.2) m", + value.toString(true, RoundingMode.HALF_EVEN)); + assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN)); + } + + /** + * Tests converting a certain LinearUnitValue to a string. + * + * @since 2021-11-04 + * @since v0.3.2 + */ + @Test + public void testValueToString2() { + final var value = LinearUnitValue.of(Metric.METRE, + UncertainDouble.of(10, 0)); + + assertEquals("10.0 m", value.toString()); + assertEquals("(10.0 ± 0.0) m", + value.toString(true, RoundingMode.HALF_EVEN)); + assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN)); + } + + /** + * Tests converting an unnamed LinearUnitValue to a string. + * + * @since 2021-11-04 + * @since v0.3.2 + */ + @Test + public void testValueToString3() { + final var value = LinearUnitValue.of( + Metric.METRE.withName(NameSymbol.EMPTY), + UncertainDouble.of(10, 0.24)); + + assertEquals("10.0 unnamed unit (= 10.0 m)", + value.toString(false, RoundingMode.HALF_EVEN)); + } + + /** + * Tests converting a named UnitValue to a string. + * + * @since 2021-11-04 + * @since v0.3.2 + */ + @Test + public void testValueToString4() { + final var value = UnitValue.of(BritishImperial.FAHRENHEIT, 80); + + assertEquals("80.0 \u00B0F", value.toString()); + } + + /** + * Tests converting an unnamed UnitValue to a string. + * + * @since 2021-11-04 + * @since v0.3.2 + */ + @Test + public void testValueToString5() { + final var value = UnitValue + .of(USCustomary.FAHRENHEIT.withName(NameSymbol.EMPTY), 50); + + assertEquals("50.0 unnamed unit (= 283.15 K)", value.toString()); + } +} diff --git a/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java b/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java index 868385b..8711847 100644 --- a/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java +++ b/src/test/java/sevenUnits/utils/ConditionalExistenceCollectionsTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -38,31 +37,34 @@ import sevenUnits.utils.ConditionalExistenceCollections.ConditionalExistenceIter * normal operations on conditional existence collections and ensures that * elements that do not pass the existence condition are not included in the * results. - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ class ConditionalExistenceCollectionsTest { /** * The returned iterator ignores elements that don't start with "a". - * + * * @return test iterator * @since 2019-10-17 + * @since v0.3.0 */ ConditionalExistenceIterator<String> getTestIterator() { final List<String> items = Arrays.asList("aa", "ab", "ba"); - final Iterator<String> it = items.iterator(); - final ConditionalExistenceIterator<String> cit = (ConditionalExistenceIterator<String>) ConditionalExistenceCollections + final var it = items.iterator(); + final var cit = (ConditionalExistenceIterator<String>) ConditionalExistenceCollections .conditionalExistenceIterator(it, s -> s.startsWith("a")); return cit; } /** * The returned map ignores mappings where the value is zero. - * + * * @return map to be used for test data * @since 2019-10-16 + * @since v0.3.0 */ Map<String, Integer> getTestMap() { final Map<String, Integer> map = new HashMap<>(); @@ -76,60 +78,49 @@ class ConditionalExistenceCollectionsTest { return conditionalMap; } - /** - * Test method for the ConditionalExistenceMap's containsKey method. - */ + /** Test method for the ConditionalExistenceMap's containsKey method. */ @Test void testContainsKeyObject() { - final Map<String, Integer> map = this.getTestMap(); + final var map = this.getTestMap(); assertTrue(map.containsKey("one")); assertTrue(map.containsKey("ten")); assertFalse(map.containsKey("five")); assertFalse(map.containsKey("zero")); } - /** - * Test method for the ConditionalExistenceMap's containsValue method. - */ + /** Test method for the ConditionalExistenceMap's containsValue method. */ @Test void testContainsValueObject() { - final Map<String, Integer> map = this.getTestMap(); + final var map = this.getTestMap(); assertTrue(map.containsValue(1)); assertTrue(map.containsValue(10)); assertFalse(map.containsValue(5)); assertFalse(map.containsValue(0)); } - /** - * Test method for the ConditionalExistenceMap's entrySet method. - */ + /** Test method for the ConditionalExistenceMap's entrySet method. */ @Test void testEntrySet() { - final Map<String, Integer> map = this.getTestMap(); + final var map = this.getTestMap(); for (final Entry<String, Integer> e : map.entrySet()) { assertTrue(e.getValue() != 0); } } - /** - * Test method for the ConditionalExistenceMap's get method. - */ + /** Test method for the ConditionalExistenceMap's get method. */ @Test void testGetObject() { - final Map<String, Integer> map = this.getTestMap(); + final var map = this.getTestMap(); assertEquals(1, map.get("one")); assertEquals(10, map.get("ten")); assertEquals(null, map.get("five")); assertEquals(null, map.get("zero")); } - /** - * Test method for the ConditionalExistenceCollection's iterator. - */ + /** Test method for the ConditionalExistenceCollection's iterator. */ @Test void testIterator() { - final ConditionalExistenceIterator<String> testIterator = this - .getTestIterator(); + final var testIterator = this.getTestIterator(); assertTrue(testIterator.hasNext); assertTrue(testIterator.hasNext()); @@ -147,22 +138,18 @@ class ConditionalExistenceCollectionsTest { assertThrows(NoSuchElementException.class, testIterator::next); } - /** - * Test method for the ConditionalExistenceMap's keySet operation. - */ + /** Test method for the ConditionalExistenceMap's keySet operation. */ @Test void testKeySet() { - final Map<String, Integer> map = this.getTestMap(); - assertFalse(map.keySet().contains("zero")); + final var map = this.getTestMap(); + assertFalse(map.containsKey("zero")); } - /** - * Test method for the ConditionalExistenceMap's values operation. - */ + /** Test method for the ConditionalExistenceMap's values operation. */ @Test void testValues() { - final Map<String, Integer> map = this.getTestMap(); - assertFalse(map.values().contains(0)); + final var map = this.getTestMap(); + assertFalse(map.containsValue(0)); } } diff --git a/src/test/java/sevenUnits/utils/ExpressionParserTest.java b/src/test/java/sevenUnits/utils/ExpressionParserTest.java index 3a95285..2e0b4b0 100644 --- a/src/test/java/sevenUnits/utils/ExpressionParserTest.java +++ b/src/test/java/sevenUnits/utils/ExpressionParserTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,6 +17,7 @@ package sevenUnits.utils; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.List; import java.util.stream.IntStream; @@ -26,11 +27,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -// TODO add tests for expression-to-RPN and RPN-to-result /** * A test for the {@code ExpressionParser} class. This is NOT part of this * program's public API. - * + * * @author Adrien Hopkins * @since 2019-03-22 * @since v0.2.0 @@ -39,13 +39,13 @@ class ExpressionParserTest { private static final ExpressionParser<Integer> numberParser = new ExpressionParser.Builder<>( Integer::parseInt).addBinaryOperator("+", (o1, o2) -> o1 + o2, 0) .addBinaryOperator("-", (o1, o2) -> o1 - o2, 0) - .addBinaryOperator("*", (o1, o2) -> o1 * o2, 1) - .addBinaryOperator("/", (o1, o2) -> o1 / o2, 1) - .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build(); + .addUnaryOperator("neg", o1 -> -o1, 1) + .addBinaryOperator("*", (o1, o2) -> o1 * o2, 2) + .addBinaryOperator("/", (o1, o2) -> o1 / o2, 2) + .addUnaryOperator("recip", o1 -> 1 / o1, 3) + .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 4).build(); - /** - * The expressions used in the expression parsing tests - */ + /** The expressions used in the expression parsing tests */ private static final List<String> TEST_EXPRESSIONS = List.of( // test parsing of expressions "1 + 2 ^ 5 * 3", "(1 + 2) ^ 5 * 3", @@ -54,21 +54,81 @@ class ExpressionParserTest { // ensure it normally goes from left to right "1 + 2 + 3 + 4", "12 - 4 - 3", "12 - (4 - 3)", "1 / 2 + 3"); - /** - * The expected results for evaluating these expressions - */ + /** The expected results for evaluating these expressions */ private static final int[] RESULTS = { 97, 729, 133, 10, 5, 11, 3 }; + private static final Stream<Arguments> testConvertExpressionToRPN() { + return Stream.of(Arguments.of("1 + 2 ^ 5 * 3", "1 2 5 ^ 3 * +"), + Arguments.of("(1 + 2) ^ 5 * 3", "1 2 + 5 ^ 3 *"), + Arguments.of("12 * 5 + (3 ^ (2 * 3) - 72) / (3 + 3 * 2)", + "12 5 * 3 2 3 * ^ 72 - 3 3 2 * + / +"), + Arguments.of("1 + 2 + 3 + 4", "1 2 + 3 + 4 +"), + Arguments.of("12 - 4 - 3", "12 4 - 3 -"), + Arguments.of("12 - (4 - 3)", "12 4 3 - -"), + Arguments.of("1 / 2 + 3", "1 2 / 3 +"), Arguments.of("12", "12"), + Arguments.of("2 * 3 + 4", "2 3 * 4 +"), + Arguments.of("(2 * 3) + 4", "2 3 * 4 +"), + Arguments.of("2 * 3 - 4", "2 3 * 4 -"), + Arguments.of("(2 * 3) - 4", "2 3 * 4 -"), + Arguments.of("2 * (3 + 4)", "2 3 4 + *"), + Arguments.of("2 * (3 - 4)", "2 3 4 - *"), + Arguments.of("neg 2", "2 neg"), + Arguments.of("1 + neg 2", "1 2 neg +")); + } + + private static final Stream<String> testInvalidExpression() { + return Stream.of("+", "1 +", "1 + * 2", "1 (+ 1)", "neg"); + } + + private static final Stream<String> testInvalidRPN() { + return Stream.of("+", "1 +", "1 + * 2", "1 * 2", "1 2", "neg"); + } + /** * @return A stream of objects, where each one is an expression and the * expected result * @since 2021-09-27 + * @since v0.3.2 */ private static final Stream<Arguments> testParseExpressionData() { return IntStream.range(0, TEST_EXPRESSIONS.size()) .mapToObj(i -> Arguments.of(TEST_EXPRESSIONS.get(i), RESULTS[i])); } + private static final Stream<Arguments> testParseRPN() { + return Stream.of(Arguments.of("1 2 5 ^ 3 * +", 97), + Arguments.of("1 2 + 5 ^ 3 *", 729), + Arguments.of("12 5 * 3 2 3 * ^ 72 - 3 3 2 * + / +", 133), + Arguments.of("1 2 + 3 + 4 +", 10), Arguments.of("12 4 - 3 -", 5), + Arguments.of("12 4 3 - -", 11), Arguments.of("1 2 / 3 +", 3), + Arguments.of("12", 12), Arguments.of("2 3 * 4 +", 10), + Arguments.of("2 3 * 4 -", 2), Arguments.of("2 3 4 + *", 14), + Arguments.of("2 3 4 - *", -2), Arguments.of("2 neg", -2), + Arguments.of("1 2 neg +", -1)); + } + + @ParameterizedTest + @MethodSource + public void testConvertExpressionToRPN(String expression, + String expectedRPN) { + assertEquals(expectedRPN, + numberParser.convertExpressionToReversePolish(expression)); + } + + @ParameterizedTest + @MethodSource + public void testInvalidExpression(String expression) { + assertThrows(RuntimeException.class, + () -> numberParser.convertExpressionToReversePolish(expression)); + } + + @ParameterizedTest + @MethodSource + public void testInvalidRPN(String expressionRPN) { + assertThrows(RuntimeException.class, + () -> numberParser.parseReversePolishExpression(expressionRPN)); + } + /** * Test method for * {@link sevenUnits.utils.ExpressionParser#parseExpression(java.lang.String)}. @@ -78,4 +138,11 @@ class ExpressionParserTest { public void testParseExpression(String expression, int value) { assertEquals(value, numberParser.parseExpression(expression)); } + + @ParameterizedTest + @MethodSource + public void testParseRPN(String expressionRPN, int value) { + assertEquals(value, + numberParser.parseReversePolishExpression(expressionRPN)); + } } diff --git a/src/test/java/sevenUnits/utils/NameSymbolTest.java b/src/test/java/sevenUnits/utils/NameSymbolTest.java new file mode 100644 index 0000000..f8843e0 --- /dev/null +++ b/src/test/java/sevenUnits/utils/NameSymbolTest.java @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2025 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnits.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests for the {@link NameSymbol} class. + * + * @since v1.0.0 + */ +class NameSymbolTest { + private static Stream<Arguments> testEqualsHashCode() { + return Stream.of( + Arguments.of(NameSymbol.ofName("test"), NameSymbol.ofName("test"), + true), + Arguments.of(NameSymbol.ofName("a"), NameSymbol.ofName("b"), false), + Arguments.of(NameSymbol.ofSymbol("test"), + NameSymbol.ofSymbol("test"), true), + Arguments.of(NameSymbol.ofSymbol("a"), NameSymbol.ofSymbol("b"), + false), + Arguments.of(NameSymbol.ofName("test"), NameSymbol.ofSymbol("test"), + false), + Arguments.of(NameSymbol.of("main", "s"), NameSymbol.of("main", "s"), + true), + Arguments.of(NameSymbol.of("main", "s"), + NameSymbol.of("main", "s", "m"), false), + Arguments.of(new NameSymbol(null, null, new HashSet<>()), + new NameSymbol(null, null, new HashSet<>()), true), + Arguments.of( + new NameSymbol(Optional.of("main"), Optional.of("s"), + new HashSet<>()), + new NameSymbol(null, null, new HashSet<>()), false), + Arguments.of(new NameSymbol(null, null, new HashSet<>()), + new NameSymbol(Optional.of("main"), Optional.of("s"), + new HashSet<>()), + false), + Arguments.of( + new NameSymbol(Optional.of("main"), null, new HashSet<>()), + new NameSymbol(Optional.of("main"), Optional.of("s"), + new HashSet<>()), + false)); + } + + @Test + public void testCreate() { + final Set<String> names = Set.of("a", "b", "c"); + final var ns = NameSymbol.ofNullable(null, null, names); + assertTrue(ns.getPrimaryName().isPresent(), + "NameSymbol created without primary name."); + assertTrue(names.contains(ns.getPrimaryName().orElseThrow()), + String.format("Primary name (%s) was not obtained from names set.", + ns.getPrimaryName())); + assertFalse( + ns.getOtherNames().contains(ns.getPrimaryName().orElseThrow()), + String.format("Primary name (%s) was included in other names set.", + ns.getPrimaryName())); + assertEquals(Set.of("a", "b", "c"), names, + "names input was changed by ofNullable()"); + } + + /** + * Tests that two NameSymbols are or are not equal. If they are equal, also + * ensures they have the same hash code. + * + * @param a first NameSymbol to test + * @param b second NameSymbol to test + * @param equal true iff a should be equal to be, otherwise false + */ + @ParameterizedTest + @MethodSource + public void testEqualsHashCode(NameSymbol a, NameSymbol b, boolean equal) { + if (equal) { + assertTrue(Objects.equals(a, b)); + assertEquals(a.hashCode(), b.hashCode(), + "Equal NameSymbol instances have different hash codes."); + } else { + assertFalse(Objects.equals(a, b)); + } + } +} diff --git a/src/test/java/sevenUnits/utils/ObjectProductTest.java b/src/test/java/sevenUnits/utils/ObjectProductTest.java index 8c6b353..7c5df88 100644 --- a/src/test/java/sevenUnits/utils/ObjectProductTest.java +++ b/src/test/java/sevenUnits/utils/ObjectProductTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2021, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -34,7 +34,7 @@ import sevenUnits.unit.Metric; /** * Tests for {@link ObjectProduct} using BaseDimension as a test object. This is * NOT part of this program's public API. - * + * * @author Adrien Hopkins * @since 2018-12-12 * @since v0.1.0 @@ -42,7 +42,7 @@ import sevenUnits.unit.Metric; class ObjectProductTest { /** * Tests {@link UnitDimension#equals} - * + * * @since 2018-12-12 * @since v0.1.0 */ @@ -54,7 +54,7 @@ class ObjectProductTest { /** * Tests {@code UnitDimension}'s exponentiation - * + * * @since 2019-01-15 * @since v0.1.0 */ @@ -66,7 +66,7 @@ class ObjectProductTest { /** * Tests {@code UnitDimension}'s multiplication and division. - * + * * @since 2018-12-12 * @since v0.1.0 */ diff --git a/src/test/java/sevenUnits/utils/SemanticVersionTest.java b/src/test/java/sevenUnits/utils/SemanticVersionTest.java index 1e59ae3..5b74812 100644 --- a/src/test/java/sevenUnits/utils/SemanticVersionTest.java +++ b/src/test/java/sevenUnits/utils/SemanticVersionTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -35,12 +35,14 @@ import org.junit.jupiter.api.Test; * Tests for {@link SemanticVersionNumber} * * @since 2022-02-19 + * @since v0.4.0 */ public final class SemanticVersionTest { /** * Test for {@link SemanticVersionNumber#compatible} - * + * * @since 2022-02-20 + * @since v0.4.0 */ @Test public void testCompatibility() { @@ -64,38 +66,37 @@ public final class SemanticVersionTest { /** * Tests {@link SemanticVersionNumber#toString} for complex version numbers - * + * * @since 2022-02-19 + * @since v0.4.0 */ @Test public void testComplexToString() { - final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) - .build(); + final var v1 = builder(1, 2, 3).preRelease(1, 2, 3).build(); assertEquals("1.2.3-1.2.3", v1.toString()); - final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + final var v2 = builder(4, 5, 6).preRelease("abc", 123) .buildMetadata("2022-02-19").build(); assertEquals("4.5.6-abc.123+2022-02-19", v2.toString()); - final SemanticVersionNumber v3 = builder(1, 0, 0) - .preRelease("x-y-z", "--").build(); + final var v3 = builder(1, 0, 0).preRelease("x-y-z", "--").build(); assertEquals("1.0.0-x-y-z.--", v3.toString()); } /** * Tests that complex version can be created and their parts read - * + * * @since 2022-02-19 + * @since v0.4.0 */ @Test public void testComplexVersions() { - final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) - .build(); + final var v1 = builder(1, 2, 3).preRelease(1, 2, 3).build(); assertEquals(1, v1.majorVersion()); assertEquals(2, v1.minorVersion()); assertEquals(3, v1.patchVersion()); assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers()); assertEquals(List.of(), v1.buildMetadata()); - final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + final var v2 = builder(4, 5, 6).preRelease("abc", 123) .buildMetadata("2022-02-19").build(); assertEquals(4, v2.majorVersion()); assertEquals(5, v2.minorVersion()); @@ -103,8 +104,7 @@ public final class SemanticVersionTest { assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers()); assertEquals(List.of("2022-02-19"), v2.buildMetadata()); - final SemanticVersionNumber v3 = builder(1, 0, 0) - .preRelease("x-y-z", "--").build(); + final var v3 = builder(1, 0, 0).preRelease("x-y-z", "--").build(); assertEquals(1, v3.majorVersion()); assertEquals(0, v3.minorVersion()); assertEquals(0, v3.patchVersion()); @@ -114,8 +114,9 @@ public final class SemanticVersionTest { /** * Test that semantic version strings can be parsed correctly - * + * * @since 2022-02-19 + * @since v0.4.0 * @see SemanticVersionNumber#fromString * @see SemanticVersionNumber#isValidVersionString */ @@ -153,9 +154,7 @@ public final class SemanticVersionTest { "Could not parse 1.2.3-abc.56.def+2022abc99"); } - /** - * Ensures it is impossible to create invalid version numbers - */ + /** Ensures it is impossible to create invalid version numbers */ @Test public void testInvalidVersionNumbers() { // stableVersion() @@ -199,7 +198,7 @@ public final class SemanticVersionTest { assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7), "Negative major version number tolerated by builder"); - final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3); + final var testBuilder = builder(1, 2, 3); // note: builder.buildMetadata(null) doesn't even compile lol // builder.buildMetadata assertThrows(NullPointerException.class, @@ -265,8 +264,9 @@ public final class SemanticVersionTest { /** * Test for {@link SemanticVersionNumber#isStable} - * + * * @since 2022-02-19 + * @since v0.4.0 */ @Test public void testIsStable() { @@ -288,29 +288,27 @@ public final class SemanticVersionTest { * {@link SemanticVersionNumber#compareTo} according to official rules. Tests * all of the versions compared in section 11 of the SemVer 2.0.0 document * and some more. - * + * * @since 2022-02-19 + * @since v0.4.0 */ @Test public void testOrder() { - final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha") - .build(); // 1.0.0-alpha - final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1 - final SemanticVersionNumber v100ab = builder(1, 0, 0) - .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta - final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta") - .build(); // 1.0.0-alpha - final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2 - final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11 - final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1 - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); - final SemanticVersionNumber v100plus = builder(1, 0, 0) + final var v100a = builder(1, 0, 0).preRelease("alpha").build(); // 1.0.0-alpha + final var v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1 + final var v100ab = builder(1, 0, 0).preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta + final var v100b = builder(1, 0, 0).preRelease("beta").build(); // 1.0.0-alpha + final var v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2 + final var v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11 + final var v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1 + final var v100 = stableVersion(1, 0, 0); + final var v100plus = builder(1, 0, 0) .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah - final SemanticVersionNumber v200 = stableVersion(2, 0, 0); - final SemanticVersionNumber v201 = stableVersion(2, 0, 1); - final SemanticVersionNumber v210 = stableVersion(2, 1, 0); - final SemanticVersionNumber v211 = stableVersion(2, 1, 1); - final SemanticVersionNumber v300 = stableVersion(3, 0, 0); + final var v200 = stableVersion(2, 0, 0); + final var v201 = stableVersion(2, 0, 1); + final var v210 = stableVersion(2, 1, 0); + final var v211 = stableVersion(2, 1, 1); + final var v300 = stableVersion(3, 0, 0); // test order of version numbers assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1"); @@ -348,17 +346,18 @@ public final class SemanticVersionTest { /** * Tests that simple stable versions can be created and their parts read - * + * * @since 2022-02-19 + * @since v0.4.0 */ @Test public void testSimpleStableVersions() { - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + final var v100 = stableVersion(1, 0, 0); assertEquals(1, v100.majorVersion()); assertEquals(0, v100.minorVersion()); assertEquals(0, v100.patchVersion()); - final SemanticVersionNumber v925 = stableVersion(9, 2, 5); + final var v925 = stableVersion(9, 2, 5); assertEquals(9, v925.majorVersion()); assertEquals(2, v925.minorVersion()); assertEquals(5, v925.patchVersion()); @@ -367,26 +366,28 @@ public final class SemanticVersionTest { /** * Tests that {@link SemanticVersionNumber#toString} works for simple version * numbers - * + * * @since 2022-02-19 + * @since v0.4.0 */ @Test public void testSimpleToString() { - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + final var v100 = stableVersion(1, 0, 0); assertEquals("1.0.0", v100.toString()); - final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1); + final var v845a1 = preRelease(8, 4, 5, "alpha", 1); assertEquals("8.4.5-alpha.1", v845a1.toString()); } /** * Tests that simple unstable versions can be created and their parts read - * + * * @since 2022-02-19 + * @since v0.4.0 */ @Test public void testSimpleUnstableVersions() { - final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1); + final var v350a1 = preRelease(3, 5, 0, "alpha", 1); assertEquals(3, v350a1.majorVersion(), "Incorrect major version for v3.5.0a1"); assertEquals(5, v350a1.minorVersion(), diff --git a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java index 36b373b..733a308 100644 --- a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java +++ b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * Copyright (C) 2021, 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -32,11 +32,10 @@ import org.junit.jupiter.api.Test; * * @author Adrien Hopkins * @since 2021-11-29 + * @since v0.3.2 */ class UncertainDoubleTest { - /** - * Ensures that the compareTo function behaves correctly. - */ + /** Ensures that the compareTo function behaves correctly. */ @Test final void testCompareTo() { assertTrue(of(2.0, 0.5).compareTo(of(2.0, 0.1)) == 0); @@ -44,23 +43,18 @@ class UncertainDoubleTest { assertTrue(of(2.0, 0.5).compareTo(of(3.0, 0.1)) < 0); } - /** - * Tests the ___exact operations - */ + /** Tests the ___exact operations */ @Test final void testExactOperations() { - final UncertainDouble x = UncertainDouble.of(Math.PI, 0.1); + final var x = UncertainDouble.of(Math.PI, 0.1); // slightly different because roundoff errors - final UncertainDouble x1 = UncertainDouble.of(Math.PI + Math.E - Math.E, - 0.1); - final UncertainDouble x2 = UncertainDouble.of(Math.PI * Math.E / Math.E, - 0.1); + final var x1 = UncertainDouble.of(Math.PI + Math.E - Math.E, 0.1); + final var x2 = UncertainDouble.of(Math.PI * Math.E / Math.E, 0.1); // get results - final UncertainDouble result1 = x.plusExact(Math.E).minusExact(Math.E); - final UncertainDouble result2 = x.timesExact(Math.E) - .dividedByExact(Math.E); + final var result1 = x.plusExact(Math.E).minusExact(Math.E); + final var result2 = x.timesExact(Math.E).dividedByExact(Math.E); // test that these operations work & don't change uncertainty assertEquals(x1, result1); @@ -75,36 +69,35 @@ class UncertainDoubleTest { /** * Test for {@link UncertainDouble#fromRoundedString} - * + * * @since 2022-04-18 + * @since v0.4.0 */ @Test final void testFromRoundedString() { assertEquals(of(12345.678, 0.001), fromRoundedString("12345.678")); } - /** - * Test for {@link UncertainDouble#fromString} - */ + /** Test for {@link UncertainDouble#fromString} */ @Test final void testFromString() { // valid strings - assertEquals(of(2.0, 0.5), fromString("2.0 � 0.5")); + assertEquals(of(2.0, 0.5), fromString("2.0 ± 0.5")); assertEquals(of(2.0, 0.5), fromString("2.0 +- 0.5")); assertEquals(of(2.0, 0.0), fromString("2.0")); // invalid strings - for (final String s : List.of("2.A", "A", "2.0 � .", "� 3.5")) { + for (final String s : List.of("2.A", "A", "2.0 ± ", " ± 3.5")) { assertThrows(IllegalArgumentException.class, () -> fromString(s)); } // back and forth - assertEquals("2.0 � 0.5", of(2.0, 0.5).toString()); + assertEquals("2.0 ± 0.5", of(2.0, 0.5).toString()); assertEquals("2.0", of(2.0, 0).toString()); } @Test final void testHashCode() { - assertEquals(of(2.0, 0.5).hashCode(), fromString("2.0 � 0.5").hashCode()); + assertEquals(of(2.0, 0.5).hashCode(), fromString("2.0 ± 0.5").hashCode()); } } diff --git a/src/test/java/sevenUnitsGUI/I18nTest.java b/src/test/java/sevenUnitsGUI/I18nTest.java new file mode 100644 index 0000000..2f90d76 --- /dev/null +++ b/src/test/java/sevenUnitsGUI/I18nTest.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2025 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnitsGUI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests for the internationalization system. + * + * @since v1.0.0 + */ +class I18nTest { + private static final Stream<String> testLocaleSupported() { + return Stream.of("en", "fr"); + } + + private static final Stream<Arguments> testLocalization() { + return Stream.of(Arguments.of("tv.title", "en", "7Units [v]"), + Arguments.of("tv.title", "fr", "7Unités [v]"), + Arguments.of("tv.convert_units.title", "en", "Convert Units"), + Arguments.of("tv.convert_units.title", "fr", "Convertir Unités")); + } + + /** + * Tests that the default locale is supported. + * + * @since v1.0.0 + * @see Presenter#DEFAULT_LOCALE + */ + @Test + void testDefaultLocaleSupported() { + final var p = new Presenter(new ViewBot()); + assertNotNull(p.locales.get(Presenter.DEFAULT_LOCALE), + "Default locale is not supported."); + } + + /** + * Ensures that the system supports the provided locale. + * + * @param localeName locale to test for support + * + * @since 2025-06-04 + * @since v1.0.0 + */ + @ParameterizedTest + @MethodSource + void testLocaleSupported(String localeName) { + final var p = new Presenter(new ViewBot()); + assertNotNull(p.locales.get(localeName), + "Locale \"" + localeName + "\" is not supported."); + } + + /** + * Tests that the system can correctly localize text, using the default + * locales. + * + * @param key key of text to localize + * @param locale locale to use + * @param expected expected value of output text + * + * @since 2025-06-04 + * @since v1.0.0 + */ + @ParameterizedTest + @MethodSource + void testLocalization(String key, String locale, String expected) { + final var p = new Presenter(new ViewBot()); + p.setUserLocale(locale); + final var actual = p.getLocalizedText(key); + assertEquals(expected, actual); + } + +} diff --git a/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java b/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java index 476e407..ead5f4a 100644 --- a/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java +++ b/src/test/java/sevenUnitsGUI/PrefixRepetitionTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -31,15 +31,15 @@ import sevenUnits.unit.Metric; /** * Tests for the default prefix repetition rules. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ class PrefixRepetitionTest { /** * Ensures that the complex repetition rule disallows invalid prefix lists. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testInvalidComplexRepetition() { @@ -57,9 +57,9 @@ class PrefixRepetitionTest { /** * Tests the {@code NO_REPETITION} rule. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testNoRepetition() { @@ -71,9 +71,9 @@ class PrefixRepetitionTest { /** * Tests the {@code NO_RESTRICTION} rule. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testNoRestriction() { @@ -85,9 +85,9 @@ class PrefixRepetitionTest { /** * Ensures that the complex repetition rule allows valid prefix lists. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testValidComplexRepetition() { diff --git a/src/test/java/sevenUnitsGUI/PrefixSearchTest.java b/src/test/java/sevenUnitsGUI/PrefixSearchTest.java index 305d0d7..00dd960 100644 --- a/src/test/java/sevenUnitsGUI/PrefixSearchTest.java +++ b/src/test/java/sevenUnitsGUI/PrefixSearchTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -18,6 +18,7 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static sevenUnitsGUI.PrefixSearchRule.COMMON_PREFIXES; import static sevenUnitsGUI.PrefixSearchRule.NO_PREFIXES; import static sevenUnitsGUI.PrefixSearchRule.getCoherentOnlyRule; @@ -35,13 +36,11 @@ import sevenUnits.unit.Metric; /** * Tests for {@link PrefixSearchRule} * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ class PrefixSearchTest { - /** - * A method that creates duplicate copies of the common prefix rule. - */ + /** A method that creates duplicate copies of the common prefix rule. */ private static final PrefixSearchRule getCommonRuleCopy() { return getCoherentOnlyRule(Set.of(Metric.KILO, Metric.MILLI)); } @@ -51,8 +50,8 @@ class PrefixSearchTest { * {@link sevenUnitsGUI.PrefixSearchRule#apply(java.util.Map.Entry)}, for a * coherent unit and {@link PrefixSearchRule#COMMON_PREFIXES}. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @Test final void testCoherentPrefixSearch() { @@ -69,8 +68,8 @@ class PrefixSearchTest { * Test method for * {@link sevenUnitsGUI.PrefixSearchRule#equals(java.lang.Object)}. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @Test final void testEquals() { @@ -84,8 +83,8 @@ class PrefixSearchTest { /** * Test method for {@link sevenUnitsGUI.PrefixSearchRule#getPrefixes()}. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @Test final void testGetPrefixes() { @@ -97,8 +96,8 @@ class PrefixSearchTest { /** * Test method for {@link sevenUnitsGUI.PrefixSearchRule#hashCode()}. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @Test final void testHashCode() { @@ -109,9 +108,9 @@ class PrefixSearchTest { /** * Tests prefix searching for a non-coherent unit and * {@link PrefixSearchRule#COMMON_PREFIXES}. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test final void testNonCoherentPrefixSearch() { @@ -124,9 +123,9 @@ class PrefixSearchTest { /** * Tests that {@link PrefixSearchRule#NO_PREFIXES} returns the original unit. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testNoPrefixes() { @@ -145,14 +144,17 @@ class PrefixSearchTest { /** * Test method for {@link sevenUnitsGUI.PrefixSearchRule#toString()}. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @Test final void testToString() { - assertEquals( - "Apply the following prefixes: [kilo (\u00D7 1000.0), milli (\u00D7 0.001)]", - COMMON_PREFIXES.toString()); + final var toString = COMMON_PREFIXES.toString(); + final var valid1 = "Apply the following prefixes: [kilo (\u00D7 1000.0), milli (\u00D7 0.001)]"; + final var valid2 = "Apply the following prefixes: [milli (\u00D7 0.001), kilo (\u00D7 1000.0)]"; + + assertTrue(valid1.equals(toString) || valid2.equals(toString), + "COMMON_PREFIXES.toString invalid (was \"" + toString + "\")."); } } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 9e25a08..9ac5b84 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022-2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,13 +17,12 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import java.math.RoundingMode; import java.nio.file.Path; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -32,12 +31,12 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; -import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; @@ -53,11 +52,11 @@ import sevenUnits.utils.UncertainDouble; * <em>Note: this test outputs a lot to the standard output, because creating a * {@link UnitDatabase} and converting with a {@link ViewBot} both trigger * println statements.</em> - * + * * @author Adrien Hopkins * - * @since v0.4.0 * @since 2022-02-10 + * @since v0.4.0 */ public final class PresenterTest { private static final Path TEST_SETTINGS = Path.of("src", "test", "resources", @@ -74,10 +73,10 @@ public final class PresenterTest { /** * @return rounding rules used by {@link #testRoundingRules} - * @since v0.4.0 * @since 2022-04-16 + * @since v0.4.0 */ - private static final Stream<Function<UncertainDouble, String>> getRoundingRules() { + private static Stream<Function<UncertainDouble, String>> getRoundingRules() { final var SCIENTIFIC_ROUNDING = StandardDisplayRules.uncertaintyBased(); final var INTEGER_ROUNDING = StandardDisplayRules.fixedDecimals(0); final var SIG_FIG_ROUNDING = StandardDisplayRules.fixedPrecision(4); @@ -85,81 +84,99 @@ public final class PresenterTest { return Stream.of(SCIENTIFIC_ROUNDING, INTEGER_ROUNDING, SIG_FIG_ROUNDING); } - private static final Stream<Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>>> getSearchRules() { + private static Stream<Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>>> getSearchRules() { return SEARCH_RULES; } - private static final Set<String> names(Set<? extends Nameable> units) { + private static Set<String> names(Set<? extends Nameable> units) { return units.stream().map(Nameable::getName).collect(Collectors.toSet()); } + private static Stream<Arguments> testConvertExpressions() { + return Stream.of( + Arguments.of("10000.0 m", "km", "10000.0 m = 10.00000 km"), + Arguments.of("1.0 m", "ft; in", "1.0 m = 3 ft + 3 in"), + Arguments.of("1.0 m", "ftin", "1.0 m = 3 ft + 3 in")); + } + + private static Stream<Arguments> testConvertUnits() { + return Stream.of( + Arguments.of("m", "km", 10000.0, "10000.0 m = 10.00000 km"), + Arguments.of("m", "ftin", 1.0, "1.0 m = 3 ft + 3 in")); + } + /** * Test method for {@link Presenter#convertExpressions} - * - * @since v0.4.0 + * * @since 2022-02-12 + * @since v0.4.0 */ - @Test - void testConvertExpressions() { + @ParameterizedTest + @MethodSource + void testConvertExpressions(String from, String to, String expectedOutput) { // setup - final ViewBot viewBot = new ViewBot(); - final Presenter presenter = new Presenter(viewBot); + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); - viewBot.setFromExpression("10000.0 m"); - viewBot.setToExpression("km"); + viewBot.setFromExpression(from); + viewBot.setToExpression(to); // convert expression presenter.convertExpressions(); // test result - final List<UnitConversionRecord> outputs = viewBot - .expressionConversionList(); - assertEquals("10000.0 m = 10.00000 km", - outputs.get(outputs.size() - 1).toString()); + final var outputs = viewBot.expressionConversionList(); + assertEquals(expectedOutput, outputs.get(outputs.size() - 1).toString()); } /** - * Tests that unit-conversion Views can correctly convert units - * - * @since v0.4.0 + * Test method for {@link Presenter#convertUnits} + * * @since 2022-02-12 + * @since v0.4.0 */ - @Test - void testConvertUnits() { + @ParameterizedTest + @MethodSource + void testConvertUnits(String from, String to, double value, + String expectedOutput) { // setup - final ViewBot viewBot = new ViewBot(); - final Presenter presenter = new Presenter(viewBot); + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); - viewBot.setFromUnitNames(names(testUnits)); - viewBot.setToUnitNames(names(testUnits)); - viewBot.setFromSelection("metre"); - viewBot.setToSelection("kilometre"); - viewBot.setInputValue("10000.0"); + viewBot.setFromSelection(from); + viewBot.setToSelection(to); + viewBot.setInputValue(Double.toString(value)); - // convert units + // convert expression presenter.convertUnits(); - /* - * use result from system as expected - I'm not testing unit conversion - * here (that's for the backend tests), I'm just testing that it correctly - * calls the unit conversion system - */ - final LinearUnitValue expectedInput = LinearUnitValue.of(Metric.METRE, - UncertainDouble.fromRoundedString("10000.0")); - final LinearUnitValue expectedOutput = expectedInput - .convertTo(Metric.KILOMETRE); - final UnitConversionRecord expectedUC = UnitConversionRecord.valueOf( - expectedInput.getUnit().getName(), - expectedOutput.getUnit().getName(), "10000.0", - expectedOutput.getValue().toString(false, RoundingMode.HALF_EVEN)); - assertEquals(List.of(expectedUC), viewBot.unitConversionList()); + // test result + final var outputs = viewBot.unitConversionList(); + assertEquals(expectedOutput, outputs.get(outputs.size() - 1).toString()); + } + + /** + * Ensures that the default unitfile can be disabled. + * + * @since 2025-02-23 + * @since v1.0.0 + */ + @Test + void testDisableDefault() { + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); + assumeTrue(presenter.database.containsUnitName("joule"), + "Attempted to test disabling default on unit not in default file."); + presenter.setUseDefaultDatafiles(false); + assertFalse(presenter.database.containsUnitName("joule"), + "Presenter disabled default datafiles, but still contains the joule."); } /** * Tests that duplicate units are successfully removed, if that is asked for - * - * @since v0.4.0 + * * @since 2022-04-16 + * @since v0.4.0 */ @Test void testDuplicateUnits() { @@ -190,9 +207,9 @@ public final class PresenterTest { /** * Tests that one-way conversion correctly filters From and To units - * - * @since v0.4.0 + * * @since 2022-04-16 + * @since v0.4.0 */ @Test void testOneWayConversion() { @@ -224,9 +241,9 @@ public final class PresenterTest { /** * Tests the prefix-viewing functionality. - * - * @since v0.4.0 + * * @since 2022-04-16 + * @since v0.4.0 */ @Test void testPrefixViewing() { @@ -254,9 +271,9 @@ public final class PresenterTest { /** * Tests that rounding rules are used correctly. - * - * @since v0.4.0 + * * @since 2022-04-16 + * @since v0.4.0 */ @ParameterizedTest @MethodSource("getRoundingRules") @@ -273,9 +290,9 @@ public final class PresenterTest { presenter.convertUnits(); // test the result of the rounding - final String expectedOutputString = roundingRule + final var expectedOutputString = roundingRule .apply(UncertainDouble.fromRoundedString("12.3456789")); - final String actualOutputString = viewBot.unitConversionList().get(0) + final var actualOutputString = viewBot.unitConversionList().get(0) .outputValueString(); assertEquals(expectedOutputString, actualOutputString); } @@ -284,8 +301,8 @@ public final class PresenterTest { * Tests that the Presenter correctly applies search rules. * * @param searchRule search rule to test - * @since v0.4.0 * @since 2022-07-08 + * @since v0.4.0 */ @ParameterizedTest @MethodSource("getSearchRules") @@ -309,7 +326,7 @@ public final class PresenterTest { .apply(Map.entry("inch", BritishImperial.Length.INCH)).keySet()); expectedOutput.addAll( searchRule.apply(Map.entry("metre", Metric.METRE)).keySet()); - final Set<String> actualOutput = viewBot.getFromUnitNames(); + final var actualOutput = viewBot.getFromUnitNames(); // test output assertEquals(expectedOutput, actualOutput); @@ -317,9 +334,9 @@ public final class PresenterTest { /** * Tests that settings can be saved to and loaded from a file. - * - * @since v0.4.0 + * * @since 2022-04-16 + * @since v0.4.0 */ @Test void testSettingsSaving() { @@ -351,9 +368,9 @@ public final class PresenterTest { /** * Ensures the Presenter generates the correct data upon a unit-viewing. - * - * @since v0.4.0 + * * @since 2022-04-16 + * @since v0.4.0 */ @Test void testUnitViewing() { @@ -385,15 +402,15 @@ public final class PresenterTest { /** * Test for {@link Presenter#updateView()} - * - * @since v0.4.0 + * * @since 2022-02-12 + * @since v0.4.0 */ @Test void testUpdateView() { // setup - final ViewBot viewBot = new ViewBot(); - final Presenter presenter = new Presenter(viewBot); + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); presenter.setOneWayConversionEnabled(false); presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES); @@ -415,8 +432,8 @@ public final class PresenterTest { // filter to length units only, then get the filtered sets of units presenter.updateView(); - final Set<String> fromUnits = viewBot.getFromUnitNames(); - final Set<String> toUnits = viewBot.getToUnitNames(); + final var fromUnits = viewBot.getFromUnitNames(); + final var toUnits = viewBot.getToUnitNames(); // test that fromUnits/toUnits is [METRE, KILOMETRE] assertEquals(Set.of("metre", "kilometre"), fromUnits); diff --git a/src/test/java/sevenUnitsGUI/RoundingTest.java b/src/test/java/sevenUnitsGUI/RoundingTest.java index f749f85..589b8d0 100644 --- a/src/test/java/sevenUnitsGUI/RoundingTest.java +++ b/src/test/java/sevenUnitsGUI/RoundingTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -41,8 +41,8 @@ import sevenUnitsGUI.StandardDisplayRules.UncertaintyBased; /** * Tests that ensure the rounding rules work as intended. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ class RoundingTest { // rounding rules to test @@ -69,8 +69,8 @@ class RoundingTest { /** * @return arguments for * {@link #testFixedDecimalRounding(UncertainDouble, String, String, String)} - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ private static final Stream<Arguments> fixedDecimalRoundingExamples() { // input, zero decimal string, two decimal string, six decimal string @@ -83,8 +83,8 @@ class RoundingTest { /** * @return arguments for * {@link #testFixedPrecisionRounding(UncertainDouble, String, String, String)} - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ private static final Stream<Arguments> fixedPrecisionRoundingExamples() { // input, one sig fig string, three s.f. string, six s.f. string @@ -97,8 +97,8 @@ class RoundingTest { /** * @return arguments for * {@link #testUncertaintyRounding(UncertainDouble, String)} - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ private static final Stream<Arguments> uncertaintyRoundingExamples() { // input, uncertainty rounding string @@ -112,8 +112,8 @@ class RoundingTest { * Test for {@link FixedDecimals#decimalPlaces()} and * {@link FixedPrecision#significantFigures()}. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @Test void testDataMethods() { @@ -137,9 +137,9 @@ class RoundingTest { /** * Tests that the rounding methods' equals() methods work. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testEquals() { @@ -161,7 +161,7 @@ class RoundingTest { // test that FixedDecimals is never equal to FixedPrecision // this unlikely argument is the test - the equals should return false! @SuppressWarnings("unlikely-arg-type") - final boolean differentRulesEqual = Objects.equals(fixedDecimals(4), + final var differentRulesEqual = Objects.equals(fixedDecimals(4), fixedPrecision(4)); assertFalse(differentRulesEqual, "fixedDecimals(4) == fixedPrecision(4)"); } @@ -174,6 +174,7 @@ class RoundingTest { * @param twoDecimalString expected string for two decimal places * @param sixDecimalString expected string for six decimal places * @since 2022-07-17 + * @since v0.4.0 */ @ParameterizedTest @MethodSource("fixedDecimalRoundingExamples") @@ -200,8 +201,8 @@ class RoundingTest { * @param oneSigFigString expected string for one significant figure * @param threeSigFigString expected string for three significant figures * @param twelveSigFigString expected string for twelve significant figures - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @ParameterizedTest @MethodSource("fixedPrecisionRoundingExamples") @@ -225,9 +226,9 @@ class RoundingTest { /** * Tests that {@link StandardDisplayRules#getStandardRule} gets rounding * rules as intended. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testGetStandardRule() { @@ -243,9 +244,9 @@ class RoundingTest { /** * Tests that the rounding methods' equals() methods work. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testHashCode() { @@ -257,9 +258,9 @@ class RoundingTest { /** * Tests that the {@code toString()} methods of the three rounding rule * classes work correctly. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testToString() { @@ -273,8 +274,8 @@ class RoundingTest { * * @param input input number * @param output expected output string - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ @ParameterizedTest @MethodSource("uncertaintyRoundingExamples") diff --git a/src/test/java/sevenUnitsGUI/TabbedViewTest.java b/src/test/java/sevenUnitsGUI/TabbedViewTest.java index 165718f..b32579c 100644 --- a/src/test/java/sevenUnitsGUI/TabbedViewTest.java +++ b/src/test/java/sevenUnitsGUI/TabbedViewTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -18,20 +18,24 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.concurrent.TimeUnit; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; /** - * Test for the TabbedView + * Test for the TabbedView. * - * @since v0.4.0 * @since 2022-07-17 + * @since v0.4.0 */ +@Timeout(value = 10, unit = TimeUnit.SECONDS) class TabbedViewTest { /** * @return a view with all settings set to standard values - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ private static final TabbedView setupView() { final var view = new TabbedView(); @@ -50,9 +54,9 @@ class TabbedViewTest { /** * Simulates an expression conversion operation, and ensures it works * properly. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testExpressionConversion() { @@ -71,9 +75,9 @@ class TabbedViewTest { /** * Simulates a unit conversion operation, and ensures it works properly. - * - * @since v0.4.0 + * * @since 2022-07-17 + * @since v0.4.0 */ @Test void testUnitConversion() { |