diff options
author | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-06-12 07:37:14 -0500 |
---|---|---|
committer | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-06-12 07:37:14 -0500 |
commit | 360b4261ebb9b65dcf91a6e49c5d23784e592945 (patch) | |
tree | f4b35e7dbf6ddd0b89bc46e2e34f2b84bdf161d7 | |
parent | 910b2f1b448ec56e6a66f4aa4f72e71c39de40a1 (diff) | |
parent | 41b0eda07403db9b09184b79060bcc323dcdc753 (diff) |
Merge branch 'release-0.3.0'
83 files changed, 10249 insertions, 4800 deletions
@@ -1,20 +1,32 @@ <?xml version="1.0" encoding="UTF-8"?> <classpath> - <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"> + <classpathentry kind="src" output="bin/main" path="src/main/java"> <attributes> - <attribute name="maven.pomderived" value="true"/> + <attribute name="gradle_scope" value="main"/> + <attribute name="gradle_used_by_scope" value="main,test"/> </attributes> </classpathentry> - <classpathentry kind="src" output="target/classes" path="src"> + <classpathentry kind="src" output="bin/main" path="src/main/resources"> <attributes> - <attribute name="optional" value="true"/> - <attribute name="maven.pomderived" value="true"/> + <attribute name="gradle_scope" value="main"/> + <attribute name="gradle_used_by_scope" value="main,test"/> </attributes> </classpathentry> - <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> + <classpathentry kind="src" output="bin/test" path="src/test/java"> <attributes> - <attribute name="maven.pomderived" value="true"/> + <attribute name="gradle_scope" value="test"/> + <attribute name="gradle_used_by_scope" value="test"/> + <attribute name="test" value="true"/> </attributes> </classpathentry> - <classpathentry kind="output" path="target/classes"/> + <classpathentry kind="src" output="bin/test" path="src/test/resources"> + <attributes> + <attribute name="gradle_scope" value="test"/> + <attribute name="gradle_used_by_scope" value="test"/> + <attribute name="test" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/> + <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/> + <classpathentry kind="output" path="bin/default"/> </classpath> diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + @@ -1,4 +1,10 @@ -bin/ -target/ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# Unit Converter gitignore files *.class -*~
\ No newline at end of file +*~ +settings.txt
\ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 7c2efb0..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,14 +0,0 @@ -# attempt at ci - -image: maven:3-jdk-8 - - -build: - stage: build - script: "mvn clean -B" - script: "mvn compile -B" - - -test: - stage: test - script: "mvn verify" @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <projectDescription> - <name>Unit Converter</name> - <comment></comment> + <name>UnitConverter</name> + <comment>Project UnitConverter created by Buildship.</comment> <projects> </projects> <buildSpec> @@ -11,13 +11,13 @@ </arguments> </buildCommand> <buildCommand> - <name>org.eclipse.m2e.core.maven2Builder</name> + <name>org.eclipse.buildship.core.gradleprojectbuilder</name> <arguments> </arguments> </buildCommand> </buildSpec> <natures> - <nature>org.eclipse.m2e.core.maven2Nature</nature> <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.buildship.core.gradleprojectnature</nature> </natures> </projectDescription> diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e889521 --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 8445b6b..18ad895 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,13 +1,4 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.source=11 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs deleted file mode 100644 index f897a7f..0000000 --- a/.settings/org.eclipse.m2e.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -activeProfiles= -eclipse.preferences.version=1 -resolveWorkspaceProjects=true -version=1 diff --git a/CHANGELOG.org b/CHANGELOG.org index 77e7593..883861d 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,7 +1,23 @@ * Changelog All notable changes in this project will be shown in this file. +** v0.3.0 - [2021-06-12 Sat] +*** Added + - Added a simple unit conversion method to the Unit interface + - Added a static factory to create nonlinear units to the Unit interface + - Added a settings menu with many settings to change the system's behaviour. Allows you to control the precision settings, prefix repetition, and more. +*** Changed + - Changed the unit system again! + - You can now use tabs or spaces to separate unit names and their definitions in unit files. + - Unit Converter uses Gradle instead of Maven. ** v0.2.0 - [2019-04-14] +*** Added + - A selection-based unit converter which allows you to select two units, input a value, and convert. + - The UnitDatabase now stores dimensions. + - A system to parse mathematical expressions, used to parse unit expressions. + - You can now add and subtract in unit expressions! + - Instructions for obtaining unit instances are provided in the relevant classes + - The UnitPrefix interface now provides default times, dividedBy and toExponent methods. *** Changed - When searching for units, units with no prefixes are searched for before prefixed units - Smaller prefixes are searched for before larger prefixes @@ -10,13 +26,6 @@ All notable changes in this project will be shown in this file. - BaseUnit is now a subclass of LinearUnit - In unit files, Comments can now start in the middle of lines - UnitsDatabase.addAllFromFile() has been renamed to loadUnitsFile() -*** Added - - A selection-based unit converter which allows you to select two units, input a value, and convert. - - The UnitDatabase now stores dimensions. - - A system to parse mathematical expressions, used to parse unit expressions. - - You can now add and subtract in unit expressions! - - Instructions for obtaining unit instances are provided in the relevant classes - - The UnitPrefix interface now provides default times, dividedBy and toExponent methods. ** v0.1.0 - [2019-02-01] NOTE: At this stage, the API is subject to significant change. *** Added @@ -1,10 +1,10 @@ -* Unit Converter v0.2.0 +* Unit Converter v0.3.0 (this project uses Semantic Versioning) ** What is it? This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in. ** Features - Convert between units and expressions of units - - linear or base unit can use unit prefixes (including non-metric units!) + - linear or base units can use unit prefixes (including non-metric 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 @@ -65,11 +65,11 @@ Nonlinear units cannot: - use prefixes - be defined by unit files -To define a nonlinear unit, make an anonymous inner type (or any other subclass) of AbstractUnit, and define the conversion methods. +To define a nonlinear unit, make an anonymous inner type (or any other subclass) of Unit, and define the conversion methods. You can do this easily using Unit's static methods. ** Unit and Prefix Viewers The unit and prefix viewers can be used to see the available units (without prefixes) and prefixes. Upon opening them, you will see a list of units or prefixes on your left. Using the text box above, the list can be filtered. When a unit is clicked on, details about will be displayed on the right. ** Copyright and Licences -The Unit Converter program is Copyright (C) 2018, 2019 Adrien Hopkins. It is released under the terms of the Aferro GNU General Public License, version 3.0 or any later version published by the Free Software Foundation. A copy of this license should be provided with this program, and a human-readable summary of the very similar GNU General Public License can be found at the following link: https://www.gnu.org/licenses/quick-guide-gplv3.html, although this summary is NOT a replacement for the actual license. +The Unit Converter program is Copyright (C) 2018-2021 Adrien Hopkins. It is released under the terms of the Aferro GNU General Public License, version 3.0 or any later version published by the Free Software Foundation. A copy of this license should be provided with this program, and a human-readable summary of the very similar GNU General Public License can be found at the following link: https://www.gnu.org/licenses/quick-guide-gplv3.html, although this summary is NOT a replacement for the actual license. This document is Copyright (C) 2019 Adrien Hopkins. This document is dual-licensed under the terms of the GNU Free Documentation License and the Creative Commons Attribution-ShareAlike License. More details are in the next paragraphs: diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..7eed456 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +/main/ +/test/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2922695 --- /dev/null +++ b/build.gradle @@ -0,0 +1,46 @@ +plugins { + id "java" + id "application" + id "jacoco" +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 +} + +mainClassName = "org.unitConverter.converterGUI.UnitConverterGUI" + +repositories { + 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' +} + +jar { + manifest { + attributes 'Main-Class': mainClassName + } +} + +test { + useJUnitPlatform() + testLogging { + events 'passed', 'skipped', 'failed' + } + finalizedBy jacocoTestReport +} + +jacoco { + toolVersion = "0.8.6" +} + +jacocoTestReport { + dependsOn test +} + +run { +}
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..490fda8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a4b4429 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..62bd9b9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 5b3e468..0000000 --- a/pom.xml +++ /dev/null @@ -1,55 +0,0 @@ -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> - <groupId>org.unitConverter</groupId> - <artifactId>unitConverter</artifactId> - <version>0.1.0</version> - <name>Unit Converter</name> - <description>A Java unit converter inspired by GNU Units</description> - <build> - <sourceDirectory>src</sourceDirectory> - <plugins> - <plugin> - <artifactId>maven-compiler-plugin</artifactId> - <version>3.8.0</version> - <configuration> - <source>1.8</source> - <target>1.8</target> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-surefire-plugin</artifactId> - <configuration> - <useSystemClassLoader>false</useSystemClassLoader> - </configuration> - </plugin> - <plugin> - <groupId>org.codehaus.mojo</groupId> - <artifactId>exec-maven-plugin</artifactId> - <configuration> - <mainClass>org.unitConverter.converterGUI.UnitConverterGUI</mainClass> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> - <configuration> - <archive> - <manifest> - <mainClass>org.unitConverter.converterGUI.UnitConverterGUI</mainClass> - </manifest> - </archive> - </configuration> - </plugin> - </plugins> - </build> - <dependencies> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>4.11</version> - </dependency> - </dependencies> -</project> diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..34ea90c --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.3/userguide/multi_project_builds.html + */ + +rootProject.name = 'UnitConverter' diff --git a/src/main/java/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/main/java/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..bdc3a2e --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,95 @@ +/** + * @since 2020-08-26 + */ +package org.unitConverter.converterGUI; + +import java.util.List; +import java.util.function.Predicate; + +import org.unitConverter.unit.SI; +import org.unitConverter.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { + NO_REPETITION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!SI.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (SI.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) { + part = 0; + } else if (SI.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; + } + }; +} diff --git a/src/org/unitConverter/converterGUI/DelegateListModel.java b/src/main/java/org/unitConverter/converterGUI/DelegateListModel.java index b80f63d..b80f63d 100755..100644 --- a/src/org/unitConverter/converterGUI/DelegateListModel.java +++ b/src/main/java/org/unitConverter/converterGUI/DelegateListModel.java diff --git a/src/org/unitConverter/converterGUI/FilterComparator.java b/src/main/java/org/unitConverter/converterGUI/FilterComparator.java index 7b17bfc..9b77f21 100755..100644 --- a/src/org/unitConverter/converterGUI/FilterComparator.java +++ b/src/main/java/org/unitConverter/converterGUI/FilterComparator.java @@ -1,129 +1,129 @@ -/**
- * Copyright (C) 2018 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 org.unitConverter.converterGUI;
-
-import java.util.Comparator;
-import java.util.Objects;
-
-/**
- * A comparator that compares strings using a filter.
- *
- * @author Adrien Hopkins
- * @since 2019-01-15
- * @since v0.1.0
- */
-final class FilterComparator implements Comparator<String> {
- /**
- * 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<String> comparator;
- /**
- * Whether or not the comparison is case-sensitive.
- *
- * @since 2019-04-14
- * @since v0.2.0
- */
- private final boolean caseSensitive;
-
- /**
- * Creates the {@code FilterComparator}.
- *
- * @param filter
- * @since 2019-01-15
- * @since v0.1.0
- */
- public FilterComparator(final String filter) {
- this(filter, null);
- }
-
- /**
- * Creates the {@code FilterComparator}.
- *
- * @param filter
- * string to filter by
- * @param comparator
- * comparator to fall back to if all else fails, null is compareTo.
- * @throws NullPointerException
- * if filter is null
- * @since 2019-01-15
- * @since v0.1.0
- */
- public FilterComparator(final String filter, final Comparator<String> comparator) {
- this(filter, comparator, false);
- }
-
- /**
- * Creates the {@code FilterComparator}.
- *
- * @param filter
- * string to filter by
- * @param comparator
- * comparator to fall back to if all else fails, null is compareTo.
- * @param caseSensitive
- * whether or not the comparator is case-sensitive
- * @throws NullPointerException
- * if filter is null
- * @since 2019-04-14
- * @since v0.2.0
- */
- public FilterComparator(final String filter, final Comparator<String> comparator, final boolean caseSensitive) {
- this.filter = Objects.requireNonNull(filter, "filter must not be null.");
- this.comparator = comparator;
- this.caseSensitive = caseSensitive;
- }
-
- @Override
- public int compare(final String arg0, final String arg1) {
- // if this is case insensitive, make them lowercase
- final String str0, str1;
- if (this.caseSensitive) {
- str0 = arg0;
- str1 = arg1;
- } else {
- str0 = arg0.toLowerCase();
- str1 = arg1.toLowerCase();
- }
-
- // elements that start with the filter always go first
- if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
- return -1;
- else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
- return 1;
-
- // elements that contain the filter but don't start with them go next
- if (str0.contains(this.filter) && !str1.contains(this.filter))
- return -1;
- else if (!str0.contains(this.filter) && !str1.contains(this.filter))
- return 1;
-
- // other elements go last
- if (this.comparator == null)
- return str0.compareTo(str1);
- else
- return this.comparator.compare(str0, str1);
- }
-}
+/** + * Copyright (C) 2018 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 org.unitConverter.converterGUI; + +import java.util.Comparator; +import java.util.Objects; + +/** + * A comparator that compares strings using a filter. + * + * @author Adrien Hopkins + * @since 2019-01-15 + * @since v0.1.0 + */ +final class FilterComparator implements Comparator<String> { + /** + * 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<String> comparator; + /** + * Whether or not the comparison is case-sensitive. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private final boolean caseSensitive; + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter) { + this(filter, null); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * string to filter by + * @param comparator + * comparator to fall back to if all else fails, null is compareTo. + * @throws NullPointerException + * if filter is null + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter, final Comparator<String> comparator) { + this(filter, comparator, false); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * string to filter by + * @param comparator + * comparator to fall back to if all else fails, null is compareTo. + * @param caseSensitive + * whether or not the comparator is case-sensitive + * @throws NullPointerException + * if filter is null + * @since 2019-04-14 + * @since v0.2.0 + */ + public FilterComparator(final String filter, final Comparator<String> comparator, final boolean caseSensitive) { + this.filter = Objects.requireNonNull(filter, "filter must not be null."); + this.comparator = comparator; + this.caseSensitive = caseSensitive; + } + + @Override + public int compare(final String arg0, final String arg1) { + // if this is case insensitive, make them lowercase + final String str0, str1; + if (this.caseSensitive) { + str0 = arg0; + str1 = arg1; + } else { + str0 = arg0.toLowerCase(); + str1 = arg1.toLowerCase(); + } + + // elements that start with the filter always go first + if (str0.startsWith(this.filter) && !str1.startsWith(this.filter)) + return -1; + else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) + return 1; + + // elements that contain the filter but don't start with them go next + if (str0.contains(this.filter) && !str1.contains(this.filter)) + return -1; + else if (!str0.contains(this.filter) && !str1.contains(this.filter)) + return 1; + + // other elements go last + if (this.comparator == null) + return str0.compareTo(str1); + else + return this.comparator.compare(str0, str1); + } +} diff --git a/src/org/unitConverter/converterGUI/GridBagBuilder.java b/src/main/java/org/unitConverter/converterGUI/GridBagBuilder.java index f1229b2..f1229b2 100755..100644 --- a/src/org/unitConverter/converterGUI/GridBagBuilder.java +++ b/src/main/java/org/unitConverter/converterGUI/GridBagBuilder.java diff --git a/src/org/unitConverter/converterGUI/MutablePredicate.java b/src/main/java/org/unitConverter/converterGUI/MutablePredicate.java index e15b3cd..e15b3cd 100644 --- a/src/org/unitConverter/converterGUI/MutablePredicate.java +++ b/src/main/java/org/unitConverter/converterGUI/MutablePredicate.java diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java index 1995466..f52d57d 100644 --- a/src/org/unitConverter/converterGUI/SearchBoxList.java +++ b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java @@ -36,13 +36,13 @@ import javax.swing.JTextField; * @since v0.2.0 */ final class SearchBoxList extends JPanel { - + /** * @since 2019-04-13 * @since v0.2.0 */ private static final long serialVersionUID = 6226930279415983433L; - + /** * The text to place in an empty search box. * @@ -50,7 +50,7 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ private static final String EMPTY_TEXT = "Search..."; - + /** * The color to use for an empty foreground. * @@ -58,94 +58,92 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); - + // the components private final Collection<String> itemsToFilter; private final DelegateListModel<String> listModel; private final JTextField searchBox; private final JList<String> searchItems; - + private boolean searchBoxEmpty = true; - - // I need to do this because, for some reason, Swing is auto-focusing my search box without triggering a focus + + // I need to do this because, for some reason, Swing is auto-focusing my + // search box without triggering a focus // event. private boolean searchBoxFocused = false; - + private Predicate<String> customSearchFilter = o -> true; private final Comparator<String> defaultOrdering; private final boolean caseSensitive; - + /** * Creates the {@code SearchBoxList}. * - * @param itemsToFilter - * items to put in the list + * @param itemsToFilter items to put in the list * @since 2019-04-14 */ public SearchBoxList(final Collection<String> itemsToFilter) { this(itemsToFilter, null, false); } - + /** * Creates the {@code SearchBoxList}. * - * @param itemsToFilter - * items to put in the list - * @param defaultOrdering - * default ordering of items after filtration (null=Comparable) - * @param caseSensitive - * whether or not the filtration is case-sensitive + * @param itemsToFilter items to put in the list + * @param defaultOrdering default ordering of items after filtration + * (null=Comparable) + * @param caseSensitive whether or not the filtration is case-sensitive * * @since 2019-04-13 * @since v0.2.0 */ - public SearchBoxList(final Collection<String> itemsToFilter, final Comparator<String> defaultOrdering, + public SearchBoxList(final Collection<String> itemsToFilter, + final Comparator<String> defaultOrdering, final boolean caseSensitive) { super(new BorderLayout(), true); - this.itemsToFilter = itemsToFilter; + this.itemsToFilter = new ArrayList<>(itemsToFilter); this.defaultOrdering = defaultOrdering; this.caseSensitive = caseSensitive; - + // create the components this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); this.searchItems = new JList<>(this.listModel); - + this.searchBox = new JTextField(EMPTY_TEXT); this.searchBox.setForeground(EMPTY_FOREGROUND); - + // add them to the panel this.add(this.searchBox, BorderLayout.PAGE_START); this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); - + // set up the search box this.searchBox.addFocusListener(new FocusListener() { @Override public void focusGained(final FocusEvent e) { SearchBoxList.this.searchBoxFocusGained(e); } - + @Override public void focusLost(final FocusEvent e) { SearchBoxList.this.searchBoxFocusLost(e); } }); - + this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); this.searchBoxEmpty = true; } - + /** * Adds an additional filter for searching. * - * @param filter - * filter to add. + * @param filter filter to add. * @since 2019-04-13 * @since v0.2.0 */ public void addSearchFilter(final Predicate<String> filter) { this.customSearchFilter = this.customSearchFilter.and(filter); } - + /** * Resets the search filter. * @@ -155,7 +153,7 @@ final class SearchBoxList extends JPanel { public void clearSearchFilters() { this.customSearchFilter = o -> true; } - + /** * @return this component's search box component * @since 2019-04-14 @@ -164,11 +162,11 @@ final class SearchBoxList extends JPanel { public final JTextField getSearchBox() { return this.searchBox; } - + /** - * @param searchText - * text to search for - * @return a filter that filters out that text, based on this list's case sensitive setting + * @param searchText text to search for + * @return a filter that filters out that text, based on this list's case + * sensitive setting * @since 2019-04-14 * @since v0.2.0 */ @@ -176,9 +174,10 @@ final class SearchBoxList extends JPanel { if (this.caseSensitive) return string -> string.contains(searchText); else - return string -> string.toLowerCase().contains(searchText.toLowerCase()); + return string -> string.toLowerCase() + .contains(searchText.toLowerCase()); } - + /** * @return this component's list component * @since 2019-04-14 @@ -187,7 +186,7 @@ final class SearchBoxList extends JPanel { public final JList<String> getSearchList() { return this.searchItems; } - + /** * @return index selected in item list * @since 2019-04-14 @@ -196,7 +195,7 @@ final class SearchBoxList extends JPanel { public int getSelectedIndex() { return this.searchItems.getSelectedIndex(); } - + /** * @return value selected in item list * @since 2019-04-13 @@ -205,7 +204,7 @@ final class SearchBoxList extends JPanel { public String getSelectedValue() { return this.searchItems.getSelectedValue(); } - + /** * Re-applies the filters. * @@ -213,29 +212,30 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ public void reapplyFilter() { - final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); final Predicate<String> searchFilter = this.getSearchFilter(searchText); - + this.listModel.clear(); this.itemsToFilter.forEach(string -> { if (searchFilter.test(string)) { this.listModel.add(string); } }); - + // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); - + // sorts the remaining items this.listModel.sort(comparator); } - + /** * Runs whenever the search box gains focus. * - * @param e - * focus event + * @param e focus event * @since 2019-04-13 * @since v0.2.0 */ @@ -246,12 +246,11 @@ final class SearchBoxList extends JPanel { this.searchBox.setForeground(Color.BLACK); } } - + /** * Runs whenever the search box loses focus. * - * @param e - * focus event + * @param e focus event * @since 2019-04-13 * @since v0.2.0 */ @@ -262,7 +261,7 @@ final class SearchBoxList extends JPanel { this.searchBox.setForeground(EMPTY_FOREGROUND); } } - + /** * Runs whenever the text in the search box is changed. * <p> @@ -276,10 +275,12 @@ final class SearchBoxList extends JPanel { if (this.searchBoxFocused) { this.searchBoxEmpty = this.searchBox.getText().equals(""); } - final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); final Predicate<String> searchFilter = this.getSearchFilter(searchText); - + // initialize list with items that match the filter then sort this.listModel.clear(); this.itemsToFilter.forEach(string -> { @@ -287,11 +288,33 @@ final class SearchBoxList extends JPanel { this.listModel.add(string); } }); - + // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); - + // sorts the remaining items this.listModel.sort(comparator); } + + /** + * Resets the search box list's contents to the provided items, removing any + * old items + * + * @param newItems new items to put in list + * @since 2021-05-22 + */ + public void setItems(Collection<String> newItems) { + this.itemsToFilter.clear(); + this.itemsToFilter.addAll(newItems); + this.reapplyFilter(); + } + + /** + * Manually updates the search box's item list. + * + * @since 2020-08-27 + */ + public void updateList() { + this.searchBoxTextChanged(); + } } diff --git a/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java new file mode 100644 index 0000000..17ec5f9 --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -0,0 +1,1503 @@ +/** + * Copyright (C) 2018-2021 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.converterGUI; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.KeyEvent; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFormattedTextField; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.WindowConstants; +import javax.swing.border.TitledBorder; + +import org.unitConverter.math.ConditionalExistenceCollections; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.unit.BaseDimension; +import org.unitConverter.unit.BritishImperial; +import org.unitConverter.unit.LinearUnit; +import org.unitConverter.unit.LinearUnitValue; +import org.unitConverter.unit.NameSymbol; +import org.unitConverter.unit.SI; +import org.unitConverter.unit.Unit; +import org.unitConverter.unit.UnitDatabase; +import org.unitConverter.unit.UnitPrefix; +import org.unitConverter.unit.UnitValue; + +/** + * @author Adrien Hopkins + * @since 2018-12-27 + * @since v0.1.0 + */ +final class UnitConverterGUI { + /** + * A tab in the View. + */ + private enum Pane { + UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT, + SETTINGS; + } + + private static class Presenter { + /** The default place where settings are stored. */ + private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** The default place where units are stored. */ + private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; + /** The default place where dimensions are stored. */ + private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; + /** The default place where exceptions are stored. */ + private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; + + /** + * Adds default units and dimensions to a database. + * + * @param database database to add to + * @since 2019-04-14 + * @since v0.2.0 + */ + private static void addDefaults(final UnitDatabase database) { + database.addUnit("metre", SI.METRE); + database.addUnit("kilogram", SI.KILOGRAM); + database.addUnit("gram", SI.KILOGRAM.dividedBy(1000)); + database.addUnit("second", SI.SECOND); + database.addUnit("ampere", SI.AMPERE); + database.addUnit("kelvin", SI.KELVIN); + database.addUnit("mole", SI.MOLE); + database.addUnit("candela", SI.CANDELA); + database.addUnit("bit", SI.BIT); + database.addUnit("unit", SI.ONE); + // nonlinear units - must be loaded manually + database.addUnit("tempCelsius", SI.CELSIUS); + database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); + + // load initial dimensions + database.addDimension("LENGTH", SI.Dimensions.LENGTH); + database.addDimension("MASS", SI.Dimensions.MASS); + database.addDimension("TIME", SI.Dimensions.TIME); + database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); + } + + /** + * Gets the text of a resource file as a set of strings (each one is one + * line of the text). + * + * @param filename filename to get resource from + * @return contents of file + * @since 2021-03-27 + */ + public static final List<String> getLinesFromResource(String filename) { + final List<String> lines = new ArrayList<>(); + + try (InputStream stream = inputStream(filename); + Scanner scanner = new Scanner(stream)) { + while (scanner.hasNextLine()) { + lines.add(scanner.nextLine()); + } + } catch (final IOException e) { + throw new AssertionError( + "Error occurred while loading file " + filename, e); + } + + return lines; + } + + /** + * Gets an input stream for a resource file. + * + * @param filepath file to use as resource + * @return obtained Path + * @since 2021-03-27 + */ + private static final InputStream inputStream(String filepath) { + return UnitConverterGUI.class.getResourceAsStream(filepath); + } + + /** + * @return {@code line} with any comments removed. + * @since 2021-03-13 + */ + private static final String withoutComments(String line) { + final int index = line.indexOf('#'); + return index == -1 ? line : line.substring(index); + } + + /** The presenter's associated view. */ + private final View view; + + /** The units known by the program. */ + private final UnitDatabase database; + + /** The names of all of the units */ + private final List<String> unitNames; + + /** The names of all of the prefixes */ + private final List<String> prefixNames; + + /** The names of all of the dimensions */ + private final List<String> dimensionNames; + + /** Unit names that are ignored by the metric-only/imperial-only filter */ + private final Set<String> metricExceptions; + + private final Comparator<String> prefixNameComparator; + + /** A boolean remembering whether or not one-way conversion is on */ + private boolean oneWay = true; + /** The prefix rule */ + private DefaultPrefixRepetitionRule prefixRule = null; + + // conditions for existence of From and To entries + // used for one-way conversion + private final MutablePredicate<String> fromExistenceCondition = new MutablePredicate<>( + s -> true); + + private final MutablePredicate<String> toExistenceCondition = new MutablePredicate<>( + s -> true); + + /* + * Rounding-related settings. I am using my own system, and not + * MathContext, because MathContext does not support decimal place based + * or scientific rounding, only significant digit based rounding. + */ + private int precision = 6; + + private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; + + // The "include duplicate units" setting + private boolean includeDuplicateUnits = true; + + /** + * Creates the presenter. + * + * @param view presenter's associated view + * @since 2018-12-27 + * @since v0.1.0 + */ + Presenter(final View view) { + this.view = view; + + // load initial units + this.database = new UnitDatabase( + DefaultPrefixRepetitionRule.NO_RESTRICTION); + Presenter.addDefaults(this.database); + + // load units and prefixes + try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { + this.database.loadUnitsFromStream(units); + } catch (final IOException e) { + throw new AssertionError("Loading of unitsfile.txt failed.", e); + } + + // load dimensions + try (final InputStream dimensions = inputStream( + DEFAULT_DIMENSIONS_FILEPATH)) { + this.database.loadDimensionsFromStream(dimensions); + } catch (final IOException e) { + throw new AssertionError("Loading of dimensionfile.txt failed.", e); + } + + // load metric exceptions + try { + this.metricExceptions = new HashSet<>(); + try (InputStream exceptions = inputStream( + DEFAULT_EXCEPTIONS_FILEPATH); + Scanner scanner = new Scanner(exceptions)) { + while (scanner.hasNextLine()) { + final String line = Presenter + .withoutComments(scanner.nextLine()); + if (!line.isBlank()) { + this.metricExceptions.add(line); + } + } + } + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } + + // load settings - requires database to exist + if (Files.exists(this.getSettingsFile())) { + this.loadSettings(); + } + + // a comparator that can be used to compare prefix names + // any name that does not exist is less than a name that does. + // otherwise, they are compared by value + this.prefixNameComparator = (o1, o2) -> { + if (!Presenter.this.database.containsPrefixName(o1)) + return -1; + else if (!Presenter.this.database.containsPrefixName(o2)) + return 1; + + final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); + final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); + + if (p1.getMultiplier() < p2.getMultiplier()) + return -1; + else if (p1.getMultiplier() > p2.getMultiplier()) + return 1; + + return o1.compareTo(o2); + }; + + this.unitNames = new ArrayList<>( + this.database.unitMapPrefixless(true).keySet()); + this.unitNames.sort(null); // sorts it using Comparable + + this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); + this.prefixNames.sort(this.prefixNameComparator); // sorts it using my + // comparator + + this.dimensionNames = new DelegateListModel<>( + new ArrayList<>(this.database.dimensionMap().keySet())); + this.dimensionNames.sort(null); // sorts it using Comparable + + // a Predicate that returns true iff the argument is a full base unit + final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit + && ((LinearUnit) unit).isBase(); + + // print out unit counts + System.out.printf( + "Successfully loaded %d units with %d unit names (%d base units).%n", + this.database.unitMapPrefixless(false).size(), + this.database.unitMapPrefixless(true).size(), + this.database.unitMapPrefixless(false).values().stream() + .filter(isFullBase).count()); + } + + /** + * Converts in the dimension-based converter + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public final void convertDimensionBased() { + final String fromSelection = this.view.getFromSelection(); + if (fromSelection == null) { + this.view.showErrorDialog("Error", + "No unit selected in From field"); + return; + } + final String toSelection = this.view.getToSelection(); + if (toSelection == null) { + this.view.showErrorDialog("Error", "No unit selected in To field"); + return; + } + + final Unit from = this.database.getUnit(fromSelection); + final Unit to = this.database.getUnit(toSelection) + .withName(NameSymbol.ofName(toSelection)); + + final UnitValue beforeValue; + try { + beforeValue = UnitValue.of(from, + this.view.getDimensionConverterInput()); + } catch (final ParseException e) { + this.view.showErrorDialog("Error", + "Error in parsing: " + e.getMessage()); + return; + } + final UnitValue value = beforeValue.convertTo(to); + + final String output = this.getRoundedString(value); + + this.view.setDimensionConverterOutputText( + String.format("%s = %s", beforeValue, output)); + } + + /** + * Runs whenever the convert button is pressed. + * + * <p> + * Reads and parses a unit expression from the from and to boxes, then + * converts {@code from} to {@code to}. Any errors are shown in + * JOptionPanes. + * </p> + * + * @since 2019-01-26 + * @since v0.1.0 + */ + public final void convertExpressions() { + final String fromUnitString = this.view.getFromText(); + final String toUnitString = this.view.getToText(); + + if (fromUnitString.isEmpty()) { + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the From: box."); + return; + } + if (toUnitString.isEmpty()) { + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the To: box."); + return; + } + + final LinearUnitValue from; + final Unit to; + try { + from = this.database.evaluateUnitExpression(fromUnitString); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorDialog("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); + return; + } + try { + to = this.database.getUnitFromExpression(toUnitString); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorDialog("Parse Error", + "Could not recognize text in To entry: " + e.getMessage()); + return; + } + + if (to instanceof LinearUnit) { + // convert to LinearUnitValue + final LinearUnitValue from2; + final LinearUnit to2 = ((LinearUnit) to) + .withName(NameSymbol.ofName(toUnitString)); + final boolean useSlash; + + if (from.canConvertTo(to2)) { + from2 = from; + useSlash = false; + } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) { + from2 = LinearUnitValue.ONE.dividedBy(from); + useSlash = true; + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + return; + } + + final LinearUnitValue converted = from2.convertTo(to2); + this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + + String.format("%s = %s", fromUnitString, + this.getRoundedString(converted, false))); + return; + } else { + // convert to UnitValue + final UnitValue from2 = from.asUnitValue(); + if (from2.canConvertTo(to)) { + final UnitValue converted = from2.convertTo(to); + + this.view + .setExpressionConverterOutputText(String.format("%s = %s", + fromUnitString, this.getRoundedString(converted))); + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + } + } + } + + /** + * @return a list of all of the unit dimensions + * @since 2019-04-13 + * @since v0.2.0 + */ + public final List<String> dimensionNameList() { + return this.dimensionNames; + } + + /** + * @return a list of all the entries in the dimension-based converter's + * From box + * @since 2020-08-27 + */ + public final Set<String> fromEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.fromExistenceCondition); + } + + /** + * @return a comparator to compare prefix names + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Comparator<String> getPrefixNameComparator() { + return this.prefixNameComparator; + } + + /** + * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit + * converter's rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final LinearUnitValue value, + boolean showUncertainty) { + switch (this.roundingType) { + case DECIMAL_PLACES: + case SIGNIFICANT_DIGITS: + return this.getRoundedString(value.asUnitValue()); + case SCIENTIFIC: + return value.toString(showUncertainty); + default: + throw new AssertionError("Invalid switch condition."); + } + } + + /** + * Like {@link UnitValue#toString()}, but obeys this unit converter's + * rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final UnitValue value) { + final BigDecimal unrounded = new BigDecimal(value.getValue()); + final BigDecimal rounded; + int precision = this.precision; + + switch (this.roundingType) { + case DECIMAL_PLACES: + rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); + break; + case SCIENTIFIC: + precision = 12; + //$FALL-THROUGH$ + case SIGNIFICANT_DIGITS: + rounded = unrounded + .round(new MathContext(precision, RoundingMode.HALF_EVEN)); + break; + default: + throw new AssertionError("Invalid switch condition."); + } + + String output = rounded.toString(); + + // remove trailing zeroes + if (output.contains(".")) { + while (output.endsWith("0")) { + output = output.substring(0, output.length() - 1); + } + if (output.endsWith(".")) { + output = output.substring(0, output.length() - 1); + } + } + + return output + " " + value.getUnit().getPrimaryName().get(); + } + + /** + * @return The file where settings are stored; + * @since 2020-12-11 + */ + private final Path getSettingsFile() { + return Path.of(DEFAULT_SETTINGS_FILEPATH); + } + + /** + * Loads settings from the settings file. + * + * @since 2021-02-17 + */ + public final void loadSettings() { + try { + // read file line by line + final int lineNum = 0; + for (final String line : Files + .readAllLines(this.getSettingsFile())) { + final int equalsIndex = line.indexOf('='); + if (equalsIndex == -1) + throw new IllegalStateException( + "Settings file is malformed at line " + lineNum); + + final String param = line.substring(0, equalsIndex); + final String value = line.substring(equalsIndex + 1); + + switch (param) { + // set manually to avoid the unnecessary saving of the non-manual + // methods + case "precision": + this.precision = Integer.valueOf(value); + break; + case "rounding_type": + this.roundingType = RoundingType.valueOf(value); + break; + case "prefix_rule": + this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value); + this.database.setPrefixRepetitionRule(this.prefixRule); + break; + case "one_way": + this.oneWay = Boolean.valueOf(value); + if (this.oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName) + .isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + break; + case "include_duplicates": + this.includeDuplicateUnits = Boolean.valueOf(value); + if (this.view.presenter != null) { + this.view.update(); + } + break; + default: + System.err.printf("Warning: unrecognized setting \"%s\".", + param); + break; + } + } + } catch (final IOException e) {} + } + + /** + * @return a set of all prefix names in the database + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Set<String> prefixNameSet() { + return this.database.prefixMap().keySet(); + } + + /** + * Runs whenever a prefix is selected in the viewer. + * <p> + * Shows its information in the text box to the right. + * </p> + * + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void prefixSelected() { + final String prefixName = this.view.getPrefixViewerSelection(); + if (prefixName == null) + return; + else { + final UnitPrefix prefix = this.database.getPrefix(prefixName); + + this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", + prefixName, prefix.getMultiplier())); + } + } + + /** + * Saves the settings to the settings file. + * + * @since 2021-02-17 + */ + public final void saveSettings() { + try (BufferedWriter writer = Files + .newBufferedWriter(this.getSettingsFile())) { + writer.write(String.format("precision=%d\n", this.precision)); + writer.write( + String.format("rounding_type=%s\n", this.roundingType)); + writer.write(String.format("prefix_rule=%s\n", this.prefixRule)); + writer.write(String.format("one_way=%s\n", this.oneWay)); + writer.write(String.format("include_duplicates=%s\n", + this.includeDuplicateUnits)); + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorDialog("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + } + } + + public final void setIncludeDuplicateUnits( + boolean includeDuplicateUnits) { + this.includeDuplicateUnits = includeDuplicateUnits; + + this.view.update(); + this.saveSettings(); + } + + /** + * Enables or disables one-way conversion. + * + * @param oneWay whether one-way conversion should be on (true) or off + * (false) + * @since 2020-08-27 + */ + public final void setOneWay(boolean oneWay) { + this.oneWay = oneWay; + if (oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName).isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + + this.saveSettings(); + } + + /** + * @param precision new value of precision + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void setPrecision(final int precision) { + this.precision = precision; + + this.saveSettings(); + } + + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public void setPrefixRepetitionRule( + Predicate<List<UnitPrefix>> prefixRepetitionRule) { + if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) { + this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule; + } else { + this.prefixRule = null; + } + this.database.setPrefixRepetitionRule(prefixRepetitionRule); + + this.saveSettings(); + } + + /** + * @param roundingType the roundingType to set + * @since 2020-07-16 + */ + public final void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + + this.saveSettings(); + } + + /** + * @return a list of all the entries in the dimension-based converter's To + * box + * @since 2020-08-27 + */ + public final Set<String> toEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.toExistenceCondition); + } + + /** + * Returns true if and only if the unit represented by {@code unitName} + * has the dimension represented by {@code dimensionName}. + * + * @param unitName name of unit to test + * @param dimensionName name of dimension to test + * @return whether unit has dimenision + * @since 2019-04-13 + * @since v0.2.0 + */ + public final boolean unitMatchesDimension(final String unitName, + final String dimensionName) { + final Unit unit = this.database.getUnit(unitName); + final ObjectProduct<BaseDimension> dimension = this.database + .getDimension(dimensionName); + return unit.getDimension().equals(dimension); + } + + /** + * Runs whenever a unit is selected in the viewer. + * <p> + * Shows its information in the text box to the right. + * </p> + * + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void unitNameSelected() { + final String unitName = this.view.getUnitViewerSelection(); + if (unitName == null) + return; + else { + final Unit unit = this.database.getUnit(unitName); + + this.view.setUnitTextBoxText(unit.toString()); + } + } + + /** + * @return a set of all of the unit names + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Set<String> unitNameSet() { + return this.database.unitMapPrefixless(this.includeDuplicateUnits) + .keySet(); + } + } + + /** + * Different types of rounding. + * + * Significant digits: Rounds to a number of digits. i.e. with precision 5, + * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits + * after the decimal point, i.e. with precision 5, 12345.6789 rounds to + * 12345.67890. Scientific: Rounds based on the number of digits and + * operations, following standard scientific rounding. + */ + private static enum RoundingType { + SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; + } + + private static class View { + private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + + /** The view's frame. */ + private final JFrame frame; + /** The view's associated presenter. */ + private final Presenter presenter; + /** The master pane containing all of the tabs. */ + private final JTabbedPane masterPane; + + // DIMENSION-BASED CONVERTER + /** The panel for inputting values in the dimension-based converter */ + private final JTextField valueInput; + /** The panel for "From" in the dimension-based converter */ + private final SearchBoxList fromSearch; + /** The panel for "To" in the dimension-based converter */ + private final SearchBoxList toSearch; + /** The output area in the dimension-based converter */ + private final JTextArea dimensionBasedOutput; + + // EXPRESSION-BASED CONVERTER + /** The "From" entry in the conversion panel */ + private final JTextField fromEntry; + /** The "To" entry in the conversion panel */ + private final JTextField toEntry; + /** The output area in the conversion panel */ + private final JTextArea output; + + // UNIT AND PREFIX VIEWERS + /** The searchable list of unit names in the unit viewer */ + private final SearchBoxList unitNameList; + /** The searchable list of prefix names in the prefix viewer */ + private final SearchBoxList prefixNameList; + /** The text box for unit data in the unit viewer */ + private final JTextArea unitTextBox; + /** The text box for prefix data in the prefix viewer */ + private final JTextArea prefixTextBox; + + /** + * Creates the {@code View}. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + public View() { + this.presenter = new Presenter(this); + this.frame = new JFrame("Unit Converter"); + this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + // enable system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | UnsupportedLookAndFeelException e) { + // oh well, just use default theme + System.err.println("Failed to enable system look-and-feel."); + e.printStackTrace(); + } + + // create the components + this.masterPane = new JTabbedPane(); + this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); + this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), + this.presenter.getPrefixNameComparator(), true); + this.unitTextBox = new JTextArea(); + this.prefixTextBox = new JTextArea(); + this.fromSearch = new SearchBoxList(this.presenter.fromEntries()); + this.toSearch = new SearchBoxList(this.presenter.toEntries()); + this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); + this.dimensionBasedOutput = new JTextArea(2, 32); + this.fromEntry = new JTextField(); + this.toEntry = new JTextField(); + this.output = new JTextArea(2, 32); + + // create more components + this.initComponents(); + + this.frame.pack(); + } + + /** + * @return the currently selected pane. + * @throws AssertionError if no pane (or an invalid pane) is selected + */ + public Pane getActivePane() { + switch (this.masterPane.getSelectedIndex()) { + case 0: + return Pane.UNIT_CONVERTER; + case 1: + return Pane.EXPRESSION_CONVERTER; + case 2: + return Pane.UNIT_VIEWER; + case 3: + return Pane.PREFIX_VIEWER; + case 4: + return Pane.ABOUT; + case 5: + return Pane.SETTINGS; + default: + throw new AssertionError("No selected pane, or invalid pane."); + } + } + + /** + * @return value in dimension-based converter + * @throws ParseException + * @since 2020-07-07 + */ + public double getDimensionConverterInput() throws ParseException { + final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); + if (value instanceof Double) + return (double) value; + else if (value instanceof Long) + return ((Long) value).longValue(); + else + throw new AssertionError(); + } + + /** + * @return selection in "From" selector in dimension-based converter + * @since 2019-04-13 + * @since v0.2.0 + */ + public String getFromSelection() { + return this.fromSearch.getSelectedValue(); + } + + /** + * @return text in "From" box in converter panel + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getFromText() { + return this.fromEntry.getText(); + } + + /** + * @return index of selected prefix in prefix viewer + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getPrefixViewerSelection() { + return this.prefixNameList.getSelectedValue(); + } + + /** + * @return selection in "To" selector in dimension-based converter + * @since 2019-04-13 + * @since v0.2.0 + */ + public String getToSelection() { + return this.toSearch.getSelectedValue(); + } + + /** + * @return text in "To" box in converter panel + * @since 2019-01-26 + * @since v0.1.0 + */ + public String getToText() { + return this.toEntry.getText(); + } + + /** + * @return index of selected unit in unit viewer + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getUnitViewerSelection() { + return this.unitNameList.getSelectedValue(); + } + + /** + * Starts up the application. + * + * @since 2018-12-27 + * @since v0.1.0 + */ + public final void init() { + this.frame.setVisible(true); + } + + /** + * Initializes the view's components. + * + * @since 2018-12-27 + * @since v0.1.0 + */ + private final void initComponents() { + final JPanel masterPanel = new JPanel(); + this.frame.add(masterPanel); + + masterPanel.setLayout(new BorderLayout()); + + { // pane with all of the tabs + masterPanel.add(this.masterPane, BorderLayout.CENTER); + + // update stuff + this.masterPane.addChangeListener(e -> this.update()); + + { // a panel for unit conversion using a selector + final JPanel convertUnitPanel = new JPanel(); + this.masterPane.addTab("Convert Units", convertUnitPanel); + this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); + + convertUnitPanel.setLayout(new BorderLayout()); + + { // panel for input part + final JPanel inputPanel = new JPanel(); + convertUnitPanel.add(inputPanel, BorderLayout.CENTER); + + inputPanel.setLayout(new GridLayout(1, 3)); + + final JComboBox<String> dimensionSelector = new JComboBox<>( + this.presenter.dimensionNameList() + .toArray(new String[0])); + dimensionSelector.setSelectedItem("LENGTH"); + + // handle dimension filter + final MutablePredicate<String> dimensionFilter = new MutablePredicate<>( + s -> true); + + // panel for From things + inputPanel.add(this.fromSearch); + + this.fromSearch.addSearchFilter(dimensionFilter); + + { // for dimension selector and arrow that represents + // conversion + final JPanel inBetweenPanel = new JPanel(); + inputPanel.add(inBetweenPanel); + + inBetweenPanel.setLayout(new BorderLayout()); + + { // dimension selector + inBetweenPanel.add(dimensionSelector, + BorderLayout.PAGE_START); + } + + { // the arrow in the middle + final JLabel arrowLabel = new JLabel("->"); + inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); + } + } + + // panel for To things + + inputPanel.add(this.toSearch); + + this.toSearch.addSearchFilter(dimensionFilter); + + // code for dimension filter + dimensionSelector.addItemListener(e -> { + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); + this.fromSearch.reapplyFilter(); + this.toSearch.reapplyFilter(); + }); + + // apply the item listener once because I have a default + // selection + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); + this.fromSearch.reapplyFilter(); + this.toSearch.reapplyFilter(); + } + + { // panel for submit and output, and also value entry + final JPanel outputPanel = new JPanel(); + convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); + + outputPanel.setLayout(new GridLayout(3, 1)); + + { // unit input + final JPanel valueInputPanel = new JPanel(); + outputPanel.add(valueInputPanel); + + valueInputPanel.setLayout(new BorderLayout()); + + { // prompt + final JLabel valuePrompt = new JLabel( + "Value to convert: "); + valueInputPanel.add(valuePrompt, + BorderLayout.LINE_START); + } + + { // value to convert + valueInputPanel.add(this.valueInput, + BorderLayout.CENTER); + } + } + + { // button to convert + final JButton convertButton = new JButton("Convert"); + outputPanel.add(convertButton); + + convertButton.addActionListener( + e -> this.presenter.convertDimensionBased()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + } + + { // output of conversion + outputPanel.add(this.dimensionBasedOutput); + this.dimensionBasedOutput.setEditable(false); + } + } + } + + { // panel for unit conversion using expressions + final JPanel convertExpressionPanel = new JPanel(); + this.masterPane.addTab("Convert Unit Expressions", + convertExpressionPanel); + this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); + + convertExpressionPanel.setLayout(new GridLayout(4, 1)); + + { // panel for units to convert from + final JPanel fromPanel = new JPanel(); + convertExpressionPanel.add(fromPanel); + + fromPanel.setBorder(BorderFactory.createTitledBorder("From")); + fromPanel.setLayout(new GridLayout(1, 1)); + + { // entry for units + fromPanel.add(this.fromEntry); + } + } + + { // panel for units to convert to + final JPanel toPanel = new JPanel(); + convertExpressionPanel.add(toPanel); + + toPanel.setBorder(BorderFactory.createTitledBorder("To")); + toPanel.setLayout(new GridLayout(1, 1)); + + { // entry for units + toPanel.add(this.toEntry); + } + } + + { // button to convert + final JButton convertButton = new JButton("Convert"); + convertExpressionPanel.add(convertButton); + + convertButton.addActionListener( + e -> this.presenter.convertExpressions()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + } + + { // output of conversion + final JPanel outputPanel = new JPanel(); + convertExpressionPanel.add(outputPanel); + + outputPanel + .setBorder(BorderFactory.createTitledBorder("Output")); + outputPanel.setLayout(new GridLayout(1, 1)); + + { // output + outputPanel.add(this.output); + this.output.setEditable(false); + } + } + } + + { // panel to look up units + final JPanel unitLookupPanel = new JPanel(); + this.masterPane.addTab("Unit Viewer", unitLookupPanel); + this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); + + unitLookupPanel.setLayout(new GridLayout()); + + { // search panel + unitLookupPanel.add(this.unitNameList); + + this.unitNameList.getSearchList().addListSelectionListener( + e -> this.presenter.unitNameSelected()); + } + + { // the text box for unit's toString + unitLookupPanel.add(this.unitTextBox); + this.unitTextBox.setEditable(false); + this.unitTextBox.setLineWrap(true); + } + } + + { // panel to look up prefixes + final JPanel prefixLookupPanel = new JPanel(); + this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); + this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); + + prefixLookupPanel.setLayout(new GridLayout(1, 2)); + + { // panel for listing and seaching + prefixLookupPanel.add(this.prefixNameList); + + this.prefixNameList.getSearchList().addListSelectionListener( + e -> this.presenter.prefixSelected()); + } + + { // the text box for prefix's toString + prefixLookupPanel.add(this.prefixTextBox); + this.prefixTextBox.setEditable(false); + this.prefixTextBox.setLineWrap(true); + } + } + + { // Info panel + final JPanel infoPanel = new JPanel(); + this.masterPane.addTab("\uD83D\uDEC8", // info (i) character + new JScrollPane(infoPanel)); + + final JTextArea infoTextArea = new JTextArea(); + infoTextArea.setEditable(false); + infoTextArea.setOpaque(false); + infoPanel.add(infoTextArea); + + // get info text + final String infoText = Presenter + .getLinesFromResource("/about.txt").stream() + .map(Presenter::withoutComments) + .collect(Collectors.joining("\n")); + infoTextArea.setText(infoText); + } + + { // Settings panel + final JPanel settingsPanel = new JPanel(); + this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); + this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); + + settingsPanel.setLayout( + new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); + + { // rounding settings + final JPanel roundingPanel = new JPanel(); + settingsPanel.add(roundingPanel); + roundingPanel + .setBorder(new TitledBorder("Rounding Settings")); + roundingPanel.setLayout(new GridBagLayout()); + + // rounding rule selection + final ButtonGroup roundingRuleButtons = new ButtonGroup(); + + final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); + roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrecision = new JRadioButton( + "Fixed Precision"); + if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { + fixedPrecision.setSelected(true); + } + fixedPrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + roundingRuleButtons.add(fixedPrecision); + roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedDecimals = new JRadioButton( + "Fixed Decimal Places"); + if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { + fixedDecimals.setSelected(true); + } + fixedDecimals.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.DECIMAL_PLACES)); + roundingRuleButtons.add(fixedDecimals); + roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton relativePrecision = new JRadioButton( + "Scientific Precision"); + if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { + relativePrecision.setSelected(true); + } + relativePrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SCIENTIFIC)); + roundingRuleButtons.add(relativePrecision); + roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JLabel sliderLabel = new JLabel("Precision:"); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JSlider sigDigSlider = new JSlider(0, 12); + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) + .setAnchor(GridBagConstraints.LINE_START).build()); + + sigDigSlider.setMajorTickSpacing(4); + sigDigSlider.setMinorTickSpacing(1); + sigDigSlider.setSnapToTicks(true); + sigDigSlider.setPaintTicks(true); + sigDigSlider.setPaintLabels(true); + sigDigSlider.setValue(this.presenter.precision); + + sigDigSlider.addChangeListener(e -> this.presenter + .setPrecision(sigDigSlider.getValue())); + } + + { // prefix repetition settings + final JPanel prefixRepetitionPanel = new JPanel(); + settingsPanel.add(prefixRepetitionPanel); + prefixRepetitionPanel.setBorder( + new TitledBorder("Prefix Repetition Settings")); + prefixRepetitionPanel.setLayout(new GridBagLayout()); + + // prefix rules + final ButtonGroup prefixRuleButtons = new ButtonGroup(); + + final JRadioButton noRepetition = new JRadioButton( + "No Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { + noRepetition.setSelected(true); + } + noRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_REPETITION)); + prefixRuleButtons.add(noRepetition); + prefixRepetitionPanel.add(noRepetition, + new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + + final JRadioButton noRestriction = new JRadioButton( + "No Restriction"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { + noRestriction.setSelected(true); + } + noRestriction.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_RESTRICTION)); + prefixRuleButtons.add(noRestriction); + prefixRepetitionPanel.add(noRestriction, + new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + + final JRadioButton customRepetition = new JRadioButton( + "Complex Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { + customRepetition.setSelected(true); + } + customRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); + prefixRuleButtons.add(customRepetition); + prefixRepetitionPanel.add(customRepetition, + new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + } + + { // search settings + final JPanel searchingPanel = new JPanel(); + settingsPanel.add(searchingPanel); + searchingPanel.setBorder(new TitledBorder("Search Settings")); + searchingPanel.setLayout(new GridBagLayout()); + + // searching rules + final ButtonGroup searchRuleButtons = new ButtonGroup(); + + final JRadioButton noPrefixes = new JRadioButton( + "Never Include Prefixed Units"); + noPrefixes.setEnabled(false); + searchRuleButtons.add(noPrefixes); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrefixes = new JRadioButton( + "Include Some Prefixes"); + fixedPrefixes.setEnabled(false); + searchRuleButtons.add(fixedPrefixes); + searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton explicitPrefixes = new JRadioButton( + "Include Explicit Prefixes"); + explicitPrefixes.setEnabled(false); + searchRuleButtons.add(explicitPrefixes); + searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton alwaysInclude = new JRadioButton( + "Include All Single Prefixes"); + alwaysInclude.setEnabled(false); + searchRuleButtons.add(alwaysInclude); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + + { // miscellaneous settings + final JPanel miscPanel = new JPanel(); + settingsPanel.add(miscPanel); + miscPanel + .setBorder(new TitledBorder("Miscellaneous Settings")); + miscPanel.setLayout(new GridBagLayout()); + + final JCheckBox oneWay = new JCheckBox( + "Convert One Way Only"); + oneWay.setSelected(this.presenter.oneWay); + oneWay.addItemListener( + e -> this.presenter.setOneWay(e.getStateChange() == 1)); + miscPanel.add(oneWay, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JCheckBox showAllVariations = new JCheckBox( + "Show Duplicates in \"Convert Units\""); + showAllVariations + .setSelected(this.presenter.includeDuplicateUnits); + showAllVariations.addItemListener(e -> this.presenter + .setIncludeDuplicateUnits(e.getStateChange() == 1)); + miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JButton unitFileButton = new JButton( + "Manage Unit Data Files"); + unitFileButton.setEnabled(false); + miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + } + } + } + + /** + * Sets the text in the output of the dimension-based converter. + * + * @param text text to set + * @since 2019-04-13 + * @since v0.2.0 + */ + public void setDimensionConverterOutputText(final String text) { + this.dimensionBasedOutput.setText(text); + } + + /** + * Sets the text in the output of the conversion panel. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setExpressionConverterOutputText(final String text) { + this.output.setText(text); + } + + /** + * Sets the text of the prefix text box in the prefix viewer. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setPrefixTextBoxText(final String text) { + this.prefixTextBox.setText(text); + } + + /** + * Sets the text of the unit text box in the unit viewer. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setUnitTextBoxText(final String text) { + this.unitTextBox.setText(text); + } + + /** + * Shows an error dialog. + * + * @param title title of dialog + * @param message message in dialog + * @since 2019-01-14 + * @since v0.1.0 + */ + public void showErrorDialog(final String title, final String message) { + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); + } + + public void update() { + this.unitNameList.setItems(this.presenter.unitNameSet()); + this.fromSearch.setItems(this.presenter.fromEntries()); + this.toSearch.setItems(this.presenter.toEntries()); + + switch (this.getActivePane()) { + case UNIT_CONVERTER: + this.fromSearch.updateList(); + this.toSearch.updateList(); + break; + default: + // do nothing, for now + break; + } + } + } + + public static void main(final String[] args) { + new View().init(); + } +} diff --git a/src/org/unitConverter/converterGUI/package-info.java b/src/main/java/org/unitConverter/converterGUI/package-info.java index 1555291..d85ecab 100644 --- a/src/org/unitConverter/converterGUI/package-info.java +++ b/src/main/java/org/unitConverter/converterGUI/package-info.java @@ -15,7 +15,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** - * All classes that work to convert units. + * The GUI interface of the Unit Converter. * * @author Adrien Hopkins * @since 2019-01-25 diff --git a/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java b/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java new file mode 100644 index 0000000..000658b --- /dev/null +++ b/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java @@ -0,0 +1,468 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.math; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Elements in these wrapper collections only exist if they pass a condition. + * <p> + * All of the collections in this class are "views" of the provided collections. + * They are mutable if the provided collections are mutable, they allow null if + * the provided collections allow null, they will reflect changes in the + * provided collection, etc. + * <p> + * The modification operations will always run the corresponding operations, + * even if the conditional existence collection doesn't change. For example, if + * you have a set that ignores even numbers, add(2) will still add a 2 to the + * backing set (but the conditional existence set will say it doesn't exist). + * <p> + * The returned collections do <i>not</i> pass the hashCode and equals + * operations through to the backing collections, but rely on {@code Object}'s + * {@code equals} and {@code hashCode} methods. This is necessary to preserve + * the contracts of these operations in the case that the backing collections + * are sets or lists. + * <p> + * 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 + */ +// 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 + * @param <E> type of element in collection + */ + static final class ConditionalExistenceCollection<E> + extends AbstractCollection<E> { + final Collection<E> collection; + final Predicate<E> existenceCondition; + + /** + * Creates the {@code ConditionalExistenceCollection}. + * + * @param collection + * @param existenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceCollection(final Collection<E> collection, + final Predicate<E> existenceCondition) { + this.collection = collection; + this.existenceCondition = existenceCondition; + } + + @Override + public boolean add(final E e) { + return this.collection.add(e) && this.existenceCondition.test(e); + } + + @Override + public void clear() { + this.collection.clear(); + } + + @Override + public boolean contains(final Object o) { + if (!this.collection.contains(o)) + return false; + + // this collection can only contain instances of E + // since the object is in the collection, we know that it must be an + // instance of E + // therefore this cast will always work + @SuppressWarnings("unchecked") + final E e = (E) o; + + return this.existenceCondition.test(e); + } + + @Override + public Iterator<E> iterator() { + return conditionalExistenceIterator(this.collection.iterator(), + this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + // remove() must be first in the && statement, otherwise it may not + // execute + final boolean containedObject = this.contains(o); + return this.collection.remove(o) && containedObject; + } + + @Override + public int size() { + return (int) this.collection.stream().filter(this.existenceCondition) + .count(); + } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public <T> T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(a); + } + } + + /** + * Elements in this wrapper iterator only exist if they pass a condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> type of elements in iterator + */ + static final class ConditionalExistenceIterator<E> implements Iterator<E> { + final Iterator<E> iterator; + final Predicate<E> existenceCondition; + E nextElement; + boolean hasNext; + + /** + * Creates the {@code ConditionalExistenceIterator}. + * + * @param iterator + * @param condition + * @since 2019-10-17 + */ + private ConditionalExistenceIterator(final Iterator<E> iterator, + final Predicate<E> condition) { + this.iterator = iterator; + this.existenceCondition = condition; + this.getAndSetNextElement(); + } + + /** + * Gets the next element, and sets nextElement and hasNext accordingly. + * + * @since 2019-10-17 + */ + private void getAndSetNextElement() { + do { + if (!this.iterator.hasNext()) { + this.nextElement = null; + this.hasNext = false; + return; + } + this.nextElement = this.iterator.next(); + } while (!this.existenceCondition.test(this.nextElement)); + this.hasNext = true; + } + + @Override + public boolean hasNext() { + return this.hasNext; + } + + @Override + public E next() { + if (this.hasNext()) { + final E next = this.nextElement; + this.getAndSetNextElement(); + return next; + } else + throw new NoSuchElementException(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + } + + /** + * Mappings in this map only exist if the entry passes some condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <K> key type + * @param <V> value type + */ + static final class ConditionalExistenceMap<K, V> extends AbstractMap<K, V> { + Map<K, V> map; + Predicate<Entry<K, V>> entryExistenceCondition; + + /** + * Creates the {@code ConditionalExistenceMap}. + * + * @param map + * @param entryExistenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceMap(final Map<K, V> map, + final Predicate<Entry<K, V>> entryExistenceCondition) { + this.map = map; + this.entryExistenceCondition = entryExistenceCondition; + } + + @Override + public boolean containsKey(final Object key) { + if (!this.map.containsKey(key)) + return false; + + // only instances of K have mappings in the backing map + // since we know that key is a valid key, it must be an instance of K + @SuppressWarnings("unchecked") + final K keyAsK = (K) key; + + // get and test entry + final V value = this.map.get(key); + final Entry<K, V> entry = new SimpleEntry<>(keyAsK, value); + return this.entryExistenceCondition.test(entry); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return conditionalExistenceSet(this.map.entrySet(), + this.entryExistenceCondition); + } + + @Override + public V get(final Object key) { + return this.containsKey(key) ? this.map.get(key) : null; + } + + private final Entry<K, V> getEntry(K key) { + return new Entry<>() { + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return ConditionalExistenceMap.this.map.get(key); + } + + @Override + public V setValue(V value) { + return ConditionalExistenceMap.this.map.put(key, value); + } + }; + } + + @Override + public Set<K> keySet() { + return conditionalExistenceSet(super.keySet(), + k -> this.entryExistenceCondition.test(this.getEntry(k))); + } + + @Override + public V put(final K key, final V value) { + final V oldValue = this.map.put(key, value); + + // get and test entry + final Entry<K, V> entry = new SimpleEntry<>(key, oldValue); + return this.entryExistenceCondition.test(entry) ? oldValue : null; + } + + @Override + public V remove(final Object key) { + final V oldValue = this.map.remove(key); + return this.containsKey(key) ? oldValue : null; + } + + @Override + public Collection<V> values() { + // maybe change this to use ConditionalExistenceCollection + return super.values(); + } + } + + /** + * Elements in this set only exist if a certain condition is true. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> type of element in set + */ + static final class ConditionalExistenceSet<E> extends AbstractSet<E> { + private final Set<E> set; + private final Predicate<E> existenceCondition; + + /** + * Creates the {@code ConditionalNonexistenceSet}. + * + * @param set set to use + * @param existenceCondition condition where element exists + * @since 2019-10-17 + */ + private ConditionalExistenceSet(final Set<E> set, + final Predicate<E> existenceCondition) { + this.set = set; + this.existenceCondition = existenceCondition; + } + + /** + * {@inheritDoc} + * <p> + * Note that this method returns {@code false} if {@code e} does not pass + * the existence condition. + */ + @Override + public boolean add(final E e) { + return this.set.add(e) && this.existenceCondition.test(e); + } + + @Override + public void clear() { + this.set.clear(); + } + + @Override + public boolean contains(final Object o) { + if (!this.set.contains(o)) + return false; + + // this set can only contain instances of E + // since the object is in the set, we know that it must be an instance + // of E + // therefore this cast will always work + @SuppressWarnings("unchecked") + final E e = (E) o; + + return this.existenceCondition.test(e); + } + + @Override + public Iterator<E> iterator() { + return conditionalExistenceIterator(this.set.iterator(), + this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + // remove() must be first in the && statement, otherwise it may not + // execute + final boolean containedObject = this.contains(o); + return this.set.remove(o) && containedObject; + } + + @Override + public int size() { + return (int) this.set.stream().filter(this.existenceCondition).count(); + } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public <T> T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(a); + } + } + + /** + * Elements in the returned wrapper collection are ignored if they don't pass + * a condition. + * + * @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 + */ + public static final <E> Collection<E> conditionalExistenceCollection( + final Collection<E> collection, + final Predicate<E> existenceCondition) { + return new ConditionalExistenceCollection<>(collection, + existenceCondition); + } + + /** + * 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 + */ + public static final <E> Iterator<E> conditionalExistenceIterator( + final Iterator<E> iterator, final Predicate<E> existenceCondition) { + return new ConditionalExistenceIterator<>(iterator, existenceCondition); + } + + /** + * 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 + */ + public static final <K, V> Map<K, V> conditionalExistenceMap( + final Map<K, V> map, + final Predicate<Entry<K, V>> entryExistenceCondition) { + return new ConditionalExistenceMap<>(map, entryExistenceCondition); + } + + /** + * 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 + */ + public static final <E> Set<E> conditionalExistenceSet(final Set<E> set, + final Predicate<E> existenceCondition) { + return new ConditionalExistenceSet<>(set, existenceCondition); + } +} diff --git a/src/main/java/org/unitConverter/math/DecimalComparison.java b/src/main/java/org/unitConverter/math/DecimalComparison.java new file mode 100644 index 0000000..0f5b91e --- /dev/null +++ b/src/main/java/org/unitConverter/math/DecimalComparison.java @@ -0,0 +1,256 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.math; + +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 + */ +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 + */ + public static final double DOUBLE_EPSILON = 1.0e-15; + + /** + * The value used for float comparison. If two float values are within this + * value multiplied by the larger value, they are considered equal. + * + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final float FLOAT_EPSILON = 1.0e-6f; + + /** + * Tests for equality of double values using {@link #DOUBLE_EPSILON}. + * <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))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon using + * {@link #equals(double, double, double)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + * <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) { + 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))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + * <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 + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final double a, final double b, + final double epsilon) { + 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))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon using {@link #equals(float, float, float)} + * (this does not make a violation of transitivity impossible, it just + * significantly reduces the chances of it happening) + * <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) { + 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))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + * <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 + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final float a, final float b, + final float epsilon) { + return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); + } + + /** + * Tests for equality of {@code UncertainDouble} values using + * {@link #DOUBLE_EPSILON}. + * <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))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon using + * {@link #equals(UncertainDouble, UncertainDouble, double)} (this does not + * make a violation of transitivity impossible, it just significantly reduces + * the chances of it happening) + * <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) + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b) { + return DecimalComparison.equals(a.value(), b.value()) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty()); + } + + /** + * Tests for {@code UncertainDouble} equality using a custom epsilon value. + * + * <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))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + * <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 + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b, final double epsilon) { + return DecimalComparison.equals(a.value(), b.value(), epsilon) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty(), + epsilon); + } + + /** + * Takes the hash code of doubles. Values that are equal according to + * {@link #equals(double, double)} will have the same hash code. + * + * @param d double to hash + * @return hash code of double + * @since 2019-10-16 + */ + public static final int hash(final double d) { + return Float.hashCode((float) d); + } + + // You may NOT get any DecimalComparison instances + private DecimalComparison() { + throw new AssertionError(); + } + +} diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/main/java/org/unitConverter/math/ExpressionParser.java index b2261ed..deee51d 100644 --- a/src/org/unitConverter/math/ExpressionParser.java +++ b/src/main/java/org/unitConverter/math/ExpressionParser.java @@ -32,8 +32,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 + * @param <T> type of object that exists in parsed expressions * @since 2019-03-14 * @since v0.2.0 */ @@ -42,21 +41,21 @@ 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 + * @param <T> type of object that exists in parsed expressions * @since 2019-03-17 * @since v0.2.0 */ public static final class Builder<T> { /** - * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} - * would use {@code Integer::parseInt}. + * 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 */ - private final Function<String, T> objectObtainer; - + private final Function<String, ? extends T> objectObtainer; + /** * The function of the space as an operator (like 3 x y) * @@ -64,110 +63,115 @@ public final class ExpressionParser<T> { * @since v0.2.0 */ private String spaceFunction = null; - + /** - * A map mapping operator strings to operator functions, for unary operators. + * A map mapping operator strings to operator functions, for unary + * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map<String, PriorityUnaryOperator<T>> unaryOperators; - + /** - * A map mapping operator strings to operator functions, for binary operators. + * A map mapping operator strings to operator functions, for binary + * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map<String, PriorityBinaryOperator<T>> binaryOperators; - + /** * 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 + * @param objectObtainer a function that can turn strings into objects of + * the type handled by the parser. + * @throws NullPointerException if {@code objectObtainer} is null * @since 2019-03-17 * @since v0.2.0 */ - public Builder(final Function<String, T> objectObtainer) { - this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null."); + public Builder(final Function<String, ? extends T> objectObtainer) { + this.objectObtainer = Objects.requireNonNull(objectObtainer, + "objectObtainer must not be null."); this.unaryOperators = new HashMap<>(); this.binaryOperators = new HashMap<>(); } - + /** * 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 are applied first + * @param text text used to reference the operator, like '+' + * @param operator operator to add + * @param priority operator's priority, which determines which operators + * are applied first * @return this builder - * @throws NullPointerException - * if {@code text} or {@code operator} is null + * @throws NullPointerException if {@code text} or {@code operator} is + * null * @since 2019-03-17 * @since v0.2.0 */ - public Builder<T> addBinaryOperator(final String text, final BinaryOperator<T> operator, final int priority) { + public Builder<T> addBinaryOperator(final String text, + final BinaryOperator<T> operator, final int priority) { Objects.requireNonNull(text, "text must not be null."); Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments. - final PriorityBinaryOperator<T> priorityOperator = new PriorityBinaryOperator<T>(priority) { + + // Unfortunately, I cannot use a lambda because the + // PriorityBinaryOperator requires arguments. + final PriorityBinaryOperator<T> priorityOperator = new PriorityBinaryOperator<>( + priority) { @Override public T apply(final T t, final T u) { return operator.apply(t, u); } - + }; this.binaryOperators.put(text, priorityOperator); return this; } - + /** - * Adds a function for spaces. You must use the text of an existing binary operator. + * Adds a function for spaces. You must use the text of an existing binary + * operator. * - * @param operator - * text of operator to use + * @param operator text of operator to use * @return this builder * @since 2019-03-22 * @since v0.2.0 */ public Builder<T> addSpaceFunction(final String operator) { Objects.requireNonNull(operator, "operator must not be null."); - + if (!this.binaryOperators.containsKey(operator)) - throw new IllegalArgumentException(String.format("Could not find binary operator '%s'", operator)); - + throw new IllegalArgumentException(String + .format("Could not find binary operator '%s'", operator)); + this.spaceFunction = operator; return this; } - + /** * 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 are applied first + * @param text text used to reference the operator, like '-' + * @param operator operator to add + * @param priority operator's priority, which determines which operators + * are applied first * @return this builder - * @throws NullPointerException - * if {@code text} or {@code operator} is null + * @throws NullPointerException if {@code text} or {@code operator} is + * null * @since 2019-03-17 * @since v0.2.0 */ - public Builder<T> addUnaryOperator(final String text, final UnaryOperator<T> operator, final int priority) { + public Builder<T> addUnaryOperator(final String text, + final UnaryOperator<T> operator, final int priority) { Objects.requireNonNull(text, "text must not be null."); Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments. - final PriorityUnaryOperator<T> priorityOperator = new PriorityUnaryOperator<T>(priority) { + + // Unfortunately, I cannot use a lambda because the + // PriorityUnaryOperator requires arguments. + final PriorityUnaryOperator<T> priorityOperator = new PriorityUnaryOperator<>( + priority) { @Override public T apply(final T t) { return operator.apply(t); @@ -176,49 +180,50 @@ public final class ExpressionParser<T> { this.unaryOperators.put(text, priorityOperator); return this; } - + /** - * @return an {@code ExpressionParser<T>} instance with the properties given to this builder + * @return an {@code ExpressionParser<T>} instance with the properties + * given to this builder * @since 2019-03-17 * @since v0.2.0 */ public ExpressionParser<T> build() { - return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators, - this.spaceFunction); + return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, + this.binaryOperators, this.spaceFunction); } } - + /** - * A binary operator with a priority field that determines which operators apply first. + * A binary operator with a priority field that determines which operators + * apply first. * * @author Adrien Hopkins - * @param <T> - * type of operand and result + * @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>> { /** - * The operator's priority. Higher-priority operators are applied before lower-priority operators + * The operator's priority. Higher-priority operators are applied before + * lower-priority operators * * @since 2019-03-17 * @since v0.2.0 */ private final int priority; - + /** * Creates the {@code PriorityBinaryOperator}. * - * @param priority - * operator's priority + * @param priority operator's priority * @since 2019-03-17 * @since v0.2.0 */ public PriorityBinaryOperator(final int priority) { this.priority = priority; } - + /** * Compares this object to another by priority. * @@ -238,7 +243,7 @@ public final class ExpressionParser<T> { else return 0; } - + /** * @return priority * @since 2019-03-22 @@ -248,38 +253,38 @@ public final class ExpressionParser<T> { return this.priority; } } - + /** - * A unary operator with a priority field that determines which operators apply first. + * A unary operator with a priority field that determines which operators + * apply first. * * @author Adrien Hopkins - * @param <T> - * type of operand and result + * @param <T> type of operand and result * @since 2019-03-17 * @since v0.2.0 */ private static abstract class PriorityUnaryOperator<T> implements UnaryOperator<T>, Comparable<PriorityUnaryOperator<T>> { /** - * The operator's priority. Higher-priority operators are applied before lower-priority operators + * The operator's priority. Higher-priority operators are applied before + * lower-priority operators * * @since 2019-03-17 * @since v0.2.0 */ private final int priority; - + /** * Creates the {@code PriorityUnaryOperator}. * - * @param priority - * operator's priority + * @param priority operator's priority * @since 2019-03-17 * @since v0.2.0 */ public PriorityUnaryOperator(final int priority) { this.priority = priority; } - + /** * Compares this object to another by priority. * @@ -299,7 +304,7 @@ public final class ExpressionParser<T> { else return 0; } - + /** * @return priority * @since 2019-03-22 @@ -309,7 +314,7 @@ public final class ExpressionParser<T> { return this.priority; } } - + /** * The types of tokens that are available. * @@ -320,7 +325,7 @@ public final class ExpressionParser<T> { private static enum TokenType { OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; } - + /** * The opening bracket. * @@ -328,7 +333,7 @@ public final class ExpressionParser<T> { * @since v0.2.0 */ public static final char OPENING_BRACKET = '('; - + /** * The closing bracket. * @@ -336,48 +341,49 @@ public final class ExpressionParser<T> { * @since v0.2.0 */ public static final char CLOSING_BRACKET = ')'; - + /** * 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 + * @param string string that contains brackets + * @param bracketPosition position of first bracket * @return position of matching bracket - * @throws NullPointerException - * if string is null + * @throws NullPointerException if string is null * @since 2019-03-22 * @since v0.2.0 */ - private static int findBracketPair(final String string, final int bracketPosition) { + private static int findBracketPair(final String string, + final int bracketPosition) { Objects.requireNonNull(string, "string must not be null."); - + final char openingBracket = string.charAt(bracketPosition); - + // figure out what closing bracket to look for final char closingBracket; switch (openingBracket) { - case '(': - closingBracket = ')'; - break; - case '[': - closingBracket = ']'; - break; - case '{': - closingBracket = '}'; - break; - default: - throw new IllegalArgumentException(String.format("Invalid bracket '%s'", openingBracket)); + case '(': + closingBracket = ')'; + break; + case '[': + closingBracket = ']'; + break; + case '{': + closingBracket = '}'; + break; + default: + throw new IllegalArgumentException( + String.format("Invalid bracket '%s'", openingBracket)); } - - // level of brackets. every opening bracket increments this; every closing bracket decrements it + + // level of brackets. every opening bracket increments this; every closing + // bracket decrements it int bracketLevel = 0; - + // iterate over the string to find the closing bracket - for (int currentPosition = bracketPosition; currentPosition < string.length(); currentPosition++) { + for (int currentPosition = bracketPosition; currentPosition < string + .length(); currentPosition++) { final char currentCharacter = string.charAt(currentPosition); - + if (currentCharacter == openingBracket) { bracketLevel++; } else if (currentCharacter == closingBracket) { @@ -386,19 +392,19 @@ public final class ExpressionParser<T> { return currentPosition; } } - + throw new IllegalArgumentException("No matching bracket found."); } - + /** - * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would - * use {@code Integer::parseInt}. + * 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 */ - private final Function<String, T> objectObtainer; - + private final Function<String, ? extends T> objectObtainer; + /** * A map mapping operator strings to operator functions, for unary operators. * @@ -406,15 +412,16 @@ public final class ExpressionParser<T> { * @since v0.2.0 */ private final Map<String, PriorityUnaryOperator<T>> unaryOperators; - + /** - * A map mapping operator strings to operator functions, for binary operators. + * A map mapping operator strings to operator functions, for binary + * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map<String, PriorityBinaryOperator<T>> binaryOperators; - + /** * The operator for space, or null if spaces have no function. * @@ -422,127 +429,144 @@ public final class ExpressionParser<T> { * @since v0.2.0 */ private final String spaceOperator; - + /** * 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 - * @param spaceOperator - * operator used by spaces + * @param objectObtainer function to get objects from strings + * @param unaryOperators unary operators available to the parser + * @param binaryOperators binary operators available to the parser + * @param spaceOperator operator used by spaces * @since 2019-03-14 * @since v0.2.0 */ - private ExpressionParser(final Function<String, T> objectObtainer, + private ExpressionParser(final Function<String, ? extends T> objectObtainer, final Map<String, PriorityUnaryOperator<T>> unaryOperators, - final Map<String, PriorityBinaryOperator<T>> binaryOperators, final String spaceOperator) { + final Map<String, PriorityBinaryOperator<T>> binaryOperators, + final String spaceOperator) { this.objectObtainer = objectObtainer; this.unaryOperators = unaryOperators; this.binaryOperators = binaryOperators; this.spaceOperator = spaceOperator; } - + /** - * Converts a given mathematical expression to reverse Polish notation (operators after operands). + * Converts a given mathematical expression to reverse Polish notation + * (operators after operands). * <p> * For example,<br> * {@code 2 * (3 + 4)}<br> * becomes<br> * {@code 2 3 4 + *}. * - * @param expression - * expression + * @param expression expression * @return expression in RPN * @since 2019-03-17 * @since v0.2.0 */ private String convertExpressionToReversePolish(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + final List<String> components = new ArrayList<>(); - + // the part of the expression remaining to parse String partialExpression = expression; - + // find and deal with brackets while (partialExpression.indexOf(OPENING_BRACKET) != -1) { - final int openingBracketPosition = partialExpression.indexOf(OPENING_BRACKET); - final int closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition); - + final int openingBracketPosition = partialExpression + .indexOf(OPENING_BRACKET); + final int closingBracketPosition = findBracketPair(partialExpression, + openingBracketPosition); + // check for function - if (openingBracketPosition > 0 && partialExpression.charAt(openingBracketPosition - 1) != ' ') { + if (openingBracketPosition > 0 + && partialExpression.charAt(openingBracketPosition - 1) != ' ') { // function like sin(2) or tempF(32) // find the position of the last space int spacePosition = openingBracketPosition; - while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') { + while (spacePosition >= 0 + && partialExpression.charAt(spacePosition) != ' ') { spacePosition--; } - // then split the function into pre-function and function, using the space position - components.addAll(Arrays.asList(partialExpression.substring(0, spacePosition + 1).split(" "))); - components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1)); - partialExpression = partialExpression.substring(closingBracketPosition + 1); + // then split the function into pre-function and function, using the + // space position + components.addAll(Arrays.asList(partialExpression + .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.substring(0, openingBracketPosition).split(" "))); + components.addAll(Arrays.asList(partialExpression + .substring(0, openingBracketPosition).split(" "))); components.add(this.convertExpressionToReversePolish( - partialExpression.substring(openingBracketPosition + 1, closingBracketPosition))); - partialExpression = partialExpression.substring(closingBracketPosition + 1); + partialExpression.substring(openingBracketPosition + 1, + closingBracketPosition))); + partialExpression = partialExpression + .substring(closingBracketPosition + 1); } } - + // add everything else components.addAll(Arrays.asList(partialExpression.split(" "))); - + // remove empty entries while (components.contains("")) { components.remove(""); } - + // deal with space multiplication (x y) if (this.spaceOperator != null) { for (int i = 0; i < components.size() - 1; i++) { - if (this.getTokenType(components.get(i)) == TokenType.OBJECT - && this.getTokenType(components.get(i + 1)) == TokenType.OBJECT) { + if (this.getTokenType(components.get(i)) == TokenType.OBJECT && this + .getTokenType(components.get(i + 1)) == TokenType.OBJECT) { components.add(++i, this.spaceOperator); } } } - + // turn the expression into reverse Polish while (true) { - final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components); + final int highestPriorityOperatorPosition = this + .findHighestPriorityOperatorPosition(components); if (highestPriorityOperatorPosition == -1) { break; } - + // swap components based on what kind of operator there is // 1 + 2 becomes 2 1 + // - 1 becomes 1 - - switch (this.getTokenType(components.get(highestPriorityOperatorPosition))) { - case UNARY_OPERATOR: - final String unaryOperator = components.remove(highestPriorityOperatorPosition); - final String operand = components.remove(highestPriorityOperatorPosition); - components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator); - break; - case BINARY_OPERATOR: - final String binaryOperator = components.remove(highestPriorityOperatorPosition); - final String operand1 = components.remove(highestPriorityOperatorPosition - 1); - final String operand2 = components.remove(highestPriorityOperatorPosition - 1); - components.add(highestPriorityOperatorPosition - 1, - operand2 + " " + operand1 + " " + binaryOperator); - break; - default: - throw new AssertionError("Expected operator, found non-operator."); + switch (this + .getTokenType(components.get(highestPriorityOperatorPosition))) { + case UNARY_OPERATOR: + final String unaryOperator = components + .remove(highestPriorityOperatorPosition); + final String operand = components + .remove(highestPriorityOperatorPosition); + components.add(highestPriorityOperatorPosition, + operand + " " + unaryOperator); + break; + case BINARY_OPERATOR: + final String binaryOperator = components + .remove(highestPriorityOperatorPosition); + final String operand1 = components + .remove(highestPriorityOperatorPosition - 1); + final String operand2 = components + .remove(highestPriorityOperatorPosition - 1); + components.add(highestPriorityOperatorPosition - 1, + operand2 + " " + operand1 + " " + binaryOperator); + break; + default: + throw new AssertionError("Expected operator, found non-operator."); } } - - // join all of the components together, then ensure there is only one space in a row + + // 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); } @@ -551,73 +575,72 @@ public final class ExpressionParser<T> { } return expressionRPN; } - + /** * 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 operators - * @throws NullPointerException - * if components is null + * @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 + * operators + * @throws NullPointerException if components is null * @since 2019-03-22 * @since v0.2.0 */ - private int findHighestPriorityOperatorPosition(final List<String> components) { + private int findHighestPriorityOperatorPosition( + final List<String> components) { Objects.requireNonNull(components, "components must not be null."); // find highest priority int maxPriority = Integer.MIN_VALUE; int 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++) { - + switch (this.getTokenType(components.get(i))) { - case UNARY_OPERATOR: - final PriorityUnaryOperator<T> unaryOperator = this.unaryOperators.get(components.get(i)); - final int unaryPriority = unaryOperator.getPriority(); - - if (unaryPriority > maxPriority) { - maxPriority = unaryPriority; - maxPriorityPosition = i; - } - break; - case BINARY_OPERATOR: - final PriorityBinaryOperator<T> binaryOperator = this.binaryOperators.get(components.get(i)); - final int binaryPriority = binaryOperator.getPriority(); - - if (binaryPriority > maxPriority) { - maxPriority = binaryPriority; - maxPriorityPosition = i; - } - break; - default: - break; + case UNARY_OPERATOR: + final PriorityUnaryOperator<T> unaryOperator = this.unaryOperators + .get(components.get(i)); + final int unaryPriority = unaryOperator.getPriority(); + + if (unaryPriority > maxPriority) { + maxPriority = unaryPriority; + maxPriorityPosition = i; + } + break; + case BINARY_OPERATOR: + final PriorityBinaryOperator<T> binaryOperator = this.binaryOperators + .get(components.get(i)); + final int binaryPriority = binaryOperator.getPriority(); + + if (binaryPriority > maxPriority) { + maxPriority = binaryPriority; + maxPriorityPosition = i; + } + break; + default: + break; } } - + // max priority position found return maxPriorityPosition; } - + /** * Determines whether an inputted string is an object or an operator * - * @param token - * string to input + * @param token string to input * @return type of token it is - * @throws NullPointerException - * if {@code expression} is null + * @throws NullPointerException if {@code expression} is null * @since 2019-03-14 * @since v0.2.0 */ private TokenType getTokenType(final String token) { Objects.requireNonNull(token, "token must not be null."); - + if (this.unaryOperators.containsKey(token)) return TokenType.UNARY_OPERATOR; else if (this.binaryOperators.containsKey(token)) @@ -625,84 +648,88 @@ public final class ExpressionParser<T> { else return TokenType.OBJECT; } - + /** * Parses an expression. * - * @param expression - * expression to parse + * @param expression expression to parse * @return result - * @throws NullPointerException - * if {@code expression} is null + * @throws NullPointerException if {@code expression} is null * @since 2019-03-14 * @since v0.2.0 */ public T parseExpression(final String expression) { - return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression)); + return this.parseReversePolishExpression( + this.convertExpressionToReversePolish(expression)); } - + /** * Parses an expression expressed in reverse Polish notation. * - * @param expression - * expression to parse + * @param expression expression to parse * @return result - * @throws NullPointerException - * if {@code expression} is null + * @throws NullPointerException if {@code expression} is null * @since 2019-03-14 * @since v0.2.0 */ private T parseReversePolishExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + final Deque<T> stack = new ArrayDeque<>(); - + // iterate over every item in the expression, then for (final String item : expression.split(" ")) { // choose a path based on what kind of thing was just read switch (this.getTokenType(item)) { - - case BINARY_OPERATOR: - if (stack.size() < 2) - throw new IllegalStateException(String.format( - "Attempted to call binary operator %s with only %d arguments.", item, stack.size())); - - // get two arguments and operator, then apply! - final T o1 = stack.pop(); - final T o2 = stack.pop(); - final BinaryOperator<T> binaryOperator = this.binaryOperators.get(item); - - stack.push(binaryOperator.apply(o1, o2)); - break; - - case OBJECT: - // just add it to the stack - stack.push(this.objectObtainer.apply(item)); - break; - - case UNARY_OPERATOR: - if (stack.size() < 1) - throw new IllegalStateException(String.format( - "Attempted to call unary operator %s with only %d arguments.", item, stack.size())); - - // get one argument and operator, then apply! - final T o = stack.pop(); - final UnaryOperator<T> unaryOperator = this.unaryOperators.get(item); - - stack.push(unaryOperator.apply(o)); - break; - default: - throw new AssertionError( - String.format("Internal error: Invalid token type %s.", this.getTokenType(item))); - + + case BINARY_OPERATOR: + if (stack.size() < 2) + throw new IllegalStateException(String.format( + "Attempted to call binary operator %s with only %d arguments.", + item, stack.size())); + + // get two arguments and operator, then apply! + final T o1 = stack.pop(); + final T o2 = stack.pop(); + final BinaryOperator<T> binaryOperator = this.binaryOperators + .get(item); + + stack.push(binaryOperator.apply(o1, o2)); + break; + + case OBJECT: + // just add it to the stack + stack.push(this.objectObtainer.apply(item)); + break; + + case UNARY_OPERATOR: + if (stack.size() < 1) + throw new IllegalStateException(String.format( + "Attempted to call unary operator %s with only %d arguments.", + item, stack.size())); + + // get one argument and operator, then apply! + final T o = stack.pop(); + final UnaryOperator<T> unaryOperator = this.unaryOperators + .get(item); + + stack.push(unaryOperator.apply(o)); + break; + default: + throw new AssertionError( + String.format("Internal error: Invalid token type %s.", + this.getTokenType(item))); + } } - + // return answer, or throw an exception if I can't if (stack.size() > 1) - throw new IllegalStateException("Computation ended up with more than one answer."); + throw new IllegalStateException( + "Computation ended up with more than one answer."); else if (stack.size() == 0) - throw new IllegalStateException("Computation ended up without an answer."); + throw new IllegalStateException( + "Computation ended up without an answer."); return stack.pop(); } } diff --git a/src/main/java/org/unitConverter/math/ObjectProduct.java b/src/main/java/org/unitConverter/math/ObjectProduct.java new file mode 100644 index 0000000..5217d93 --- /dev/null +++ b/src/main/java/org/unitConverter/math/ObjectProduct.java @@ -0,0 +1,284 @@ +/** + * Copyright (C) 2018 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 org.unitConverter.math; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +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 + * @since 2019-10-16 + */ +public final class ObjectProduct<T> { + /** + * Returns an empty ObjectProduct of a certain type + * + * @param <T> + * type of objects that can be multiplied + * @return empty product + * @since 2019-10-16 + */ + public static final <T> ObjectProduct<T> empty() { + return new ObjectProduct<>(new HashMap<>()); + } + + /** + * 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 + */ + public static final <T> ObjectProduct<T> fromExponentMapping(final Map<T, Integer> map) { + return new ObjectProduct<>(new HashMap<>(map)); + } + + /** + * Gets an ObjectProduct that has one of the inputted argument, and nothing else. + * + * @param object + * object that will be in the product + * @return product + * @since 2019-10-16 + * @throws NullPointerException + * if object is null + */ + public static final <T> ObjectProduct<T> oneOf(final T object) { + Objects.requireNonNull(object, "object must not be null."); + final Map<T, Integer> map = new HashMap<>(); + map.put(object, 1); + return new ObjectProduct<>(map); + } + + /** + * The objects that make up the product, mapped to their exponents. This map treats zero as null, and is immutable. + * + * @since 2019-10-16 + */ + final Map<T, Integer> exponents; + + /** + * Creates the {@code ObjectProduct}. + * + * @param exponents + * objects that make up this product + * @since 2019-10-16 + */ + private ObjectProduct(final Map<T, Integer> exponents) { + this.exponents = Collections.unmodifiableMap(ConditionalExistenceCollections.conditionalExistenceMap(exponents, + e -> !Integer.valueOf(0).equals(e.getValue()))); + } + + /** + * Calculates the quotient of two products + * + * @param other + * other product + * @return quotient of two products + * @since 2019-10-16 + * @throws NullPointerException + * if other is null + */ + public ObjectProduct<T> dividedBy(final ObjectProduct<T> other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set<T> objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map<T, Integer> map = new HashMap<>(objects.size()); + for (final T key : objects) { + map.put(key, this.getExponent(key) - other.getExponent(key)); + } + + // create the product + return new ObjectProduct<>(map); + } + + // this method relies on the use of ZeroIsNullMap + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ObjectProduct)) + return false; + final ObjectProduct<?> other = (ObjectProduct<?>) obj; + return Objects.equals(this.exponents, other.exponents); + } + + /** + * @return immutable map mapping objects to exponents + * @since 2019-10-16 + */ + public Map<T, Integer> exponentMap() { + return this.exponents; + } + + /** + * @return a set of all of the base objects with non-zero exponents that make up this dimension. + * @since 2018-12-12 + * @since v0.1.0 + */ + public final Set<T> getBaseSet() { + final Set<T> dimensions = new HashSet<>(); + + // add all dimensions with a nonzero exponent - zero exponents shouldn't be there in the first place + for (final T dimension : this.exponents.keySet()) { + if (!this.exponents.get(dimension).equals(0)) { + dimensions.add(dimension); + } + } + + return dimensions; + } + + /** + * Gets the exponent for a specific dimension. + * + * @param dimension + * dimension to check + * @return exponent for that dimension + * @since 2018-12-12 + * @since v0.1.0 + */ + public int getExponent(final T dimension) { + return this.exponents.getOrDefault(dimension, 0); + } + + @Override + public int hashCode() { + return Objects.hash(this.exponents); + } + + /** + * @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 + */ + public boolean isSingleObject() { + int oneCount = 0; + boolean twoOrMore = false; // has exponents of 2 or more + for (final T b : this.getBaseSet()) { + if (this.getExponent(b) == 1) { + oneCount++; + } else if (this.getExponent(b) != 0) { + twoOrMore = true; + } + } + return oneCount == 1 && !twoOrMore; + } + + /** + * Multiplies this product by another + * + * @param other + * other product + * @return product of two products + * @since 2019-10-16 + * @throws NullPointerException + * if other is null + */ + public ObjectProduct<T> times(final ObjectProduct<T> other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set<T> objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map<T, Integer> map = new HashMap<>(objects.size()); + for (final T key : objects) { + map.put(key, this.getExponent(key) + other.getExponent(key)); + } + + // create the product + return new ObjectProduct<>(map); + } + + /** + * Returns this product, but to an exponent + * + * @param exponent + * exponent + * @return result of exponentiation + * @since 2019-10-16 + */ + public ObjectProduct<T> toExponent(final int exponent) { + final Map<T, Integer> map = new HashMap<>(this.exponents); + for (final T key : this.exponents.keySet()) { + map.put(key, this.getExponent(key) * exponent); + } + return new ObjectProduct<>(map); + } + + /** + * Converts this product to a string using the objects' {@link Object#toString()} method. If objects have a long + * toString representation, it is recommended to use {@link #toString(Function)} instead to shorten the returned + * string. + * + * <p> + * {@inheritDoc} + */ + @Override + public String toString() { + return this.toString(Object::toString); + } + + /** + * 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 + */ + public String toString(final Function<T, String> objectToString) { + final List<String> positiveStringComponents = new ArrayList<>(); + final List<String> negativeStringComponents = new ArrayList<>(); + + // for each base object that makes up this object, add it and its exponent + for (final T object : this.getBaseSet()) { + final int exponent = this.exponents.get(object); + if (exponent > 0) { + positiveStringComponents.add(String.format("%s^%d", objectToString.apply(object), exponent)); + } else if (exponent < 0) { + negativeStringComponents.add(String.format("%s^%d", objectToString.apply(object), -exponent)); + } + } + + final String positiveString = positiveStringComponents.isEmpty() ? "1" + : String.join(" * ", positiveStringComponents); + final String negativeString = negativeStringComponents.isEmpty() ? "" + : " / " + String.join(" * ", negativeStringComponents); + + return positiveString + negativeString; + } +} diff --git a/src/main/java/org/unitConverter/math/UncertainDouble.java b/src/main/java/org/unitConverter/math/UncertainDouble.java new file mode 100644 index 0000000..3651bd5 --- /dev/null +++ b/src/main/java/org/unitConverter/math/UncertainDouble.java @@ -0,0 +1,419 @@ +/** + * 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 org.unitConverter.math; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A double with an associated uncertainty value. For example, 3.2 ± 0.2. + * <p> + * All methods in this class throw a NullPointerException if any of their + * arguments is null. + * + * @since 2020-09-07 + */ +public final class UncertainDouble implements Comparable<UncertainDouble> { + /** + * The exact value 0 + */ + public static final UncertainDouble ZERO = UncertainDouble.of(0, 0); + + /** + * A regular expression that can recognize toString forms + */ + private static final Pattern TO_STRING = Pattern + .compile("([a-zA-Z_0-9\\.\\,]+)" // a number + // optional "± [number]" + + "(?:\\s*(?:±|\\+-)\\s*([a-zA-Z_0-9\\.\\,]+))?"); + + /** + * Parses a string in the form of {@link UncertainDouble#toString(boolean)} + * and returns the corresponding {@code UncertainDouble} instance. + * <p> + * This method allows some alternative forms of the string representation, + * such as using "+-" instead of "±". + * + * @param s string to parse + * @return {@code UncertainDouble} instance + * @throws IllegalArgumentException if the string is invalid + * @since 2020-09-07 + */ + public static final UncertainDouble fromString(String s) { + Objects.requireNonNull(s, "s may not be null"); + final Matcher matcher = TO_STRING.matcher(s); + + double value, uncertainty; + try { + value = Double.parseDouble(matcher.group(1)); + } catch (IllegalStateException | NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + + final String uncertaintyString = matcher.group(2); + if (uncertaintyString == null) { + uncertainty = 0; + } else { + try { + uncertainty = Double.parseDouble(uncertaintyString); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + } + + return UncertainDouble.of(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and <b>absolute</b> + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble of(double value, double uncertainty) { + return new UncertainDouble(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and <b>relative</b> + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble ofRelative(double value, + double relativeUncertainty) { + return new UncertainDouble(value, value * relativeUncertainty); + } + + private final double value; + + private final double uncertainty; + + /** + * @param value + * @param uncertainty + * @since 2020-09-07 + */ + private UncertainDouble(double value, double uncertainty) { + this.value = value; + // uncertainty should only ever be positive + this.uncertainty = Math.abs(uncertainty); + } + + /** + * Compares this {@code UncertainDouble} with another + * {@code UncertainDouble}. + * <p> + * This method only compares the values, not the uncertainties. So 3.1 ± 0.5 + * is considered less than 3.2 ± 0.5, even though they are equivalent. + * <p> + * <b>Note:</b> The natural ordering of this class is inconsistent with + * equals. Specifically, if two {@code UncertainDouble} instances {@code a} + * and {@code b} have the same value but different uncertainties, + * {@code a.compareTo(b)} will return 0 but {@code a.equals(b)} will return + * {@code false}. + */ + @Override + public final int compareTo(UncertainDouble o) { + return Double.compare(this.value, o.value); + } + + /** + * Returns the quotient of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedBy(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value / other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the quotient of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedByExact(double other) { + return UncertainDouble.of(this.value / other, this.uncertainty / other); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UncertainDouble)) + return false; + final UncertainDouble other = (UncertainDouble) obj; + if (Double.compare(this.value, other.value) != 0) + return false; + if (Double.compare(this.uncertainty, other.uncertainty) != 0) + return false; + return true; + } + + /** + * @param other another {@code UncertainDouble} + * @return true iff this and {@code other} are within each other's + * uncertainty range. + * @since 2020-09-07 + */ + public final boolean equivalent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return Math.abs(this.value - other.value) <= Math.min(this.uncertainty, + other.uncertainty); + } + + /** + * Gets the preferred scale for rounding a value for toString. + * + * @since 2020-09-07 + */ + private final int getDisplayScale() { + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal + // point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the + // uncertainty. + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // the scale that will give the uncertainty two decimal places + final int twoDecimalPlacesScale = bigUncertainty.scale() + - bigUncertainty.precision() + 2; + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN); + + if (roundedUncertainty.unscaledValue().intValue() >= 20) + return twoDecimalPlacesScale - 1; // one decimal place + else + return twoDecimalPlacesScale; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Double.hashCode(this.value); + result = prime * result + Double.hashCode(this.uncertainty); + return result; + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-09-07 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * Returns the difference of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value - other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the difference of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minusExact(double other) { + return UncertainDouble.of(this.value - other, this.uncertainty); + } + + /** + * Returns the sum of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value + other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the sum of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plusExact(double other) { + return UncertainDouble.of(this.value + other, this.uncertainty); + } + + /** + * @return relative uncertainty + * @since 2020-09-07 + */ + public final double relativeUncertainty() { + return this.uncertainty / this.value; + } + + /** + * Returns the product of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble times(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value * other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the product of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble timesExact(double other) { + return UncertainDouble.of(this.value * other, this.uncertainty * other); + } + + /** + * Returns the result of {@code this} raised to the exponent {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + + final double result = Math.pow(this.value, other.value); + final double relativeUncertainty = Math.hypot( + other.value * this.relativeUncertainty(), + Math.log(this.value) * other.uncertainty); + + return UncertainDouble.ofRelative(result, relativeUncertainty); + } + + /** + * Returns the result of {@code this} raised the exact exponent + * {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponentExact(double other) { + return UncertainDouble.ofRelative(Math.pow(this.value, other), + this.relativeUncertainty() * other); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + * <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. + * + * <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 + */ + @Override + public final String toString() { + return this.toString(!this.isExact()); + } + + /** + * 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" + * <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 + * same precision as the uncertainty, otherwise it will not be rounded. The + * string is still rounded if {@code showUncertainty} is false.<br> + * UNCERTAINTY represents a string representation of this + * {@code UncertainDouble}'s uncertainty. If the uncertainty ends in 1X + * (where X represents any digit) it will be rounded to two significant + * digits otherwise it will be rounded to one significant digit. + * <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.13).toString(false) = "3.27" + * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13" + * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01" + * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0" + * </pre> + * + * @since 2020-09-07 + */ + public final String toString(boolean showUncertainty) { + String valueString, uncertaintyString; + + // generate the string representation of value and uncertainty + if (this.isExact()) { + uncertaintyString = "0.0"; + valueString = Double.toString(this.value); + + } else { + // round the value and uncertainty according to getDisplayScale() + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + final int displayScale = this.getDisplayScale(); + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(displayScale, RoundingMode.HALF_EVEN); + final BigDecimal roundedValue = bigValue.setScale(displayScale, + RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + } + + // return "value" or "value ± uncertainty" depending on showUncertainty + return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); + } + + /** + * @return absolute uncertainty + * @since 2020-09-07 + */ + public final double uncertainty() { + return this.uncertainty; + } + + /** + * @return value without uncertainty + * @since 2020-09-07 + */ + public final double value() { + return this.value; + } +} diff --git a/src/org/unitConverter/math/package-info.java b/src/main/java/org/unitConverter/math/package-info.java index 65d6b23..65727e4 100644 --- a/src/org/unitConverter/math/package-info.java +++ b/src/main/java/org/unitConverter/math/package-info.java @@ -15,9 +15,10 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** - * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions. + * 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 */ package org.unitConverter.math;
\ No newline at end of file diff --git a/src/org/unitConverter/package-info.java b/src/main/java/org/unitConverter/package-info.java index 23dd165..68a258f 100644 --- a/src/org/unitConverter/package-info.java +++ b/src/main/java/org/unitConverter/package-info.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019-2021 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -18,7 +18,7 @@ * A program that converts units. * * @author Adrien Hopkins - * @version v0.2.0 + * @version v0.3.0 * @since 2019-01-25 */ package org.unitConverter;
\ No newline at end of file diff --git a/src/main/java/org/unitConverter/unit/BaseDimension.java b/src/main/java/org/unitConverter/unit/BaseDimension.java new file mode 100644 index 0000000..8e63a17 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BaseDimension.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; + +/** + * A dimension that defines a {@code BaseUnit} + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class BaseDimension { + /** + * 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 + */ + public static BaseDimension valueOf(final String name, final String symbol) { + return new BaseDimension(name, symbol); + } + + /** + * The name of the dimension. + */ + private final String name; + /** + * The symbol used by the dimension. Symbols should be short, generally one or two characters. + */ + private final String symbol; + + /** + * Creates the {@code BaseDimension}. + * + * @param name + * name of unit + * @param symbol + * symbol of unit + * @throws NullPointerException + * if any argument is null + * @since 2019-10-16 + */ + private BaseDimension(final String name, final String symbol) { + this.name = Objects.requireNonNull(name, "name must not be null."); + this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); + } + + /** + * @return name + * @since 2019-10-16 + */ + public final String getName() { + return this.name; + } + + /** + * @return symbol + * @since 2019-10-16 + */ + public final String getSymbol() { + return this.symbol; + } + + @Override + public String toString() { + return String.format("%s (%s)", this.getName(), this.getSymbol()); + } +} diff --git a/src/main/java/org/unitConverter/unit/BaseUnit.java b/src/main/java/org/unitConverter/unit/BaseUnit.java new file mode 100644 index 0000000..6757bd0 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BaseUnit.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * A unit that other units are defined by. + * <p> + * 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 + */ +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 + */ + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol) { + return new BaseUnit(dimension, name, symbol, new HashSet<>()); + } + + /** + * Gets a base unit from the dimension it measures, its name and its symbol. + * + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit + * @return base unit + * @since 2019-10-21 + */ + 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. + */ + 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 + */ + private BaseUnit(final BaseDimension dimension, final String primaryName, + final String symbol, final Set<String> otherNames) { + super(primaryName, symbol, otherNames); + this.dimension = Objects.requireNonNull(dimension, + "dimension must not be null."); + } + + /** + * Returns a {@code LinearUnit} with this unit as a base and a conversion + * factor of 1. This operation must be done in order to allow units to be + * created with operations. + * + * @return this unit as a {@code LinearUnit} + * @since 2019-10-16 + */ + public LinearUnit asLinearUnit() { + return LinearUnit.valueOf(this.getBase(), 1); + } + + @Override + protected double convertFromBase(final double value) { + return value; + } + + @Override + protected double convertToBase(final double value) { + return value; + } + + /** + * @return dimension + * @since 2019-10-16 + */ + public final BaseDimension getBaseDimension() { + return this.dimension; + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : ""); + } + + @Override + public BaseUnit withName(final NameSymbol ns) { + Objects.requireNonNull(ns, "ns must not be null."); + if (!ns.getPrimaryName().isPresent()) + throw new IllegalArgumentException( + "BaseUnits must have primary names."); + if (!ns.getSymbol().isPresent()) + throw new IllegalArgumentException("BaseUnits must have symbols."); + return BaseUnit.valueOf(this.getBaseDimension(), + ns.getPrimaryName().get(), ns.getSymbol().get(), + ns.getOtherNames()); + } +} diff --git a/src/main/java/org/unitConverter/unit/BritishImperial.java b/src/main/java/org/unitConverter/unit/BritishImperial.java new file mode 100644 index 0000000..ea23cd1 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BritishImperial.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the British Imperial system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class BritishImperial { + /** + * Imperial units that measure area + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Area { + public static final LinearUnit SQUARE_FOOT = Length.FOOT.toExponent(2); + public static final LinearUnit SQUARE_YARD = Length.YARD.toExponent(2); + public static final LinearUnit SQUARE_MILE = Length.MILE.toExponent(2); + public static final LinearUnit PERCH = Length.ROD.times(Length.ROD); + public static final LinearUnit ROOD = Length.ROD.times(Length.FURLONG); + public static final LinearUnit ACRE = Length.FURLONG.times(Length.CHAIN); + } + + /** + * Imperial units that measure length + * + * @author Adrien Hopkins + * @since 2019-10-28 + */ + public static final class Length { + /** + * According to the International Yard and Pound of 1959, a yard is defined as exactly 0.9144 metres. + */ + public static final LinearUnit YARD = SI.METRE.times(0.9144); + public static final LinearUnit FOOT = YARD.dividedBy(3); + public static final LinearUnit INCH = FOOT.dividedBy(12); + public static final LinearUnit THOU = INCH.dividedBy(1000); + public static final LinearUnit CHAIN = YARD.times(22); + public static final LinearUnit FURLONG = CHAIN.times(10); + public static final LinearUnit MILE = FURLONG.times(8); + public static final LinearUnit LEAGUE = MILE.times(3); + + public static final LinearUnit NAUTICAL_MILE = SI.METRE.times(1852); + public static final LinearUnit CABLE = NAUTICAL_MILE.dividedBy(10); + public static final LinearUnit FATHOM = CABLE.dividedBy(100); + + public static final LinearUnit ROD = YARD.times(5.5); + public static final LinearUnit LINK = ROD.dividedBy(25); + } + + /** + * British Imperial units that measure mass. + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Mass { + public static final LinearUnit POUND = SI.GRAM.times(453.59237); + public static final LinearUnit OUNCE = POUND.dividedBy(16); + public static final LinearUnit DRACHM = POUND.dividedBy(256); + public static final LinearUnit GRAIN = POUND.dividedBy(7000); + public static final LinearUnit STONE = POUND.times(14); + public static final LinearUnit QUARTER = STONE.times(2); + public static final LinearUnit HUNDREDWEIGHT = QUARTER.times(4); + public static final LinearUnit LONG_TON = HUNDREDWEIGHT.times(20); + public static final LinearUnit SLUG = SI.KILOGRAM.times(14.59390294); + } + + /** + * British Imperial units that measure volume + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Volume { + public static final LinearUnit FLUID_OUNCE = SI.LITRE.withPrefix(SI.MILLI).times(28.4130625); + public static final LinearUnit GILL = FLUID_OUNCE.times(5); + public static final LinearUnit PINT = FLUID_OUNCE.times(20); + public static final LinearUnit QUART = PINT.times(2); + public static final LinearUnit GALLON = QUART.times(4); + public static final LinearUnit PECK = GALLON.times(2); + public static final LinearUnit BUSHEL = PECK.times(4); + + public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); + public static final LinearUnit CUBIC_FOOT = Length.FOOT.toExponent(3); + public static final LinearUnit CUBIC_YARD = Length.YARD.toExponent(3); + public static final LinearUnit ACRE_FOOT = Area.ACRE.times(Length.FOOT); + } + + public static final LinearUnit OUNCE_FORCE = Mass.OUNCE.times(SI.Constants.EARTH_GRAVITY); + public static final LinearUnit POUND_FORCE = Mass.POUND.times(SI.Constants.EARTH_GRAVITY); + + public static final LinearUnit BRITISH_THERMAL_UNIT = SI.JOULE.times(1055.06); + public static final LinearUnit CALORIE = SI.JOULE.times(4.184); + public static final LinearUnit KILOCALORIE = SI.JOULE.times(4184); + + public static final Unit FAHRENHEIT = Unit.fromConversionFunctions(SI.KELVIN.getBase(), + tempK -> tempK * 1.8 - 459.67, tempF -> (tempF + 459.67) / 1.8); +} diff --git a/src/main/java/org/unitConverter/unit/FunctionalUnit.java b/src/main/java/org/unitConverter/unit/FunctionalUnit.java new file mode 100644 index 0000000..586e0d7 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/FunctionalUnit.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.function.DoubleUnaryOperator; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that uses functional objects to convert to and from its base. + * + * @author Adrien Hopkins + * @since 2019-05-22 + */ +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 + */ + 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 + */ + 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. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @throws NullPointerException + * if any argument is null + * @since 2019-05-22 + */ + public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + super(base, NameSymbol.EMPTY); + this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); + this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); + } + + /** + * 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. + * @throws NullPointerException + * if any argument is null + * @since 2019-05-22 + */ + public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { + super(base, ns); + this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); + this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); + } + + /** + * {@inheritDoc} + * + * Uses {@code converterFrom} to convert. + */ + @Override + public double convertFromBase(final double value) { + return this.converterFrom.applyAsDouble(value); + } + + /** + * {@inheritDoc} + * + * Uses {@code converterTo} to convert. + */ + @Override + public double convertToBase(final double value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java b/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java new file mode 100644 index 0000000..21c1fca --- /dev/null +++ b/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unitlike form that converts using two conversion functions. + * + * @since 2020-09-07 + */ +final class FunctionalUnitlike<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/org/unitConverter/unit/LinearUnit.java b/src/main/java/org/unitConverter/unit/LinearUnit.java new file mode 100644 index 0000000..b7f33d5 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/LinearUnit.java @@ -0,0 +1,441 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; + +/** + * A unit that can be expressed as a product of its base and a number. For + * example, kilometres, inches and pounds. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class LinearUnit extends Unit { + /** + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' + * + * @param unit unit to convert + * @param value value to convert + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-16 + * @throws NullPointerException if unit is null + */ + public static LinearUnit fromUnitValue(final Unit unit, final double value) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), + unit.convertToBase(value), NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' + * + * @param unit unit to convert + * @param value value to convert + * @param ns name(s) and symbol of unit + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-21 + * @throws NullPointerException if unit or ns is null + */ + public static LinearUnit fromUnitValue(final Unit unit, final double value, + final NameSymbol ns) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), + unit.convertToBase(value), ns); + } + + /** + * @return the base unit associated with {@code unit}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unit unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * @return the base unit associated with {@code unitlike}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unitlike<?> unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@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 + * @throws NullPointerException if unitBase is null + */ + public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor) { + return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. + * + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @param ns name(s) and symbol of unit + * @return product of base and conversion factor + * @since 2019-10-21 + * @throws NullPointerException if unitBase is null + */ + public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor, final NameSymbol ns) { + return new LinearUnit(unitBase, conversionFactor, ns); + } + + /** + * The value of this unit as represented in its base form. Mathematically, + * + * <pre> + * this = conversionFactor * getBase() + * </pre> + * + * @since 2019-10-16 + */ + private final double conversionFactor; + + /** + * Creates the {@code LinearUnit}. + * + * @param unitBase base of linear unit + * @param conversionFactor conversion factor between base and unit + * @since 2019-10-16 + */ + private LinearUnit(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor, final NameSymbol ns) { + super(unitBase, ns); + this.conversionFactor = conversionFactor; + } + + /** + * {@inheritDoc} + * + * Converts by dividing by {@code conversionFactor} + */ + @Override + protected double convertFromBase(final double value) { + return value / this.getConversionFactor(); + } + + /** + * Converts an {@code UncertainDouble} value expressed in this unit to an + * {@code UncertainValue} value expressed in {@code other}. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if value or other is null + */ + public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) { + Objects.requireNonNull(other, "other must not be null."); + Objects.requireNonNull(value, "value may not be null."); + if (this.canConvertTo(other)) + return value.timesExact( + this.getConversionFactor() / other.getConversionFactor()); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + + } + + /** + * {@inheritDoc} + * + * Converts by multiplying by {@code conversionFactor} + */ + @Override + protected double convertToBase(final double value) { + return value * this.getConversionFactor(); + } + + /** + * Converts an {@code UncertainDouble} to the base unit. + * + * @since 2020-09-07 + */ + UncertainDouble convertToBase(final UncertainDouble value) { + return value.timesExact(this.getConversionFactor()); + } + + /** + * Divides this unit by a scalar. + * + * @param divisor scalar to divide by + * @return quotient + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final double divisor) { + return valueOf(this.getBase(), this.getConversionFactor() / divisor); + } + + /** + * Returns the quotient of this unit and another. + * + * @param divisor unit to divide by + * @return quotient of two units + * @throws NullPointerException if {@code divisor} is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final LinearUnit divisor) { + Objects.requireNonNull(divisor, "other must not be null"); + + // divide the units + final ObjectProduct<BaseUnit> 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; + return Objects.equals(this.getBase(), other.getBase()) + && DecimalComparison.equals(this.getConversionFactor(), + other.getConversionFactor()); + } + + /** + * @return conversion factor + * @since 2019-10-16 + */ + public double getConversionFactor() { + return this.conversionFactor; + } + + /** + * {@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()); + } + + /** + * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there + * is a {@code BaseUnit b} where + * {@code b.asLinearUnit().equals(this)} returns {@code true}.) + * @since 2019-10-16 + */ + public boolean isBase() { + return this.isCoherent() && this.getBase().isSingleObject(); + } + + /** + * @return whether this unit is coherent (i.e. has conversion factor 1) + * @since 2019-10-16 + */ + public boolean isCoherent() { + return this.getConversionFactor() == 1; + } + + /** + * Returns the difference of this unit and another. + * <p> + * Two units can be subtracted if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code subtrahend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. + * </p> + * + * @param subtrahend unit to subtract + * @return difference of units + * @throws IllegalArgumentException if {@code subtrahend} is not compatible + * for subtraction as described above + * @throws NullPointerException if {@code subtrahend} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public LinearUnit minus(final LinearUnit subtrahend) { + Objects.requireNonNull(subtrahend, "addend must not be null."); + + // reject subtrahends that cannot be added to this unit + if (!this.getBase().equals(subtrahend.getBase())) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", this, + subtrahend)); + + // subtract the units + return valueOf(this.getBase(), + this.getConversionFactor() - subtrahend.getConversionFactor()); + } + + /** + * Returns the sum of this unit and another. + * <p> + * Two units can be added if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code addend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. + * </p> + * + * @param addend unit to add + * @return sum of units + * @throws IllegalArgumentException if {@code addend} is not compatible for + * addition as described above + * @throws NullPointerException if {@code addend} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public LinearUnit plus(final LinearUnit addend) { + Objects.requireNonNull(addend, "addend must not be null."); + + // reject addends that cannot be added to this unit + if (!this.getBase().equals(addend.getBase())) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this, + addend)); + + // add the units + return valueOf(this.getBase(), + this.getConversionFactor() + addend.getConversionFactor()); + } + + /** + * Multiplies this unit by a scalar. + * + * @param multiplier scalar to multiply by + * @return product + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit times(final double multiplier) { + return valueOf(this.getBase(), this.getConversionFactor() * multiplier); + } + + /** + * Returns the product of this unit and another. + * + * @param multiplier unit to multiply by + * @return product of two units + * @throws NullPointerException if {@code multiplier} is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit times(final LinearUnit multiplier) { + Objects.requireNonNull(multiplier, "other must not be null"); + + // multiply the units + final ObjectProduct<BaseUnit> base = this.getBase() + .times(multiplier.getBase()); + return valueOf(base, + this.getConversionFactor() * multiplier.getConversionFactor()); + } + + /** + * Returns this unit but to an exponent. + * + * @param exponent exponent to exponentiate unit to + * @return exponentiated unit + * @since 2019-01-15 + * @since v0.1.0 + */ + public LinearUnit toExponent(final int exponent) { + return valueOf(this.getBase().toExponent(exponent), + Math.pow(this.conversionFactor, exponent)); + } + + /** + * @return a string providing a definition of this unit + * @since 2019-10-21 + */ + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", " + Double.toString(this.conversionFactor) + " * " + + this.getBase().toString(u -> u.getSymbol().get()); + } + + @Override + public LinearUnit withName(final NameSymbol ns) { + return valueOf(this.getBase(), this.getConversionFactor(), ns); + } + + /** + * Returns the result of applying {@code prefix} to this unit. + * <p> + * If this unit and the provided prefix have a primary name, the returned + * unit will have a primary name (prefix's name + unit's name). <br> + * If this unit and the provided prefix have a symbol, the returned unit will + * 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 + * @since v0.2.0 + * @throws NullPointerException if prefix is null + */ + public LinearUnit withPrefix(final UnitPrefix prefix) { + final LinearUnit unit = this.times(prefix.getMultiplier()); + + // create new name and symbol, if possible + final String name; + if (this.getPrimaryName().isPresent() + && prefix.getPrimaryName().isPresent()) { + name = prefix.getPrimaryName().get() + this.getPrimaryName().get(); + } else { + name = null; + } + + final String symbol; + if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { + symbol = prefix.getSymbol().get() + this.getSymbol().get(); + } else { + symbol = null; + } + + return unit.withName(NameSymbol.ofNullable(name, symbol)); + } +} diff --git a/src/main/java/org/unitConverter/unit/LinearUnitValue.java b/src/main/java/org/unitConverter/unit/LinearUnitValue.java new file mode 100644 index 0000000..8de734e --- /dev/null +++ b/src/main/java/org/unitConverter/unit/LinearUnitValue.java @@ -0,0 +1,341 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.UncertainDouble; + +/** + * A possibly uncertain value expressed in a linear unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class LinearUnitValue { + public static final LinearUnitValue ONE = getExact(SI.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 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, + final double value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + UncertainDouble.of(value, 0)); + } + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, + final UncertainDouble value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + Objects.requireNonNull(value, "value may not be null")); + } + + private final LinearUnit unit; + + private final UncertainDouble value; + + /** + * @param unit unit to express as + * @param value value to express + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { + this.unit = unit; + this.value = value; + } + + /** + * @return this value as a {@code UnitValue}. All uncertainty information is + * removed from the returned value. + * @since 2020-08-04 + */ + public final UnitValue asUnitValue() { + return UnitValue.of(this.unit, this.value.value()); + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(final LinearUnit other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a LinearUnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + * @since 2020-07-26 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); + } + + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); + } + + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), + this.value.dividedBy(divisor.value)); + } + + /** + * 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 + * @see #equals(Object, boolean) + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && this.unit.convertToBase(this.value) + .equals(other.unit.convertToBase(other.value)); + } + + /** + * 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. + * + * @since 2020-07-28 + */ + public boolean equals(final Object obj, final boolean avoidFPErrors) { + if (!avoidFPErrors) + return this.equals(obj); + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && DecimalComparison.equals(this.unit.convertToBase(this.value), + other.unit.convertToBase(other.value)); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null + || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); + final LinearUnitValue thisBase = this.convertTo(base); + final LinearUnitValue otherBase = other.convertTo(base); + + return thisBase.value.equivalent(otherBase.value); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final UncertainDouble getValue() { + return this.value; + } + + /** + * @return the exact value + * @since 2020-09-07 + */ + public final double getValueExact() { + return this.value.value(); + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), + this.unit.convertToBase(this.getValue())); + } + + /** + * Returns the difference of this value and another, expressed in this + * value's unit + * + * @param subtrahend value to subtract + * @return difference of values + * @throws IllegalArgumentException if {@code subtrahend} has a unit that is + * not compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", + this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.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 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this.unit, + addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.plus(otherConverted.value)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.of(this.unit.times(multiplier.unit), + this.value.times(multiplier.value)); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.of(this.unit.toExponent(exponent), + this.value.toExponentExact(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.value.isExact()); + } + + /** + * Returns a string representing the object. <br> + * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + * <p> + * If showUncertainty is true, strings like "35 ± 8" are shown instead of + * single numbers. + * <p> + * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional<String> primaryName = this.unit.getPrimaryName(); + final Optional<String> symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final UncertainDouble baseValue = this.unit.convertToBase(this.value); + + // get rounded strings + // if showUncertainty is true, add brackets around the string + final String valueString = showUncertainty ? "(" + : "" + this.value.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + final String baseValueString = showUncertainty ? "(" + : "" + baseValue.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + + // create string + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); + } +} diff --git a/src/main/java/org/unitConverter/unit/MultiUnit.java b/src/main/java/org/unitConverter/unit/MultiUnit.java new file mode 100644 index 0000000..a1623f8 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/MultiUnit.java @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.unitConverter.math.ObjectProduct; + +/** + * A combination of units, like "5 foot + 7 inch". All but the last units should + * have a whole number value associated with them. + * + * @since 2020-10-02 + */ +public final class MultiUnit extends Unitlike<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/org/unitConverter/unit/NameSymbol.java b/src/main/java/org/unitConverter/unit/NameSymbol.java new file mode 100644 index 0000000..8d8302a --- /dev/null +++ b/src/main/java/org/unitConverter/unit/NameSymbol.java @@ -0,0 +1,280 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * A class that can be used to specify names and a symbol for a unit. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class NameSymbol { + public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), + Optional.empty(), new HashSet<>()); + + /** + * Creates a {@code NameSymbol}, ensuring that if primaryName is null and + * otherNames is not empty, one name is moved from otherNames to primaryName + * + * Ensure that otherNames is a copy of the inputted argument. + */ + private static final NameSymbol create(final String name, + final String symbol, final Set<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(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), + otherNames); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and no other + * names. + * + * @param name name to use + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name or symbol is null + */ + public static final NameSymbol of(final String name, final String symbol) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>()); + } + + /** + * 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 + * @throws NullPointerException if any argument is null + */ + public static final 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, + "otherNames must not be null."))); + } + + /** + * 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 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final String... otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, + "otherNames must not be null.")))); + } + + /** + * 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 + * @throws NullPointerException if name is null + */ + public static final NameSymbol ofName(final String name) { + return new NameSymbol(Optional.of(name), Optional.empty(), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + * <p> + * 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 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final Set<String> otherNames) { + return NameSymbol.create(name, symbol, + otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + * <p> + * 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 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final String... otherNames) { + return create(name, symbol, otherNames == null ? new HashSet<>() + : new HashSet<>(Arrays.asList(otherNames))); + } + + /** + * Gets a {@code NameSymbol} with a symbol and no names. + * + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if symbol is null + */ + public static final NameSymbol ofSymbol(final String symbol) { + return new NameSymbol(Optional.empty(), Optional.of(symbol), + new HashSet<>()); + } + + private final Optional<String> primaryName; + private final Optional<String> symbol; + + private final Set<String> otherNames; + + /** + * Creates the {@code NameSymbol}. + * + * @param primaryName primary name of unit + * @param symbol symbol used to represent unit + * @param otherNames other names and/or spellings, should be a mutable copy + * of the argument + * @since 2019-10-21 + */ + private 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 (this.primaryName.isEmpty()) { + assert this.otherNames.isEmpty(); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof NameSymbol)) + return false; + final NameSymbol other = (NameSymbol) obj; + if (this.otherNames == null) { + if (other.otherNames != null) + return false; + } else if (!this.otherNames.equals(other.otherNames)) + return false; + if (this.primaryName == null) { + if (other.primaryName != null) + return false; + } else if (!this.primaryName.equals(other.primaryName)) + return false; + if (this.symbol == null) { + if (other.symbol != null) + return false; + } else if (!this.symbol.equals(other.symbol)) + return false; + return true; + } + + /** + * @return otherNames + * @since 2019-10-21 + */ + public final Set<String> getOtherNames() { + return this.otherNames; + } + + /** + * @return primaryName + * @since 2019-10-21 + */ + public final Optional<String> getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-10-21 + */ + public final Optional<String> getSymbol() { + return this.symbol; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.otherNames == null ? 0 : this.otherNames.hashCode()); + result = prime * result + + (this.primaryName == null ? 0 : this.primaryName.hashCode()); + result = prime * result + + (this.symbol == null ? 0 : this.symbol.hashCode()); + return result; + } + + /** + * @return true iff this {@code NameSymbol} contains no names or symbols. + */ + public final boolean isEmpty() { + // if primaryName is empty, otherNames must also be empty + return this.primaryName.isEmpty() && this.symbol.isEmpty(); + } +}
\ No newline at end of file diff --git a/src/main/java/org/unitConverter/unit/Nameable.java b/src/main/java/org/unitConverter/unit/Nameable.java new file mode 100644 index 0000000..36740ab --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Nameable.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set<String> getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional<String> getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional<String> getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/main/java/org/unitConverter/unit/SI.java b/src/main/java/org/unitConverter/unit/SI.java new file mode 100644 index 0000000..81736f3 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/SI.java @@ -0,0 +1,479 @@ +/** + * Copyright (C) 2018 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 org.unitConverter.unit; + +import java.util.Set; + +import org.unitConverter.math.ObjectProduct; + +/** + * All of the units, prefixes and dimensions that are used by the SI, as well as + * some outside the SI. + * + * <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 + */ +public final class SI { + /// dimensions used by SI units + // base dimensions, as BaseDimensions + public static final class BaseDimensions { + public static final BaseDimension LENGTH = BaseDimension.valueOf("Length", + "L"); + public static final BaseDimension MASS = BaseDimension.valueOf("Mass", + "M"); + public static final BaseDimension TIME = BaseDimension.valueOf("Time", + "T"); + public static final BaseDimension ELECTRIC_CURRENT = BaseDimension + .valueOf("Electric Current", "I"); + public static final BaseDimension TEMPERATURE = BaseDimension + .valueOf("Temperature", "\u0398"); // theta symbol + public static final BaseDimension QUANTITY = BaseDimension + .valueOf("Quantity", "N"); + public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension + .valueOf("Luminous Intensity", "J"); + public static final BaseDimension INFORMATION = BaseDimension + .valueOf("Information", "Info"); // non-SI + public static final BaseDimension CURRENCY = BaseDimension + .valueOf("Currency", "$$"); // non-SI + + // You may NOT get SI.BaseDimensions instances! + private BaseDimensions() { + throw new AssertionError(); + } + } + + /// base units of the SI + // suppressing warnings since these are the same object, but in a different + /// form (class) + @SuppressWarnings("hiding") + public static final class BaseUnits { + public static final BaseUnit METRE = BaseUnit + .valueOf(BaseDimensions.LENGTH, "metre", "m"); + public static final BaseUnit KILOGRAM = BaseUnit + .valueOf(BaseDimensions.MASS, "kilogram", "kg"); + public static final BaseUnit SECOND = BaseUnit + .valueOf(BaseDimensions.TIME, "second", "s"); + public static final BaseUnit AMPERE = BaseUnit + .valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A"); + public static final BaseUnit KELVIN = BaseUnit + .valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K"); + public static final BaseUnit MOLE = BaseUnit + .valueOf(BaseDimensions.QUANTITY, "mole", "mol"); + public static final BaseUnit CANDELA = BaseUnit + .valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd"); + public static final BaseUnit BIT = BaseUnit + .valueOf(BaseDimensions.INFORMATION, "bit", "b"); + public static final BaseUnit DOLLAR = BaseUnit + .valueOf(BaseDimensions.CURRENCY, "dollar", "$"); + + public static final Set<BaseUnit> BASE_UNITS = Set.of(METRE, KILOGRAM, + SECOND, AMPERE, KELVIN, MOLE, CANDELA, BIT); + + // You may NOT get SI.BaseUnits instances! + private BaseUnits() { + throw new AssertionError(); + } + } + + /** + * Constants that relate to the SI or other systems. + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Constants { + public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND) + .dividedBy(SECOND).times(9.80665); + } + + // dimensions used in the SI, as ObjectProducts + public static final class Dimensions { + public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct + .empty(); + public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct + .oneOf(BaseDimensions.LENGTH); + public static final ObjectProduct<BaseDimension> MASS = ObjectProduct + .oneOf(BaseDimensions.MASS); + public static final ObjectProduct<BaseDimension> TIME = ObjectProduct + .oneOf(BaseDimensions.TIME); + public static final ObjectProduct<BaseDimension> ELECTRIC_CURRENT = ObjectProduct + .oneOf(BaseDimensions.ELECTRIC_CURRENT); + public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct + .oneOf(BaseDimensions.TEMPERATURE); + public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct + .oneOf(BaseDimensions.QUANTITY); + public static final ObjectProduct<BaseDimension> LUMINOUS_INTENSITY = ObjectProduct + .oneOf(BaseDimensions.LUMINOUS_INTENSITY); + public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct + .oneOf(BaseDimensions.INFORMATION); + public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct + .oneOf(BaseDimensions.CURRENCY); + + // derived dimensions without named SI units + public static final ObjectProduct<BaseDimension> AREA = LENGTH + .times(LENGTH); + public static final ObjectProduct<BaseDimension> VOLUME = AREA + .times(LENGTH); + public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY + .dividedBy(LENGTH); + public static final ObjectProduct<BaseDimension> MASS_DENSITY = MASS + .dividedBy(VOLUME); + public static final ObjectProduct<BaseDimension> SURFACE_DENSITY = MASS + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> SPECIFIC_VOLUME = VOLUME + .dividedBy(MASS); + public static final ObjectProduct<BaseDimension> CURRENT_DENSITY = ELECTRIC_CURRENT + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT + .dividedBy(LENGTH); + public static final ObjectProduct<BaseDimension> CONCENTRATION = QUANTITY + .dividedBy(VOLUME); + public static final ObjectProduct<BaseDimension> MASS_CONCENTRATION = CONCENTRATION + .times(MASS); + public static final ObjectProduct<BaseDimension> LUMINANCE = LUMINOUS_INTENSITY + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> REFRACTIVE_INDEX = VELOCITY + .dividedBy(VELOCITY); + public static final ObjectProduct<BaseDimension> REFRACTIVE_PERMEABILITY = EMPTY + .times(EMPTY); + public static final ObjectProduct<BaseDimension> ANGLE = LENGTH + .dividedBy(LENGTH); + public static final ObjectProduct<BaseDimension> SOLID_ANGLE = AREA + .dividedBy(AREA); + + // derived dimensions with named SI units + public static final ObjectProduct<BaseDimension> FREQUENCY = EMPTY + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> FORCE = MASS + .times(ACCELERATION); + public static final ObjectProduct<BaseDimension> ENERGY = FORCE + .times(LENGTH); + public static final ObjectProduct<BaseDimension> POWER = ENERGY + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> ELECTRIC_CHARGE = ELECTRIC_CURRENT + .times(TIME); + public static final ObjectProduct<BaseDimension> VOLTAGE = ENERGY + .dividedBy(ELECTRIC_CHARGE); + public static final ObjectProduct<BaseDimension> CAPACITANCE = ELECTRIC_CHARGE + .dividedBy(VOLTAGE); + public static final ObjectProduct<BaseDimension> ELECTRIC_RESISTANCE = VOLTAGE + .dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct<BaseDimension> ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT + .dividedBy(VOLTAGE); + public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX = VOLTAGE + .times(TIME); + public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> INDUCTANCE = MAGNETIC_FLUX + .dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct<BaseDimension> LUMINOUS_FLUX = LUMINOUS_INTENSITY + .times(SOLID_ANGLE); + public static final ObjectProduct<BaseDimension> ILLUMINANCE = LUMINOUS_FLUX + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> SPECIFIC_ENERGY = ENERGY + .dividedBy(MASS); + public static final ObjectProduct<BaseDimension> CATALYTIC_ACTIVITY = QUANTITY + .dividedBy(TIME); + + // You may NOT get SI.Dimension instances! + private Dimensions() { + throw new AssertionError(); + } + } + + /// The units of the SI + public static final LinearUnit ONE = LinearUnit + .valueOf(ObjectProduct.empty(), 1); + + public static final LinearUnit METRE = BaseUnits.METRE.asLinearUnit() + .withName(NameSymbol.of("metre", "m", "meter")); + public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit() + .withName(NameSymbol.of("kilogram", "kg")); + public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit() + .withName(NameSymbol.of("second", "s", "sec")); + public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit() + .withName(NameSymbol.of("ampere", "A")); + public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit() + .withName(NameSymbol.of("kelvin", "K")); + public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit() + .withName(NameSymbol.of("mole", "mol")); + public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit() + .withName(NameSymbol.of("candela", "cd")); + public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit() + .withName(NameSymbol.of("bit", "b")); + public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit() + .withName(NameSymbol.of("dollar", "$")); + // Non-base units + public static final LinearUnit RADIAN = METRE.dividedBy(METRE) + .withName(NameSymbol.of("radian", "rad")); + + public static final LinearUnit STERADIAN = RADIAN.times(RADIAN) + .withName(NameSymbol.of("steradian", "sr")); + public static final LinearUnit HERTZ = ONE.dividedBy(SECOND) + .withName(NameSymbol.of("hertz", "Hz")); + // for periodic phenomena + public static final LinearUnit NEWTON = KILOGRAM.times(METRE) + .dividedBy(SECOND.times(SECOND)) + .withName(NameSymbol.of("newton", "N")); + public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("pascal", "Pa")); + public static final LinearUnit JOULE = NEWTON.times(METRE) + .withName(NameSymbol.of("joule", "J")); + public static final LinearUnit WATT = JOULE.dividedBy(SECOND) + .withName(NameSymbol.of("watt", "W")); + public static final LinearUnit COULOMB = AMPERE.times(SECOND) + .withName(NameSymbol.of("coulomb", "C")); + public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB) + .withName(NameSymbol.of("volt", "V")); + public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT) + .withName(NameSymbol.of("farad", "F")); + public static final LinearUnit OHM = VOLT.dividedBy(AMPERE) + .withName(NameSymbol.of("ohm", "\u03A9")); // omega + public static final LinearUnit SIEMENS = ONE.dividedBy(OHM) + .withName(NameSymbol.of("siemens", "S")); + public static final LinearUnit WEBER = VOLT.times(SECOND) + .withName(NameSymbol.of("weber", "Wb")); + public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("tesla", "T")); + public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE) + .withName(NameSymbol.of("henry", "H")); + public static final LinearUnit LUMEN = CANDELA.times(STERADIAN) + .withName(NameSymbol.of("lumen", "lm")); + public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("lux", "lx")); + public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND) + .withName(NameSymbol.of("bequerel", "Bq")); + // for activity referred to a nucleotide + public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM) + .withName(NameSymbol.of("grey", "Gy")); + // for absorbed dose + public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM) + .withName(NameSymbol.of("sievert", "Sv")); + // for dose equivalent + public static final LinearUnit KATAL = MOLE.dividedBy(SECOND) + .withName(NameSymbol.of("katal", "kat")); + // common derived units included for convenience + public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000) + .withName(NameSymbol.of("gram", "g")); + + public static final LinearUnit SQUARE_METRE = METRE.toExponent(2) + .withName(NameSymbol.of("square metre", "m^2", "square meter", + "metre squared", "meter squared")); + public static final LinearUnit CUBIC_METRE = METRE.toExponent(3) + .withName(NameSymbol.of("cubic metre", "m^3", "cubic meter", + "metre cubed", "meter cubed")); + public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND) + .withName( + NameSymbol.of("metre per second", "m/s", "meter per second")); + // Non-SI units included for convenience + public static final Unit CELSIUS = Unit + .fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15, + tempC -> tempC + 273.15) + .withName(NameSymbol.of("degree Celsius", "\u00B0C")); + + public static final LinearUnit MINUTE = SECOND.times(60) + .withName(NameSymbol.of("minute", "min")); + public static final LinearUnit HOUR = MINUTE.times(60) + .withName(NameSymbol.of("hour", "h", "hr")); + public static final LinearUnit DAY = HOUR.times(60) + .withName(NameSymbol.of("day", "d")); + public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000) + .dividedBy(HOUR).withName(NameSymbol.of("kilometre per hour", "km/h", + "kilometer per hour")); + public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI)) + .withName(NameSymbol.of("degree", "\u00B0", "deg")); + public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60) + .withName(NameSymbol.of("arcminute", "arcmin")); + public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60) + .withName(NameSymbol.of("arcsecond", "arcsec")); + public static final LinearUnit ASTRONOMICAL_UNIT = METRE + .times(149597870700.0) + .withName(NameSymbol.of("astronomical unit", "au")); + public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT + .dividedBy(ARCSECOND).withName(NameSymbol.of("parsec", "pc")); + public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0) + .withName(NameSymbol.of("hectare", "ha")); + public static final LinearUnit LITRE = METRE.times(METRE).times(METRE) + .dividedBy(1000.0).withName(NameSymbol.of("litre", "L", "l", "liter")); + public static final LinearUnit TONNE = KILOGRAM.times(1000.0) + .withName(NameSymbol.of("tonne", "t", "metric ton")); + public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27) + .withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate + // value + public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19) + .withName(NameSymbol.of("electron volt", "eV")); + public static final LinearUnit BYTE = BIT.times(8) + .withName(NameSymbol.of("byte", "B")); + public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(), + pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np)) + .withName(NameSymbol.of("neper", "Np")); + public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(), + pr -> Math.log10(pr), dB -> Math.pow(10, dB)) + .withName(NameSymbol.of("bel", "B")); + public static final Unit DECIBEL = Unit + .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), + dB -> Math.pow(10, dB / 10)) + .withName(NameSymbol.of("decibel", "dB")); + + /// The prefixes of the SI + // expanding decimal prefixes + public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3) + .withName(NameSymbol.of("kilo", "k", "K")); + public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6) + .withName(NameSymbol.of("mega", "M")); + public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9) + .withName(NameSymbol.of("giga", "G")); + public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12) + .withName(NameSymbol.of("tera", "T")); + public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15) + .withName(NameSymbol.of("peta", "P")); + public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18) + .withName(NameSymbol.of("exa", "E")); + public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21) + .withName(NameSymbol.of("zetta", "Z")); + public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24) + .withName(NameSymbol.of("yotta", "Y")); + + // contracting decimal prefixes + public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3) + .withName(NameSymbol.of("milli", "m")); + public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6) + .withName(NameSymbol.of("micro", "\u03BC", "u")); // mu + public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9) + .withName(NameSymbol.of("nano", "n")); + public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12) + .withName(NameSymbol.of("pico", "p")); + public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15) + .withName(NameSymbol.of("femto", "f")); + public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18) + .withName(NameSymbol.of("atto", "a")); + public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21) + .withName(NameSymbol.of("zepto", "z")); + public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24) + .withName(NameSymbol.of("yocto", "y")); + + // prefixes that don't match the pattern of thousands + public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1) + .withName(NameSymbol.of("deka", "da", "deca", "D")); + public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2) + .withName(NameSymbol.of("hecto", "h", "H", "hekto")); + public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1) + .withName(NameSymbol.of("deci", "d")); + public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2) + .withName(NameSymbol.of("centi", "c")); + public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024) + .withName(NameSymbol.of("kibi", "Ki")); + public static final UnitPrefix MEBI = KIBI.times(1024) + .withName(NameSymbol.of("mebi", "Mi")); + public static final UnitPrefix GIBI = MEBI.times(1024) + .withName(NameSymbol.of("gibi", "Gi")); + public static final UnitPrefix TEBI = GIBI.times(1024) + .withName(NameSymbol.of("tebi", "Ti")); + public static final UnitPrefix PEBI = TEBI.times(1024) + .withName(NameSymbol.of("pebi", "Pi")); + public static final UnitPrefix EXBI = PEBI.times(1024) + .withName(NameSymbol.of("exbi", "Ei")); + + // a few prefixed units + public static final LinearUnit MICROMETRE = SI.METRE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIMETRE = SI.METRE.withPrefix(SI.MILLI); + public static final LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO); + public static final LinearUnit MEGAMETRE = SI.METRE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROLITRE = SI.LITRE.withPrefix(SI.MICRO); + public static final LinearUnit MILLILITRE = SI.LITRE.withPrefix(SI.MILLI); + public static final LinearUnit KILOLITRE = SI.LITRE.withPrefix(SI.KILO); + public static final LinearUnit MEGALITRE = SI.LITRE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROSECOND = SI.SECOND.withPrefix(SI.MICRO); + public static final LinearUnit MILLISECOND = SI.SECOND.withPrefix(SI.MILLI); + public static final LinearUnit KILOSECOND = SI.SECOND.withPrefix(SI.KILO); + public static final LinearUnit MEGASECOND = SI.SECOND.withPrefix(SI.MEGA); + + public static final LinearUnit MICROGRAM = SI.GRAM.withPrefix(SI.MICRO); + public static final LinearUnit MILLIGRAM = SI.GRAM.withPrefix(SI.MILLI); + public static final LinearUnit MEGAGRAM = SI.GRAM.withPrefix(SI.MEGA); + + public static final LinearUnit MICRONEWTON = SI.NEWTON.withPrefix(SI.MICRO); + public static final LinearUnit MILLINEWTON = SI.NEWTON.withPrefix(SI.MILLI); + public static final LinearUnit KILONEWTON = SI.NEWTON.withPrefix(SI.KILO); + public static final LinearUnit MEGANEWTON = SI.NEWTON.withPrefix(SI.MEGA); + + public static final LinearUnit MICROJOULE = SI.JOULE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIJOULE = SI.JOULE.withPrefix(SI.MILLI); + public static final LinearUnit KILOJOULE = SI.JOULE.withPrefix(SI.KILO); + public static final LinearUnit MEGAJOULE = SI.JOULE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROWATT = SI.WATT.withPrefix(SI.MICRO); + public static final LinearUnit MILLIWATT = SI.WATT.withPrefix(SI.MILLI); + public static final LinearUnit KILOWATT = SI.WATT.withPrefix(SI.KILO); + public static final LinearUnit MEGAWATT = SI.WATT.withPrefix(SI.MEGA); + + public static final LinearUnit MICROCOULOMB = SI.COULOMB + .withPrefix(SI.MICRO); + public static final LinearUnit MILLICOULOMB = SI.COULOMB + .withPrefix(SI.MILLI); + public static final LinearUnit KILOCOULOMB = SI.COULOMB.withPrefix(SI.KILO); + public static final LinearUnit MEGACOULOMB = SI.COULOMB.withPrefix(SI.MEGA); + + public static final LinearUnit MICROAMPERE = SI.AMPERE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIAMPERE = SI.AMPERE.withPrefix(SI.MILLI); + + public static final LinearUnit MICROVOLT = SI.VOLT.withPrefix(SI.MICRO); + public static final LinearUnit MILLIVOLT = SI.VOLT.withPrefix(SI.MILLI); + public static final LinearUnit KILOVOLT = SI.VOLT.withPrefix(SI.KILO); + public static final LinearUnit MEGAVOLT = SI.VOLT.withPrefix(SI.MEGA); + + public static final LinearUnit KILOOHM = SI.OHM.withPrefix(SI.KILO); + public static final LinearUnit MEGAOHM = SI.OHM.withPrefix(SI.MEGA); + + // sets of prefixes + public static final Set<UnitPrefix> ALL_PREFIXES = Set.of(DEKA, HECTO, KILO, + MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, MICRO, + NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI, + EXBI); + + public static final Set<UnitPrefix> DECIMAL_PREFIXES = Set.of(DEKA, HECTO, + KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, + MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO); + public static final Set<UnitPrefix> THOUSAND_PREFIXES = Set.of(KILO, MEGA, + GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI, MICRO, NANO, PICO, FEMTO, + ATTO, ZEPTO, YOCTO); + public static final Set<UnitPrefix> MAGNIFYING_PREFIXES = Set.of(DEKA, HECTO, + KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, KIBI, MEBI, GIBI, + TEBI, PEBI, EXBI); + public static final Set<UnitPrefix> REDUCING_PREFIXES = Set.of(DECI, CENTI, + MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO); + + // You may NOT get SI instances! + private SI() { + throw new AssertionError(); + } +} diff --git a/src/main/java/org/unitConverter/unit/USCustomary.java b/src/main/java/org/unitConverter/unit/USCustomary.java new file mode 100644 index 0000000..1c4bcfe --- /dev/null +++ b/src/main/java/org/unitConverter/unit/USCustomary.java @@ -0,0 +1,135 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the US Customary system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class USCustomary { + /** + * US Customary units that measure area + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Area { + public static final LinearUnit SQUARE_SURVEY_FOOT = Length.SURVEY_FOOT.times(Length.SURVEY_FOOT); + public static final LinearUnit SQUARE_CHAIN = Length.SURVEY_CHAIN.times(Length.SURVEY_CHAIN); + public static final LinearUnit ACRE = Length.SURVEY_CHAIN.times(Length.SURVEY_FURLONG); + public static final LinearUnit SECTION = Length.SURVEY_MILE.times(Length.SURVEY_MILE); + public static final LinearUnit SURVEY_TOWNSHIP = SECTION.times(36); + } + + /** + * US Customary units that measure length + * + * @author Adrien Hopkins + * @since 2019-10-28 + */ + public static final class Length { + public static final LinearUnit FOOT = BritishImperial.Length.FOOT; + public static final LinearUnit INCH = BritishImperial.Length.INCH; + public static final LinearUnit HAND = INCH.times(4); + public static final LinearUnit PICA = INCH.dividedBy(6); + public static final LinearUnit POINT = PICA.dividedBy(12); + public static final LinearUnit YARD = BritishImperial.Length.YARD; + public static final LinearUnit MILE = BritishImperial.Length.MILE; + + public static final LinearUnit SURVEY_FOOT = SI.METRE.times(1200.0 / 3937.0); + public static final LinearUnit SURVEY_LINK = SURVEY_FOOT.times(33.0 / 50.0); + public static final LinearUnit SURVEY_ROD = SURVEY_FOOT.times(16.5); + public static final LinearUnit SURVEY_CHAIN = SURVEY_ROD.times(4); + public static final LinearUnit SURVEY_FURLONG = SURVEY_CHAIN.times(10); + public static final LinearUnit SURVEY_MILE = SURVEY_FURLONG.times(8); + public static final LinearUnit SURVEY_LEAGUE = SURVEY_MILE.times(3); + + public static final LinearUnit NAUTICAL_MILE = BritishImperial.Length.NAUTICAL_MILE; + public static final LinearUnit FATHOM = YARD.times(2); + public static final LinearUnit CABLE = FATHOM.times(120); + } + + /** + * mass units + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Mass { + public static final LinearUnit GRAIN = BritishImperial.Mass.GRAIN; + public static final LinearUnit DRAM = BritishImperial.Mass.DRACHM; + public static final LinearUnit OUNCE = BritishImperial.Mass.OUNCE; + public static final LinearUnit POUND = BritishImperial.Mass.POUND; + public static final LinearUnit HUNDREDWEIGHT = POUND.times(100); + public static final LinearUnit SHORT_TON = HUNDREDWEIGHT.times(20); + + // troy system for precious metals + public static final LinearUnit PENNYWEIGHT = GRAIN.times(24); + public static final LinearUnit TROY_OUNCE = PENNYWEIGHT.times(20); + public static final LinearUnit TROY_POUND = TROY_OUNCE.times(12); + } + + /** + * Volume units + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Volume { + public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); + public static final LinearUnit CUBIC_FOOT = Length.FOOT.toExponent(3); + public static final LinearUnit CUBIC_YARD = Length.YARD.toExponent(3); + public static final LinearUnit ACRE_FOOT = Area.ACRE.times(Length.FOOT); + + public static final LinearUnit MINIM = SI.LITRE.withPrefix(SI.MICRO).times(61.611519921875); + public static final LinearUnit FLUID_DRAM = MINIM.times(60); + public static final LinearUnit TEASPOON = MINIM.times(80); + public static final LinearUnit TABLESPOON = TEASPOON.times(3); + public static final LinearUnit FLUID_OUNCE = TABLESPOON.times(2); + public static final LinearUnit SHOT = TABLESPOON.times(3); + public static final LinearUnit GILL = FLUID_OUNCE.times(4); + public static final LinearUnit CUP = GILL.times(2); + public static final LinearUnit PINT = CUP.times(2); + public static final LinearUnit QUART = PINT.times(2); + public static final LinearUnit GALLON = QUART.times(4); + public static final LinearUnit BARREL = GALLON.times(31.5); + public static final LinearUnit OIL_BARREL = GALLON.times(42); + public static final LinearUnit HOGSHEAD = GALLON.times(63); + + public static final LinearUnit DRY_PINT = SI.LITRE.times(0.5506104713575); + public static final LinearUnit DRY_QUART = DRY_PINT.times(2); + public static final LinearUnit DRY_GALLON = DRY_QUART.times(4); + public static final LinearUnit PECK = DRY_GALLON.times(2); + public static final LinearUnit BUSHEL = PECK.times(4); + public static final LinearUnit DRY_BARREL = CUBIC_INCH.times(7056); + } + + public static final LinearUnit OUNCE_FORCE = BritishImperial.OUNCE_FORCE; + public static final LinearUnit POUND_FORCE = BritishImperial.POUND_FORCE; + + public static final LinearUnit BRITISH_THERMAL_UNIT = BritishImperial.BRITISH_THERMAL_UNIT; + public static final LinearUnit CALORIE = BritishImperial.CALORIE; + public static final LinearUnit KILOCALORIE = BritishImperial.KILOCALORIE; + public static final LinearUnit FOOT_POUND = POUND_FORCE.times(Length.FOOT); + + public static final LinearUnit HORSEPOWER = Length.FOOT.times(POUND_FORCE).dividedBy(SI.MINUTE).times(33000); + public static final LinearUnit POUND_PER_SQUARE_INCH = POUND_FORCE.dividedBy(Length.INCH.toExponent(2)); + + public static final Unit FAHRENHEIT = BritishImperial.FAHRENHEIT; +} diff --git a/src/main/java/org/unitConverter/unit/Unit.java b/src/main/java/org/unitConverter/unit/Unit.java new file mode 100644 index 0000000..0a3298f --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Unit.java @@ -0,0 +1,377 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.DoubleUnaryOperator; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that is composed of base units. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +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. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException if any argument is null + */ + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + return new FunctionalUnit(base, converterFrom, converterTo); + } + + /** + * Returns a unit from its base and the functions it uses to convert to and + * from its base. + * + * <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. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. + * @param ns names and symbol of unit + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException if any argument is null + */ + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { + return new FunctionalUnit(base, converterFrom, converterTo, ns); + } + + /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct<BaseUnit> unitBase; + + /** + * 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; + + /** + * Creates the {@code Unit}. + * + * @param unitBase base of unit + * @param ns names and symbol of unit + * @since 2019-10-16 + * @throws NullPointerException if unitBase or ns is null + */ + Unit(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"); + } + + /** + * A constructor that constructs {@code BaseUnit} instances. + * + * @since 2019-10-16 + */ + Unit(final String primaryName, final String symbol, + final Set<String> otherNames) { + if (this instanceof BaseUnit) { + this.unitBase = ObjectProduct.oneOf((BaseUnit) this); + } else + throw new AssertionError(); + this.nameSymbol = NameSymbol.of(primaryName, symbol, + new HashSet<>(otherNames)); + } + + /** + * @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 + * @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 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> + * This must be the inverse of {@code convertToBase}, so + * {@code convertFromBase(convertToBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. + * </p> + * <p> + * If this unit <i>is</i> a base unit, this method should return + * {@code value}. + * </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 + * @since v0.1.0 + */ + protected abstract double convertFromBase(double value); + + /** + * 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 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(final Unit other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * 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)); + } + + /** + * Converts from a value expressed in this unit to a value expressed in this + * unit's base unit. + * <p> + * This must be the inverse of {@code convertFromBase}, so + * {@code convertToBase(convertFromBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. + * </p> + * <p> + * 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 + * @since v0.1.0 + */ + protected abstract double convertToBase(double 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; + } + + /** + * Returns true iff this unit is metric. + * <p> + * "Metric" is defined by three conditions: + * <ul> + * <li>Must be an instance of {@link LinearUnit}.</li> + * <li>Must be based on the SI base units (as determined by getBase())</li> + * <li>The conversion factor must be a power of 10.</li> + * </ul> + * <p> + * Note that this definition excludes some units that many would consider + * "metric", such as the degree Celsius (fails the first condition), + * calories, minutes and hours (fail the third condition). + * <p> + * All SI units (as designated by the BIPM) except the degree Celsius are + * considered "metric" by this definition. + * + * @since 2020-08-27 + */ + public final boolean isMetric() { + // first condition - check that it is a linear unit + if (!(this instanceof LinearUnit)) + return false; + final LinearUnit linear = (LinearUnit) this; + + // second condition - check that + for (final BaseUnit b : linear.getBase().getBaseSet()) { + if (!SI.BaseUnits.BASE_UNITS.contains(b)) + return false; + } + + // third condition - check that conversion factor is a power of 10 + return DecimalComparison + .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", derived from " + + this.getBase().toString(u -> u.getSymbol().get()) + + (this.getOtherNames().isEmpty() ? "" + : ", also called " + String.join(", ", this.getOtherNames())); + } + + /** + * @param ns name(s) and symbol to use + * @return a copy of this unit with provided name(s) and symbol + * @since 2019-10-21 + * @throws NullPointerException if ns is null + */ + public Unit 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/org/unitConverter/unit/UnitDatabase.java b/src/main/java/org/unitConverter/unit/UnitDatabase.java new file mode 100644 index 0000000..673f119 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitDatabase.java @@ -0,0 +1,2058 @@ +/** + * Copyright (C) 2018 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 org.unitConverter.unit; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +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.Scanner; +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 org.unitConverter.math.ConditionalExistenceCollections; +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ExpressionParser; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; + +/** + * A database of units, prefixes and dimensions, and their names. + * + * @author Adrien Hopkins + * @since 2019-01-07 + * @since v0.1.0 + */ +public final class UnitDatabase { + /** + * A map for units that allows the use of prefixes. + * <p> + * As this map implementation is intended to be used as a sort of "augmented + * view" of a unit and prefix map, it is unmodifiable but instead reflects + * the changes to the maps passed into it. Do not edit this map, instead edit + * the maps that were passed in during construction. + * </p> + * <p> + * The rules for applying prefixes onto units are the following: + * <ul> + * <li>Prefixes can only be applied to linear units.</li> + * <li>Before attempting to search for prefixes in a unit name, this map will + * first search for a unit name. So, if there are two units, "B" and "AB", + * and a prefix "A", this map will favour the unit "AB" over the unit "B" + * with the prefix "A", even though they have the same string.</li> + * <li>Longer prefixes are preferred to shorter prefixes. So, if you have + * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return + * the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li> + * </ul> + * </p> + * <p> + * This map is infinite in size if there is at least one unit and at least + * one prefix. If it is infinite, some operations that only work with finite + * collections, like converting name/entry sets to arrays, will throw an + * {@code IllegalStateException}. + * </p> + * <p> + * 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 + */ + 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 + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. + * </p> + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntrySet + extends AbstractSet<Map.Entry<String, Unit>> { + /** + * The entry for this set. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntry + implements Entry<String, Unit> { + private final String key; + private final Unit value; + + /** + * Creates the {@code PrefixedUnitEntry}. + * + * @param key key + * @param value value + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntry(final String key, final Unit value) { + this.key = key; + this.value = value; + } + + /** + * @since 2019-05-03 + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof Map.Entry)) + return false; + final Map.Entry<?, ?> other = (Map.Entry<?, ?>) o; + return Objects.equals(this.getKey(), other.getKey()) + && Objects.equals(this.getValue(), other.getValue()); + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public Unit getValue() { + return this.value; + } + + /** + * @since 2019-05-03 + */ + @Override + public int hashCode() { + return (this.getKey() == null ? 0 : this.getKey().hashCode()) + ^ (this.getValue() == null ? 0 + : this.getValue().hashCode()); + } + + @Override + public Unit setValue(final Unit value) { + throw new UnsupportedOperationException( + "Cannot set value in an immutable entry"); + } + + /** + * Returns a string representation of the entry. The format of the + * string is the string representation of the key, then the equals + * ({@code =}) character, then the string representation of the + * value. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return this.getKey() + "=" + this.getValue(); + } + } + + /** + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntryIterator + implements Iterator<Entry<String, Unit>> { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List<Integer> prefixCoordinates = new ArrayList<>(); + + // values from the unit entry set + private final Map<String, Unit> map; + private transient final List<String> unitNames; + private transient final List<String> prefixNames; + + /** + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntryIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public Entry<String, Unit> next() { + // get next element + final Entry<String, Unit> nextEntry = this.peek(); + + // iterate to next position + this.incrementPosition(); + + return nextEntry; + } + + /** + * @return the next element in the iterator, without iterating over + * it + * @since 2019-05-03 + */ + private Entry<String, Unit> peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove + // nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + final String nextName = this.getCurrentUnitName(); + + return new PrefixedUnitEntry(nextName, this.map.get(nextName)); + } + + /** + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format( + "Iterator iterating over name-unit entries; next value is \"%s\"", + this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitEntrySet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final Map.Entry<String, Unit> e) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public boolean addAll( + final Collection<? extends Map.Entry<String, Unit>> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + // get the entry + final Entry<String, Unit> entry; + + try { + // 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; + entry = tempEntry; + } catch (final ClassCastException e) { + throw new IllegalArgumentException( + "Attempted to test for an entry using a non-entry."); + } + + return this.map.containsKey(entry.getKey()) + && this.map.get(entry.getKey()).equals(entry.getValue()); + } + + @Override + public boolean containsAll(final Collection<?> c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator<Entry<String, Unit>> iterator() { + return new PrefixedUnitEntryIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf( + final Predicate<? super Entry<String, Unit>> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + 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; + } + } + + /** + * @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."); + } + + /** + * @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."); + } + + @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); + } + } + + /** + * 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 + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. + * </p> + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameSet + extends AbstractSet<String> { + /** + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameIterator + implements Iterator<String> { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List<Integer> prefixCoordinates = new ArrayList<>(); + + // values from the unit name set + private final Map<String, Unit> map; + private transient final List<String> unitNames; + private transient final List<String> prefixNames; + + /** + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitNameIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public String next() { + final String nextName = this.peek(); + + this.incrementPosition(); + + return nextName; + } + + /** + * @return the next element in the iterator, without iterating over + * it + * @since 2019-05-03 + */ + private String peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove + // nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + return this.getCurrentUnitName(); + } + + /** + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format( + "Iterator iterating over unit names; next value is \"%s\"", + this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitNameSet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final String e) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public boolean addAll(final Collection<? extends String> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + return this.map.containsKey(o); + } + + @Override + public boolean containsAll(final Collection<?> c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator<String> iterator() { + return new PrefixedUnitNameIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf(final Predicate<? super String> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + 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; + } + } + + /** + * @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."); + + } + + /** + * @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."); + } + + @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); + } + } + + /** + * The units stored in this collection, without prefixes. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map<String, Unit> units; + + /** + * The available prefixes for use. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map<String, UnitPrefix> prefixes; + + // caches + private transient Collection<Unit> values = null; + private transient Set<String> keySet = null; + private transient Set<Entry<String, Unit>> entrySet = null; + + /** + * Creates the {@code PrefixedUnitMap}. + * + * @param units map mapping unit names to units + * @param prefixes map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitMap(final Map<String, Unit> units, + final Map<String, UnitPrefix> prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make + // changes. + this.units = Collections.unmodifiableMap(units); + this.prefixes = Collections.unmodifiableMap(prefixes); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable map"); + } + + @Override + public Unit compute(final String key, + final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public Unit computeIfAbsent(final String key, + final Function<? super String, ? extends Unit> mappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public Unit computeIfPresent(final String key, + final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public boolean containsKey(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return true; + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException( + "Attempted to test for a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + return longestPrefix != null; + } + + /** + * {@inheritDoc} + * + * <p> + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method only tests for prefixless units. + * </p> + */ + @Override + public boolean containsValue(final Object value) { + return this.units.containsValue(value); + } + + @Override + public Set<Entry<String, Unit>> entrySet() { + if (this.entrySet == null) { + this.entrySet = new PrefixedUnitEntrySet(this); + } + return this.entrySet; + } + + @Override + public Unit get(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return this.units.get(key); + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException( + "Attempted to obtain a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + // if none found, returns null + if (longestPrefix == null) + return null; + else { + // get necessary data + final String rest = unitName.substring(longestLength); + // this cast will not fail because I verified that it would work + // before selecting this prefix + final LinearUnit unit = (LinearUnit) this.get(rest); + final UnitPrefix prefix = this.prefixes.get(longestPrefix); + + return unit.withPrefix(prefix); + } + } + + @Override + public boolean isEmpty() { + return this.units.isEmpty(); + } + + @Override + public Set<String> keySet() { + if (this.keySet == null) { + this.keySet = new PrefixedUnitNameSet(this); + } + return this.keySet; + } + + @Override + public Unit merge(final String key, final Unit value, + final BiFunction<? super Unit, ? super Unit, ? extends Unit> remappingFunction) { + throw new UnsupportedOperationException( + "Cannot merge into an immutable map"); + } + + @Override + public Unit put(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public void putAll(final Map<? extends String, ? extends Unit> m) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public Unit putIfAbsent(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public Unit remove(final Object key) { + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); + } + + @Override + public boolean remove(final Object key, final Object value) { + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); + } + + @Override + public Unit replace(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public boolean replace(final String key, final Unit oldValue, + final Unit newValue) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public void replaceAll( + final BiFunction<? super String, ? super Unit, ? extends Unit> function) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + 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; + } + } + + @Override + public String toString() { + if (this.units.isEmpty() || this.prefixes.isEmpty()) + return super.toString(); + else + return String.format( + "Infinite map of name-unit entries created from units %s and prefixes %s", + this.units, this.prefixes); + } + + /** + * {@inheritDoc} + * + * <p> + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method ignores prefixes. + * </p> + */ + @Override + public Collection<Unit> values() { + if (this.values == null) { + this.values = Collections + .unmodifiableCollection(this.units.values()); + } + return this.values; + } + } + + /** + * 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 + .compile("(\\S+)\\s+(\\S.*)"); + + /** + * Like normal string comparisons, but shorter strings are always less than + * longer strings. + */ + private static final Comparator<String> lengthFirstComparator = Comparator + .comparingInt(String::length).thenComparing(Comparator.naturalOrder()); + + /** + * 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, + final LinearUnit exponentUnit) { + // exponent function - first check if o2 is a number, + if (exponentUnit.getBase().equals(SI.ONE.getBase())) { + // then check if it is an integer, + 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."); + } + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2020-08-04 + */ + private static final LinearUnitValue exponentiateUnitValues( + final LinearUnitValue base, final LinearUnitValue exponentValue) { + // exponent function - first check if o2 is a number, + if (exponentValue.canConvertTo(SI.ONE)) { + // then check if it is an integer, + final double exponent = exponentValue.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."); + } + + /** + * @return true if entry represents a removable duplicate entry of unitMap. + * @since 2021-05-22 + */ + private static boolean isRemovableDuplicate(Map<String, Unit> unitMap, + Entry<String, Unit> entry) { + for (final Entry<String, Unit> e : unitMap.entrySet()) { + final String name = e.getKey(); + final Unit value = e.getValue(); + if (lengthFirstComparator.compare(entry.getKey(), name) < 0 + && Objects.equals(unitMap.get(entry.getKey()), value)) + return true; + } + return false; + } + + /** + * The units in this system, excluding prefixes. + * + * @since 2019-01-07 + * @since v0.1.0 + */ + private final Map<String, Unit> prefixlessUnits; + + /** + * The unit prefixes in this system. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + private final Map<String, UnitPrefix> prefixes; + + /** + * The dimensions in this system. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map<String, ObjectProduct<BaseDimension>> dimensions; + + /** + * A map mapping strings to units (including prefixes) + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map<String, Unit> units; + + /** + * The rule that specifies when prefix repetition is allowed. It takes in one + * argument: a list of the prefixes being applied to the unit + * <p> + * The prefixes are inputted in <em>application order</em>. This means that + * testing whether "kilomegagigametre" is a valid unit is equivalent to + * running the following code (assuming all variables are defined correctly): + * <br> + * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} + */ + private Predicate<List<UnitPrefix>> prefixRepetitionRule; + + /** + * 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("^", UnitDatabase::exponentiateUnits, 2) + .build(); + + /** + * A parser that can parse unit value expressions. + * + * @since 2020-08-04 + */ + 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("^", UnitDatabase::exponentiateUnitValues, 2) + .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.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", + (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1) + .build(); + + /** + * A parser that can parse unit dimension expressions. + * + * @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) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); + + /** + * Creates the {@code UnitsDatabase}. + * + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitDatabase() { + this(prefixes -> true); + } + + /** + * Creates the {@code UnitsDatabase} + * + * @param prefixRepetitionRule the rule that determines when prefix + * repetition is allowed + * @since 2020-08-26 + */ + public UnitDatabase(Predicate<List<UnitPrefix>> prefixRepetitionRule) { + this.prefixlessUnits = new HashMap<>(); + this.prefixes = new HashMap<>(); + this.dimensions = new HashMap<>(); + this.prefixRepetitionRule = prefixRepetitionRule; + this.units = ConditionalExistenceCollections.conditionalExistenceMap( + new PrefixedUnitMap(this.prefixlessUnits, this.prefixes), + entry -> this.prefixRepetitionRule + .test(this.getPrefixesFromName(entry.getKey()))); + } + + /** + * 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 + * @since 2019-03-14 + * @since v0.2.0 + */ + public void addDimension(final String name, + final ObjectProduct<BaseDimension> dimension) { + this.dimensions.put( + Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(dimension, "dimension must not be null.")); + } + + /** + * 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 + * @since v0.2.0 + */ + private void addDimensionFromLine(final String line, + final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), + lineCounter); + return; + } + + // divide line into name and expression + final Matcher 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); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's dimension name ends in a space", + lineCounter); + } + + // if expression is "!", search for an existing dimension + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsDimensionName(name)) + throw new IllegalArgumentException(String.format( + "! used but no dimension found (line %d).", lineCounter)); + } else { + // it's a unit, get the unit + final ObjectProduct<BaseDimension> dimension; + try { + dimension = this.getDimensionFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addDimension(name, dimension); + } + } + + /** + * Adds a unit prefix to the database. + * + * @param name prefix's name + * @param prefix prefix to add + * @throws NullPointerException if name or prefix is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public void addPrefix(final String name, final UnitPrefix prefix) { + this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(prefix, "prefix must not be null.")); + } + + /** + * Adds a unit to the database. + * + * @param name unit's name + * @param unit unit to add + * @throws NullPointerException if unit is null + * @since 2019-01-10 + * @since v0.1.0 + */ + public void addUnit(final String name, final Unit unit) { + this.prefixlessUnits.put( + Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(unit, "unit must not be null.")); + } + + /** + * Adds to the list from a line in a unit file. + * + * @param line line to look at + * @param lineCounter number of line, for error messages + * @since 2019-04-10 + * @since v0.2.0 + */ + private void addUnitOrPrefixFromLine(final String line, + final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), + lineCounter); + return; + } + + // divide line into name and expression + final Matcher 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 String expression = lineMatcher.group(2); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's unit name ends in a space", + lineCounter); + } + + // if expression is "!", search for an existing unit + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsUnitName(name)) + throw new IllegalArgumentException(String + .format("! used but no unit found (line %d).", lineCounter)); + } else { + if (name.endsWith("-")) { + final UnitPrefix prefix; + try { + prefix = this.getPrefixFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + this.addPrefix(name.substring(0, name.length() - 1), prefix); + } else { + // it's a unit, get the unit + final Unit unit; + try { + unit = this.getUnitFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addUnit(name, unit); + } + } + } + + /** + * Tests if the database has a unit dimension with this name. + * + * @param name name to test + * @return if database contains name + * @since 2019-03-14 + * @since v0.2.0 + */ + public boolean containsDimensionName(final String name) { + return this.dimensions.containsKey(name); + } + + /** + * Tests if the database has a unit prefix with this name. + * + * @param name name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsPrefixName(final String name) { + return this.prefixes.containsKey(name); + } + + /** + * Tests if the database has a unit with this name, taking prefixes into + * consideration + * + * @param name name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsUnitName(final String name) { + return this.units.containsKey(name); + } + + /** + * @return a map mapping dimension names to dimensions + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map<String, ObjectProduct<BaseDimension>> dimensionMap() { + return Collections.unmodifiableMap(this.dimensions); + } + + /** + * Evaluates a unit expression, following the same rules as + * {@link #getUnitFromExpression}. + * + * @param expression expression to parse + * @return {@code LinearUnitValue} representing value of expression + * @since 2020-08-04 + */ + public LinearUnitValue evaluateUnitExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias, or a number with precision first + if (this.containsUnitName(expression)) + return this.getLinearUnitValue(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry<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); + } + + /** + * Gets a unit dimension from the database using its name. + * + * <p> + * This method accepts exponents, like "L^3" + * </p> + * + * @param name dimension's name + * @return dimension + * @since 2019-03-14 + * @since v0.2.0 + */ + public ObjectProduct<BaseDimension> getDimension(final String name) { + Objects.requireNonNull(name, "name must not be null."); + if (name.contains("^")) { + final String[] baseAndExponent = name.split("\\^"); + + final ObjectProduct<BaseDimension> base = this + .getDimension(baseAndExponent[0]); + + final int exponent; + try { + exponent = Integer + .parseInt(baseAndExponent[baseAndExponent.length - 1]); + } catch (final NumberFormatException e2) { + throw new IllegalArgumentException("Exponent must be an integer."); + } + + return base.toExponent(exponent); + } + return this.dimensions.get(name); + } + + /** + * Uses the database's data to parse an expression into a unit dimension + * <p> + * The expression is a series of any of the following: + * <ul> + * <li>The name of a unit dimension, which multiplies or divides the result + * based on preceding operators</li> + * <li>The operators '*' and '/', which multiply and divide (note that just + * putting two unit dimensions next to each other is equivalent to + * multiplication)</li> + * <li>The operator '^' which exponentiates. Exponents must be integers.</li> + * </ul> + * + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null + * @since 2019-04-13 + * @since v0.2.0 + */ + public ObjectProduct<BaseDimension> getDimensionFromExpression( + final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a dimension as an alias first + if (this.containsDimensionName(expression)) + return this.getDimension(expression); + + // force operators to have spaces + String modifiedExpression = expression; + + // format expression + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + + return this.unitDimensionParser.parseExpression(modifiedExpression); + } + + /** + * Gets a unit. If it is linear, cast it to a LinearUnit and return it. + * Otherwise, throw an {@code IllegalArgumentException}. + * + * @param name unit's name + * @return unit + * @since 2019-03-22 + * @since v0.2.0 + */ + private LinearUnit getLinearUnit(final String name) { + // see if I am using a function-unit like tempC(100) + Objects.requireNonNull(name, "name may not be null"); + if (name.contains("(") && name.contains(")")) { + // break it into function name and value + final List<String> parts = Arrays.asList(name.split("\\(")); + if (parts.size() != 2) + throw new IllegalArgumentException( + "Format nonlinear units like: unit(value)."); + + // solve the function + final Unit unit = this.getUnit(parts.get(0)); + final double value = Double.parseDouble( + parts.get(1).substring(0, parts.get(1).length() - 1)); + 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)); + } + } + + /** + * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be + * converted to their base units. + * + * @param name name of unit + * @return {@code LinearUnitValue} instance + * @since 2020-08-04 + */ + private LinearUnitValue getLinearUnitValue(final String name) { + try { + // try to parse it as a number - otherwise it is not a number! + final BigDecimal number = new BigDecimal(name); + + final double uncertainty = Math.pow(10, -number.scale()); + return LinearUnitValue.of(SI.ONE, + UncertainDouble.of(number.doubleValue(), uncertainty)); + } catch (final NumberFormatException e) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } + } + + /** + * Gets a unit prefix from the database from its name + * + * @param name prefix's name + * @return prefix + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitPrefix getPrefix(final String name) { + try { + return UnitPrefix.valueOf(Double.parseDouble(name)); + } catch (final NumberFormatException e) { + return this.prefixes.get(name); + } + } + + /** + * Gets all of the prefixes that are on a unit name, in application order. + * + * @param unitName name of unit + * @return prefixes + * @since 2020-08-26 + */ + List<UnitPrefix> getPrefixesFromName(final String unitName) { + final List<UnitPrefix> prefixes = new ArrayList<>(); + String name = unitName; + + while (!this.prefixlessUnits.containsKey(name)) { + // find the longest prefix + String longestPrefixName = null; + int longestLength = name.length(); + + while (longestPrefixName == null) { + longestLength--; + if (longestLength <= 0) + throw new AssertionError( + "No prefix found in " + name + ", but it is not a unit!"); + if (this.prefixes.containsKey(name.substring(0, longestLength))) { + longestPrefixName = name.substring(0, longestLength); + } + } + + // longest prefix found! + final UnitPrefix prefix = this.getPrefix(longestPrefixName); + prefixes.add(0, prefix); + name = name.substring(longestLength); + } + return prefixes; + } + + /** + * Gets a unit prefix from a prefix expression + * <p> + * 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 + * @throws NullPointerException if any argument is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public UnitPrefix getPrefixFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getPrefix(expression); + + // force operators to have spaces + String modifiedExpression = expression; + + // 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 the prefixRepetitionRule + * @since 2020-08-26 + */ + public final 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 + * @since v0.1.0 + */ + public Unit getUnit(final String name) { + try { + final double value = Double.parseDouble(name); + return SI.ONE.times(value); + } catch (final NumberFormatException e) { + final Unit unit = this.units.get(name); + if (unit == null) + throw new NoSuchElementException("No unit " + name); + else if (unit.getPrimaryName().isEmpty()) + return unit.withName(NameSymbol.ofName(name)); + else if (!unit.getPrimaryName().get().equals(name)) { + final Set<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)) { + 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; + } + + } + + /** + * Uses the database's unit data to parse an expression into a unit + * <p> + * The expression is a series of any of the following: + * <ul> + * <li>The name of a unit, which multiplies or divides the result based on + * preceding operators</li> + * <li>The operators '*' and '/', which multiply and divide (note that just + * putting two units or values next to each other is equivalent to + * multiplication)</li> + * <li>The operator '^' which exponentiates. Exponents must be integers.</li> + * <li>A number which is multiplied or divided</li> + * </ul> + * This method only works with linear units. + * + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null + * @since 2019-01-07 + * @since v0.1.0 + */ + public Unit getUnitFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getUnit(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // 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) + 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.unitExpressionParser.parseExpression(modifiedExpression); + } + + /** + * Adds all dimensions from a file, using data from the database to parse + * them. + * <p> + * Each line in the file should consist of a name and an expression (parsed + * by getDimensionFromExpression) separated by any number of tab characters. + * <p> + * <p> + * Allowed exceptions: + * <ul> + * <li>Anything after a '#' character is considered a comment and + * ignored.</li> + * <li>Blank lines are also ignored</li> + * <li>If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.</li> + * </ul> + * + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadDimensionFile(final Path file) { + Objects.requireNonNull(file, "file must not be null."); + try { + long lineCounter = 0; + for (final String line : Files.readAllLines(file)) { + this.addDimensionFromLine(line, ++lineCounter); + } + } 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); + } + } + + /** + * Adds all dimensions from a {@code InputStream}. Otherwise, works like + * {@link #loadDimensionFile}. + * + * @param stream stream to load from + * @since 2021-03-27 + */ + public void loadDimensionsFromStream(final InputStream stream) { + try (final Scanner scanner = new Scanner(stream)) { + long lineCounter = 0; + while (scanner.hasNextLine()) { + this.addDimensionFromLine(scanner.nextLine(), ++lineCounter); + } + } + } + + /** + * Adds all units from a file, using data from the database to parse them. + * <p> + * Each line in the file should consist of a name and an expression (parsed + * by getUnitFromExpression) separated by any number of tab characters. + * <p> + * <p> + * Allowed exceptions: + * <ul> + * <li>Anything after a '#' character is considered a comment and + * ignored.</li> + * <li>Blank lines are also ignored</li> + * <li>If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.</li> + * </ul> + * + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadUnitsFile(final Path file) { + Objects.requireNonNull(file, "file must not be null."); + try { + long lineCounter = 0; + for (final String line : Files.readAllLines(file)) { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } + } 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); + } + } + + /** + * Adds all units from a {@code InputStream}. Otherwise, works like + * {@link #loadUnitsFile}. + * + * @param stream stream to load from + * @since 2021-03-27 + */ + public void loadUnitsFromStream(InputStream stream) { + try (final Scanner scanner = new Scanner(stream)) { + long lineCounter = 0; + while (scanner.hasNextLine()) { + this.addUnitOrPrefixFromLine(scanner.nextLine(), ++lineCounter); + } + } + } + + /** + * @return a map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map<String, UnitPrefix> prefixMap() { + return Collections.unmodifiableMap(this.prefixes); + } + + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public final void setPrefixRepetitionRule( + Predicate<List<UnitPrefix>> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + } + + /** + * @return a string stating the number of units, prefixes and dimensions in + * the database + */ + @Override + public String toString() { + return String.format( + "Unit Database with %d units, %d unit prefixes and %d dimensions", + this.prefixlessUnits.size(), this.prefixes.size(), + this.dimensions.size()); + } + + /** + * Returns a map mapping unit names to units, including units with prefixes. + * <p> + * The returned map is infinite in size if there is at least one unit and at + * least one prefix. If it is infinite, some operations that only work with + * finite collections, like converting name/entry sets to arrays, will throw + * an {@code IllegalStateException}. + * </p> + * <p> + * Specifically, the operations that will throw an IllegalStateException if + * the map is infinite in size are: + * <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 + */ + public Map<String, Unit> unitMap() { + return this.units; // PrefixedUnitMap is immutable so I don't need to make + // an unmodifiable map. + } + + /** + * @param includeDuplicates if true, duplicate units will all exist in the + * map; if false, only one of each unit will exist, + * even if the names are different + * @return a map mapping unit names to units, ignoring prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + 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))); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitPrefix.java b/src/main/java/org/unitConverter/unit/UnitPrefix.java new file mode 100644 index 0000000..31cc0b3 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitPrefix.java @@ -0,0 +1,242 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.unitConverter.math.DecimalComparison; + +/** + * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class UnitPrefix { + /** + * Gets a {@code UnitPrefix} from a multiplier + * + * @param multiplier + * multiplier of prefix + * @return prefix + * @since 2019-10-16 + */ + public static UnitPrefix valueOf(final double multiplier) { + return new UnitPrefix(multiplier, NameSymbol.EMPTY); + } + + /** + * Gets a {@code UnitPrefix} from a multiplier and a name + * + * @param multiplier + * multiplier of prefix + * @param ns + * name(s) and symbol of prefix + * @return prefix + * @since 2019-10-16 + * @throws NullPointerException + * if ns is null + */ + public static UnitPrefix valueOf(final double multiplier, final NameSymbol ns) { + return new UnitPrefix(multiplier, Objects.requireNonNull(ns, "ns must not be null.")); + } + + /** + * This prefix's primary name + */ + private final Optional<String> primaryName; + + /** + * This prefix's symbol + */ + private final Optional<String> symbol; + + /** + * Other names and symbols used by this prefix + */ + private final Set<String> otherNames; + + /** + * The number that this prefix multiplies units by + * + * @since 2019-10-16 + */ + private final double multiplier; + + /** + * Creates the {@code DefaultUnitPrefix}. + * + * @param multiplier + * @since 2019-01-14 + * @since v0.2.0 + */ + private UnitPrefix(final double multiplier, final NameSymbol ns) { + this.multiplier = multiplier; + this.primaryName = ns.getPrimaryName(); + this.symbol = ns.getSymbol(); + this.otherNames = ns.getOtherNames(); + } + + /** + * Divides this prefix by a scalar + * + * @param divisor + * number to divide by + * @return quotient of prefix and scalar + * @since 2019-10-16 + */ + public UnitPrefix dividedBy(final double divisor) { + return valueOf(this.getMultiplier() / divisor); + } + + /** + * Divides this prefix by {@code other}. + * + * @param other + * prefix to divide by + * @return quotient of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix dividedBy(final UnitPrefix other) { + return valueOf(this.getMultiplier() / other.getMultiplier()); + } + + /** + * {@inheritDoc} + * + * Uses the prefix's multiplier to determine equality. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof UnitPrefix)) + return false; + final UnitPrefix other = (UnitPrefix) obj; + return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); + } + + /** + * @return prefix's multiplier + * @since 2019-11-26 + */ + public double getMultiplier() { + return this.multiplier; + } + + /** + * @return other names + * @since 2019-11-26 + */ + public final Set<String> getOtherNames() { + return this.otherNames; + } + + /** + * @return primary name + * @since 2019-11-26 + */ + public final Optional<String> getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-11-26 + */ + public final Optional<String> getSymbol() { + return this.symbol; + } + + /** + * {@inheritDoc} + * + * Uses the prefix's multiplier to determine a hash code. + */ + @Override + public int hashCode() { + return DecimalComparison.hash(this.getMultiplier()); + } + + /** + * Multiplies this prefix by a scalar + * + * @param multiplicand + * number to multiply by + * @return product of prefix and scalar + * @since 2019-10-16 + */ + public UnitPrefix times(final double multiplicand) { + return valueOf(this.getMultiplier() * multiplicand); + } + + /** + * Multiplies this prefix by {@code other}. + * + * @param other + * prefix to multiply by + * @return product of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix times(final UnitPrefix other) { + return valueOf(this.getMultiplier() * other.getMultiplier()); + } + + /** + * Raises this prefix to an exponent. + * + * @param exponent + * exponent to raise to + * @return result of exponentiation. + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix toExponent(final double exponent) { + return valueOf(Math.pow(this.getMultiplier(), exponent)); + } + + /** + * @return a string describing the prefix and its multiplier + */ + @Override + public String toString() { + if (this.primaryName.isPresent()) + return String.format("%s (\u00D7 %s)", this.primaryName.get(), this.multiplier); + else if (this.symbol.isPresent()) + return String.format("%s (\u00D7 %s)", this.symbol.get(), this.multiplier); + else + 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 + * @throws NullPointerException + * if ns is null + */ + public UnitPrefix withName(final NameSymbol ns) { + return valueOf(this.multiplier, ns); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitValue.java b/src/main/java/org/unitConverter/unit/UnitValue.java new file mode 100644 index 0000000..c138332 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitValue.java @@ -0,0 +1,172 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +/** + * A value expressed in a unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class UnitValue { + /** + * Creates a {@code UnitValue} from a unit and the associated value. + * + * @param unit unit to use + * @param value value to use + * @return {@code UnitValue} instance + */ + public static UnitValue of(Unit unit, double value) { + return new UnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value); + } + + private final Unit unit; + private final double value; + + /** + * @param unit the unit being used + * @param value the value being represented + */ + private UnitValue(Unit unit, Double value) { + this.unit = unit; + this.value = value; + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + 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) { + 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) { + return UnitValue.of(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnitValue)) + return false; + final UnitValue other = (UnitValue) obj; + return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) + && Double.doubleToLongBits( + this.getUnit().convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.getUnit().convertToBase(other.getValue())); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final Unit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.getUnit().getBase(), + this.getUnit().convertFromBase(this.getValue())); + } + + @Override + public String toString() { + final Optional<String> primaryName = this.getUnit().getPrimaryName(); + final Optional<String> symbol = this.getUnit().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + final double baseValue = this.getUnit().convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnit().getBase()); + } else { + final String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} diff --git a/src/main/java/org/unitConverter/unit/Unitlike.java b/src/main/java/org/unitConverter/unit/Unitlike.java new file mode 100644 index 0000000..8077771 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Unitlike.java @@ -0,0 +1,260 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * An object that can convert a value between multiple forms (instances of the + * object); like a unit but the "converted value" can be any type. + * + * @since 2020-09-07 + */ +public abstract class Unitlike<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/org/unitConverter/unit/UnitlikeValue.java b/src/main/java/org/unitConverter/unit/UnitlikeValue.java new file mode 100644 index 0000000..79201c4 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitlikeValue.java @@ -0,0 +1,174 @@ +/** + * 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 org.unitConverter.unit; + +import java.util.Optional; + +/** + * + * @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/org/unitConverter/unit/package-info.java b/src/main/java/org/unitConverter/unit/package-info.java index dd5a939..2f0e097 100644 --- a/src/org/unitConverter/unit/package-info.java +++ b/src/main/java/org/unitConverter/unit/package-info.java @@ -15,10 +15,10 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** - * All of the classes that correspond to the units being converted. + * Everything to do with the units that make up Unit Converter. * * @author Adrien Hopkins - * @since 2019-01-25 + * @since 2019-10-16 * @since v0.1.0 */ package org.unitConverter.unit;
\ No newline at end of file diff --git a/src/main/resources/about.txt b/src/main/resources/about.txt new file mode 100644 index 0000000..da0ab11 --- /dev/null +++ b/src/main/resources/about.txt @@ -0,0 +1,12 @@ +About Unit Converter v0.3.0 + +Copyright Notice: + +Unit Converter Copyright (C) 2018-2021 Adrien Hopkins +This program comes with ABSOLUTELY NO WARRANTY; +for details read the LICENSE file, section 15 + +This is free software, and you are welcome to redistribute +it under certain conditions; for details go to +<https://www.gnu.org/licenses/quick-guide-gplv3.html> +or read the LICENSE file. diff --git a/dimensionfile.txt b/src/main/resources/dimensionfile.txt index 3485de5..3485de5 100644 --- a/dimensionfile.txt +++ b/src/main/resources/dimensionfile.txt diff --git a/src/main/resources/metric_exceptions.txt b/src/main/resources/metric_exceptions.txt new file mode 100644 index 0000000..73748c0 --- /dev/null +++ b/src/main/resources/metric_exceptions.txt @@ -0,0 +1,19 @@ +# This is a list of exceptions for the one-way conversion mode +# Units in this list will be included in both From: and To: +# regardless of whether or not one-way conversion is enabled. + +tempC +tempCelsius +s +second +min +minute +h +hour +d +day +wk +week +gregorianmonth +gregorianyear +km/h
\ No newline at end of file diff --git a/unitsfile.txt b/src/main/resources/unitsfile.txt index bda9b81..340e8ea 100755..100644 --- a/unitsfile.txt +++ b/src/main/resources/unitsfile.txt @@ -121,7 +121,7 @@ henry V s / A H henry tesla Wb / m^2 T tesla -hertz s^-1 +hertz 1 / s Hz hertz gram millikg @@ -157,8 +157,11 @@ hour 3600 second h hour day 86400 second d day +week 7 day +wk week julianyear 365.25 day gregorianyear 365.2425 day +gregorianmonth gregorianyear / 12 # Other non-SI "metric" units litre 0.001 m^3 @@ -180,7 +183,7 @@ waterdensity kilogram / litre # Imperial length units foot 0.3048 m ft foot -inch 1 / 12 foot +inch foot / 12 in inch yard 3 foot yd yard @@ -248,15 +251,22 @@ Calorie kilocalorie Cal Calorie Wh W h -# Extra units to only include in the dimension-based converter +# Extra units to show in the dimension-based converter km km +kilometre km cm cm +centimetre cm mm mm +millimetre mm mg mg +milligram mg mL mL ml ml +millilitre mL kJ kJ +kilojoule kJ MJ MJ +megajoule MJ kWh kWh m/s m / s km/h km / h diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java deleted file mode 100755 index e5d2f67..0000000 --- a/src/org/unitConverter/UnitsDatabase.java +++ /dev/null @@ -1,1479 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.AbstractSet; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; - -import org.unitConverter.dimension.UnitDimension; -import org.unitConverter.math.DecimalComparison; -import org.unitConverter.math.ExpressionParser; -import org.unitConverter.unit.DefaultUnitPrefix; -import org.unitConverter.unit.LinearUnit; -import org.unitConverter.unit.SI; -import org.unitConverter.unit.Unit; -import org.unitConverter.unit.UnitPrefix; - -/** - * A database of units, prefixes and dimensions, and their names. - * - * @author Adrien Hopkins - * @since 2019-01-07 - * @since v0.1.0 - */ -public final class UnitsDatabase { - /** - * A map for units that allows the use of prefixes. - * <p> - * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is - * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the - * maps that were passed in during construction. - * </p> - * <p> - * The rules for applying prefixes onto units are the following: - * <ul> - * <li>Prefixes can only be applied to linear units.</li> - * <li>Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if - * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with - * the prefix "A", even though they have the same string.</li> - * <li>Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and - * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li> - * </ul> - * </p> - * <p> - * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some - * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an - * {@code UnsupportedOperationException}. - * </p> - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitMap implements Map<String, Unit> { - /** - * The class used for entry sets. - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntrySet extends AbstractSet<Map.Entry<String, Unit>> { - /** - * The entry for this set. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntry implements Entry<String, Unit> { - private final String key; - private final Unit value; - - /** - * Creates the {@code PrefixedUnitEntry}. - * - * @param key - * key - * @param value - * value - * @since 2019-04-14 - * @since v0.2.0 - */ - public PrefixedUnitEntry(final String key, final Unit value) { - this.key = key; - this.value = value; - } - - @Override - public String getKey() { - return this.key; - } - - @Override - public Unit getValue() { - return this.value; - } - - @Override - public Unit setValue(final Unit value) { - throw new UnsupportedOperationException(); - } - } - - /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntryIterator implements Iterator<Entry<String, Unit>> { - // position in the unit list - private int unitNamePosition = 0; - // the indices of the prefixes attached to the current unit - private final List<Integer> prefixCoordinates = new ArrayList<>(); - - // values from the unit entry set - private final Map<String, Unit> map; - private final List<String> unitNames; - private final List<String> prefixNames; - - /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - public PrefixedUnitEntryIterator(final PrefixedUnitEntrySet set) { - this.map = set.map; - this.unitNames = new ArrayList<>(set.map.units.keySet()); - this.prefixNames = new ArrayList<>(set.map.prefixes.keySet()); - } - - /** - * @return current unit name - * @since 2019-04-14 - * @since v0.2.0 - */ - private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); - for (final int i : this.prefixCoordinates) { - unitName.append(this.prefixNames.get(i)); - } - unitName.append(this.unitNames.get(this.unitNamePosition)); - - return unitName.toString(); - } - - @Override - public boolean hasNext() { - if (this.unitNames.isEmpty()) - return false; - else { - if (this.prefixNames.isEmpty()) - return this.unitNamePosition >= this.unitNames.size() - 1; - else - return true; - } - } - - /** - * Changes this iterator's position to the next available one. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private void incrementPosition() { - this.unitNamePosition++; - - if (this.unitNamePosition >= this.unitNames.size()) { - // we have used all of our units, go to a different prefix - this.unitNamePosition = 0; - - // if the prefix coordinates are empty, then set it to [0] - if (this.prefixCoordinates.isEmpty()) { - this.prefixCoordinates.add(0, 0); - } else { - // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - - // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { - // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - - if (i < 0) { // we need to add a new coordinate - this.prefixCoordinates.add(0, 0); - } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - } - } - } - } - } - - @Override - public Entry<String, Unit> next() { - if (!this.hasNext()) - throw new NoSuchElementException("No units left!"); - // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units - if (!this.prefixCoordinates.isEmpty()) { - while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { - this.unitNames.remove(this.unitNamePosition); - } - } - - final String nextName = this.getCurrentUnitName(); - - this.incrementPosition(); - - return new PrefixedUnitEntry(nextName, this.map.get(nextName)); - } - } - - // the map that created this set - private final PrefixedUnitMap map; - - /** - * Creates the {@code PrefixedUnitNameSet}. - * - * @param map - * map that created this set - * @since 2019-04-13 - * @since v0.2.0 - */ - public PrefixedUnitEntrySet(final PrefixedUnitMap map) { - this.map = map; - } - - @Override - public boolean add(final Map.Entry<String, Unit> e) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean addAll(final Collection<? extends Map.Entry<String, Unit>> c) { - throw new UnsupportedOperationException(); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean contains(final Object o) { - // get the entry - final Entry<String, Unit> entry; - - try { - // This is OK because I'm in a try-catch block. - @SuppressWarnings("unchecked") - final Entry<String, Unit> tempEntry = (Entry<String, Unit>) o; - entry = tempEntry; - } catch (final ClassCastException e) { - throw new IllegalArgumentException("Attempted to test for an entry using a non-entry."); - } - - return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue()); - } - - @Override - public boolean containsAll(final Collection<?> c) { - for (final Object o : c) - if (!this.contains(o)) - return false; - return true; - } - - @Override - public boolean isEmpty() { - return this.map.isEmpty(); - } - - @Override - public Iterator<Entry<String, Unit>> iterator() { - return new PrefixedUnitEntryIterator(this); - } - - @Override - public boolean remove(final Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeAll(final Collection<?> c) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeIf(final Predicate<? super Entry<String, Unit>> filter) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean retainAll(final Collection<?> c) { - throw new UnsupportedOperationException(); - } - - @Override - 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; - } - } - - @Override - public Object[] toArray() { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toArray(); - else - // infinite set - throw new UnsupportedOperationException("Cannot make an infinite set into an array."); - } - - @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 UnsupportedOperationException("Cannot make an infinite set into an array."); - } - - } - - /** - * The class used for unit name sets. - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitNameSet extends AbstractSet<String> { - /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ - private static final class PrefixedUnitNameIterator implements Iterator<String> { - // position in the unit list - private int unitNamePosition = 0; - // the indices of the prefixes attached to the current unit - private final List<Integer> prefixCoordinates = new ArrayList<>(); - - // values from the unit name set - private final Map<String, Unit> map; - private final List<String> unitNames; - private final List<String> prefixNames; - - /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - public PrefixedUnitNameIterator(final PrefixedUnitNameSet set) { - this.map = set.map; - this.unitNames = new ArrayList<>(set.map.units.keySet()); - this.prefixNames = new ArrayList<>(set.map.prefixes.keySet()); - } - - /** - * @return current unit name - * @since 2019-04-14 - * @since v0.2.0 - */ - private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); - for (final int i : this.prefixCoordinates) { - unitName.append(this.prefixNames.get(i)); - } - unitName.append(this.unitNames.get(this.unitNamePosition)); - - return unitName.toString(); - } - - @Override - public boolean hasNext() { - if (this.unitNames.isEmpty()) - return false; - else { - if (this.prefixNames.isEmpty()) - return this.unitNamePosition >= this.unitNames.size() - 1; - else - return true; - } - } - - /** - * Changes this iterator's position to the next available one. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private void incrementPosition() { - this.unitNamePosition++; - - if (this.unitNamePosition >= this.unitNames.size()) { - // we have used all of our units, go to a different prefix - this.unitNamePosition = 0; - - // if the prefix coordinates are empty, then set it to [0] - if (this.prefixCoordinates.isEmpty()) { - this.prefixCoordinates.add(0, 0); - } else { - // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - - // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { - // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - - if (i < 0) { // we need to add a new coordinate - this.prefixCoordinates.add(0, 0); - } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - } - } - } - } - } - - @Override - public String next() { - if (!this.hasNext()) - throw new NoSuchElementException("No units left!"); - // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units - if (!this.prefixCoordinates.isEmpty()) { - while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { - this.unitNames.remove(this.unitNamePosition); - } - } - - final String nextName = this.getCurrentUnitName(); - - this.incrementPosition(); - - return nextName; - } - } - - // the map that created this set - private final PrefixedUnitMap map; - - /** - * Creates the {@code PrefixedUnitNameSet}. - * - * @param map - * map that created this set - * @since 2019-04-13 - * @since v0.2.0 - */ - public PrefixedUnitNameSet(final PrefixedUnitMap map) { - this.map = map; - } - - @Override - public boolean add(final String e) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean addAll(final Collection<? extends String> c) { - throw new UnsupportedOperationException(); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean contains(final Object o) { - return this.map.containsKey(o); - } - - @Override - public boolean containsAll(final Collection<?> c) { - for (final Object o : c) - if (!this.contains(o)) - return false; - return true; - } - - @Override - public boolean isEmpty() { - return this.map.isEmpty(); - } - - @Override - public Iterator<String> iterator() { - return new PrefixedUnitNameIterator(this); - } - - @Override - public boolean remove(final Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeAll(final Collection<?> c) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeIf(final Predicate<? super String> filter) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean retainAll(final Collection<?> c) { - throw new UnsupportedOperationException(); - } - - @Override - 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; - } - } - - @Override - public Object[] toArray() { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toArray(); - else - // infinite set - throw new UnsupportedOperationException("Cannot make an infinite set into an array."); - - } - - @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 UnsupportedOperationException("Cannot make an infinite set into an array."); - } - } - - /** - * The units stored in this collection, without prefixes. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map<String, Unit> units; - - /** - * The available prefixes for use. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map<String, UnitPrefix> prefixes; - - // caches - private Collection<Unit> values = null; - private Set<String> keySet = null; - private Set<Entry<String, Unit>> entrySet = null; - - /** - * Creates the {@code PrefixedUnitMap}. - * - * @param units - * map mapping unit names to units - * @param prefixes - * map mapping prefix names to prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public PrefixedUnitMap(final Map<String, Unit> units, final Map<String, UnitPrefix> prefixes) { - // I am making unmodifiable maps to ensure I don't accidentally make changes. - this.units = Collections.unmodifiableMap(units); - this.prefixes = Collections.unmodifiableMap(prefixes); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public Unit compute(final String key, - final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException(); - } - - @Override - public Unit computeIfAbsent(final String key, final Function<? super String, ? extends Unit> mappingFunction) { - throw new UnsupportedOperationException(); - } - - @Override - public Unit computeIfPresent(final String key, - final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean containsKey(final Object key) { - // First, test if there is a unit with the key - if (this.units.containsKey(key)) - return true; - - // Next, try to cast it to String - if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to test for a unit using a non-string name."); - final String unitName = (String) key; - - // Then, look for the longest prefix that is attached to a valid unit - String longestPrefix = null; - int longestLength = 0; - - for (final String prefixName : this.prefixes.keySet()) { - // a prefix name is valid if: - // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) - // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { - longestPrefix = prefixName; - longestLength = prefixName.length(); - } - } - } - - return longestPrefix != null; - } - - @Override - public boolean containsValue(final Object value) { - return this.units.containsValue(value); - } - - @Override - public Set<Entry<String, Unit>> entrySet() { - if (this.entrySet == null) { - this.entrySet = new PrefixedUnitEntrySet(this); - } - return this.entrySet; - } - - @Override - public Unit get(final Object key) { - // First, test if there is a unit with the key - if (this.units.containsKey(key)) - return this.units.get(key); - - // Next, try to cast it to String - if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name."); - final String unitName = (String) key; - - // Then, look for the longest prefix that is attached to a valid unit - String longestPrefix = null; - int longestLength = 0; - - for (final String prefixName : this.prefixes.keySet()) { - // a prefix name is valid if: - // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) - // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { - longestPrefix = prefixName; - longestLength = prefixName.length(); - } - } - } - - // if none found, returns null - if (longestPrefix == null) - return null; - else { - // get necessary data - final String rest = unitName.substring(longestLength); - // this cast will not fail because I verified that it would work before selecting this prefix - final LinearUnit unit = (LinearUnit) this.get(rest); - final UnitPrefix prefix = this.prefixes.get(longestPrefix); - - return unit.withPrefix(prefix); - } - } - - @Override - public boolean isEmpty() { - return this.units.isEmpty(); - } - - @Override - public Set<String> keySet() { - if (this.keySet == null) { - this.keySet = new PrefixedUnitNameSet(this); - } - return this.keySet; - } - - @Override - public Unit merge(final String key, final Unit value, - final BiFunction<? super Unit, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException(); - } - - @Override - public Unit put(final String key, final Unit value) { - throw new UnsupportedOperationException(); - } - - @Override - public void putAll(final Map<? extends String, ? extends Unit> m) { - throw new UnsupportedOperationException(); - } - - @Override - public Unit putIfAbsent(final String key, final Unit value) { - throw new UnsupportedOperationException(); - } - - @Override - public Unit remove(final Object key) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean remove(final Object key, final Object value) { - throw new UnsupportedOperationException(); - } - - @Override - public Unit replace(final String key, final Unit value) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean replace(final String key, final Unit oldValue, final Unit newValue) { - throw new UnsupportedOperationException(); - } - - @Override - public void replaceAll(final BiFunction<? super String, ? super Unit, ? extends Unit> function) { - throw new UnsupportedOperationException(); - } - - @Override - 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; - } - } - - @Override - public Collection<Unit> values() { - if (this.values == null) { - this.values = Collections.unmodifiableCollection(this.units.values()); - } - return this.values; - } - } - - /** - * 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, final LinearUnit exponentUnit) { - // exponent function - first check if o2 is a number, - if (exponentUnit.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) { - // 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."); - } - - /** - * The units in this system, excluding prefixes. - * - * @since 2019-01-07 - * @since v0.1.0 - */ - private final Map<String, Unit> prefixlessUnits; - - /** - * The unit prefixes in this system. - * - * @since 2019-01-14 - * @since v0.1.0 - */ - private final Map<String, UnitPrefix> prefixes; - - /** - * The dimensions in this system. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Map<String, UnitDimension> dimensions; - - /** - * A map mapping strings to units (including prefixes) - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map<String, Unit> units; - - /** - * 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("^", UnitsDatabase::exponentiateUnits, 2).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.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build(); - - /** - * A parser that can parse unit dimension expressions. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final ExpressionParser<UnitDimension> unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); - - /** - * Creates the {@code UnitsDatabase}. - * - * @since 2019-01-10 - * @since v0.1.0 - */ - public UnitsDatabase() { - this.prefixlessUnits = new HashMap<>(); - this.prefixes = new HashMap<>(); - this.dimensions = new HashMap<>(); - this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); - } - - /** - * Adds a unit dimension to the database. - * - * @param name - * dimension's name - * @param dimension - * dimension to add - * @throws NullPointerException - * if name or dimension is null - * @since 2019-03-14 - * @since v0.2.0 - */ - public void addDimension(final String name, final UnitDimension dimension) { - this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(dimension, "dimension must not be null.")); - } - - /** - * Adds to the list from a line in a unit dimension file. - * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages - * @since 2019-04-10 - * @since v0.2.0 - */ - private void addDimensionFromLine(final String line, final long lineCounter) { - // ignore lines that start with a # sign - they're comments - if (line.isEmpty()) - return; - if (line.contains("#")) { - this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter); - return; - } - - // divide line into name and expression - final String[] parts = line.split("\t"); - if (parts.length < 2) - throw new IllegalArgumentException(String.format( - "Lines must consist of a dimension name and its definition, separated by tab(s) (line %d).", - lineCounter)); - final String name = parts[0]; - final String expression = parts[parts.length - 1]; - - if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter); - } - - // if expression is "!", search for an existing dimension - // if no unit found, throw an error - if (expression.equals("!")) { - if (!this.containsDimensionName(name)) - throw new IllegalArgumentException( - String.format("! used but no dimension found (line %d).", lineCounter)); - } else { - // it's a unit, get the unit - final UnitDimension dimension; - try { - dimension = this.getDimensionFromExpression(expression); - } catch (final IllegalArgumentException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - this.addDimension(name, dimension); - } - } - - /** - * Adds a unit prefix to the database. - * - * @param name - * prefix's name - * @param prefix - * prefix to add - * @throws NullPointerException - * if name or prefix is null - * @since 2019-01-14 - * @since v0.1.0 - */ - public void addPrefix(final String name, final UnitPrefix prefix) { - this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(prefix, "prefix must not be null.")); - } - - /** - * Adds a unit to the database. - * - * @param name - * unit's name - * @param unit - * unit to add - * @throws NullPointerException - * if unit is null - * @since 2019-01-10 - * @since v0.1.0 - */ - public void addUnit(final String name, final Unit unit) { - this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(unit, "unit must not be null.")); - } - - /** - * Adds to the list from a line in a unit file. - * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages - * @since 2019-04-10 - * @since v0.2.0 - */ - private void addUnitOrPrefixFromLine(final String line, final long lineCounter) { - // ignore lines that start with a # sign - they're comments - if (line.isEmpty()) - return; - if (line.contains("#")) { - this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter); - return; - } - - // divide line into name and expression - final String[] parts = line.split("\t"); - if (parts.length < 2) - throw new IllegalArgumentException(String.format( - "Lines must consist of a unit name and its definition, separated by tab(s) (line %d).", - lineCounter)); - final String name = parts[0]; - final String expression = parts[parts.length - 1]; - - if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's unit name ends in a space", lineCounter); - } - - // if expression is "!", search for an existing unit - // if no unit found, throw an error - if (expression.equals("!")) { - if (!this.containsUnitName(name)) - throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter)); - } else { - if (name.endsWith("-")) { - final UnitPrefix prefix; - try { - prefix = this.getPrefixFromExpression(expression); - } catch (final IllegalArgumentException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - this.addPrefix(name.substring(0, name.length() - 1), prefix); - } else { - // it's a unit, get the unit - final Unit unit; - try { - unit = this.getUnitFromExpression(expression); - } catch (final IllegalArgumentException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - this.addUnit(name, unit); - } - } - } - - /** - * Tests if the database has a unit dimension with this name. - * - * @param name - * name to test - * @return if database contains name - * @since 2019-03-14 - * @since v0.2.0 - */ - public boolean containsDimensionName(final String name) { - return this.dimensions.containsKey(name); - } - - /** - * Tests if the database has a unit prefix with this name. - * - * @param name - * name to test - * @return if database contains name - * @since 2019-01-13 - * @since v0.1.0 - */ - public boolean containsPrefixName(final String name) { - return this.prefixes.containsKey(name); - } - - /** - * Tests if the database has a unit with this name, taking prefixes into consideration - * - * @param name - * name to test - * @return if database contains name - * @since 2019-01-13 - * @since v0.1.0 - */ - public boolean containsUnitName(final String name) { - return this.units.containsKey(name); - } - - /** - * @return a map mapping dimension names to dimensions - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map<String, UnitDimension> dimensionMap() { - return Collections.unmodifiableMap(this.dimensions); - } - - /** - * Gets a unit dimension from the database using its name. - * - * <p> - * This method accepts exponents, like "L^3" - * </p> - * - * @param name - * dimension's name - * @return dimension - * @since 2019-03-14 - * @since v0.2.0 - */ - public UnitDimension getDimension(final String name) { - Objects.requireNonNull(name, "name must not be null."); - if (name.contains("^")) { - final String[] baseAndExponent = name.split("\\^"); - - final UnitDimension base = this.getDimension(baseAndExponent[0]); - - final int exponent; - try { - exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); - } catch (final NumberFormatException e2) { - throw new IllegalArgumentException("Exponent must be an integer."); - } - - return base.toExponent(exponent); - } - return this.dimensions.get(name); - } - - /** - * Uses the database's data to parse an expression into a unit dimension - * <p> - * The expression is a series of any of the following: - * <ul> - * <li>The name of a unit dimension, which multiplies or divides the result based on preceding operators</li> - * <li>The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each - * other is equivalent to multiplication)</li> - * <li>The operator '^' which exponentiates. Exponents must be integers.</li> - * </ul> - * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null - * @since 2019-04-13 - * @since v0.2.0 - */ - public UnitDimension getDimensionFromExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // attempt to get a dimension as an alias first - if (this.containsDimensionName(expression)) - return this.getDimension(expression); - - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - - return this.unitDimensionParser.parseExpression(modifiedExpression); - } - - /** - * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an - * {@code IllegalArgumentException}. - * - * @param name - * unit's name - * @return unit - * @since 2019-03-22 - * @since v0.2.0 - */ - private LinearUnit getLinearUnit(final String name) { - // see if I am using a function-unit like tempC(100) - if (name.contains("(") && name.contains(")")) { - // break it into function name and value - final List<String> parts = Arrays.asList(name.split("\\(")); - if (parts.size() != 2) - throw new IllegalArgumentException("Format nonlinear units like: unit(value)."); - - // solve the function - final Unit unit = this.getUnit(parts.get(0)); - final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1)); - return unit.getBase().times(unit.convertToBase(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)); - } - } - - /** - * Gets a unit prefix from the database from its name - * - * @param name - * prefix's name - * @return prefix - * @since 2019-01-10 - * @since v0.1.0 - */ - public UnitPrefix getPrefix(final String name) { - try { - return new DefaultUnitPrefix(Double.parseDouble(name)); - } catch (final NumberFormatException e) { - return this.prefixes.get(name); - } - } - - /** - * Gets a unit prefix from a prefix expression - * <p> - * 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 - * @throws NullPointerException - * if any argument is null - * @since 2019-01-14 - * @since v0.1.0 - */ - public UnitPrefix getPrefixFromExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // attempt to get a unit as an alias first - if (this.containsUnitName(expression)) - return this.getPrefix(expression); - - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - - return this.prefixExpressionParser.parseExpression(modifiedExpression); - } - - /** - * Gets a unit from the database from its name, looking for prefixes. - * - * @param name - * unit's name - * @return unit - * @since 2019-01-10 - * @since v0.1.0 - */ - public Unit getUnit(final String name) { - try { - final double value = Double.parseDouble(name); - return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value); - } catch (final NumberFormatException e) { - return this.units.get(name); - } - - } - - /** - * Uses the database's unit data to parse an expression into a unit - * <p> - * The expression is a series of any of the following: - * <ul> - * <li>The name of a unit, which multiplies or divides the result based on preceding operators</li> - * <li>The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each - * other is equivalent to multiplication)</li> - * <li>The operator '^' which exponentiates. Exponents must be integers.</li> - * <li>A number which is multiplied or divided</li> - * </ul> - * This method only works with linear units. - * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null - * @since 2019-01-07 - * @since v0.1.0 - */ - public Unit getUnitFromExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // attempt to get a unit as an alias first - if (this.containsUnitName(expression)) - return this.getUnit(expression); - - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); - modifiedExpression = modifiedExpression.replaceAll("-", " - "); - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - for (int i = 2; i < modifiedExpression.length(); i++) { - if (modifiedExpression.charAt(i) == '-' - && Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2))) { - // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2); - } - } - - return this.unitExpressionParser.parseExpression(modifiedExpression); - } - - /** - * Adds all dimensions from a file, using data from the database to parse them. - * <p> - * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated - * by any number of tab characters. - * <p> - * <p> - * Allowed exceptions: - * <ul> - * <li>Anything after a '#' character is considered a comment and ignored.</li> - * <li>Blank lines are also ignored</li> - * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.</li> - * </ul> - * - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null - * @since 2019-01-13 - * @since v0.1.0 - */ - public void loadDimensionFile(final File file) { - Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it - long lineCounter = 0; - while (reader.ready()) { - this.addDimensionFromLine(reader.readLine(), ++lineCounter); - } - } 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); - } - } - - /** - * Adds all units from a file, using data from the database to parse them. - * <p> - * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by - * any number of tab characters. - * <p> - * <p> - * Allowed exceptions: - * <ul> - * <li>Anything after a '#' character is considered a comment and ignored.</li> - * <li>Blank lines are also ignored</li> - * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.</li> - * </ul> - * - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null - * @since 2019-01-13 - * @since v0.1.0 - */ - public void loadUnitsFile(final File file) { - Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it - long lineCounter = 0; - while (reader.ready()) { - this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); - } - } 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 a map mapping prefix names to prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map<String, UnitPrefix> prefixMap() { - return Collections.unmodifiableMap(this.prefixes); - } - - /** - * @return a map mapping unit names to units, including prefixed names - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map<String, Unit> unitMap() { - return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map. - } - - /** - * @return a map mapping unit names to units, ignoring prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map<String, Unit> unitMapPrefixless() { - return Collections.unmodifiableMap(this.prefixlessUnits); - } -} diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java deleted file mode 100755 index e258c6f..0000000 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ /dev/null @@ -1,827 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.converterGUI; - -import java.awt.BorderLayout; -import java.awt.GridLayout; -import java.io.File; -import java.math.BigDecimal; -import java.math.MathContext; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JFormattedTextField; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.JTabbedPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; - -import org.unitConverter.UnitsDatabase; -import org.unitConverter.dimension.StandardDimensions; -import org.unitConverter.dimension.UnitDimension; -import org.unitConverter.unit.BaseUnit; -import org.unitConverter.unit.NonlinearUnits; -import org.unitConverter.unit.SI; -import org.unitConverter.unit.Unit; -import org.unitConverter.unit.UnitPrefix; - -/** - * @author Adrien Hopkins - * @since 2018-12-27 - * @since v0.1.0 - */ -final class UnitConverterGUI { - private static class Presenter { - /** - * Adds default units and dimensions to a database. - * - * @param database - * database to add to - * @since 2019-04-14 - * @since v0.2.0 - */ - private static void addDefaults(final UnitsDatabase database) { - database.addUnit("metre", SI.METRE); - database.addUnit("kilogram", SI.KILOGRAM); - database.addUnit("gram", SI.KILOGRAM.dividedBy(1000)); - database.addUnit("second", SI.SECOND); - database.addUnit("ampere", SI.AMPERE); - database.addUnit("kelvin", SI.KELVIN); - database.addUnit("mole", SI.MOLE); - database.addUnit("candela", SI.CANDELA); - database.addUnit("bit", SI.SI.getBaseUnit(StandardDimensions.INFORMATION)); - database.addUnit("unit", SI.SI.getBaseUnit(UnitDimension.EMPTY)); - // nonlinear units - must be loaded manually - database.addUnit("tempCelsius", NonlinearUnits.CELSIUS); - database.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT); - - // load initial dimensions - database.addDimension("LENGTH", StandardDimensions.LENGTH); - database.addDimension("MASS", StandardDimensions.MASS); - database.addDimension("TIME", StandardDimensions.TIME); - database.addDimension("TEMPERATURE", StandardDimensions.TEMPERATURE); - } - - /** The presenter's associated view. */ - private final View view; - - /** The units known by the program. */ - private final UnitsDatabase database; - - /** The names of all of the units */ - private final List<String> unitNames; - - /** The names of all of the prefixes */ - private final List<String> prefixNames; - - /** The names of all of the dimensions */ - private final List<String> dimensionNames; - - private final Comparator<String> prefixNameComparator; - - private int significantFigures = 6; - - /** - * Creates the presenter. - * - * @param view - * presenter's associated view - * @since 2018-12-27 - * @since v0.1.0 - */ - Presenter(final View view) { - this.view = view; - - // load initial units - this.database = new UnitsDatabase(); - Presenter.addDefaults(this.database); - - this.database.loadUnitsFile(new File("unitsfile.txt")); - this.database.loadDimensionFile(new File("dimensionfile.txt")); - - // a comparator that can be used to compare prefix names - // any name that does not exist is less than a name that does. - // otherwise, they are compared by value - this.prefixNameComparator = (o1, o2) -> { - if (!Presenter.this.database.containsPrefixName(o1)) - return -1; - else if (!Presenter.this.database.containsPrefixName(o2)) - return 1; - - final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); - final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); - - if (p1.getMultiplier() < p2.getMultiplier()) - return -1; - else if (p1.getMultiplier() > p2.getMultiplier()) - return 1; - - return o1.compareTo(o2); - }; - - this.unitNames = new ArrayList<>(this.database.unitMapPrefixless().keySet()); - this.unitNames.sort(null); // sorts it using Comparable - - this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); - this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator - - this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.database.dimensionMap().keySet())); - this.dimensionNames.sort(null); // sorts it using Comparable - - // a Predicate that returns true iff the argument is a full base unit - final Predicate<Unit> isFullBase = unit -> unit instanceof BaseUnit && ((BaseUnit) unit).isFullBase(); - - // print out unit counts - System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n", - new HashSet<>(this.database.unitMapPrefixless().values()).size(), - this.database.unitMapPrefixless().size(), - new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count()); - } - - /** - * Converts in the dimension-based converter - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public final void convertDimensionBased() { - final String fromSelection = this.view.getFromSelection(); - if (fromSelection == null) { - this.view.showErrorDialog("Error", "No unit selected in From field"); - return; - } - final String toSelection = this.view.getToSelection(); - if (toSelection == null) { - this.view.showErrorDialog("Error", "No unit selected in To field"); - return; - } - - final Unit from = this.database.getUnit(fromSelection); - final Unit to = this.database.getUnit(toSelection); - - final String input = this.view.getDimensionConverterInput(); - if (input.equals("")) { - this.view.showErrorDialog("Error", "No value to convert entered."); - return; - } - final double beforeValue = Double.parseDouble(input); - final double value = to.convertFromBase(from.convertToBase(beforeValue)); - - final String output = this.getRoundedString(value); - - this.view.setDimensionConverterOutputText( - String.format("%s %s = %s %s", input, fromSelection, output, toSelection)); - } - - /** - * Runs whenever the convert button is pressed. - * - * <p> - * Reads and parses a unit expression from the from and to boxes, then converts {@code from} to {@code to}. Any - * errors are shown in JOptionPanes. - * </p> - * - * @since 2019-01-26 - * @since v0.1.0 - */ - public final void convertExpressions() { - final String fromUnitString = this.view.getFromText(); - final String toUnitString = this.view.getToText(); - - if (fromUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the From: box."); - return; - } - if (toUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the To: box."); - return; - } - - // try to parse from - final Unit from; - try { - from = this.database.getUnitFromExpression(fromUnitString); - } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); - return; - } - - final double value; - // try to parse to - final Unit to; - try { - if (this.database.containsUnitName(toUnitString)) { - // if it's a unit, convert to that - to = this.database.getUnit(toUnitString); - } else { - to = this.database.getUnitFromExpression(toUnitString); - } - } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); - return; - } - - // if I can't convert, leave - if (!from.canConvertTo(to)) { - this.view.showErrorDialog("Conversion Error", - String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); - return; - } - - value = to.convertFromBase(from.convertToBase(1)); - - // round value - final String output = this.getRoundedString(value); - - this.view.setExpressionConverterOutputText( - String.format("%s = %s %s", fromUnitString, output, toUnitString)); - } - - /** - * @return a list of all of the unit dimensions - * @since 2019-04-13 - * @since v0.2.0 - */ - public final List<String> dimensionNameList() { - return this.dimensionNames; - } - - /** - * @return a comparator to compare prefix names - * @since 2019-04-14 - * @since v0.2.0 - */ - public final Comparator<String> getPrefixNameComparator() { - return this.prefixNameComparator; - } - - /** - * @param value - * value to round - * @return string of that value rounded to {@code significantDigits} significant digits. - * @since 2019-04-14 - * @since v0.2.0 - */ - private final String getRoundedString(final double value) { - // round value - final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures)); - String output = bigValue.toString(); - - // remove trailing zeroes - if (output.contains(".")) { - while (output.endsWith("0")) { - output = output.substring(0, output.length() - 1); - } - if (output.endsWith(".")) { - output = output.substring(0, output.length() - 1); - } - } - - return output; - } - - /** - * @return a set of all prefix names in the database - * @since 2019-04-14 - * @since v0.2.0 - */ - public final Set<String> prefixNameSet() { - return this.database.prefixMap().keySet(); - } - - /** - * Runs whenever a prefix is selected in the viewer. - * <p> - * Shows its information in the text box to the right. - * </p> - * - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void prefixSelected() { - final String prefixName = this.view.getPrefixViewerSelection(); - if (prefixName == null) - return; - else { - final UnitPrefix prefix = this.database.getPrefix(prefixName); - - this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier())); - } - } - - /** - * @param significantFigures - * new value of significantFigures - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void setSignificantFigures(final int significantFigures) { - this.significantFigures = significantFigures; - } - - /** - * Returns true if and only if the unit represented by {@code unitName} has the dimension represented by - * {@code dimensionName}. - * - * @param unitName - * name of unit to test - * @param dimensionName - * name of dimension to test - * @return whether unit has dimenision - * @since 2019-04-13 - * @since v0.2.0 - */ - public final boolean unitMatchesDimension(final String unitName, final String dimensionName) { - final Unit unit = this.database.getUnit(unitName); - final UnitDimension dimension = this.database.getDimension(dimensionName); - return unit.getDimension().equals(dimension); - } - - /** - * Runs whenever a unit is selected in the viewer. - * <p> - * Shows its information in the text box to the right. - * </p> - * - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void unitNameSelected() { - final String unitName = this.view.getUnitViewerSelection(); - if (unitName == null) - return; - else { - final Unit unit = this.database.getUnit(unitName); - - this.view.setUnitTextBoxText(unit.toString()); - } - } - - /** - * @return a set of all of the unit names - * @since 2019-04-14 - * @since v0.2.0 - */ - public final Set<String> unitNameSet() { - return this.database.unitMapPrefixless().keySet(); - } - } - - private static class View { - /** The view's frame. */ - private final JFrame frame; - /** The view's associated presenter. */ - private final Presenter presenter; - - // DIMENSION-BASED CONVERTER - /** The panel for inputting values in the dimension-based converter */ - private final JTextField valueInput; - /** The panel for "From" in the dimension-based converter */ - private final SearchBoxList fromSearch; - /** The panel for "To" in the dimension-based converter */ - private final SearchBoxList toSearch; - /** The output area in the dimension-based converter */ - private final JTextArea dimensionBasedOutput; - - // EXPRESSION-BASED CONVERTER - /** The "From" entry in the conversion panel */ - private final JTextField fromEntry; - /** The "To" entry in the conversion panel */ - private final JTextField toEntry; - /** The output area in the conversion panel */ - private final JTextArea output; - - // UNIT AND PREFIX VIEWERS - /** The searchable list of unit names in the unit viewer */ - private final SearchBoxList unitNameList; - /** The searchable list of prefix names in the prefix viewer */ - private final SearchBoxList prefixNameList; - /** The text box for unit data in the unit viewer */ - private final JTextArea unitTextBox; - /** The text box for prefix data in the prefix viewer */ - private final JTextArea prefixTextBox; - - /** - * Creates the {@code View}. - * - * @since 2019-01-14 - * @since v0.1.0 - */ - public View() { - this.presenter = new Presenter(this); - this.frame = new JFrame("Unit Converter"); - this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - // create the components - this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); - this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), - this.presenter.getPrefixNameComparator(), true); - this.unitTextBox = new JTextArea(); - this.prefixTextBox = new JTextArea(); - this.fromSearch = new SearchBoxList(this.presenter.unitNameSet()); - this.toSearch = new SearchBoxList(this.presenter.unitNameSet()); - this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################")); - this.dimensionBasedOutput = new JTextArea(2, 32); - this.fromEntry = new JTextField(); - this.toEntry = new JTextField(); - this.output = new JTextArea(2, 32); - - // create more components - this.initComponents(); - - this.frame.pack(); - } - - /** - * @return value in dimension-based converter - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getDimensionConverterInput() { - return this.valueInput.getText(); - } - - /** - * @return selection in "From" selector in dimension-based converter - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getFromSelection() { - return this.fromSearch.getSelectedValue(); - } - - /** - * @return text in "From" box in converter panel - * @since 2019-01-15 - * @since v0.1.0 - */ - public String getFromText() { - return this.fromEntry.getText(); - } - - /** - * @return index of selected prefix in prefix viewer - * @since 2019-01-15 - * @since v0.1.0 - */ - public String getPrefixViewerSelection() { - return this.prefixNameList.getSelectedValue(); - } - - /** - * @return selection in "To" selector in dimension-based converter - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getToSelection() { - return this.toSearch.getSelectedValue(); - } - - /** - * @return text in "To" box in converter panel - * @since 2019-01-26 - * @since v0.1.0 - */ - public String getToText() { - return this.toEntry.getText(); - } - - /** - * @return index of selected unit in unit viewer - * @since 2019-01-15 - * @since v0.1.0 - */ - public String getUnitViewerSelection() { - return this.unitNameList.getSelectedValue(); - } - - /** - * Starts up the application. - * - * @since 2018-12-27 - * @since v0.1.0 - */ - public final void init() { - this.frame.setVisible(true); - } - - /** - * Initializes the view's components. - * - * @since 2018-12-27 - * @since v0.1.0 - */ - private final void initComponents() { - final JPanel masterPanel = new JPanel(); - this.frame.add(masterPanel); - - masterPanel.setLayout(new BorderLayout()); - - { // pane with all of the tabs - final JTabbedPane masterPane = new JTabbedPane(); - masterPanel.add(masterPane, BorderLayout.CENTER); - - { // a panel for unit conversion using a selector - final JPanel convertUnitPanel = new JPanel(); - masterPane.addTab("Convert Units", convertUnitPanel); - - convertUnitPanel.setLayout(new BorderLayout()); - - { // panel for input part - final JPanel inputPanel = new JPanel(); - convertUnitPanel.add(inputPanel, BorderLayout.CENTER); - - inputPanel.setLayout(new GridLayout(1, 3)); - - final JComboBox<String> dimensionSelector = new JComboBox<>( - this.presenter.dimensionNameList().toArray(new String[0])); - dimensionSelector.setSelectedItem("LENGTH"); - - // handle dimension filter - final MutablePredicate<String> dimensionFilter = new MutablePredicate<>(s -> true); - - // panel for From things - inputPanel.add(this.fromSearch); - - this.fromSearch.addSearchFilter(dimensionFilter); - - { // for dimension selector and arrow that represents conversion - final JPanel inBetweenPanel = new JPanel(); - inputPanel.add(inBetweenPanel); - - inBetweenPanel.setLayout(new BorderLayout()); - - { // dimension selector - inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START); - } - - { // the arrow in the middle - final JLabel arrowLabel = new JLabel("->"); - inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); - } - } - - // panel for To things - - inputPanel.add(this.toSearch); - - this.toSearch.addSearchFilter(dimensionFilter); - - // code for dimension filter - dimensionSelector.addItemListener(e -> { - dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); - this.fromSearch.reapplyFilter(); - this.toSearch.reapplyFilter(); - }); - - // apply the item listener once because I have a default selection - dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); - this.fromSearch.reapplyFilter(); - this.toSearch.reapplyFilter(); - } - - { // panel for submit and output, and also value entry - final JPanel outputPanel = new JPanel(); - convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); - - outputPanel.setLayout(new GridLayout(3, 1)); - - { // unit input - final JPanel valueInputPanel = new JPanel(); - outputPanel.add(valueInputPanel); - - valueInputPanel.setLayout(new BorderLayout()); - - { // prompt - final JLabel valuePrompt = new JLabel("Value to convert: "); - valueInputPanel.add(valuePrompt, BorderLayout.LINE_START); - } - - { // value to convert - valueInputPanel.add(this.valueInput, BorderLayout.CENTER); - } - } - - { // button to convert - final JButton convertButton = new JButton("Convert"); - outputPanel.add(convertButton); - - convertButton.addActionListener(e -> this.presenter.convertDimensionBased()); - } - - { // output of conversion - outputPanel.add(this.dimensionBasedOutput); - this.dimensionBasedOutput.setEditable(false); - } - } - } - - { // panel for unit conversion using expressions - final JPanel convertExpressionPanel = new JPanel(); - masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); - - convertExpressionPanel.setLayout(new GridLayout(5, 1)); - - { // panel for units to convert from - final JPanel fromPanel = new JPanel(); - convertExpressionPanel.add(fromPanel); - - fromPanel.setBorder(BorderFactory.createTitledBorder("From")); - fromPanel.setLayout(new GridLayout(1, 1)); - - { // entry for units - fromPanel.add(this.fromEntry); - } - } - - { // panel for units to convert to - final JPanel toPanel = new JPanel(); - convertExpressionPanel.add(toPanel); - - toPanel.setBorder(BorderFactory.createTitledBorder("To")); - toPanel.setLayout(new GridLayout(1, 1)); - - { // entry for units - toPanel.add(this.toEntry); - } - } - - { // button to convert - final JButton convertButton = new JButton("Convert!"); - convertExpressionPanel.add(convertButton); - - convertButton.addActionListener(e -> this.presenter.convertExpressions()); - } - - { // output of conversion - final JPanel outputPanel = new JPanel(); - convertExpressionPanel.add(outputPanel); - - outputPanel.setBorder(BorderFactory.createTitledBorder("Output")); - outputPanel.setLayout(new GridLayout(1, 1)); - - { // output - outputPanel.add(this.output); - this.output.setEditable(false); - } - } - - { // panel for specifying precision - final JPanel sigDigPanel = new JPanel(); - convertExpressionPanel.add(sigDigPanel); - - sigDigPanel.setBorder(BorderFactory.createTitledBorder("Significant Digits")); - - { // slider - final JSlider sigDigSlider = new JSlider(0, 12); - sigDigPanel.add(sigDigSlider); - - sigDigSlider.setMajorTickSpacing(4); - sigDigSlider.setMinorTickSpacing(1); - sigDigSlider.setSnapToTicks(true); - sigDigSlider.setPaintTicks(true); - sigDigSlider.setPaintLabels(true); - - sigDigSlider.addChangeListener( - e -> this.presenter.setSignificantFigures(sigDigSlider.getValue())); - } - } - } - - { // panel to look up units - final JPanel unitLookupPanel = new JPanel(); - masterPane.addTab("Unit Viewer", unitLookupPanel); - - unitLookupPanel.setLayout(new GridLayout()); - - { // search panel - unitLookupPanel.add(this.unitNameList); - - this.unitNameList.getSearchList() - .addListSelectionListener(e -> this.presenter.unitNameSelected()); - } - - { // the text box for unit's toString - unitLookupPanel.add(this.unitTextBox); - this.unitTextBox.setEditable(false); - this.unitTextBox.setLineWrap(true); - } - } - - { // panel to look up prefixes - final JPanel prefixLookupPanel = new JPanel(); - masterPane.addTab("Prefix Viewer", prefixLookupPanel); - - prefixLookupPanel.setLayout(new GridLayout(1, 2)); - - { // panel for listing and seaching - prefixLookupPanel.add(this.prefixNameList); - - this.prefixNameList.getSearchList() - .addListSelectionListener(e -> this.presenter.prefixSelected()); - } - - { // the text box for prefix's toString - prefixLookupPanel.add(this.prefixTextBox); - this.prefixTextBox.setEditable(false); - this.prefixTextBox.setLineWrap(true); - } - } - } - } - - /** - * Sets the text in the output of the dimension-based converter. - * - * @param text - * text to set - * @since 2019-04-13 - * @since v0.2.0 - */ - public void setDimensionConverterOutputText(final String text) { - this.dimensionBasedOutput.setText(text); - } - - /** - * Sets the text in the output of the conversion panel. - * - * @param text - * text to set - * @since 2019-01-15 - * @since v0.1.0 - */ - public void setExpressionConverterOutputText(final String text) { - this.output.setText(text); - } - - /** - * Sets the text of the prefix text box in the prefix viewer. - * - * @param text - * text to set - * @since 2019-01-15 - * @since v0.1.0 - */ - public void setPrefixTextBoxText(final String text) { - this.prefixTextBox.setText(text); - } - - /** - * Sets the text of the unit text box in the unit viewer. - * - * @param text - * text to set - * @since 2019-01-15 - * @since v0.1.0 - */ - public void setUnitTextBoxText(final String text) { - this.unitTextBox.setText(text); - } - - /** - * Shows an error dialog. - * - * @param title - * title of dialog - * @param message - * message in dialog - * @since 2019-01-14 - * @since v0.1.0 - */ - public void showErrorDialog(final String title, final String message) { - JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); - } - } - - public static void main(final String[] args) { - new View().init(); - } -} diff --git a/src/org/unitConverter/dimension/BaseDimension.java b/src/org/unitConverter/dimension/BaseDimension.java deleted file mode 100755 index 5e3ddad..0000000 --- a/src/org/unitConverter/dimension/BaseDimension.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.dimension; - -/** - * A base dimension that makes up {@code UnitDimension} objects. - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -public interface BaseDimension { - /** - * @return the dimension's name - * @since 2018-12-22 - * @since v0.1.0 - */ - String getName(); - - /** - * @return a short string (usually one character) that represents this base dimension - * @since 2018-12-22 - * @since v0.1.0 - */ - String getSymbol(); -} diff --git a/src/org/unitConverter/dimension/OtherBaseDimension.java b/src/org/unitConverter/dimension/OtherBaseDimension.java deleted file mode 100755 index 8aea2b9..0000000 --- a/src/org/unitConverter/dimension/OtherBaseDimension.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.dimension; - -import java.util.Objects; - -/** - * Non-SI base dimensions. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public enum OtherBaseDimension implements BaseDimension { - INFORMATION("Info"), CURRENCY("$$"); - - /** The dimension's symbol */ - private final String symbol; - - /** - * Creates the {@code SIBaseDimension}. - * - * @param symbol - * dimension's symbol - * @since 2018-12-11 - * @since v0.1.0 - */ - private OtherBaseDimension(final String symbol) { - this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); - } - - @Override - public String getName() { - return this.toString(); - } - - @Override - public String getSymbol() { - return this.symbol; - } -} diff --git a/src/org/unitConverter/dimension/SIBaseDimension.java b/src/org/unitConverter/dimension/SIBaseDimension.java deleted file mode 100755 index c459963..0000000 --- a/src/org/unitConverter/dimension/SIBaseDimension.java +++ /dev/null @@ -1,57 +0,0 @@ -/**
- * Copyright (C) 2018 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 org.unitConverter.dimension;
-
-import java.util.Objects;
-
-/**
- * The seven base dimensions that make up the SI.
- *
- * @author Adrien Hopkins
- * @since 2018-12-11
- * @since v0.1.0
- */
-public enum SIBaseDimension implements BaseDimension {
- LENGTH("L"), MASS("M"), TIME("T"), ELECTRIC_CURRENT("I"), TEMPERATURE("\u0398"), // u0398 is the theta symbol
- QUANTITY("N"), LUMINOUS_INTENSITY("J");
-
- /** The dimension's symbol */
- private final String symbol;
-
- /**
- * Creates the {@code SIBaseDimension}.
- *
- * @param symbol
- * dimension's symbol
- * @since 2018-12-11
- * @since v0.1.0
- */
- private SIBaseDimension(final String symbol) {
- this.symbol = Objects.requireNonNull(symbol, "symbol must not be null.");
- }
-
- @Override
- public String getName() {
- return this.toString();
- }
-
- @Override
- public String getSymbol() {
- return this.symbol;
- }
-
-}
diff --git a/src/org/unitConverter/dimension/StandardDimensions.java b/src/org/unitConverter/dimension/StandardDimensions.java deleted file mode 100755 index 4b1b814..0000000 --- a/src/org/unitConverter/dimension/StandardDimensions.java +++ /dev/null @@ -1,80 +0,0 @@ -/**
- * Copyright (C) 2018 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 org.unitConverter.dimension;
-
-/**
- * All of the dimensions that are used by the SI.
- *
- * @author Adrien Hopkins
- * @since 2018-12-11
- * @since v0.1.0
- */
-public final class StandardDimensions {
- // base dimensions
- public static final UnitDimension EMPTY = UnitDimension.EMPTY;
- public static final UnitDimension LENGTH = UnitDimension.getBase(SIBaseDimension.LENGTH);
- public static final UnitDimension MASS = UnitDimension.getBase(SIBaseDimension.MASS);
- public static final UnitDimension TIME = UnitDimension.getBase(SIBaseDimension.TIME);
- public static final UnitDimension ELECTRIC_CURRENT = UnitDimension.getBase(SIBaseDimension.ELECTRIC_CURRENT);
- public static final UnitDimension TEMPERATURE = UnitDimension.getBase(SIBaseDimension.TEMPERATURE);
- public static final UnitDimension QUANTITY = UnitDimension.getBase(SIBaseDimension.QUANTITY);
- public static final UnitDimension LUMINOUS_INTENSITY = UnitDimension.getBase(SIBaseDimension.LUMINOUS_INTENSITY);
- public static final UnitDimension INFORMATION = UnitDimension.getBase(OtherBaseDimension.INFORMATION);
- public static final UnitDimension CURRENCY = UnitDimension.getBase(OtherBaseDimension.CURRENCY);
- // derived dimensions without named SI units
- public static final UnitDimension AREA = LENGTH.times(LENGTH);
-
- public static final UnitDimension VOLUME = AREA.times(LENGTH);
- public static final UnitDimension VELOCITY = LENGTH.dividedBy(TIME);
- public static final UnitDimension ACCELERATION = VELOCITY.dividedBy(TIME);
- public static final UnitDimension WAVENUMBER = EMPTY.dividedBy(LENGTH);
- public static final UnitDimension MASS_DENSITY = MASS.dividedBy(VOLUME);
- public static final UnitDimension SURFACE_DENSITY = MASS.dividedBy(AREA);
- public static final UnitDimension SPECIFIC_VOLUME = VOLUME.dividedBy(MASS);
- public static final UnitDimension CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA);
- public static final UnitDimension MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH);
- public static final UnitDimension CONCENTRATION = QUANTITY.dividedBy(VOLUME);
- public static final UnitDimension MASS_CONCENTRATION = CONCENTRATION.times(MASS);
- public static final UnitDimension LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA);
- public static final UnitDimension REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY);
- public static final UnitDimension REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY);
- public static final UnitDimension ANGLE = LENGTH.dividedBy(LENGTH);
- public static final UnitDimension SOLID_ANGLE = AREA.dividedBy(AREA);
- // derived dimensions with named SI units
- public static final UnitDimension FREQUENCY = EMPTY.dividedBy(TIME);
-
- public static final UnitDimension FORCE = MASS.times(ACCELERATION);
- public static final UnitDimension ENERGY = FORCE.times(LENGTH);
- public static final UnitDimension POWER = ENERGY.dividedBy(TIME);
- public static final UnitDimension ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME);
- public static final UnitDimension VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE);
- public static final UnitDimension CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE);
- public static final UnitDimension ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT);
- public static final UnitDimension ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE);
- public static final UnitDimension MAGNETIC_FLUX = VOLTAGE.times(TIME);
- public static final UnitDimension MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA);
- public static final UnitDimension INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT);
- public static final UnitDimension LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE);
- public static final UnitDimension ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA);
- public static final UnitDimension SPECIFIC_ENERGY = ENERGY.dividedBy(MASS);
- public static final UnitDimension CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME);
-
- // You may NOT get StandardDimensions instances!
- private StandardDimensions() {
- throw new AssertionError();
- }
-}
diff --git a/src/org/unitConverter/dimension/UnitDimension.java b/src/org/unitConverter/dimension/UnitDimension.java deleted file mode 100755 index dbeaeff..0000000 --- a/src/org/unitConverter/dimension/UnitDimension.java +++ /dev/null @@ -1,241 +0,0 @@ -/**
- * Copyright (C) 2018 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 org.unitConverter.dimension;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * An object that represents what a unit measures, like length, mass, area, energy, etc.
- *
- * @author Adrien Hopkins
- * @since 2018-12-11
- * @since v0.1.0
- */
-public final class UnitDimension {
- /**
- * The unit dimension where every exponent is zero
- *
- * @since 2018-12-12
- * @since v0.1.0
- */
- public static final UnitDimension EMPTY = new UnitDimension(new HashMap<>());
-
- /**
- * Gets an UnitDimension that has 1 of a certain dimension and nothing else
- *
- * @param dimension
- * dimension to get
- * @return unit dimension
- * @since 2018-12-11
- * @since v0.1.0
- */
- public static final UnitDimension getBase(final BaseDimension dimension) {
- final Map<BaseDimension, Integer> map = new HashMap<>();
- map.put(dimension, 1);
- return new UnitDimension(map);
- }
-
- /**
- * The base dimensions that make up this dimension.
- *
- * @since 2018-12-11
- * @since v0.1.0
- */
- final Map<BaseDimension, Integer> exponents;
-
- /**
- * Creates the {@code UnitDimension}.
- *
- * @param exponents
- * base dimensions that make up this dimension
- * @since 2018-12-11
- * @since v0.1.0
- */
- private UnitDimension(final Map<BaseDimension, Integer> exponents) {
- this.exponents = new HashMap<>(exponents);
- }
-
- /**
- * Divides this dimension by another
- *
- * @param other
- * other dimension
- * @return quotient of two dimensions
- * @since 2018-12-11
- * @since v0.1.0
- */
- public UnitDimension dividedBy(final UnitDimension other) {
- final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
-
- for (final BaseDimension key : other.exponents.keySet()) {
- if (map.containsKey(key)) {
- // add the dimensions
- map.put(key, map.get(key) - other.exponents.get(key));
- } else {
- map.put(key, -other.exponents.get(key));
- }
- }
- return new UnitDimension(map);
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (!(obj instanceof UnitDimension))
- return false;
- final UnitDimension other = (UnitDimension) obj;
-
- // anything with a value of 0 is equal to a nonexistent value
- for (final BaseDimension b : this.getBaseSet()) {
- if (this.exponents.get(b) != other.exponents.get(b))
- if (!(this.exponents.get(b) == 0 && !other.exponents.containsKey(b)))
- return false;
- }
- for (final BaseDimension b : other.getBaseSet()) {
- if (this.exponents.get(b) != other.exponents.get(b))
- if (!(other.exponents.get(b) == 0 && !this.exponents.containsKey(b)))
- return false;
- }
- return true;
- }
-
- /**
- * @return a set of all of the base dimensions with non-zero exponents that make up this dimension.
- * @since 2018-12-12
- * @since v0.1.0
- */
- public final Set<BaseDimension> getBaseSet() {
- final Set<BaseDimension> dimensions = new HashSet<>();
-
- // add all dimensions with a nonzero exponent - they shouldn't be there in the first place
- for (final BaseDimension dimension : this.exponents.keySet()) {
- if (!this.exponents.get(dimension).equals(0)) {
- dimensions.add(dimension);
- }
- }
-
- return dimensions;
- }
-
- /**
- * Gets the exponent for a specific dimension.
- *
- * @param dimension
- * dimension to check
- * @return exponent for that dimension
- * @since 2018-12-12
- * @since v0.1.0
- */
- public int getExponent(final BaseDimension dimension) {
- return this.exponents.getOrDefault(dimension, 0);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(this.exponents);
- }
-
- /**
- * @return true if this dimension is a base, i.e. it has one exponent of one and no other nonzero exponents
- * @since 2019-01-15
- * @since v0.1.0
- */
- public boolean isBase() {
- int oneCount = 0;
- boolean twoOrMore = false; // has exponents of 2 or more
- for (final BaseDimension b : this.getBaseSet()) {
- if (this.exponents.get(b) == 1) {
- oneCount++;
- } else if (this.exponents.get(b) != 0) {
- twoOrMore = true;
- }
- }
- return (oneCount == 0 || oneCount == 1) && !twoOrMore;
- }
-
- /**
- * Multiplies this dimension by another
- *
- * @param other
- * other dimension
- * @return product of two dimensions
- * @since 2018-12-11
- * @since v0.1.0
- */
- public UnitDimension times(final UnitDimension other) {
- final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
-
- for (final BaseDimension key : other.exponents.keySet()) {
- if (map.containsKey(key)) {
- // add the dimensions
- map.put(key, map.get(key) + other.exponents.get(key));
- } else {
- map.put(key, other.exponents.get(key));
- }
- }
- return new UnitDimension(map);
- }
-
- /**
- * Returns this dimension, but to an exponent
- *
- * @param exp
- * exponent
- * @return result of exponientation
- * @since 2019-01-15
- * @since v0.1.0
- */
- public UnitDimension toExponent(final int exp) {
- final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
- for (final BaseDimension key : this.exponents.keySet()) {
- map.put(key, this.getExponent(key) * exp);
- }
- return new UnitDimension(map);
- }
-
- @Override
- public String toString() {
- final List<String> positiveStringComponents = new ArrayList<>();
- final List<String> negativeStringComponents = new ArrayList<>();
-
- // for each base dimension that makes up this dimension, add it and its exponent
- for (final BaseDimension dimension : this.getBaseSet()) {
- final int exponent = this.exponents.get(dimension);
- if (exponent > 0) {
- positiveStringComponents.add(String.format("%s^%d", dimension.getSymbol(), exponent));
- } else if (exponent < 0) {
- negativeStringComponents.add(String.format("%s^%d", dimension.getSymbol(), -exponent));
- }
- }
-
- final String positiveString = positiveStringComponents.isEmpty() ? "1"
- : String.join(" ", positiveStringComponents);
- final String negativeString = negativeStringComponents.isEmpty() ? ""
- : " / " + String.join(" ", negativeStringComponents);
-
- return positiveString + negativeString;
- }
-}
diff --git a/src/org/unitConverter/dimension/package-info.java b/src/org/unitConverter/dimension/package-info.java deleted file mode 100755 index 8cb26b1..0000000 --- a/src/org/unitConverter/dimension/package-info.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (C) 2018 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/>. - */ -/** - * Everything to do with what a unit measures, or its dimension. - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -package org.unitConverter.dimension;
\ No newline at end of file diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java deleted file mode 100644 index 7cdbe5b..0000000 --- a/src/org/unitConverter/math/DecimalComparison.java +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (C) 2019 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package org.unitConverter.math; - -/** - * A class that contains methods to compare float and double values. - * - * @author Adrien Hopkins - * @since 2019-03-18 - * @since v0.2.0 - */ -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 - */ - public static final double DOUBLE_EPSILON = 1.0e-15; - - /** - * The value used for float comparison. If two float values are within this value multiplied by the larger value, - * they are considered equal. - * - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final float FLOAT_EPSILON = 1.0e-6f; - - /** - * Tests for equality of double values using {@link #DOUBLE_EPSILON}. - * - * @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 double a, final double b) { - return DecimalComparison.equals(a, b, DOUBLE_EPSILON); - } - - /** - * Tests for double equality using a custom epsilon value. - * - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference - * @return whether they are equal - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final boolean equals(final double a, final double b, final double epsilon) { - return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); - } - - /** - * Tests for equality of float values using {@link #FLOAT_EPSILON}. - * - * @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) { - return DecimalComparison.equals(a, b, FLOAT_EPSILON); - } - - /** - * Tests for float equality using a custom epsilon value. - * - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference - * @return whether they are equal - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final boolean equals(final float a, final float b, final float epsilon) { - return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); - } - - // You may NOT get any DecimalComparison instances - private DecimalComparison() { - throw new AssertionError(); - } - -} diff --git a/src/org/unitConverter/unit/AbstractUnit.java b/src/org/unitConverter/unit/AbstractUnit.java deleted file mode 100644 index 05a6c17..0000000 --- a/src/org/unitConverter/unit/AbstractUnit.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (C) 2019 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package org.unitConverter.unit; - -import java.util.Objects; - -import org.unitConverter.dimension.UnitDimension; - -/** - * The default abstract implementation of the {@code Unit} interface. - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -public abstract class AbstractUnit implements Unit { - /** - * The dimension, or what the unit measures. - * - * @since 2018-12-22 - * @since v0.1.0 - */ - private final UnitDimension dimension; - - /** - * The unit's base unit. Values converted by {@code convertFromBase} and {@code convertToBase} are expressed in this - * unit. - * - * @since 2018-12-22 - * @since v0.1.0 - */ - private final BaseUnit base; - - /** - * The system that this unit is a part of. - * - * @since 2018-12-23 - * @since v0.1.0 - */ - private final UnitSystem system; - - /** - * Creates the {@code AbstractUnit}. - * - * @param base - * unit's base - * @throws NullPointerException - * if name, symbol or base is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public AbstractUnit(final BaseUnit base) { - this.base = Objects.requireNonNull(base, "base must not be null."); - this.dimension = this.base.getDimension(); - this.system = this.base.getSystem(); - } - - /** - * Creates the {@code AbstractUnit} using a unique dimension. This constructor is for making base units and should - * only be used by {@code BaseUnit}. - * - * @param dimension - * dimension measured by unit - * @param system - * system that unit is a part of - * @throws AssertionError - * if this constructor is not run by {@code BaseUnit} or a subclass - * @throws NullPointerException - * if name, symbol or dimension is null - * @since 2018-12-23 - * @since v0.1.0 - */ - AbstractUnit(final UnitDimension dimension, final UnitSystem system) { - // try to set this as a base unit - if (this instanceof BaseUnit) { - this.base = (BaseUnit) this; - } else - throw new AssertionError(); - - this.dimension = Objects.requireNonNull(dimension, "dimension must not be null."); - this.system = Objects.requireNonNull(system, "system must not be null."); - } - - @Override - public final BaseUnit getBase() { - return this.base; - } - - @Override - public final UnitDimension getDimension() { - return this.dimension; - } - - @Override - public final UnitSystem getSystem() { - return this.system; - } - - @Override - public String toString() { - return String.format("%s-derived unit of dimension %s", this.getSystem(), this.getDimension()); - } -} diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java deleted file mode 100755 index 67309cf..0000000 --- a/src/org/unitConverter/unit/BaseUnit.java +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -import java.util.Objects; - -import org.unitConverter.dimension.StandardDimensions; -import org.unitConverter.dimension.UnitDimension; - -/** - * A unit that is the base for its dimension. It does not have to be for a base dimension, so units like the Newton and - * Joule are still base units. - * <p> - * {@code BaseUnit} does not have any public constructors or static factories. There are two ways to obtain - * {@code BaseUnit} instances. - * <ol> - * <li>The class {@link SI} in this package has constants for all of the SI base units. You can use these constants and - * multiply or divide them to get other units. For example: - * - * <pre> - * BaseUnit JOULE = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); - * </pre> - * - * </li> - * <li>You can also query a unit system for a base unit using a unit dimension. The previously mentioned {@link SI} - * class can do this for SI and SI-derived units (including imperial and USC), but if you want to use another system, - * this is the way to do it. {@link StandardDimensions} contains common unit dimensions that you can use for this. Here - * is an example: - * - * <pre> - * BaseUnit JOULE = SI.SI.getBaseUnit(StandardDimensions.ENERGY); - * </pre> - * - * </li> - * </ol> - * - * @author Adrien Hopkins - * @since 2018-12-23 - * @since v0.1.0 - */ -public final class BaseUnit extends LinearUnit { - /** - * Is this unit a full base (i.e. m, s, ... but not N, J, ...) - * - * @since 2019-01-15 - * @since v0.1.0 - */ - private final boolean isFullBase; - - /** - * Creates the {@code BaseUnit}. - * - * @param dimension - * dimension measured by unit - * @param system - * system that unit is a part of - * @param name - * name of unit - * @param symbol - * symbol of unit - * @since 2018-12-23 - * @since v0.1.0 - */ - BaseUnit(final UnitDimension dimension, final UnitSystem system) { - super(dimension, system, 1); - this.isFullBase = dimension.isBase(); - } - - /** - * Returns the quotient of this unit and another. - * <p> - * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> - * - * @param divisor - * unit to divide by - * @return quotient of two units - * @throws IllegalArgumentException - * if {@code divisor} is not compatible for division as described above - * @throws NullPointerException - * if {@code divisor} is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public BaseUnit dividedBy(final BaseUnit divisor) { - Objects.requireNonNull(divisor, "other must not be null."); - - // check that these units can be multiplied - if (!this.getSystem().equals(divisor.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor)); - - return new BaseUnit(this.getDimension().dividedBy(divisor.getDimension()), this.getSystem()); - } - - /** - * @return true if the unit is a "full base" unit like the metre or second. - * @since 2019-04-10 - * @since v0.2.0 - */ - public final boolean isFullBase() { - return this.isFullBase; - } - - /** - * Returns the product of this unit and another. - * <p> - * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> - * - * @param multiplier - * unit to multiply by - * @return product of two units - * @throws IllegalArgumentException - * if {@code multiplier} is not compatible for multiplication as described above - * @throws NullPointerException - * if {@code multiplier} is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public BaseUnit times(final BaseUnit multiplier) { - Objects.requireNonNull(multiplier, "other must not be null"); - - // check that these units can be multiplied - if (!this.getSystem().equals(multiplier.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier)); - - // multiply the units - return new BaseUnit(this.getDimension().times(multiplier.getDimension()), this.getSystem()); - } - - /** - * Returns this unit, but to an exponent. - * - * @param exponent - * exponent - * @return result of exponentiation - * @since 2019-01-15 - * @since v0.1.0 - */ - @Override - public BaseUnit toExponent(final int exponent) { - return this.getSystem().getBaseUnit(this.getDimension().toExponent(exponent)); - } - - @Override - public String toString() { - return String.format("%s base unit of%s dimension %s", this.getSystem(), this.isFullBase ? " base" : "", - this.getDimension()); - } -} diff --git a/src/org/unitConverter/unit/DefaultUnitPrefix.java b/src/org/unitConverter/unit/DefaultUnitPrefix.java deleted file mode 100755 index 4a9e487..0000000 --- a/src/org/unitConverter/unit/DefaultUnitPrefix.java +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -import java.util.Objects; - -/** - * The default implementation of {@code UnitPrefix}, which contains a multiplier and nothing else. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public final class DefaultUnitPrefix implements UnitPrefix { - private final double multiplier; - - /** - * Creates the {@code DefaultUnitPrefix}. - * - * @param multiplier - * @since 2019-01-14 - * @since v0.2.0 - */ - public DefaultUnitPrefix(final double multiplier) { - this.multiplier = multiplier; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!(obj instanceof DefaultUnitPrefix)) - return false; - final DefaultUnitPrefix other = (DefaultUnitPrefix) obj; - return Double.doubleToLongBits(this.multiplier) == Double.doubleToLongBits(other.multiplier); - } - - @Override - public double getMultiplier() { - return this.multiplier; - } - - @Override - public int hashCode() { - return Objects.hash(this.multiplier); - } - - @Override - public String toString() { - return String.format("Unit prefix equal to %s", this.multiplier); - } -} diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java deleted file mode 100644 index 1b1ac97..0000000 --- a/src/org/unitConverter/unit/LinearUnit.java +++ /dev/null @@ -1,294 +0,0 @@ -/** - * Copyright (C) 2019 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package org.unitConverter.unit; - -import java.util.Objects; - -import org.unitConverter.dimension.UnitDimension; -import org.unitConverter.math.DecimalComparison; - -/** - * A unit that is equal to a certain number multiplied by its base. - * <p> - * {@code LinearUnit} does not have any public constructors or static factories. In order to obtain a {@code LinearUnit} - * instance, multiply its base by the conversion factor. Example: - * - * <pre> - * LinearUnit foot = METRE.times(0.3048); - * </pre> - * - * (where {@code METRE} is a {@code BaseUnit} instance) - * </p> - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -public class LinearUnit extends AbstractUnit { - /** - * The value of one of this unit in this unit's base unit - * - * @since 2018-12-22 - * @since v0.1.0 - */ - private final double conversionFactor; - - /** - * - * Creates the {@code LinearUnit}. - * - * @param base - * unit's base - * @param conversionFactor - * value of one of this unit in its base - * @since 2018-12-23 - * @since v0.1.0 - */ - LinearUnit(final BaseUnit base, final double conversionFactor) { - super(base); - this.conversionFactor = conversionFactor; - } - - /** - * Creates the {@code LinearUnit} as a base unit. - * - * @param dimension - * dimension measured by unit - * @param system - * system unit is part of - * @since 2019-01-25 - * @since v0.1.0 - */ - LinearUnit(final UnitDimension dimension, final UnitSystem system, final double conversionFactor) { - super(dimension, system); - this.conversionFactor = conversionFactor; - } - - @Override - public double convertFromBase(final double value) { - return value / this.getConversionFactor(); - } - - @Override - public double convertToBase(final double value) { - return value * this.getConversionFactor(); - } - - /** - * Divides this unit by a scalar. - * - * @param divisor - * scalar to divide by - * @return quotient - * @since 2018-12-23 - * @since v0.1.0 - */ - public LinearUnit dividedBy(final double divisor) { - return new LinearUnit(this.getBase(), this.getConversionFactor() / divisor); - } - - /** - * Returns the quotient of this unit and another. - * <p> - * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> - * - * @param divisor - * unit to divide by - * @return quotient of two units - * @throws IllegalArgumentException - * if {@code divisor} is not compatible for division as described above - * @throws NullPointerException - * if {@code divisor} is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public LinearUnit dividedBy(final LinearUnit divisor) { - Objects.requireNonNull(divisor, "other must not be null"); - - // check that these units can be multiplied - if (!this.getSystem().equals(divisor.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor)); - - // divide the units - final BaseUnit base = this.getBase().dividedBy(divisor.getBase()); - return new LinearUnit(base, this.getConversionFactor() / divisor.getConversionFactor()); - } - - @Override - public boolean equals(final Object obj) { - if (!(obj instanceof LinearUnit)) - return false; - final LinearUnit other = (LinearUnit) obj; - return Objects.equals(this.getSystem(), other.getSystem()) - && Objects.equals(this.getDimension(), other.getDimension()) - && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); - } - - /** - * @return conversion factor between this unit and its base - * @since 2018-12-22 - * @since v0.1.0 - */ - public final double getConversionFactor() { - return this.conversionFactor; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = result * prime + this.getSystem().hashCode(); - result = result * prime + this.getDimension().hashCode(); - result = result * prime + Double.hashCode(this.getConversionFactor()); - return result; - } - - /** - * Returns the difference of this unit and another. - * <p> - * Two units can be subtracted if they have the same base. If {@code subtrahend} 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 for subtraction as described above - * @throws NullPointerException - * if {@code subtrahend} is null - * @since 2019-03-17 - * @since v0.2.0 - */ - public LinearUnit minus(final LinearUnit subtrahendend) { - Objects.requireNonNull(subtrahendend, "addend must not be null."); - - // reject subtrahends that cannot be added to this unit - if (!this.getBase().equals(subtrahendend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend)); - - // add the units - return new LinearUnit(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); - } - - /** - * Returns the sum of this unit and another. - * <p> - * Two units can be added if they have the same base. If {@code addend} 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 addition as described above - * @throws NullPointerException - * if {@code addend} is null - * @since 2019-03-17 - * @since v0.2.0 - */ - public LinearUnit plus(final LinearUnit addend) { - Objects.requireNonNull(addend, "addend must not be null."); - - // reject addends that cannot be added to this unit - if (!this.getBase().equals(addend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); - - // add the units - return new LinearUnit(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); - } - - /** - * Multiplies this unit by a scalar. - * - * @param multiplier - * scalar to multiply by - * @return product - * @since 2018-12-23 - * @since v0.1.0 - */ - public LinearUnit times(final double multiplier) { - return new LinearUnit(this.getBase(), this.getConversionFactor() * multiplier); - } - - /** - * Returns the product of this unit and another. - * <p> - * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> - * - * @param multiplier - * unit to multiply by - * @return product of two units - * @throws IllegalArgumentException - * if {@code multiplier} is not compatible for multiplication as described above - * @throws NullPointerException - * if {@code multiplier} is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public LinearUnit times(final LinearUnit multiplier) { - Objects.requireNonNull(multiplier, "other must not be null"); - - // check that these units can be multiplied - if (!this.getSystem().equals(multiplier.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier)); - - // multiply the units - final BaseUnit base = this.getBase().times(multiplier.getBase()); - return new LinearUnit(base, this.getConversionFactor() * multiplier.getConversionFactor()); - } - - /** - * Returns this unit but to an exponent. - * - * @param exponent - * exponent to exponientate unit to - * @return exponientated unit - * @since 2019-01-15 - * @since v0.1.0 - */ - public LinearUnit toExponent(final int exponent) { - return new LinearUnit(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); - } - - @Override - public String toString() { - return super.toString() + String.format(" (equal to %s * base)", this.getConversionFactor()); - } - - /** - * Returns the result of applying {@code prefix} to this unit. - * - * @param prefix - * prefix to apply - * @return unit with prefix - * @since 2019-03-18 - * @since v0.2.0 - */ - public LinearUnit withPrefix(final UnitPrefix prefix) { - return this.times(prefix.getMultiplier()); - } -} diff --git a/src/org/unitConverter/unit/NonlinearUnits.java b/src/org/unitConverter/unit/NonlinearUnits.java deleted file mode 100755 index e47c28f..0000000 --- a/src/org/unitConverter/unit/NonlinearUnits.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -/** - * Some major nonlinear units. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public final class NonlinearUnits { - public static final Unit CELSIUS = new AbstractUnit(SI.KELVIN) { - - @Override - public double convertFromBase(final double value) { - return value - 273.15; - } - - @Override - public double convertToBase(final double value) { - return value + 273.15; - } - }; - - public static final Unit FAHRENHEIT = new AbstractUnit(SI.KELVIN) { - - @Override - public double convertFromBase(final double value) { - return 1.8 * value - 459.67; - } - - @Override - public double convertToBase(final double value) { - return (value + 459.67) / 1.8; - } - }; - - // You may NOT get a NonlinearUnits instance. - private NonlinearUnits() { - throw new AssertionError(); - } -} diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java deleted file mode 100644 index 46e6ff1..0000000 --- a/src/org/unitConverter/unit/SI.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import org.unitConverter.dimension.StandardDimensions; -import org.unitConverter.dimension.UnitDimension; - -/** - * The SI, which holds all SI units - * - * @author Adrien Hopkins - * @since 2018-12-23 - * @since v0.1.0 - */ -public enum SI implements UnitSystem { - SI; - - /** - * This system's base units. - * - * @since 2019-01-25 - * @since v0.1.0 - */ - private static final Set<BaseUnit> baseUnits = new HashSet<>(); - - // base units - public static final BaseUnit METRE = SI.getBaseUnit(StandardDimensions.LENGTH); - public static final BaseUnit KILOGRAM = SI.getBaseUnit(StandardDimensions.MASS); - public static final BaseUnit SECOND = SI.getBaseUnit(StandardDimensions.TIME); - public static final BaseUnit AMPERE = SI.getBaseUnit(StandardDimensions.ELECTRIC_CURRENT); - public static final BaseUnit KELVIN = SI.getBaseUnit(StandardDimensions.TEMPERATURE); - public static final BaseUnit MOLE = SI.getBaseUnit(StandardDimensions.QUANTITY); - public static final BaseUnit CANDELA = SI.getBaseUnit(StandardDimensions.LUMINOUS_INTENSITY); - - @Override - public BaseUnit getBaseUnit(final UnitDimension dimension) { - // try to find an existing unit before creating a new one - - Objects.requireNonNull(dimension, "dimension must not be null."); - for (final BaseUnit unit : baseUnits) { - // it will be equal since the conditions for equality are dimension and system, - // and system is always SI. - if (unit.getDimension().equals(dimension)) - return unit; - } - // could not find an existing base unit - final BaseUnit unit = new BaseUnit(dimension, this); - baseUnits.add(unit); - return unit; - } - - @Override - public String getName() { - return "SI"; - } -} diff --git a/src/org/unitConverter/unit/SIPrefix.java b/src/org/unitConverter/unit/SIPrefix.java deleted file mode 100755 index 31d7ff2..0000000 --- a/src/org/unitConverter/unit/SIPrefix.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -/** - * The SI prefixes. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public enum SIPrefix implements UnitPrefix { - DECA(10), HECTO(100), KILO(1e3), MEGA(1e6), GIGA(1e9), TERA(1e12), PETA(1e15), EXA(1e18), ZETTA(1e21), YOTTA( - 1e24), DECI(0.1), CENTI(0.01), MILLI( - 1e-3), MICRO(1e-6), NANO(1e-9), PICO(1e-12), FEMTO(1e-15), ATTO(1e-18), ZEPTO(1e-21), YOCTO(1e-24); - - private final double multiplier; - - /** - * Creates the {@code SIPrefix}. - * - * @param multiplier - * prefix's multiplier - * @since 2019-01-14 - * @since v0.1.0 - */ - private SIPrefix(final double multiplier) { - this.multiplier = multiplier; - } - - /** - * @return value - * @since 2019-01-14 - * @since v0.1.0 - */ - @Override - public final double getMultiplier() { - return this.multiplier; - } -} diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java deleted file mode 100755 index 86fc5a2..0000000 --- a/src/org/unitConverter/unit/Unit.java +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -import java.util.Objects; - -import org.unitConverter.dimension.UnitDimension; - -/** - * A unit that has an associated base unit, and can convert a value expressed in it to and from that base. - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -public interface Unit { - /** - * Checks if a value expressed in this unit can be converted to a value expressed in {@code other} - * - * @param other - * unit to test with - * @return true if the units are compatible - * @since 2019-01-13 - * @since v0.1.0 - */ - default boolean canConvertTo(final Unit other) { - 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> - * This must be the inverse of {@code convertToBase}, so {@code convertFromBase(convertToBase(value))} must be equal - * to {@code value} for any value, ignoring precision loss by roundoff error. - * </p> - * <p> - * If this unit <i>is</i> a base unit, this method should return {@code value}. - * </p> - * - * @param value - * value expressed in <b>base</b> unit - * @return value expressed in <b>this</b> unit - * @since 2018-12-22 - * @since v0.1.0 - */ - double convertFromBase(double value); - - /** - * Converts from a value expressed in this unit to a value expressed in this unit's base unit. - * <p> - * This must be the inverse of {@code convertFromBase}, so {@code convertToBase(convertFromBase(value))} must be - * equal to {@code value} for any value, ignoring precision loss by roundoff error. - * </p> - * <p> - * If this unit <i>is</i> a base unit, this method should return {@code value}. - * </p> - * - * @param value - * value expressed in <b>this</b> unit - * @return value expressed in <b>base</b> unit - * @since 2018-12-22 - * @since v0.1.0 - */ - double convertToBase(double value); - - /** - * <p> - * Returns the base unit associated with this unit. - * </p> - * <p> - * The dimension of this unit must be equal to the dimension of the returned unit. - * </p> - * <p> - * If this unit <i>is</i> a base unit, this method should return this unit.\ - * </p> - * - * @return base unit associated with this unit - * @since 2018-12-22 - * @since v0.1.0 - */ - BaseUnit getBase(); - - /** - * @return dimension measured by this unit - * @since 2018-12-22 - * @since v0.1.0 - */ - UnitDimension getDimension(); - - /** - * @return system that this unit is a part of - * @since 2018-12-23 - * @since v0.1.0 - */ - UnitSystem getSystem(); -} diff --git a/src/org/unitConverter/unit/UnitPrefix.java b/src/org/unitConverter/unit/UnitPrefix.java deleted file mode 100755 index 9f9645d..0000000 --- a/src/org/unitConverter/unit/UnitPrefix.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -/** - * A prefix that can be attached onto the front of any unit, which multiplies it by a certain value - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public interface UnitPrefix { - /** - * Divides this prefix by {@code other}. - * - * @param other - * prefix to divide by - * @return quotient of prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - default UnitPrefix dividedBy(final UnitPrefix other) { - return new DefaultUnitPrefix(this.getMultiplier() / other.getMultiplier()); - } - - /** - * @return this prefix's multiplier - * @since 2019-01-14 - * @since v0.1.0 - */ - double getMultiplier(); - - /** - * Multiplies this prefix by {@code other}. - * - * @param other - * prefix to multiply by - * @return product of prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - default UnitPrefix times(final UnitPrefix other) { - return new DefaultUnitPrefix(this.getMultiplier() * other.getMultiplier()); - } - - /** - * Raises this prefix to an exponent. - * - * @param exponent - * exponent to raise to - * @return result of exponentiation. - * @since 2019-04-13 - * @since v0.2.0 - */ - default UnitPrefix toExponent(final double exponent) { - return new DefaultUnitPrefix(Math.pow(getMultiplier(), exponent)); - } -} diff --git a/src/org/unitConverter/unit/UnitSystem.java b/src/org/unitConverter/unit/UnitSystem.java deleted file mode 100755 index 550eff6..0000000 --- a/src/org/unitConverter/unit/UnitSystem.java +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (C) 2018 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 org.unitConverter.unit; - -import java.util.Objects; - -import org.unitConverter.dimension.UnitDimension; - -/** - * A system of units. Each unit should be aware of its system. - * - * @author Adrien Hopkins - * @since 2018-12-23 - * @since v0.1.0 - */ -public interface UnitSystem { - /** - * Gets a base unit for this system and the provided dimension. - * - * @param dimension - * dimension used by base unit - * @return base unit - * @throws NullPointerException - * if dimension is null - * @since 2019-01-25 - * @since v0.1.0 - */ - default BaseUnit getBaseUnit(final UnitDimension dimension) { - Objects.requireNonNull(dimension, "dimension must not be null."); - return new BaseUnit(dimension, this); - } - - /** - * @return name of system - * @since 2018-12-23 - * @since v0.1.0 - */ - String getName(); -} diff --git a/src/test/java/UnitTest.java b/src/test/java/UnitTest.java deleted file mode 100755 index 00fcf3c..0000000 --- a/src/test/java/UnitTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (C) 2018 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 test.java; - -import static org.junit.Assert.assertEquals; - -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; - -import org.junit.Test; -import org.unitConverter.dimension.StandardDimensions; -import org.unitConverter.math.DecimalComparison; -import org.unitConverter.unit.BaseUnit; -import org.unitConverter.unit.LinearUnit; -import org.unitConverter.unit.SI; -import org.unitConverter.unit.SIPrefix; -import org.unitConverter.unit.Unit; - -/** - * 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 - */ -public class UnitTest { - /** A random number generator */ - private static final Random rng = ThreadLocalRandom.current(); - - @Test - public void testAdditionAndSubtraction() { - final LinearUnit inch = SI.METRE.times(0.0254); - final LinearUnit foot = SI.METRE.times(0.3048); - - assertEquals(inch.plus(foot), SI.METRE.times(0.3302)); - assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); - } - - @Test - public void testBaseUnitExclusives() { - // this test should have a compile error if I am doing something wrong - final BaseUnit metrePerSecondSquared = SI.METRE.dividedBy(SI.SECOND.toExponent(2)); - - assertEquals(metrePerSecondSquared, SI.SI.getBaseUnit(StandardDimensions.ACCELERATION)); - } - - @Test - public void testConversion() { - final BaseUnit metre = SI.METRE; - final Unit inch = metre.times(0.0254); - - assertEquals(1.9, inch.convertToBase(75), 0.01); - - // try random stuff - for (int i = 0; i < 1000; i++) { - // initiate random values - final double conversionFactor = rng.nextDouble() * 1000000; - final double testValue = rng.nextDouble() * 1000000; - final double expected = testValue * conversionFactor; - - // test - final Unit unit = SI.METRE.times(conversionFactor); - final double actual = unit.convertToBase(testValue); - - assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON); - } - } - - @Test - public void testEquals() { - final BaseUnit metre = SI.METRE; - final Unit meter = SI.SI.getBaseUnit(StandardDimensions.LENGTH); - - assertEquals(metre, meter); - } - - @Test - public void testMultiplicationAndDivision() { - // test unit-times-unit multiplication - final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); - final LinearUnit actualJoule = SI.SI.getBaseUnit(StandardDimensions.ENERGY); - - assertEquals(generatedJoule, actualJoule); - - // test multiplication by conversion factors - final LinearUnit kilometre = SI.METRE.times(1000); - final LinearUnit hour = SI.SECOND.times(3600); - final LinearUnit generatedKPH = kilometre.dividedBy(hour); - - final LinearUnit actualKPH = SI.SI.getBaseUnit(StandardDimensions.VELOCITY).dividedBy(3.6); - - assertEquals(generatedKPH, actualKPH); - } - - @Test - public void testPrefixes() { - final LinearUnit generatedKilometre = SI.METRE.withPrefix(SIPrefix.KILO); - final LinearUnit actualKilometre = SI.METRE.times(1000); - - assertEquals(generatedKilometre, actualKilometre); - } -} diff --git a/src/test/java/org/unitConverter/math/ConditionalExistenceCollectionsTest.java b/src/test/java/org/unitConverter/math/ConditionalExistenceCollectionsTest.java new file mode 100644 index 0000000..311ace5 --- /dev/null +++ b/src/test/java/org/unitConverter/math/ConditionalExistenceCollectionsTest.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.math; + +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.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; +import org.unitConverter.math.ConditionalExistenceCollections.ConditionalExistenceIterator; + +/** + * Tests the {@link #ConditionalExistenceCollections}. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +class ConditionalExistenceCollectionsTest { + + /** + * The returned iterator ignores elements that don't start with "a". + * + * @return test iterator + * @since 2019-10-17 + */ + 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 + .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 + */ + Map<String, Integer> getTestMap() { + final Map<String, Integer> map = new HashMap<>(); + map.put("one", 1); + map.put("two", 2); + map.put("zero", 0); + map.put("ten", 10); + final Map<String, Integer> conditionalMap = ConditionalExistenceCollections.conditionalExistenceMap(map, + e -> !Integer.valueOf(0).equals(e.getValue())); + return conditionalMap; + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsKey(java.lang.Object)}. + */ + @Test + void testContainsKeyObject() { + final Map<String, Integer> map = this.getTestMap(); + assertTrue(map.containsKey("one")); + assertTrue(map.containsKey("ten")); + assertFalse(map.containsKey("five")); + assertFalse(map.containsKey("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsValue(java.lang.Object)}. + */ + @Test + void testContainsValueObject() { + final Map<String, Integer> map = this.getTestMap(); + assertTrue(map.containsValue(1)); + assertTrue(map.containsValue(10)); + assertFalse(map.containsValue(5)); + assertFalse(map.containsValue(0)); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#entrySet()}. + */ + @Test + void testEntrySet() { + final Map<String, Integer> map = this.getTestMap(); + for (final Entry<String, Integer> e : map.entrySet()) { + assertTrue(e.getValue() != 0); + } + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#get(java.lang.Object)}. + */ + @Test + void testGetObject() { + final Map<String, Integer> map = this.getTestMap(); + assertEquals(1, map.get("one")); + assertEquals(10, map.get("ten")); + assertEquals(null, map.get("five")); + assertEquals(null, map.get("zero")); + } + + @Test + void testIterator() { + final ConditionalExistenceIterator<String> testIterator = this.getTestIterator(); + + assertTrue(testIterator.hasNext); + assertTrue(testIterator.hasNext()); + assertEquals("aa", testIterator.nextElement); + assertEquals("aa", testIterator.next()); + + assertTrue(testIterator.hasNext); + assertTrue(testIterator.hasNext()); + assertEquals("ab", testIterator.nextElement); + assertEquals("ab", testIterator.next()); + + assertFalse(testIterator.hasNext); + assertFalse(testIterator.hasNext()); + assertEquals(null, testIterator.nextElement); + assertThrows(NoSuchElementException.class, testIterator::next); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#keySet()}. + */ + @Test + void testKeySet() { + final Map<String, Integer> map = this.getTestMap(); + assertFalse(map.keySet().contains("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#values()}. + */ + @Test + void testValues() { + final Map<String, Integer> map = this.getTestMap(); + assertFalse(map.values().contains(0)); + } + +} diff --git a/src/test/java/ExpressionParserTest.java b/src/test/java/org/unitConverter/math/ExpressionParserTest.java index 40c91ac..f3180c1 100644 --- a/src/test/java/ExpressionParserTest.java +++ b/src/test/java/org/unitConverter/math/ExpressionParserTest.java @@ -14,12 +14,11 @@ * 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 test.java; +package org.unitConverter.math; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.Test; -import org.unitConverter.math.ExpressionParser; +import org.junit.jupiter.api.Test; /** * A test for the {@code ExpressionParser} class. This is NOT part of this program's public API. @@ -28,7 +27,7 @@ import org.unitConverter.math.ExpressionParser; * @since 2019-03-22 * @since v0.2.0 */ -public class ExpressionParserTest { +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) diff --git a/src/test/java/UnitDimensionTest.java b/src/test/java/org/unitConverter/math/ObjectProductTest.java index 587cf4c..afd18b7 100755..100644 --- a/src/test/java/UnitDimensionTest.java +++ b/src/test/java/org/unitConverter/math/ObjectProductTest.java @@ -14,31 +14,30 @@ * 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 test.java; +package org.unitConverter.math; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.unitConverter.dimension.StandardDimensions.AREA; -import static org.unitConverter.dimension.StandardDimensions.ENERGY; -import static org.unitConverter.dimension.StandardDimensions.LENGTH; -import static org.unitConverter.dimension.StandardDimensions.MASS; -import static org.unitConverter.dimension.StandardDimensions.MASS_DENSITY; -import static org.unitConverter.dimension.StandardDimensions.QUANTITY; -import static org.unitConverter.dimension.StandardDimensions.TIME; -import static org.unitConverter.dimension.StandardDimensions.VOLUME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.unitConverter.unit.SI.Dimensions.AREA; +import static org.unitConverter.unit.SI.Dimensions.ENERGY; +import static org.unitConverter.unit.SI.Dimensions.LENGTH; +import static org.unitConverter.unit.SI.Dimensions.MASS; +import static org.unitConverter.unit.SI.Dimensions.MASS_DENSITY; +import static org.unitConverter.unit.SI.Dimensions.QUANTITY; +import static org.unitConverter.unit.SI.Dimensions.TIME; +import static org.unitConverter.unit.SI.Dimensions.VOLUME; -import org.junit.Test; -import org.unitConverter.dimension.SIBaseDimension; -import org.unitConverter.dimension.UnitDimension; +import org.junit.jupiter.api.Test; +import org.unitConverter.unit.SI; /** - * Tests for {@link UnitDimension}. This is NOT part of this program's public API. + * 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 */ -public class UnitDimensionTest { +class ObjectProductTest { /** * Tests {@link UnitDimension#equals} * @@ -59,8 +58,8 @@ public class UnitDimensionTest { */ @Test public void testExponents() { - assertEquals(1, LENGTH.getExponent(SIBaseDimension.LENGTH)); - assertEquals(3, VOLUME.getExponent(SIBaseDimension.LENGTH)); + assertEquals(1, LENGTH.getExponent(SI.BaseDimensions.LENGTH)); + assertEquals(3, VOLUME.getExponent(SI.BaseDimensions.LENGTH)); } /** diff --git a/src/test/java/org/unitConverter/unit/MultiUnitTest.java b/src/test/java/org/unitConverter/unit/MultiUnitTest.java new file mode 100644 index 0000000..5ea9d07 --- /dev/null +++ b/src/test/java/org/unitConverter/unit/MultiUnitTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.Test; + +/** + * Tests related to the {@code MultiUnit}. + * + * @since 2020-10-03 + */ +class MultiUnitTest { + + @Test + final void testConvert() { + final Random rng = ThreadLocalRandom.current(); + final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH); + + assertEquals(1702.0, footInch.convertTo(SI.METRE.withPrefix(SI.MILLI), + Arrays.asList(5.0, 7.0)), 1.0); + + for (int i = 0; i < 1000; i++) { + final double feet = rng.nextInt(1000); + final double inches = rng.nextDouble() * 12; + final double millimetres = feet * 304.8 + inches * 25.4; + + final List<Double> feetAndInches = SI.METRE.withPrefix(SI.MILLI) + .convertTo(footInch, millimetres); + assertEquals(feet, feetAndInches.get(0), 1e-10); + assertEquals(inches, feetAndInches.get(1), 1e-10); + } + } + + /** + * Test method for + * {@link org.unitConverter.unit.MultiUnit#convertFromBase(double)}. + */ + @Test + final void testConvertFromBase() { + final Random rng = ThreadLocalRandom.current(); + final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH); + + // 1.7 m =~ 5' + 7" + final List<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 org.unitConverter.unit.MultiUnit#convertToBase(java.util.List)}. + */ + @Test + final void testConvertToBase() { + final Random rng = ThreadLocalRandom.current(); + final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH); + + // 1.7 m =~ 5' + 7" + assertEquals(1.7018, footInch.convertToBase(Arrays.asList(5.0, 7.0)), + 1e-12); + + for (int i = 0; i < 1000; i++) { + final double feet = rng.nextInt(1000); + final double inches = rng.nextDouble() * 12; + final double metres = feet * 0.3048 + inches * 0.0254; + + assertEquals(metres, + footInch.convertToBase(Arrays.asList(feet, inches)), 1e-12); + } + } +} diff --git a/src/test/java/UnitsDatabaseTest.java b/src/test/java/org/unitConverter/unit/UnitDatabaseTest.java index 9222740..7f957f3 100644 --- a/src/test/java/UnitsDatabaseTest.java +++ b/src/test/java/org/unitConverter/unit/UnitDatabaseTest.java @@ -14,61 +14,82 @@ * 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 test.java; +package org.unitConverter.unit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +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.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; -import org.junit.Test; -import org.unitConverter.UnitsDatabase; -import org.unitConverter.unit.AbstractUnit; -import org.unitConverter.unit.DefaultUnitPrefix; -import org.unitConverter.unit.LinearUnit; -import org.unitConverter.unit.SI; -import org.unitConverter.unit.Unit; -import org.unitConverter.unit.UnitPrefix; +import org.junit.jupiter.api.Test; /** - * A test for the {@link UnitsDatabase} class. This is NOT part of this program's public API. + * A test for the {@link UnitDatabase} class. This is NOT part of this program's + * public API. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ -public class UnitsDatabaseTest { +class UnitDatabaseTest { // some linear units and one nonlinear private static final Unit U = SI.METRE; private static final Unit V = SI.KILOGRAM; private static final Unit W = SI.SECOND; - + // used for testing expressions // J = U^2 * V / W^2 - private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); - private static final Unit NONLINEAR = new AbstractUnit(SI.METRE) { - - @Override - public double convertFromBase(final double value) { - return value + 1; - } - - @Override - public double convertToBase(final double value) { - return value - 1; - } - }; - + private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)) + .dividedBy(SI.SECOND.toExponent(2)); + private static final LinearUnit K = SI.KELVIN; + + private static final Unit NONLINEAR = Unit + .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); + // make the prefix values prime so I can tell which multiplications were made - private static final UnitPrefix A = new DefaultUnitPrefix(2); - private static final UnitPrefix B = new DefaultUnitPrefix(3); - private static final UnitPrefix C = new DefaultUnitPrefix(5); - private static final UnitPrefix AB = new DefaultUnitPrefix(7); - private static final UnitPrefix BC = new DefaultUnitPrefix(11); - + private static final UnitPrefix A = UnitPrefix.valueOf(2) + .withName(NameSymbol.ofName("A")); + private static final UnitPrefix B = UnitPrefix.valueOf(3) + .withName(NameSymbol.ofName("B")); + private static final UnitPrefix C = UnitPrefix.valueOf(5) + .withName(NameSymbol.ofName("C")); + private static final UnitPrefix AB = UnitPrefix.valueOf(7); + private static final UnitPrefix BC = UnitPrefix.valueOf(11); + + /** + * Confirms that operations that shouldn't function for infinite databases + * throw an {@code IllegalStateException}. + * + * @since 2019-05-03 + */ +// @Test +// @Timeout(value = 1, unit = TimeUnit.SECONDS) + public void testInfiniteSetExceptions() { + // load units + final UnitDatabase infiniteDatabase = new UnitDatabase(); + + infiniteDatabase.addUnit("J", J); + infiniteDatabase.addUnit("K", K); + + infiniteDatabase.addPrefix("A", A); + infiniteDatabase.addPrefix("B", B); + infiniteDatabase.addPrefix("C", C); + + final Set<Entry<String, Unit>> entrySet = infiniteDatabase.unitMap() + .entrySet(); + final Set<String> keySet = infiniteDatabase.unitMap().keySet(); + assertThrows(IllegalStateException.class, () -> entrySet.toArray()); + assertThrows(IllegalStateException.class, () -> keySet.toArray()); + } + /** * Test that prefixes correctly apply to units. * @@ -77,24 +98,29 @@ public class UnitsDatabaseTest { */ @Test public void testPrefixes() { - final UnitsDatabase database = new UnitsDatabase(); - + final UnitDatabase database = new UnitDatabase(); + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + + // test the getPrefixesFromName method + final List<UnitPrefix> expected = Arrays.asList(C, B, A); + assertEquals(expected, database.getPrefixesFromName("ABCU")); + // get the product final Unit abcuNonlinear = database.getUnit("ABCU"); assert abcuNonlinear instanceof LinearUnit; - + final LinearUnit abcu = (LinearUnit) abcuNonlinear; - assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15); + assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), + abcu.getConversionFactor(), 1e-15); } - + /** * Tests the functionnalites of the prefixless unit map. * @@ -107,42 +133,44 @@ public class UnitsDatabaseTest { */ @Test public void testPrefixlessUnitMap() { - final UnitsDatabase database = new UnitsDatabase(); - final Map<String, Unit> prefixlessUnits = database.unitMapPrefixless(); - + final UnitDatabase database = new UnitDatabase(); + final Map<String, Unit> prefixlessUnits = database + .unitMapPrefixless(true); + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + // this should work because the map should be an auto-updating view assertTrue(prefixlessUnits.containsKey("U")); assertFalse(prefixlessUnits.containsKey("Z")); - + assertTrue(prefixlessUnits.containsValue(U)); assertFalse(prefixlessUnits.containsValue(NONLINEAR)); } - + /** - * Tests that the database correctly stores and retrieves units, ignoring prefixes. + * Tests that the database correctly stores and retrieves units, ignoring + * prefixes. * * @since 2019-04-14 * @since v0.2.0 */ @Test public void testPrefixlessUnits() { - final UnitsDatabase database = new UnitsDatabase(); - + final UnitDatabase database = new UnitDatabase(); + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + assertTrue(database.containsUnitName("U")); assertFalse(database.containsUnitName("Z")); - + assertEquals(U, database.getUnit("U")); - assertEquals(null, database.getUnit("Z")); + assertThrows(NoSuchElementException.class, () -> database.getUnit("Z")); } - + /** * Test that unit expressions return the correct value. * @@ -152,31 +180,32 @@ public class UnitsDatabaseTest { @Test public void testUnitExpressions() { // load units - final UnitsDatabase database = new UnitsDatabase(); - + final UnitDatabase database = new UnitDatabase(); + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); database.addUnit("fj", J.times(5)); database.addUnit("ej", J.times(8)); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + // first test - test prefixes and operations - final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C); + final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C) + .withPrefix(C); final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W"); - + assertEquals(expected1, actual1); - + // second test - test addition and subtraction final Unit expected2 = J.times(58); final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej"); - + assertEquals(expected2, actual2); } - + /** * Tests both the unit name iterator and the name-unit entry iterator * @@ -186,44 +215,65 @@ public class UnitsDatabaseTest { @Test public void testUnitIterator() { // load units - final UnitsDatabase database = new UnitsDatabase(); - + final UnitDatabase database = new UnitDatabase(); + database.addUnit("J", J); - + database.addUnit("K", K); + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - - final Iterator<String> nameIterator = database.unitMap().keySet().iterator(); - final Iterator<Entry<String, Unit>> entryIterator = database.unitMap().entrySet().iterator(); - + + final int NUM_UNITS = database.unitMapPrefixless(true).size(); + final int NUM_PREFIXES = database.prefixMap().size(); + + final Iterator<String> nameIterator = database.unitMap().keySet() + .iterator(); + final Iterator<Entry<String, Unit>> entryIterator = database.unitMap() + .entrySet().iterator(); + int expectedLength = 1; int unitsWithThisLengthSoFar = 0; - + // loop 1000 times for (int i = 0; i < 1000; i++) { // expected length of next - if (unitsWithThisLengthSoFar >= (int) Math.pow(3, expectedLength - 1)) { + if (unitsWithThisLengthSoFar >= NUM_UNITS + * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { expectedLength++; unitsWithThisLengthSoFar = 0; } - + + // test that stuff is valid final String nextName = nameIterator.next(); final Unit nextUnit = database.getUnit(nextName); final Entry<String, Unit> nextEntry = entryIterator.next(); - + assertEquals(expectedLength, nextName.length()); assertEquals(nextName, nextEntry.getKey()); assertEquals(nextUnit, nextEntry.getValue()); - + unitsWithThisLengthSoFar++; } + + // test toString for consistency + final String entryIteratorString = entryIterator.toString(); + for (int i = 0; i < 3; i++) { + assertEquals(entryIteratorString, entryIterator.toString()); + } + + final String nameIteratorString = nameIterator.toString(); + for (int i = 0; i < 3; i++) { + assertEquals(nameIteratorString, nameIterator.toString()); + } } - + /** - * Determine, given a unit name that could mean multiple things, which meaning is chosen. + * Determine, given a unit name that could mean multiple things, which + * meaning is chosen. * <p> - * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this case, "AB-C-U" is the correct choice. + * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this + * case, "AB-C-U" is the correct choice. * </p> * * @since 2019-04-14 @@ -232,29 +282,29 @@ public class UnitsDatabaseTest { @Test public void testUnitPrefixCombinations() { // load units - final UnitsDatabase database = new UnitsDatabase(); - + final UnitDatabase database = new UnitDatabase(); + database.addUnit("J", J); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); database.addPrefix("AB", AB); database.addPrefix("BC", BC); - + // test 1 - AB-C-J vs A-BC-J vs A-B-C-J final Unit expected1 = J.withPrefix(AB).withPrefix(C); final Unit actual1 = database.getUnit("ABCJ"); - + assertEquals(expected1, actual1); - + // test 2 - ABC-J vs AB-CJ vs AB-C-J database.addUnit("CJ", J.times(13)); - database.addPrefix("ABC", new DefaultUnitPrefix(17)); - + database.addPrefix("ABC", UnitPrefix.valueOf(17)); + final Unit expected2 = J.times(17); final Unit actual2 = database.getUnit("ABCJ"); - + assertEquals(expected2, actual2); } } diff --git a/src/test/java/org/unitConverter/unit/UnitTest.java b/src/test/java/org/unitConverter/unit/UnitTest.java new file mode 100644 index 0000000..3b594f2 --- /dev/null +++ b/src/test/java/org/unitConverter/unit/UnitTest.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2018 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 org.unitConverter.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.Test; +import org.unitConverter.math.DecimalComparison; + +/** + * Testing the various Unit classes. This is NOT part of this program's public + * API. + * + * @author Adrien Hopkins + * @since 2018-12-22 + * @since v0.1.0 + */ +class UnitTest { + /** A random number generator */ + private static final Random rng = ThreadLocalRandom.current(); + + @Test + public void testAdditionAndSubtraction() { + final LinearUnit inch = SI.METRE.times(0.0254) + .withName(NameSymbol.of("inch", "in")); + final LinearUnit foot = SI.METRE.times(0.3048) + .withName(NameSymbol.of("foot", "ft")); + + assertEquals(inch.plus(foot), SI.METRE.times(0.3302)); + assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); + + // test with LinearUnitValue + final LinearUnitValue value1 = LinearUnitValue.getExact(SI.METRE, 15); + final LinearUnitValue value2 = LinearUnitValue.getExact(foot, 120); + final LinearUnitValue value3 = LinearUnitValue.getExact(SI.METRE, 0.5); + final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60); + + // make sure addition is done correctly + assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001); + assertEquals(15.5, value1.plus(value3).getValueExact()); + assertEquals(52.076, value1.plus(value2).plus(value3).getValueExact(), + 0.001); + + // make sure addition uses the correct unit, and is still associative + // (ignoring floating-point rounding errors) + assertEquals(SI.METRE, value1.plus(value2).getUnit()); + assertEquals(SI.METRE, value1.plus(value2).plus(value3).getUnit()); + assertEquals(foot, value2.plus(value1).getUnit()); + assertTrue(value1.plus(value2).equals(value2.plus(value1), true)); + + // make sure errors happen when they should + assertThrows(IllegalArgumentException.class, () -> value1.plus(value4)); + } + + @Test + public void testConversion() { + final LinearUnit metre = SI.METRE; + final Unit inch = metre.times(0.0254); + + final UnitValue value = UnitValue.of(inch, 75); + + assertEquals(1.9, inch.convertTo(metre, 75), 0.01); + assertEquals(1.9, value.convertTo(metre).getValue(), 0.01); + + // try random stuff + for (int i = 0; i < 1000; i++) { + // initiate random values + final double conversionFactor = UnitTest.rng.nextDouble() * 1000000; + final double testValue = UnitTest.rng.nextDouble() * 1000000; + final double expected = testValue * conversionFactor; + + // test + final Unit unit = SI.METRE.times(conversionFactor); + final double actual = unit.convertToBase(testValue); + + assertEquals(actual, expected, + expected * DecimalComparison.DOUBLE_EPSILON); + } + } + + @Test + public void testEquals() { + final LinearUnit metre = SI.METRE; + final Unit meter = SI.BaseUnits.METRE.asLinearUnit(); + + assertEquals(metre, meter); + } + + @Test + public void testIsMetric() { + final Unit metre = SI.METRE; + final Unit megasecond = SI.SECOND.withPrefix(SI.MEGA); + final Unit hour = SI.HOUR; + + assertTrue(metre.isMetric()); + assertTrue(megasecond.isMetric()); + assertFalse(hour.isMetric()); + } + + @Test + public void testMultiplicationAndDivision() { + // test unit-times-unit multiplication + final LinearUnit generatedJoule = SI.KILOGRAM + .times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); + final LinearUnit actualJoule = SI.JOULE; + + assertEquals(generatedJoule, actualJoule); + + // test multiplication by conversion factors + final LinearUnit kilometre = SI.METRE.times(1000); + final LinearUnit hour = SI.SECOND.times(3600); + final LinearUnit generatedKPH = kilometre.dividedBy(hour); + + final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6); + + assertEquals(generatedKPH, actualKPH); + } + + @Test + public void testPrefixes() { + final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO); + final LinearUnit actualKilometre = SI.METRE.times(1000); + + assertEquals(generatedKilometre, actualKilometre); + } +} diff --git a/src/test/java/package-info.java b/src/test/java/package-info.java deleted file mode 100644 index 3da7fcb..0000000 --- a/src/test/java/package-info.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (C) 2019 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -/** - * All of the Unit Converter tests. Everything in this package is NOT part of Unit Converter's public API. - * - * @author Adrien Hopkins - * @since 2019-03-16 - * @since v0.2.0 - */ -package test.java;
\ No newline at end of file |