summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2019-04-14 17:46:43 -0400
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2019-04-14 17:46:43 -0400
commit910b2f1b448ec56e6a66f4aa4f72e71c39de40a1 (patch)
tree231fcb8f72a467a77efc1e6050de12bd70f7b152
parent70273e127b061c69ce4b3d9d6c3881c6b0c2b829 (diff)
parent2ce65fa76908d77a5e3b045a8eb40c798939b8be (diff)
Release v0.2.0v0.2.0
-rw-r--r--.DS_Storebin0 -> 6148 bytes
-rw-r--r--.classpath21
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml14
-rw-r--r--.project6
-rw-r--r--.settings/org.eclipse.jdt.core.prefs2
-rw-r--r--.settings/org.eclipse.m2e.core.prefs4
-rw-r--r--CHANGELOG.org18
-rw-r--r--README.org507
-rw-r--r--dimensionfile.txt18
-rw-r--r--pom.xml55
-rwxr-xr-xsrc/org/unitConverter/UnitsDatabase.java1479
-rwxr-xr-xsrc/org/unitConverter/converterGUI/DelegateListModel.java (renamed from src/unitConverter/converterGUI/DelegateListModel.java)12
-rwxr-xr-xsrc/org/unitConverter/converterGUI/FilterComparator.java (renamed from src/unitConverter/converterGUI/FilterComparator.java)54
-rwxr-xr-xsrc/org/unitConverter/converterGUI/GridBagBuilder.java (renamed from src/unitConverter/converterGUI/GridBagBuilder.java)2
-rw-r--r--src/org/unitConverter/converterGUI/MutablePredicate.java70
-rw-r--r--src/org/unitConverter/converterGUI/SearchBoxList.java297
-rwxr-xr-xsrc/org/unitConverter/converterGUI/UnitConverterGUI.java827
-rw-r--r--src/org/unitConverter/converterGUI/package-info.java (renamed from src/unitConverter/converterGUI/package-info.java)3
-rwxr-xr-xsrc/org/unitConverter/dimension/BaseDimension.java (renamed from src/unitConverter/dimension/BaseDimension.java)2
-rwxr-xr-xsrc/org/unitConverter/dimension/OtherBaseDimension.java (renamed from src/unitConverter/dimension/OtherBaseDimension.java)2
-rwxr-xr-xsrc/org/unitConverter/dimension/SIBaseDimension.java (renamed from src/unitConverter/dimension/SIBaseDimension.java)2
-rwxr-xr-xsrc/org/unitConverter/dimension/StandardDimensions.java (renamed from src/unitConverter/dimension/StandardDimensions.java)2
-rwxr-xr-xsrc/org/unitConverter/dimension/UnitDimension.java (renamed from src/unitConverter/dimension/UnitDimension.java)2
-rwxr-xr-xsrc/org/unitConverter/dimension/package-info.java (renamed from src/unitConverter/dimension/package-info.java)3
-rw-r--r--src/org/unitConverter/math/DecimalComparison.java114
-rw-r--r--src/org/unitConverter/math/ExpressionParser.java708
-rw-r--r--src/org/unitConverter/math/package-info.java23
-rw-r--r--src/org/unitConverter/package-info.java (renamed from src/unitConverter/package-info.java)3
-rw-r--r--src/org/unitConverter/unit/AbstractUnit.java (renamed from src/unitConverter/unit/AbstractUnit.java)59
-rwxr-xr-xsrc/org/unitConverter/unit/BaseUnit.java168
-rwxr-xr-xsrc/org/unitConverter/unit/DefaultUnitPrefix.java (renamed from src/unitConverter/unit/DefaultUnitPrefix.java)3
-rw-r--r--src/org/unitConverter/unit/LinearUnit.java294
-rwxr-xr-xsrc/org/unitConverter/unit/NonlinearUnits.java (renamed from src/unitConverter/unit/NonlinearUnits.java)2
-rw-r--r--src/org/unitConverter/unit/SI.java (renamed from src/unitConverter/unit/SI.java)6
-rwxr-xr-xsrc/org/unitConverter/unit/SIPrefix.java (renamed from src/unitConverter/unit/SIPrefix.java)2
-rwxr-xr-xsrc/org/unitConverter/unit/Unit.java (renamed from src/unitConverter/unit/Unit.java)4
-rwxr-xr-xsrc/org/unitConverter/unit/UnitPrefix.java (renamed from src/unitConverter/unit/UnitPrefix.java)41
-rwxr-xr-xsrc/org/unitConverter/unit/UnitSystem.java (renamed from src/unitConverter/unit/UnitSystem.java)4
-rw-r--r--src/org/unitConverter/unit/package-info.java (renamed from src/unitConverter/unit/package-info.java)3
-rw-r--r--src/test/java/ExpressionParserTest.java53
-rwxr-xr-xsrc/test/java/UnitDimensionTest.java (renamed from src/unitConverter/dimension/UnitDimensionTest.java)36
-rwxr-xr-xsrc/test/java/UnitTest.java116
-rw-r--r--src/test/java/UnitsDatabaseTest.java260
-rw-r--r--src/test/java/package-info.java24
-rwxr-xr-xsrc/unitConverter/UnitsDatabase.java584
-rwxr-xr-xsrc/unitConverter/converterGUI/UnitConverterGUI.java671
-rwxr-xr-xsrc/unitConverter/unit/BaseUnit.java180
-rw-r--r--src/unitConverter/unit/LinearUnit.java184
-rwxr-xr-xsrc/unitConverter/unit/UnitTest.java47
-rwxr-xr-xunitsfile.txt38
51 files changed, 5231 insertions, 1799 deletions
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..5f8c120
--- /dev/null
+++ b/.DS_Store
Binary files differ
diff --git a/.classpath b/.classpath
index 145a671..ef141e6 100644
--- a/.classpath
+++ b/.classpath
@@ -1,7 +1,20 @@
<?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" path="src"/>
- <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
- <classpathentry kind="output" path="bin"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="src" output="target/classes" path="src">
+ <attributes>
+ <attribute name="optional" value="true"/>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="output" path="target/classes"/>
</classpath>
diff --git a/.gitignore b/.gitignore
index 866d01d..52a523a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
bin/
+target/
*.class
*~ \ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..7c2efb0
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,14 @@
+# 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"
diff --git a/.project b/.project
index 656186b..56f686e 100644
--- a/.project
+++ b/.project
@@ -10,8 +10,14 @@
<arguments>
</arguments>
</buildCommand>
+ <buildCommand>
+ <name>org.eclipse.m2e.core.maven2Builder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
</buildSpec>
<natures>
+ <nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 3a21537..8445b6b 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -8,4 +8,6 @@ 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
diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 1dbe268..77e7593 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.1.0
+** v0.2.0 - [2019-04-14]
+*** Changed
+ - When searching for units, units with no prefixes are searched for before prefixed units
+ - Smaller prefixes are searched for before larger prefixes
+ - Moved project to Maven
+ - Downgraded JUnit to 4.11
+ - 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
- Unit interface, implemented and supporting classes
diff --git a/README.org b/README.org
index 175db07..2a6fa95 100644
--- a/README.org
+++ b/README.org
@@ -1,26 +1,37 @@
-* What is it?
+* Unit Converter v0.2.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
+** Features
- Convert between units and expressions of units
- linear or base unit 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
- Choose your precision
-* How to Convert
+** How to Convert (in "Convert Units")
+To convert units, simply:
+ 1. Select the kind of units to convert (length/mass/time, etc.).
+ 2. Select the units to convert from and to (you can use the text boxes above to search)
+ 3. Enter a value to convert
+ 4. Press "Convert"
+** How to Convert (in "Convert Expressions")
To convert units, simply enter the first unit in the From box and the second unit in the To box. Press Convert and a result will appear in the output box. Both boxes accept unit expressions, so you can input things like ‘1.7 m’, ‘85 km/h’ and ‘35 A * 14 s’ into either box.
*Warning*: The space between the number and the unit name is required, or else the whole expression will be interpreted as a unit name. Spaces are not required for operators.
Use the slider at the bottom to choose the maximum precision of the result, in significant digits, not decimal places.
-* Units Files and Expressions
+** Units Files and Expressions
As mentioned previously, all units are loaded from a units file. The format for lines consists of a name for the unit, some tabs (not spaces), and an expression. The following operations are supported in expressions:
+ - Addition using the plus sign (+), which only works on compatible units
+ - Subtraction using the minus sign (-), which only works on compatible units
- Multiplication using spaces ( ) or asterisks (*)
- Division using the forward slash (/)
- Exponentiation using the caret (^)
-Every argument can be either a linear unit, a base unit or a number, except exponents which must be integers. There is no way to use brackets to manipulate order of operations.
+ - Brackets to manipulate order of operations
+Every argument can be either a linear unit, a base unit or a number, except exponents which must be integers.
-Example (Explanation provided after # sign, this will *not* work in a real units file):
+Example:
inch 25.4 mm # Define ‘inch’ as equal to the product of 25.4 and ‘mm’
@@ -28,20 +39,21 @@ mph mile / hour # Define ‘mph’ as equal to the quotient of ‘mile’ and â
litre 0.001 m^3 # Define ‘litre’ as equal to the product of 0.001 and the unit ‘m’ raised to the exponent 3
-Lines that start with the number sign (#) and blank lines will be ignored.
+Anything after a number sign (#) and blank lines will be ignored.
-Unit prefixes are defined differently. When a unit name ends with the dash (-) character, it is interpreted as a prefix. Prefix expressions are not as powerful as unit expressions, they can only be:
- - a number (1000)
- - an exponent (10^3)
- - the name of another prefix without the dash (kilo)
-* Unit Prefixes
+Unit prefixes are defined differently. When a unit name ends with the dash (-) character, it is interpreted as a prefix. Prefix expressions are not as powerful as unit expressions, they can only do:
+ - Multiplication using spaces ( ) or asterisks (*)
+ - Division using the forward slash (/)
+ - Exponentiation using the caret (^)
+Every argument can be a number or a prefix name, except exponents which much be integers
+** Unit Prefixes
In SI, you can have a unit prefix, which you attach to the start of a unit and it multiplies the unit by a certain value. For example, milli-metre = 0.001 * metre. This unit converter supports unit prefixes, and you can put any number of prefixes before a linear or base unit to transform it (this includes non-SI units).
You can use more than one prefix at once (yottayottametre = really really long distance), but you may not convert prefixes alone. If you want to do that, use ‘unit’ or ‘u’ (ku = 1000000 mu). You can also use u for converting from numbers (pi = 3.141593 u).
-*Warning*: While the standard symbol for ‘deca’ is ‘da’, this program uses ‘D’ instead since ‘da’ can be confused with ‘deciatto’. Also, you may use capital letters for the symbols of ‘hecto’ and ‘kilo’.
+The default unit file allows you to use D-, H- and K- in place of da-, h- and k- if you want.
*Warning*: The standard prefixes will never use 1024 instead of 1000, even when operating on bits and bytes. Use ‘Ki’, ‘Mi’, ‘Gi’, ‘Ti’ and so on instead.
-* Nonlinear Units
+** Nonlinear Units
Sometimes, units cannot be converted from and to by simply multiplying and dividing. A common example of this is the Celsius and Fahrenheit temperature scales, which require multiplication and addition to convert to each other (and to their base, Kelvin).
To use nonlinear units, use the following syntax:
@@ -49,14 +61,471 @@ FROM: unit1(value)
TO: unit2
Nonlinear units cannot:
- - multiply, divide or exponentiate
+ - add, subtract, multiply, divide or exponentiate
- 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.
-* Unit and Prefix Viewers
+** 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 AGPL v3 licence.
+** 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.
+
+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:
+
+Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".
+
+This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
+*** GNU Free Documentation License
+
+ GNU Free Documentation License
+ Version 1.3, 3 November 2008
+
+
+ Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc.
+ <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+0. PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other
+functional and useful document "free" in the sense of freedom: to
+assure everyone the effective freedom to copy and redistribute it,
+with or without modifying it, either commercially or noncommercially.
+Secondarily, this License preserves for the author and publisher a way
+to get credit for their work, while not being considered responsible
+for modifications made by others.
+
+This License is a kind of "copyleft", which means that derivative
+works of the document must themselves be free in the same sense. It
+complements the GNU General Public License, which is a copyleft
+license designed for free software.
+
+We have designed this License in order to use it for manuals for free
+software, because free software needs free documentation: a free
+program should come with manuals providing the same freedoms that the
+software does. But this License is not limited to software manuals;
+it can be used for any textual work, regardless of subject matter or
+whether it is published as a printed book. We recommend this License
+principally for works whose purpose is instruction or reference.
+
+
+1. APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that
+contains a notice placed by the copyright holder saying it can be
+distributed under the terms of this License. Such a notice grants a
+world-wide, royalty-free license, unlimited in duration, to use that
+work under the conditions stated herein. The "Document", below,
+refers to any such manual or work. Any member of the public is a
+licensee, and is addressed as "you". You accept the license if you
+copy, modify or distribute the work in a way requiring permission
+under copyright law.
+
+A "Modified Version" of the Document means any work containing the
+Document or a portion of it, either copied verbatim, or with
+modifications and/or translated into another language.
+
+A "Secondary Section" is a named appendix or a front-matter section of
+the Document that deals exclusively with the relationship of the
+publishers or authors of the Document to the Document's overall
+subject (or to related matters) and contains nothing that could fall
+directly within that overall subject. (Thus, if the Document is in
+part a textbook of mathematics, a Secondary Section may not explain
+any mathematics.) The relationship could be a matter of historical
+connection with the subject or with related matters, or of legal,
+commercial, philosophical, ethical or political position regarding
+them.
+
+The "Invariant Sections" are certain Secondary Sections whose titles
+are designated, as being those of Invariant Sections, in the notice
+that says that the Document is released under this License. If a
+section does not fit the above definition of Secondary then it is not
+allowed to be designated as Invariant. The Document may contain zero
+Invariant Sections. If the Document does not identify any Invariant
+Sections then there are none.
+
+The "Cover Texts" are certain short passages of text that are listed,
+as Front-Cover Texts or Back-Cover Texts, in the notice that says that
+the Document is released under this License. A Front-Cover Text may
+be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A "Transparent" copy of the Document means a machine-readable copy,
+represented in a format whose specification is available to the
+general public, that is suitable for revising the document
+straightforwardly with generic text editors or (for images composed of
+pixels) generic paint programs or (for drawings) some widely available
+drawing editor, and that is suitable for input to text formatters or
+for automatic translation to a variety of formats suitable for input
+to text formatters. A copy made in an otherwise Transparent file
+format whose markup, or absence of markup, has been arranged to thwart
+or discourage subsequent modification by readers is not Transparent.
+An image format is not Transparent if used for any substantial amount
+of text. A copy that is not "Transparent" is called "Opaque".
+
+Examples of suitable formats for Transparent copies include plain
+ASCII without markup, Texinfo input format, LaTeX input format, SGML
+or XML using a publicly available DTD, and standard-conforming simple
+HTML, PostScript or PDF designed for human modification. Examples of
+transparent image formats include PNG, XCF and JPG. Opaque formats
+include proprietary formats that can be read and edited only by
+proprietary word processors, SGML or XML for which the DTD and/or
+processing tools are not generally available, and the
+machine-generated HTML, PostScript or PDF produced by some word
+processors for output purposes only.
+
+The "Title Page" means, for a printed book, the title page itself,
+plus such following pages as are needed to hold, legibly, the material
+this License requires to appear in the title page. For works in
+formats which do not have any title page as such, "Title Page" means
+the text near the most prominent appearance of the work's title,
+preceding the beginning of the body of the text.
+
+The "publisher" means any person or entity that distributes copies of
+the Document to the public.
+
+A section "Entitled XYZ" means a named subunit of the Document whose
+title either is precisely XYZ or contains XYZ in parentheses following
+text that translates XYZ in another language. (Here XYZ stands for a
+specific section name mentioned below, such as "Acknowledgements",
+"Dedications", "Endorsements", or "History".) To "Preserve the Title"
+of such a section when you modify the Document means that it remains a
+section "Entitled XYZ" according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which
+states that this License applies to the Document. These Warranty
+Disclaimers are considered to be included by reference in this
+License, but only as regards disclaiming warranties: any other
+implication that these Warranty Disclaimers may have is void and has
+no effect on the meaning of this License.
+
+2. VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either
+commercially or noncommercially, provided that this License, the
+copyright notices, and the license notice saying this License applies
+to the Document are reproduced in all copies, and that you add no
+other conditions whatsoever to those of this License. You may not use
+technical measures to obstruct or control the reading or further
+copying of the copies you make or distribute. However, you may accept
+compensation in exchange for copies. If you distribute a large enough
+number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and
+you may publicly display copies.
+
+
+3. COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have
+printed covers) of the Document, numbering more than 100, and the
+Document's license notice requires Cover Texts, you must enclose the
+copies in covers that carry, clearly and legibly, all these Cover
+Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
+the back cover. Both covers must also clearly and legibly identify
+you as the publisher of these copies. The front cover must present
+the full title with all words of the title equally prominent and
+visible. You may add other material on the covers in addition.
+Copying with changes limited to the covers, as long as they preserve
+the title of the Document and satisfy these conditions, can be treated
+as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit
+legibly, you should put the first ones listed (as many as fit
+reasonably) on the actual cover, and continue the rest onto adjacent
+pages.
+
+If you publish or distribute Opaque copies of the Document numbering
+more than 100, you must either include a machine-readable Transparent
+copy along with each Opaque copy, or state in or with each Opaque copy
+a computer-network location from which the general network-using
+public has access to download using public-standard network protocols
+a complete Transparent copy of the Document, free of added material.
+If you use the latter option, you must take reasonably prudent steps,
+when you begin distribution of Opaque copies in quantity, to ensure
+that this Transparent copy will remain thus accessible at the stated
+location until at least one year after the last time you distribute an
+Opaque copy (directly or through your agents or retailers) of that
+edition to the public.
+
+It is requested, but not required, that you contact the authors of the
+Document well before redistributing any large number of copies, to
+give them a chance to provide you with an updated version of the
+Document.
+
+
+4. MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under
+the conditions of sections 2 and 3 above, provided that you release
+the Modified Version under precisely this License, with the Modified
+Version filling the role of the Document, thus licensing distribution
+and modification of the Modified Version to whoever possesses a copy
+of it. In addition, you must do these things in the Modified Version:
+
+A. Use in the Title Page (and on the covers, if any) a title distinct
+ from that of the Document, and from those of previous versions
+ (which should, if there were any, be listed in the History section
+ of the Document). You may use the same title as a previous version
+ if the original publisher of that version gives permission.
+B. List on the Title Page, as authors, one or more persons or entities
+ responsible for authorship of the modifications in the Modified
+ Version, together with at least five of the principal authors of the
+ Document (all of its principal authors, if it has fewer than five),
+ unless they release you from this requirement.
+C. State on the Title page the name of the publisher of the
+ Modified Version, as the publisher.
+D. Preserve all the copyright notices of the Document.
+E. Add an appropriate copyright notice for your modifications
+ adjacent to the other copyright notices.
+F. Include, immediately after the copyright notices, a license notice
+ giving the public permission to use the Modified Version under the
+ terms of this License, in the form shown in the Addendum below.
+G. Preserve in that license notice the full lists of Invariant Sections
+ and required Cover Texts given in the Document's license notice.
+H. Include an unaltered copy of this License.
+I. Preserve the section Entitled "History", Preserve its Title, and add
+ to it an item stating at least the title, year, new authors, and
+ publisher of the Modified Version as given on the Title Page. If
+ there is no section Entitled "History" in the Document, create one
+ stating the title, year, authors, and publisher of the Document as
+ given on its Title Page, then add an item describing the Modified
+ Version as stated in the previous sentence.
+J. Preserve the network location, if any, given in the Document for
+ public access to a Transparent copy of the Document, and likewise
+ the network locations given in the Document for previous versions
+ it was based on. These may be placed in the "History" section.
+ You may omit a network location for a work that was published at
+ least four years before the Document itself, or if the original
+ publisher of the version it refers to gives permission.
+K. For any section Entitled "Acknowledgements" or "Dedications",
+ Preserve the Title of the section, and preserve in the section all
+ the substance and tone of each of the contributor acknowledgements
+ and/or dedications given therein.
+L. Preserve all the Invariant Sections of the Document,
+ unaltered in their text and in their titles. Section numbers
+ or the equivalent are not considered part of the section titles.
+M. Delete any section Entitled "Endorsements". Such a section
+ may not be included in the Modified Version.
+N. Do not retitle any existing section to be Entitled "Endorsements"
+ or to conflict in title with any Invariant Section.
+O. Preserve any Warranty Disclaimers.
+
+If the Modified Version includes new front-matter sections or
+appendices that qualify as Secondary Sections and contain no material
+copied from the Document, you may at your option designate some or all
+of these sections as invariant. To do this, add their titles to the
+list of Invariant Sections in the Modified Version's license notice.
+These titles must be distinct from any other section titles.
+
+You may add a section Entitled "Endorsements", provided it contains
+nothing but endorsements of your Modified Version by various
+parties--for example, statements of peer review or that the text has
+been approved by an organization as the authoritative definition of a
+standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a
+passage of up to 25 words as a Back-Cover Text, to the end of the list
+of Cover Texts in the Modified Version. Only one passage of
+Front-Cover Text and one of Back-Cover Text may be added by (or
+through arrangements made by) any one entity. If the Document already
+includes a cover text for the same cover, previously added by you or
+by arrangement made by the same entity you are acting on behalf of,
+you may not add another; but you may replace the old one, on explicit
+permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License
+give permission to use their names for publicity for or to assert or
+imply endorsement of any Modified Version.
+
+
+5. COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this
+License, under the terms defined in section 4 above for modified
+versions, provided that you include in the combination all of the
+Invariant Sections of all of the original documents, unmodified, and
+list them all as Invariant Sections of your combined work in its
+license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and
+multiple identical Invariant Sections may be replaced with a single
+copy. If there are multiple Invariant Sections with the same name but
+different contents, make the title of each such section unique by
+adding at the end of it, in parentheses, the name of the original
+author or publisher of that section if known, or else a unique number.
+Make the same adjustment to the section titles in the list of
+Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled "History"
+in the various original documents, forming one section Entitled
+"History"; likewise combine any sections Entitled "Acknowledgements",
+and any sections Entitled "Dedications". You must delete all sections
+Entitled "Endorsements".
+
+
+6. COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other
+documents released under this License, and replace the individual
+copies of this License in the various documents with a single copy
+that is included in the collection, provided that you follow the rules
+of this License for verbatim copying of each of the documents in all
+other respects.
+
+You may extract a single document from such a collection, and
+distribute it individually under this License, provided you insert a
+copy of this License into the extracted document, and follow this
+License in all other respects regarding verbatim copying of that
+document.
+
+
+7. AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate
+and independent documents or works, in or on a volume of a storage or
+distribution medium, is called an "aggregate" if the copyright
+resulting from the compilation is not used to limit the legal rights
+of the compilation's users beyond what the individual works permit.
+When the Document is included in an aggregate, this License does not
+apply to the other works in the aggregate which are not themselves
+derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these
+copies of the Document, then if the Document is less than one half of
+the entire aggregate, the Document's Cover Texts may be placed on
+covers that bracket the Document within the aggregate, or the
+electronic equivalent of covers if the Document is in electronic form.
+Otherwise they must appear on printed covers that bracket the whole
+aggregate.
+
+
+8. TRANSLATION
+
+Translation is considered a kind of modification, so you may
+distribute translations of the Document under the terms of section 4.
+Replacing Invariant Sections with translations requires special
+permission from their copyright holders, but you may include
+translations of some or all Invariant Sections in addition to the
+original versions of these Invariant Sections. You may include a
+translation of this License, and all the license notices in the
+Document, and any Warranty Disclaimers, provided that you also include
+the original English version of this License and the original versions
+of those notices and disclaimers. In case of a disagreement between
+the translation and the original version of this License or a notice
+or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled "Acknowledgements",
+"Dedications", or "History", the requirement (section 4) to Preserve
+its Title (section 1) will typically require changing the actual
+title.
+
+
+9. TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense, or distribute it is void, and
+will automatically terminate your rights under this License.
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, receipt of a copy of some or all of the same material does
+not give you any rights to use it.
+
+
+10. FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions of the
+GNU Free Documentation License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in
+detail to address new problems or concerns. See
+https://www.gnu.org/licenses/.
+
+Each version of the License is given a distinguishing version number.
+If the Document specifies that a particular numbered version of this
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that specified version or
+of any later version that has been published (not as a draft) by the
+Free Software Foundation. If the Document does not specify a version
+number of this License, you may choose any version ever published (not
+as a draft) by the Free Software Foundation. If the Document
+specifies that a proxy can decide which future versions of this
+License can be used, that proxy's public statement of acceptance of a
+version permanently authorizes you to choose that version for the
+Document.
+
+11. RELICENSING
+
+"Massive Multiauthor Collaboration Site" (or "MMC Site") means any
+World Wide Web server that publishes copyrightable works and also
+provides prominent facilities for anybody to edit those works. A
+public wiki that anybody can edit is an example of such a server. A
+"Massive Multiauthor Collaboration" (or "MMC") contained in the site
+means any set of copyrightable works thus published on the MMC site.
+
+"CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0
+license published by Creative Commons Corporation, a not-for-profit
+corporation with a principal place of business in San Francisco,
+California, as well as future copyleft versions of that license
+published by that same organization.
+
+"Incorporate" means to publish or republish a Document, in whole or in
+part, as part of another Document.
+
+An MMC is "eligible for relicensing" if it is licensed under this
+License, and if all works that were first published under this License
+somewhere other than this MMC, and subsequently incorporated in whole or
+in part into the MMC, (1) had no cover texts or invariant sections, and
+(2) were thus incorporated prior to November 1, 2008.
+
+The operator of an MMC Site may republish an MMC contained in the site
+under CC-BY-SA on the same site at any time before August 1, 2009,
+provided the MMC is eligible for relicensing.
+
+
+ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of
+the License in the document and put the following copyright and
+license notices just after the title page:
+
+ Copyright (c) YEAR YOUR NAME.
+ Permission is granted to copy, distribute and/or modify this document
+ under the terms of the GNU Free Documentation License, Version 1.3
+ or any later version published by the Free Software Foundation;
+ with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
+ A copy of the license is included in the section entitled "GNU
+ Free Documentation License".
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
+replace the "with...Texts." line with this:
+
+ with the Invariant Sections being LIST THEIR TITLES, with the
+ Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
+
+If you have Invariant Sections without Cover Texts, or some other
+combination of the three, merge those two alternatives to suit the
+situation.
+
+If your document contains nontrivial examples of program code, we
+recommend releasing these examples in parallel under your choice of
+free software license, such as the GNU General Public License,
+to permit their use in free software.
-This document is Copyright (C) 2019 Adrien Hopkins. It is released under the terms of the CC BY-SA (Creative Commons Attribution-ShareAlike) licence.
diff --git a/dimensionfile.txt b/dimensionfile.txt
new file mode 100644
index 0000000..3485de5
--- /dev/null
+++ b/dimensionfile.txt
@@ -0,0 +1,18 @@
+# A file for the unit dimensions in my unit converter program
+
+# SI Base Dimensions
+# ! means "look for an existing dimension which I will load at the start"
+# This is necessary because every dimension must be defined by others, and I need somewhere to start.
+
+# I have excluded electric current, quantity and luminous intensity since their units are exclusively SI.
+
+LENGTH !
+MASS !
+TIME !
+TEMPERATURE !
+
+# Derived Dimensions
+AREA LENGTH^2
+VOLUME LENGTH^3
+VELOCITY LENGTH / TIME
+ENERGY MASS * VELOCITY^2 \ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..5b3e468
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,55 @@
+<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/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
new file mode 100755
index 0000000..e5d2f67
--- /dev/null
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -0,0 +1,1479 @@
+/**
+ * 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/unitConverter/converterGUI/DelegateListModel.java b/src/org/unitConverter/converterGUI/DelegateListModel.java
index 0e9b342..b80f63d 100755
--- a/src/unitConverter/converterGUI/DelegateListModel.java
+++ b/src/org/unitConverter/converterGUI/DelegateListModel.java
@@ -14,8 +14,9 @@
* 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 unitConverter.converterGUI;
+package org.unitConverter.converterGUI;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -50,6 +51,15 @@ final class DelegateListModel<E> extends AbstractListModel<E> implements List<E>
private final List<E> delegate;
/**
+ * Creates an empty {@code DelegateListModel}.
+ *
+ * @since 2019-04-13
+ */
+ public DelegateListModel() {
+ this(new ArrayList<>());
+ }
+
+ /**
* Creates the {@code DelegateListModel}.
*
* @param delegate
diff --git a/src/unitConverter/converterGUI/FilterComparator.java b/src/org/unitConverter/converterGUI/FilterComparator.java
index 27ec3ab..7b17bfc 100755
--- a/src/unitConverter/converterGUI/FilterComparator.java
+++ b/src/org/unitConverter/converterGUI/FilterComparator.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.converterGUI;
+package org.unitConverter.converterGUI;
import java.util.Comparator;
import java.util.Objects;
@@ -26,7 +26,7 @@ import java.util.Objects;
* @since 2019-01-15
* @since v0.1.0
*/
-public final class FilterComparator implements Comparator<String> {
+final class FilterComparator implements Comparator<String> {
/**
* The filter that the comparator is filtered by.
*
@@ -41,6 +41,13 @@ public final class FilterComparator implements Comparator<String> {
* @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}.
@@ -60,34 +67,63 @@ public final class FilterComparator implements Comparator<String> {
* 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) {
+ 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 (arg0.startsWith(this.filter) && !arg1.startsWith(this.filter))
+ if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
return -1;
- else if (!arg0.startsWith(this.filter) && arg1.startsWith(this.filter))
+ 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 (arg0.contains(this.filter) && !arg1.contains(this.filter))
+ if (str0.contains(this.filter) && !str1.contains(this.filter))
return -1;
- else if (!arg0.contains(this.filter) && !arg1.contains(this.filter))
+ else if (!str0.contains(this.filter) && !str1.contains(this.filter))
return 1;
// other elements go last
if (this.comparator == null)
- return arg0.compareTo(arg1);
+ return str0.compareTo(str1);
else
- return this.comparator.compare(arg0, arg1);
+ return this.comparator.compare(str0, str1);
}
}
diff --git a/src/unitConverter/converterGUI/GridBagBuilder.java b/src/org/unitConverter/converterGUI/GridBagBuilder.java
index e036677..f1229b2 100755
--- a/src/unitConverter/converterGUI/GridBagBuilder.java
+++ b/src/org/unitConverter/converterGUI/GridBagBuilder.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.converterGUI;
+package org.unitConverter.converterGUI;
import java.awt.GridBagConstraints;
import java.awt.Insets;
diff --git a/src/org/unitConverter/converterGUI/MutablePredicate.java b/src/org/unitConverter/converterGUI/MutablePredicate.java
new file mode 100644
index 0000000..e15b3cd
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/MutablePredicate.java
@@ -0,0 +1,70 @@
+/**
+ * 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.converterGUI;
+
+import java.util.function.Predicate;
+
+/**
+ * A container for a predicate, which can be changed later.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+final class MutablePredicate<T> implements Predicate<T> {
+ /**
+ * The predicate stored in this {@code MutablePredicate}
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private Predicate<T> predicate;
+
+ /**
+ * Creates the {@code MutablePredicate}.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public MutablePredicate(final Predicate<T> predicate) {
+ this.predicate = predicate;
+ }
+
+ /**
+ * @return predicate
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final Predicate<T> getPredicate() {
+ return this.predicate;
+ }
+
+ /**
+ * @param predicate
+ * new value of predicate
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final void setPredicate(final Predicate<T> predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean test(final T t) {
+ return this.predicate.test(t);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java
new file mode 100644
index 0000000..1995466
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/SearchBoxList.java
@@ -0,0 +1,297 @@
+/**
+ * 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.converterGUI;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.function.Predicate;
+
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @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.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final String EMPTY_TEXT = "Search...";
+
+ /**
+ * The color to use for an empty foreground.
+ *
+ * @since 2019-04-13
+ * @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
+ // 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
+ * @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
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public SearchBoxList(final Collection<String> itemsToFilter, final Comparator<String> defaultOrdering,
+ final boolean caseSensitive) {
+ super(new BorderLayout(), true);
+ this.itemsToFilter = itemsToFilter;
+ this.defaultOrdering = defaultOrdering;
+ this.caseSensitive = caseSensitive;
+
+ // create the components
+ this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter));
+ this.searchItems = new JList<>(this.listModel);
+
+ this.searchBox = new JTextField(EMPTY_TEXT);
+ this.searchBox.setForeground(EMPTY_FOREGROUND);
+
+ // add them to the panel
+ this.add(this.searchBox, BorderLayout.PAGE_START);
+ this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER);
+
+ // set up the search box
+ this.searchBox.addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(final FocusEvent e) {
+ SearchBoxList.this.searchBoxFocusGained(e);
+ }
+
+ @Override
+ public void focusLost(final FocusEvent e) {
+ SearchBoxList.this.searchBoxFocusLost(e);
+ }
+ });
+
+ this.searchBox.addCaretListener(e -> this.searchBoxTextChanged());
+ this.searchBoxEmpty = true;
+ }
+
+ /**
+ * Adds an additional filter for searching.
+ *
+ * @param filter
+ * filter to add.
+ * @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.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void clearSearchFilters() {
+ this.customSearchFilter = o -> true;
+ }
+
+ /**
+ * @return this component's search box component
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ 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
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private Predicate<String> getSearchFilter(final String searchText) {
+ if (this.caseSensitive)
+ return string -> string.contains(searchText);
+ else
+ return string -> string.toLowerCase().contains(searchText.toLowerCase());
+ }
+
+ /**
+ * @return this component's list component
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public final JList<String> getSearchList() {
+ return this.searchItems;
+ }
+
+ /**
+ * @return index selected in item list
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public int getSelectedIndex() {
+ return this.searchItems.getSelectedIndex();
+ }
+
+ /**
+ * @return value selected in item list
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public String getSelectedValue() {
+ return this.searchItems.getSelectedValue();
+ }
+
+ /**
+ * Re-applies the filters.
+ *
+ * @since 2019-04-13
+ * @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 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
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private void searchBoxFocusGained(final FocusEvent e) {
+ this.searchBoxFocused = true;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText("");
+ this.searchBox.setForeground(Color.BLACK);
+ }
+ }
+
+ /**
+ * Runs whenever the search box loses focus.
+ *
+ * @param e
+ * focus event
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private void searchBoxFocusLost(final FocusEvent e) {
+ this.searchBoxFocused = false;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText(EMPTY_TEXT);
+ this.searchBox.setForeground(EMPTY_FOREGROUND);
+ }
+ }
+
+ /**
+ * Runs whenever the text in the search box is changed.
+ * <p>
+ * Reapplies the search filter, and custom filters.
+ * </p>
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private void searchBoxTextChanged() {
+ 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 Predicate<String> searchFilter = this.getSearchFilter(searchText);
+
+ // initialize list with items that match the filter then sort
+ 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);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
new file mode 100755
index 0000000..e258c6f
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -0,0 +1,827 @@
+/**
+ * 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/unitConverter/converterGUI/package-info.java b/src/org/unitConverter/converterGUI/package-info.java
index 9f7fa57..1555291 100644
--- a/src/unitConverter/converterGUI/package-info.java
+++ b/src/org/unitConverter/converterGUI/package-info.java
@@ -19,5 +19,6 @@
*
* @author Adrien Hopkins
* @since 2019-01-25
+ * @since v0.2.0
*/
-package unitConverter.converterGUI; \ No newline at end of file
+package org.unitConverter.converterGUI; \ No newline at end of file
diff --git a/src/unitConverter/dimension/BaseDimension.java b/src/org/unitConverter/dimension/BaseDimension.java
index 0c09dce..5e3ddad 100755
--- a/src/unitConverter/dimension/BaseDimension.java
+++ b/src/org/unitConverter/dimension/BaseDimension.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.dimension;
+package org.unitConverter.dimension;
/**
* A base dimension that makes up {@code UnitDimension} objects.
diff --git a/src/unitConverter/dimension/OtherBaseDimension.java b/src/org/unitConverter/dimension/OtherBaseDimension.java
index 8c6d25d..8aea2b9 100755
--- a/src/unitConverter/dimension/OtherBaseDimension.java
+++ b/src/org/unitConverter/dimension/OtherBaseDimension.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.dimension;
+package org.unitConverter.dimension;
import java.util.Objects;
diff --git a/src/unitConverter/dimension/SIBaseDimension.java b/src/org/unitConverter/dimension/SIBaseDimension.java
index 928d8d6..c459963 100755
--- a/src/unitConverter/dimension/SIBaseDimension.java
+++ b/src/org/unitConverter/dimension/SIBaseDimension.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.dimension;
+package org.unitConverter.dimension;
import java.util.Objects;
diff --git a/src/unitConverter/dimension/StandardDimensions.java b/src/org/unitConverter/dimension/StandardDimensions.java
index b3edb7d..4b1b814 100755
--- a/src/unitConverter/dimension/StandardDimensions.java
+++ b/src/org/unitConverter/dimension/StandardDimensions.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.dimension;
+package org.unitConverter.dimension;
/**
* All of the dimensions that are used by the SI.
diff --git a/src/unitConverter/dimension/UnitDimension.java b/src/org/unitConverter/dimension/UnitDimension.java
index 40e5bbc..dbeaeff 100755
--- a/src/unitConverter/dimension/UnitDimension.java
+++ b/src/org/unitConverter/dimension/UnitDimension.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.dimension;
+package org.unitConverter.dimension;
import java.util.ArrayList;
import java.util.HashMap;
diff --git a/src/unitConverter/dimension/package-info.java b/src/org/unitConverter/dimension/package-info.java
index 74895ce..8cb26b1 100755
--- a/src/unitConverter/dimension/package-info.java
+++ b/src/org/unitConverter/dimension/package-info.java
@@ -19,5 +19,6 @@
*
* @author Adrien Hopkins
* @since 2018-12-22
+ * @since v0.1.0
*/
-package unitConverter.dimension; \ No newline at end of file
+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
new file mode 100644
index 0000000..7cdbe5b
--- /dev/null
+++ b/src/org/unitConverter/math/DecimalComparison.java
@@ -0,0 +1,114 @@
+/**
+ * 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/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
new file mode 100644
index 0000000..b2261ed
--- /dev/null
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -0,0 +1,708 @@
+/**
+ * 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.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * An object that can parse expressions with unary or binary operators.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of object that exists in parsed expressions
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+public final class ExpressionParser<T> {
+ /**
+ * A builder that can create {@code ExpressionParser<T>} instances.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of object that exists in parsed expressions
+ * @since 2019-03-17
+ * @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}.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Function<String, T> objectObtainer;
+
+ /**
+ * The function of the space as an operator (like 3 x y)
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private String spaceFunction = null;
+
+ /**
+ * 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.
+ *
+ * @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
+ * @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.");
+ 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
+ * @return this builder
+ * @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) {
+ 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) {
+ @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.
+ *
+ * @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));
+
+ 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
+ * @return this builder
+ * @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) {
+ 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) {
+ @Override
+ public T apply(final T t) {
+ return operator.apply(t);
+ }
+ };
+ this.unaryOperators.put(text, priorityOperator);
+ return this;
+ }
+
+ /**
+ * @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);
+ }
+ }
+
+ /**
+ * A binary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of operand and result
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private static abstract class PriorityBinaryOperator<T>
+ implements BinaryOperator<T>, Comparable<PriorityBinaryOperator<T>> {
+ /**
+ * 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
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public PriorityBinaryOperator(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Compares this object to another by priority.
+ *
+ * <p>
+ * {@inheritDoc}
+ * </p>
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ @Override
+ public int compareTo(final PriorityBinaryOperator<T> o) {
+ if (this.priority < o.priority)
+ return -1;
+ else if (this.priority > o.priority)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * A unary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of operand and result
+ * @since 2019-03-17
+ * @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
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private final int priority;
+
+ /**
+ * Creates the {@code PriorityUnaryOperator}.
+ *
+ * @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.
+ *
+ * <p>
+ * {@inheritDoc}
+ * </p>
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ @Override
+ public int compareTo(final PriorityUnaryOperator<T> o) {
+ if (this.priority < o.priority)
+ return -1;
+ else if (this.priority > o.priority)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * The types of tokens that are available.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private static enum TokenType {
+ OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
+ }
+
+ /**
+ * The opening bracket.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public static final char OPENING_BRACKET = '(';
+
+ /**
+ * The closing bracket.
+ *
+ * @since 2019-03-22
+ * @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
+ * @return position of matching bracket
+ * @throws NullPointerException
+ * if string is null
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ 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));
+ }
+
+ // 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++) {
+ final char currentCharacter = string.charAt(currentPosition);
+
+ if (currentCharacter == openingBracket) {
+ bracketLevel++;
+ } else if (currentCharacter == closingBracket) {
+ bracketLevel--;
+ if (bracketLevel == 0)
+ 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}.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Function<String, T> objectObtainer;
+
+ /**
+ * 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.
+ *
+ * @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.
+ *
+ * @since 2019-03-22
+ * @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
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private ExpressionParser(final Function<String, T> objectObtainer,
+ final Map<String, PriorityUnaryOperator<T>> unaryOperators,
+ 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).
+ * <p>
+ * For example,<br>
+ * {@code 2 * (3 + 4)}<br>
+ * becomes<br>
+ * {@code 2 3 4 + *}.
+ *
+ * @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);
+
+ // check for function
+ 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) != ' ') {
+ 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);
+ } else {
+ // normal brackets like (1 + 2) * (3 / 5)
+ components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
+ components.add(this.convertExpressionToReversePolish(
+ 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) {
+ components.add(++i, this.spaceOperator);
+ }
+ }
+ }
+
+ // turn the expression into reverse Polish
+ while (true) {
+ 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.");
+ }
+ }
+
+ // join all of the components together, then ensure there is only one space in a row
+ String expressionRPN = String.join(" ", components).replaceAll(" +", " ");
+
+ while (expressionRPN.charAt(0) == ' ') {
+ expressionRPN = expressionRPN.substring(1);
+ }
+ while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') {
+ expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1);
+ }
+ return expressionRPN;
+ }
+
+ /**
+ * 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
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ 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;
+ }
+ }
+
+ // max priority position found
+ return maxPriorityPosition;
+ }
+
+ /**
+ * Determines whether an inputted string is an object or an operator
+ *
+ * @param token
+ * string to input
+ * @return type of token it is
+ * @throws NullPointerException
+ * if {@code expression} is null
+ * @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))
+ return TokenType.BINARY_OPERATOR;
+ else
+ return TokenType.OBJECT;
+ }
+
+ /**
+ * Parses an expression.
+ *
+ * @param expression
+ * expression to parse
+ * @return result
+ * @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));
+ }
+
+ /**
+ * Parses an expression expressed in reverse Polish notation.
+ *
+ * @param expression
+ * expression to parse
+ * @return result
+ * @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)));
+
+ }
+ }
+
+ // 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.");
+ else if (stack.size() == 0)
+ throw new IllegalStateException("Computation ended up without an answer.");
+ return stack.pop();
+ }
+}
diff --git a/src/org/unitConverter/math/package-info.java b/src/org/unitConverter/math/package-info.java
new file mode 100644
index 0000000..65d6b23
--- /dev/null
+++ b/src/org/unitConverter/math/package-info.java
@@ -0,0 +1,23 @@
+/**
+ * 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/>.
+ */
+/**
+ * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-14
+ */
+package org.unitConverter.math; \ No newline at end of file
diff --git a/src/unitConverter/package-info.java b/src/org/unitConverter/package-info.java
index e2f7ff7..23dd165 100644
--- a/src/unitConverter/package-info.java
+++ b/src/org/unitConverter/package-info.java
@@ -18,6 +18,7 @@
* A program that converts units.
*
* @author Adrien Hopkins
+ * @version v0.2.0
* @since 2019-01-25
*/
-package unitConverter; \ No newline at end of file
+package org.unitConverter; \ No newline at end of file
diff --git a/src/unitConverter/unit/AbstractUnit.java b/src/org/unitConverter/unit/AbstractUnit.java
index 24814e8..05a6c17 100644
--- a/src/unitConverter/unit/AbstractUnit.java
+++ b/src/org/unitConverter/unit/AbstractUnit.java
@@ -14,11 +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 unitConverter.unit;
+package org.unitConverter.unit;
import java.util.Objects;
-import unitConverter.dimension.UnitDimension;
+import org.unitConverter.dimension.UnitDimension;
/**
* The default abstract implementation of the {@code Unit} interface.
@@ -29,60 +29,6 @@ import unitConverter.dimension.UnitDimension;
*/
public abstract class AbstractUnit implements Unit {
/**
- * The number of units created, including base units.
- *
- * @since 2019-01-02
- * @since v0.1.0
- */
- private static long unitCount = 0;
-
- /**
- * The number of base units created.
- *
- * @since 2019-01-02
- * @since v0.1.0
- */
- private static long baseUnitCount = 0;
-
- /**
- * @return number of base units created
- * @since 2019-01-02
- * @since v0.1.0
- */
- public static final long getBaseUnitCount() {
- return baseUnitCount;
- }
-
- /**
- * @return number of units created
- * @since 2019-01-02
- * @since v0.1.0
- */
- public static final long getUnitCount() {
- return unitCount;
- }
-
- /**
- * Increments the number of base units.
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public static final void incrementBaseUnitCounter() {
- baseUnitCount++;
- }
-
- /**
- * Increments the number of units.
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public static final void incrementUnitCounter() {
- unitCount++;
- }
-
- /**
* The dimension, or what the unit measures.
*
* @since 2018-12-22
@@ -164,7 +110,6 @@ public abstract class AbstractUnit implements Unit {
return this.system;
}
- // TODO document and revise units' toString methods
@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
new file mode 100755
index 0000000..67309cf
--- /dev/null
+++ b/src/org/unitConverter/unit/BaseUnit.java
@@ -0,0 +1,168 @@
+/**
+ * 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/unitConverter/unit/DefaultUnitPrefix.java b/src/org/unitConverter/unit/DefaultUnitPrefix.java
index d19161b..4a9e487 100755
--- a/src/unitConverter/unit/DefaultUnitPrefix.java
+++ b/src/org/unitConverter/unit/DefaultUnitPrefix.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.unit;
+package org.unitConverter.unit;
import java.util.Objects;
@@ -33,6 +33,7 @@ public final class DefaultUnitPrefix implements UnitPrefix {
*
* @param multiplier
* @since 2019-01-14
+ * @since v0.2.0
*/
public DefaultUnitPrefix(final double multiplier) {
this.multiplier = multiplier;
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java
new file mode 100644
index 0000000..1b1ac97
--- /dev/null
+++ b/src/org/unitConverter/unit/LinearUnit.java
@@ -0,0 +1,294 @@
+/**
+ * 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/unitConverter/unit/NonlinearUnits.java b/src/org/unitConverter/unit/NonlinearUnits.java
index ec1874c..e47c28f 100755
--- a/src/unitConverter/unit/NonlinearUnits.java
+++ b/src/org/unitConverter/unit/NonlinearUnits.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.unit;
+package org.unitConverter.unit;
/**
* Some major nonlinear units.
diff --git a/src/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java
index 54eb4c5..46e6ff1 100644
--- a/src/unitConverter/unit/SI.java
+++ b/src/org/unitConverter/unit/SI.java
@@ -14,14 +14,14 @@
* 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 unitConverter.unit;
+package org.unitConverter.unit;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
-import unitConverter.dimension.StandardDimensions;
-import unitConverter.dimension.UnitDimension;
+import org.unitConverter.dimension.StandardDimensions;
+import org.unitConverter.dimension.UnitDimension;
/**
* The SI, which holds all SI units
diff --git a/src/unitConverter/unit/SIPrefix.java b/src/org/unitConverter/unit/SIPrefix.java
index 54625fb..31d7ff2 100755
--- a/src/unitConverter/unit/SIPrefix.java
+++ b/src/org/unitConverter/unit/SIPrefix.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.unit;
+package org.unitConverter.unit;
/**
* The SI prefixes.
diff --git a/src/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java
index 54f1423..86fc5a2 100755
--- a/src/unitConverter/unit/Unit.java
+++ b/src/org/unitConverter/unit/Unit.java
@@ -14,11 +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 unitConverter.unit;
+package org.unitConverter.unit;
import java.util.Objects;
-import unitConverter.dimension.UnitDimension;
+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.
diff --git a/src/unitConverter/unit/UnitPrefix.java b/src/org/unitConverter/unit/UnitPrefix.java
index a0c1b7c..9f9645d 100755
--- a/src/unitConverter/unit/UnitPrefix.java
+++ b/src/org/unitConverter/unit/UnitPrefix.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package unitConverter.unit;
+package org.unitConverter.unit;
/**
* A prefix that can be attached onto the front of any unit, which multiplies it by a certain value
@@ -25,9 +25,48 @@ package unitConverter.unit;
*/
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/unitConverter/unit/UnitSystem.java b/src/org/unitConverter/unit/UnitSystem.java
index ce8c249..550eff6 100755
--- a/src/unitConverter/unit/UnitSystem.java
+++ b/src/org/unitConverter/unit/UnitSystem.java
@@ -14,11 +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 unitConverter.unit;
+package org.unitConverter.unit;
import java.util.Objects;
-import unitConverter.dimension.UnitDimension;
+import org.unitConverter.dimension.UnitDimension;
/**
* A system of units. Each unit should be aware of its system.
diff --git a/src/unitConverter/unit/package-info.java b/src/org/unitConverter/unit/package-info.java
index b5deb0c..dd5a939 100644
--- a/src/unitConverter/unit/package-info.java
+++ b/src/org/unitConverter/unit/package-info.java
@@ -19,5 +19,6 @@
*
* @author Adrien Hopkins
* @since 2019-01-25
+ * @since v0.1.0
*/
-package unitConverter.unit; \ No newline at end of file
+package org.unitConverter.unit; \ No newline at end of file
diff --git a/src/test/java/ExpressionParserTest.java b/src/test/java/ExpressionParserTest.java
new file mode 100644
index 0000000..40c91ac
--- /dev/null
+++ b/src/test/java/ExpressionParserTest.java
@@ -0,0 +1,53 @@
+/**
+ * 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 test.java;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.unitConverter.math.ExpressionParser;
+
+/**
+ * A test for the {@code ExpressionParser} class. This is NOT part of this program's public API.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+public class ExpressionParserTest {
+ private static final ExpressionParser<Integer> numberParser = new ExpressionParser.Builder<>(Integer::parseInt)
+ .addBinaryOperator("+", (o1, o2) -> o1 + o2, 0).addBinaryOperator("-", (o1, o2) -> o1 - o2, 0)
+ .addBinaryOperator("*", (o1, o2) -> o1 * o2, 1).addBinaryOperator("/", (o1, o2) -> o1 / o2, 1)
+ .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build();
+
+ /**
+ * Test method for {@link org.unitConverter.math.ExpressionParser#parseExpression(java.lang.String)}.
+ */
+ @Test
+ public void testParseExpression() {
+ // test parsing of expressions
+ assertEquals((int) numberParser.parseExpression("1 + 2 ^ 5 * 3"), 97);
+ assertEquals((int) numberParser.parseExpression("(1 + 2) ^ 5 * 3"), 729);
+
+ // ensure it normally goes left to right
+ assertEquals((int) numberParser.parseExpression("1 + 2 + 3 + 4"), 10);
+ assertEquals((int) numberParser.parseExpression("12 - 4 - 3"), 5);
+ assertEquals((int) numberParser.parseExpression("12 - (4 - 3)"), 11);
+ assertEquals((int) numberParser.parseExpression("1 / 2 + 3"), 3);
+ }
+
+}
diff --git a/src/unitConverter/dimension/UnitDimensionTest.java b/src/test/java/UnitDimensionTest.java
index 86db1b8..587cf4c 100755
--- a/src/unitConverter/dimension/UnitDimensionTest.java
+++ b/src/test/java/UnitDimensionTest.java
@@ -14,29 +14,31 @@
* 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 unitConverter.dimension;
+package test.java;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static unitConverter.dimension.StandardDimensions.AREA;
-import static unitConverter.dimension.StandardDimensions.ENERGY;
-import static unitConverter.dimension.StandardDimensions.LENGTH;
-import static unitConverter.dimension.StandardDimensions.MASS;
-import static unitConverter.dimension.StandardDimensions.MASS_DENSITY;
-import static unitConverter.dimension.StandardDimensions.QUANTITY;
-import static unitConverter.dimension.StandardDimensions.TIME;
-import static unitConverter.dimension.StandardDimensions.VOLUME;
+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 org.junit.jupiter.api.Test;
+import org.junit.Test;
+import org.unitConverter.dimension.SIBaseDimension;
+import org.unitConverter.dimension.UnitDimension;
/**
- * Tests for {@link UnitDimension}.
+ * Tests for {@link UnitDimension}. This is NOT part of this program's public API.
*
* @author Adrien Hopkins
* @since 2018-12-12
* @since v0.1.0
*/
-class UnitDimensionTest {
+public class UnitDimensionTest {
/**
* Tests {@link UnitDimension#equals}
*
@@ -44,7 +46,7 @@ class UnitDimensionTest {
* @since v0.1.0
*/
@Test
- void testEquals() {
+ public void testEquals() {
assertEquals(LENGTH, LENGTH);
assertFalse(LENGTH.equals(QUANTITY));
}
@@ -56,7 +58,7 @@ class UnitDimensionTest {
* @since v0.1.0
*/
@Test
- void testExponents() {
+ public void testExponents() {
assertEquals(1, LENGTH.getExponent(SIBaseDimension.LENGTH));
assertEquals(3, VOLUME.getExponent(SIBaseDimension.LENGTH));
}
@@ -68,7 +70,7 @@ class UnitDimensionTest {
* @since v0.1.0
*/
@Test
- void testMultiplicationAndDivision() {
+ public void testMultiplicationAndDivision() {
assertEquals(AREA, LENGTH.times(LENGTH));
assertEquals(MASS_DENSITY, MASS.dividedBy(VOLUME));
assertEquals(ENERGY, AREA.times(MASS).dividedBy(TIME).dividedBy(TIME));
diff --git a/src/test/java/UnitTest.java b/src/test/java/UnitTest.java
new file mode 100755
index 0000000..00fcf3c
--- /dev/null
+++ b/src/test/java/UnitTest.java
@@ -0,0 +1,116 @@
+/**
+ * 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/UnitsDatabaseTest.java b/src/test/java/UnitsDatabaseTest.java
new file mode 100644
index 0000000..9222740
--- /dev/null
+++ b/src/test/java/UnitsDatabaseTest.java
@@ -0,0 +1,260 @@
+/**
+ * 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 test.java;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+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;
+
+/**
+ * A test for the {@link UnitsDatabase} 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 {
+ // 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;
+ }
+ };
+
+ // 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);
+
+ /**
+ * Test that prefixes correctly apply to units.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ @Test
+ public void testPrefixes() {
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("U", U);
+ database.addUnit("V", V);
+ database.addUnit("W", W);
+
+ database.addPrefix("A", A);
+ database.addPrefix("B", B);
+ database.addPrefix("C", C);
+
+ // 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);
+ }
+
+ /**
+ * Tests the functionnalites of the prefixless unit map.
+ *
+ * <p>
+ * The map should be an auto-updating view of the units in the database.
+ * </p>
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ @Test
+ public void testPrefixlessUnitMap() {
+ final UnitsDatabase database = new UnitsDatabase();
+ final Map<String, Unit> prefixlessUnits = database.unitMapPrefixless();
+
+ database.addUnit("U", U);
+ database.addUnit("V", V);
+ database.addUnit("W", W);
+
+ // this should work because the map should be an auto-updating view
+ assertTrue(prefixlessUnits.containsKey("U"));
+ assertFalse(prefixlessUnits.containsKey("Z"));
+
+ assertTrue(prefixlessUnits.containsValue(U));
+ assertFalse(prefixlessUnits.containsValue(NONLINEAR));
+ }
+
+ /**
+ * Tests that the database correctly stores and retrieves units, ignoring prefixes.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ @Test
+ public void testPrefixlessUnits() {
+ final UnitsDatabase database = new UnitsDatabase();
+
+ 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"));
+ }
+
+ /**
+ * Test that unit expressions return the correct value.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ @Test
+ public void testUnitExpressions() {
+ // load units
+ final UnitsDatabase database = new UnitsDatabase();
+
+ 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 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
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ @Test
+ public void testUnitIterator() {
+ // load units
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("J", J);
+
+ 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();
+
+ 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)) {
+ expectedLength++;
+ unitsWithThisLengthSoFar = 0;
+ }
+
+ 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++;
+ }
+ }
+
+ /**
+ * 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.
+ * </p>
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ @Test
+ public void testUnitPrefixCombinations() {
+ // load units
+ final UnitsDatabase database = new UnitsDatabase();
+
+ 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));
+
+ final Unit expected2 = J.times(17);
+ final Unit actual2 = database.getUnit("ABCJ");
+
+ assertEquals(expected2, actual2);
+ }
+}
diff --git a/src/test/java/package-info.java b/src/test/java/package-info.java
new file mode 100644
index 0000000..3da7fcb
--- /dev/null
+++ b/src/test/java/package-info.java
@@ -0,0 +1,24 @@
+/**
+ * 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
diff --git a/src/unitConverter/UnitsDatabase.java b/src/unitConverter/UnitsDatabase.java
deleted file mode 100755
index 4816db1..0000000
--- a/src/unitConverter/UnitsDatabase.java
+++ /dev/null
@@ -1,584 +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 unitConverter;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-import unitConverter.dimension.UnitDimension;
-import unitConverter.unit.AbstractUnit;
-import unitConverter.unit.BaseUnit;
-import unitConverter.unit.DefaultUnitPrefix;
-import unitConverter.unit.LinearUnit;
-import unitConverter.unit.SI;
-import unitConverter.unit.Unit;
-import unitConverter.unit.UnitPrefix;
-
-/**
- * A database of units.
- *
- * @author Adrien Hopkins
- * @since 2019-01-07
- * @since v0.1.0
- */
-public final class UnitsDatabase {
- /**
- * The units in this system.
- *
- * @since 2019-01-07
- * @since v0.1.0
- */
- private final Map<String, Unit> units;
-
- /**
- * The unit prefixes in this system.
- *
- * @since 2019-01-14
- * @since v0.1.0
- */
- private final Map<String, UnitPrefix> prefixes;
-
- /**
- * Creates the {@code UnitsDatabase}.
- *
- * @since 2019-01-10
- * @since v0.1.0
- */
- public UnitsDatabase() {
- this.units = new HashMap<>();
- this.prefixes = new HashMap<>();
- }
-
- /**
- * 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>Any line that begins with the '#' 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 addAllFromFile(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()) {
- final String line = reader.readLine();
- lineCounter++;
-
- // ignore lines that start with a # sign - they're comments
- if (line.startsWith("#") || line.isEmpty()) {
- continue;
- }
-
- // 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;
- }
- AbstractUnit.incrementUnitCounter();
- if (unit instanceof BaseUnit) {
- AbstractUnit.incrementBaseUnitCounter();
- }
- this.addUnit(name, unit);
- }
- }
- }
- } 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 a unit prefix to the database using a custom name
- *
- * @param name
- * prefix's name
- * @param prefix
- * prefix to add
- * @throws NullPointerException
- * if name or unit 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 using a custom name
- *
- * @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.units.put(name, Objects.requireNonNull(unit, "unit must not be null."));
- }
-
- /**
- * Tests if the database has a unit with this name, ignoring prefixes
- *
- * @param name
- * name to test
- * @return if database contains name
- * @since 2019-01-13
- * @since v0.1.0
- */
- public boolean containsPrefixlessUnitName(final String name) {
- return this.units.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) {
- // check for prefixes
- for (final String prefixName : this.prefixNameSet()) {
- if (name.startsWith(prefixName))
- if (this.containsUnitName(name.substring(prefixName.length())))
- return true;
- }
- return this.units.containsKey(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) {
- 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.");
-
- try {
- return new DefaultUnitPrefix(Double.parseDouble(expression));
- } catch (final NumberFormatException e) {
- if (expression.contains("^")) {
- final String[] baseAndExponent = expression.split("\\^");
-
- final double base;
- try {
- base = Double.parseDouble(baseAndExponent[0]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Base of exponientation must be a number.");
- }
-
- final int exponent;
- try {
- exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponent must be an integer.");
- }
-
- return new DefaultUnitPrefix(Math.pow(base, exponent));
- } else {
- if (!this.containsPrefixName(expression))
- throw new IllegalArgumentException("Unrecognized prefix name \"" + expression + "\".");
- return this.getPrefix(expression);
- }
- }
- }
-
- /**
- * Gets a unit from the database from its name, ignoring prefixes.
- *
- * @param name
- * unit's name
- * @return unit
- * @since 2019-01-10
- * @since v0.1.0
- */
- public Unit getPrefixlessUnit(final String name) {
- return this.units.get(name);
- }
-
- /**
- * 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) {
- if (name.contains("^")) {
- final String[] baseAndExponent = name.split("\\^");
-
- LinearUnit base;
- try {
- base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0]));
- } catch (final NumberFormatException e2) {
- final Unit unit = this.getUnit(baseAndExponent[0]);
- if (unit instanceof LinearUnit) {
- base = (LinearUnit) unit;
- } else if (unit instanceof BaseUnit) {
- base = ((BaseUnit) unit).asLinearUnit();
- } else
- throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
- }
-
- final int exponent;
- try {
- exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponent must be an integer.");
- }
-
- final LinearUnit exponentiated = base.toExponent(exponent);
- if (exponentiated.getConversionFactor() == 1)
- return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension());
- else
- return exponentiated;
- } else {
- for (final String prefixName : this.prefixNameSet()) {
- // check for a prefix
- if (name.startsWith(prefixName)) {
- // prefix found! Make sure what comes after it is actually a unit!
- final String prefixless = name.substring(prefixName.length());
- if (this.containsUnitName(prefixless)) {
- // yep, it's a proper prefix! Get the unit!
- final Unit unit = this.getUnit(prefixless);
- final UnitPrefix prefix = this.getPrefix(prefixName);
-
- // Prefixes only work with linear and base units, so make sure it's one of those
- if (unit instanceof LinearUnit) {
- final LinearUnit linearUnit = (LinearUnit) unit;
- return linearUnit.times(prefix.getMultiplier());
- } else if (unit instanceof BaseUnit) {
- final BaseUnit baseUnit = (BaseUnit) unit;
- return baseUnit.times(prefix.getMultiplier());
- }
- }
- }
- }
- 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 any argument is null
- * @since 2019-01-07
- * @since v0.1.0
- */
- public Unit getUnitFromExpression(final String expression) {
- Objects.requireNonNull(expression, "expression must not be null.");
-
- // parse the expression
- // start with an "empty" unit then apply operations on it
- LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY).asLinearUnit();
- boolean dividing = false;
-
- // if I'm just creating an alias, just create one instead of going through the parsing process
- if (!expression.contains(" ") && !expression.contains("*") && !expression.contains("/")
- && !expression.contains("(") && !expression.contains(")") && !expression.contains("^")) {
- try {
- return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(expression));
- } catch (final NumberFormatException e) {
- if (!this.containsUnitName(expression))
- throw new IllegalArgumentException("Unrecognized unit name \"" + expression + "\".");
- return this.getUnit(expression);
- }
- }
-
- // \\* means "asterisk", * is reserved
- for (final String part : expression.replaceAll("\\*", " \\* ").replaceAll("/", " / ").replaceAll(" \\^", "\\^")
- .replaceAll("\\^ ", "\\^").split(" ")) {
- if ("".equals(part)) {
- continue;
- }
- // "unit1 unit2" is the same as "unit1 * unit2", so multiplication signs do nothing
- if ("*".equals(part)) {
- continue;
- }
- // When I reach a division sign, don't parse a unit, instead tell myself I'm going to divide the next
- // thing
- if ("/".equals(part) || "per".equals(part)) {
- dividing = true;
- continue;
- }
-
- try {
- final double partAsNumber = Double.parseDouble(part); // if this works, it's a number
- // this code should not throw any exceptions, so I'm going to throw AssertionErrors if it does
- try {
- if (dividing) {
- unit = unit.dividedBy(partAsNumber);
- dividing = false;
- } else {
- unit = unit.times(partAsNumber);
- }
- } catch (final Exception e) {
- throw new AssertionError(e);
- }
- } catch (final NumberFormatException e) {
- // it's a unit, try that
-
- if (part.contains("(") && part.endsWith(")")) {
- // the unitsfile is looking for a nonlinear unit
- final String[] unitAndValue = part.split("\\(");
-
- // this will work because I've checked that it contains a (
- final String unitName = unitAndValue[0];
- final String valueStringWithBracket = unitAndValue[unitAndValue.length - 1];
- final String valueString = valueStringWithBracket.substring(0, valueStringWithBracket.length() - 1);
- final double value;
-
- // try to get the value - else throw an error
- try {
- value = Double.parseDouble(valueString);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Unparseable value " + valueString);
- }
-
- // get this unit in a linear form
- if (!this.containsPrefixlessUnitName(unitName))
- throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
- final Unit partUnit = this.getPrefixlessUnit(unitName);
- final LinearUnit multiplier = partUnit.getBase().times(partUnit.convertToBase(value));
-
- // finally, add it to the expression
- if (dividing) {
- unit = unit.dividedBy(multiplier);
- dividing = false;
- } else {
- unit = unit.times(multiplier);
- }
- } else {
- // check for exponientation
- if (part.contains("^")) {
- final String[] valueAndExponent = part.split("\\^");
- // this will always work because of the contains check
- final String valueString = valueAndExponent[0];
- final String exponentString = valueAndExponent[valueAndExponent.length - 1];
-
- LinearUnit value;
-
- // first, try to get the value
- try {
- final double valueAsNumber = Double.parseDouble(valueString);
-
- value = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(valueAsNumber);
- } catch (final NumberFormatException e2) {
-
- // look for a unit
- if (!this.containsUnitName(valueString))
- throw new IllegalArgumentException("Unrecognized unit name \"" + valueString + "\".");
- final Unit valueUnit = this.getUnit(valueString);
-
- // try to turn the value into a linear unit
- if (valueUnit instanceof LinearUnit) {
- value = (LinearUnit) valueUnit;
- } else if (valueUnit instanceof BaseUnit) {
- value = ((BaseUnit) valueUnit).asLinearUnit();
- } else
- throw new IllegalArgumentException("Only linear and base units can be exponientated.");
- }
-
- // now, try to get the exponent
- final int exponent;
- try {
- exponent = Integer.parseInt(exponentString);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponents must be integers.");
- }
-
- final LinearUnit exponientated = value.toExponent(exponent);
-
- if (dividing) {
- unit = unit.dividedBy(exponientated);
- dividing = false;
- } else {
- unit = unit.times(exponientated);
- }
- } else {
- // no exponent - look for a unit
- // the unitsfile is looking for a linear unit
- if (!this.containsUnitName(part))
- throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
- Unit other = this.getUnit(part);
- if (other instanceof BaseUnit) {
- other = ((BaseUnit) other).asLinearUnit();
- }
- if (other instanceof LinearUnit) {
- if (dividing) {
- unit = unit.dividedBy((LinearUnit) other);
- dividing = false;
- } else {
- unit = unit.times((LinearUnit) other);
- }
- } else
- throw new IllegalArgumentException(
- "Only linear or base units can be multiplied and divided. If you want to use a non-linear unit, try the format UNITNAME(VALUE).");
- }
- }
- }
- }
-
- // replace conversion-factor-1 linear units with base units
- // this improves the autogenerated names, allowing them to use derived SI names
- if (unit != null && unit.getConversionFactor() == 1)
- return unit.getSystem().getBaseUnit(unit.getDimension());
- else
- return unit;
- }
-
- /**
- * @return an immutable set of all of the unit names in this database, ignoring prefixes
- * @since 2019-01-14
- * @since v0.1.0
- */
- public Set<String> prefixlessUnitNameSet() {
- return Collections.unmodifiableSet(this.units.keySet());
- }
-
- /**
- * @return an immutable set of all of the prefix names in this database
- * @since 2019-01-14
- * @since v0.1.0
- */
- public Set<String> prefixNameSet() {
- return Collections.unmodifiableSet(this.prefixes.keySet());
- }
-}
diff --git a/src/unitConverter/converterGUI/UnitConverterGUI.java b/src/unitConverter/converterGUI/UnitConverterGUI.java
deleted file mode 100755
index b54e0da..0000000
--- a/src/unitConverter/converterGUI/UnitConverterGUI.java
+++ /dev/null
@@ -1,671 +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 unitConverter.converterGUI;
-
-import java.awt.BorderLayout;
-import java.awt.GridLayout;
-import java.io.File;
-import java.math.BigDecimal;
-import java.math.MathContext;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import java.util.function.Predicate;
-
-import javax.swing.BorderFactory;
-import javax.swing.JButton;
-import javax.swing.JFrame;
-import javax.swing.JList;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JScrollPane;
-import javax.swing.JSlider;
-import javax.swing.JTabbedPane;
-import javax.swing.JTextArea;
-import javax.swing.JTextField;
-import javax.swing.ListModel;
-import javax.swing.ListSelectionModel;
-
-import unitConverter.UnitsDatabase;
-import unitConverter.dimension.StandardDimensions;
-import unitConverter.dimension.UnitDimension;
-import unitConverter.unit.AbstractUnit;
-import unitConverter.unit.NonlinearUnits;
-import unitConverter.unit.SI;
-import unitConverter.unit.Unit;
-import unitConverter.unit.UnitPrefix;
-
-/**
- * @author Adrien Hopkins
- * @since 2018-12-27
- * @since v0.1.0
- */
-final class UnitConverterGUI {
- private static class Presenter {
- /** The presenter's associated view. */
- private final View view;
-
- /** The units known by the program. */
- private final UnitsDatabase units;
-
- /** The names of all of the units */
- private final List<String> unitNames;
-
- /** The names of all of the units, but filtered */
- private final DelegateListModel<String> unitNamesFiltered;
-
- /** The names of all of the prefixes */
- private final List<String> prefixNames;
-
- /** The names of all of the prefixes */
- private final DelegateListModel<String> prefixNamesFiltered;
-
- 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.units = new UnitsDatabase();
- this.units.addUnit("metre", SI.METRE);
- this.units.addUnit("kilogram", SI.KILOGRAM);
- this.units.addUnit("gram", SI.KILOGRAM.dividedBy(1000));
- this.units.addUnit("second", SI.SECOND);
- this.units.addUnit("ampere", SI.AMPERE);
- this.units.addUnit("kelvin", SI.KELVIN);
- this.units.addUnit("mole", SI.MOLE);
- this.units.addUnit("candela", SI.CANDELA);
- this.units.addUnit("bit", SI.SI.getBaseUnit(StandardDimensions.INFORMATION));
- this.units.addUnit("unit", SI.SI.getBaseUnit(UnitDimension.EMPTY));
- // nonlinear units - must be loaded manually
- this.units.addUnit("tempCelsius", NonlinearUnits.CELSIUS);
- this.units.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT);
-
- this.units.addAllFromFile(new File("unitsfile.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.units.containsPrefixName(o1))
- return -1;
- else if (!Presenter.this.units.containsPrefixName(o2))
- return 1;
-
- final UnitPrefix p1 = Presenter.this.units.getPrefix(o1);
- final UnitPrefix p2 = Presenter.this.units.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.units.prefixlessUnitNameSet());
- this.unitNames.sort(null); // sorts it using Comparable
-
- this.unitNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixlessUnitNameSet()));
- this.unitNamesFiltered.sort(null); // sorts it using Comparable
-
- this.prefixNames = new ArrayList<>(this.units.prefixNameSet());
- this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator
-
- this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixNameSet()));
- this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator
-
- System.out.printf("Successfully loaded %d units (%d base units)", AbstractUnit.getUnitCount(),
- AbstractUnit.getBaseUnitCount());
- }
-
- /**
- * 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 convert() {
- final String fromUnitString = this.view.getFromText();
- final String toUnitString = this.view.getToText();
-
- // try to parse from
- final Unit from;
- try {
- from = this.units.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 {
- to = this.units.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 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);
- }
- }
-
- this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString));
- }
-
- /**
- * Filters the filtered model for units
- *
- * @param filter
- * filter to use
- * @since 2019-01-15
- * @since v0.1.0
- */
- private final void filterFilteredPrefixModel(final Predicate<String> filter) {
- this.prefixNamesFiltered.clear();
- for (final String prefixName : this.prefixNames) {
- if (filter.test(prefixName)) {
- this.prefixNamesFiltered.add(prefixName);
- }
- }
- }
-
- /**
- * Filters the filtered model for units
- *
- * @param filter
- * filter to use
- * @since 2019-01-15
- * @since v0.1.0
- */
- private final void filterFilteredUnitModel(final Predicate<String> filter) {
- this.unitNamesFiltered.clear();
- for (final String unitName : this.unitNames) {
- if (filter.test(unitName)) {
- this.unitNamesFiltered.add(unitName);
- }
- }
- }
-
- /**
- * @return a list model of all of the unit keys
- * @since 2019-01-14
- * @since v0.1.0
- */
- public final ListModel<String> keyListModel() {
- return this.unitNamesFiltered;
- }
-
- /**
- * Runs whenever the prefix filter is changed.
- * <p>
- * Filters the prefix list then sorts it using a {@code FilterComparator}.
- * </p>
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void prefixFilterUpdated() {
- final String filter = this.view.getPrefixFilterText();
- if (filter.equals("")) {
- this.filterFilteredPrefixModel(t -> true);
- } else {
- this.filterFilteredPrefixModel(t -> t.contains(filter));
- }
- this.prefixNamesFiltered.sort(new FilterComparator(filter));
- }
-
- /**
- * @return a list model of all of the prefix names
- * @since 2019-01-15
- */
- public final ListModel<String> prefixNameListModel() {
- return this.prefixNamesFiltered;
- }
-
- /**
- * 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 int index = this.view.getPrefixListSelection();
- if (index == -1)
- return;
- else {
- final String prefixName = this.prefixNamesFiltered.get(index);
- final UnitPrefix prefix = this.units.getPrefix(prefixName);
-
- this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier()));
- }
- }
-
- /**
- * @param significantFigures
- * new value of significantFigures
- * @since 2019-01-15
- */
- public final void setSignificantFigures(final int significantFigures) {
- this.significantFigures = significantFigures;
- }
-
- /**
- * Runs whenever the unit filter is changed.
- * <p>
- * Filters the unit list then sorts it using a {@code FilterComparator}.
- * </p>
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void unitFilterUpdated() {
- final String filter = this.view.getUnitFilterText();
- if (filter.equals("")) {
- this.filterFilteredUnitModel(t -> true);
- } else {
- this.filterFilteredUnitModel(t -> t.contains(filter));
- }
- this.unitNamesFiltered.sort(new FilterComparator(filter));
- }
-
- /**
- * 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 void unitNameSelected() {
- final int index = this.view.getUnitListSelection();
- if (index == -1)
- return;
- else {
- final String unitName = this.unitNamesFiltered.get(index);
- final Unit unit = this.units.getUnit(unitName);
-
- this.view.setUnitTextBoxText(unit.toString());
- }
- }
- }
-
- private static class View {
- /** The view's frame. */
- private final JFrame frame;
- /** The view's associated presenter. */
- private final Presenter presenter;
-
- /** The list of unit names in the unit viewer */
- private final JList<String> unitNameList;
- /** The list of prefix names in the prefix viewer */
- private final JList<String> prefixNameList;
- /** The unit search box in the unit viewer */
- private final JTextField unitFilterEntry;
- /** The text box for unit data in the unit viewer */
- private final JTextArea unitTextBox;
- /** The prefix search box in the prefix viewer */
- private final JTextField prefixFilterEntry;
- /** The text box for prefix data in the prefix viewer */
- private final JTextArea prefixTextBox;
- /** 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;
-
- /**
- * 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 JList<>(this.presenter.keyListModel());
- this.prefixNameList = new JList<>(this.presenter.prefixNameListModel());
- this.unitFilterEntry = new JTextField();
- this.unitTextBox = new JTextArea();
- this.prefixFilterEntry = new JTextField();
- this.prefixTextBox = new JTextArea();
- this.fromEntry = new JTextField();
- this.toEntry = new JTextField();
- this.output = new JTextArea(2, 32);
-
- // create more components
- this.initComponents();
-
- this.frame.pack();
- }
-
- /**
- * @return text in "From" box in converter panel
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getFromText() {
- return this.fromEntry.getText();
- }
-
- /**
- * @return text in prefix filter
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getPrefixFilterText() {
- return this.prefixFilterEntry.getText();
- }
-
- /**
- * @return index of selected prefix
- * @since 2019-01-15
- * @since v0.1.0
- */
- public int getPrefixListSelection() {
- return this.prefixNameList.getSelectedIndex();
- }
-
- /**
- * @return text in "To" box in converter panel
- * @since 2019-01-26
- * @since v0.1.0
- */
- public String getToText() {
- return this.toEntry.getText();
- }
-
- /**
- * @return text in unit filter
- * @see javax.swing.text.JTextComponent#getText()
- */
- public String getUnitFilterText() {
- return this.unitFilterEntry.getText();
- }
-
- /**
- * @return index of selected unit
- * @since 2019-01-15
- * @since v0.1.0
- */
- public int getUnitListSelection() {
- return this.unitNameList.getSelectedIndex();
- }
-
- /**
- * 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);
-
- { // panel for unit conversion
- final JPanel convertPanel = new JPanel();
- masterPane.addTab("Convert Units", convertPanel);
-
- convertPanel.setLayout(new GridLayout(5, 1));
-
- { // panel for units to convert from
- final JPanel fromPanel = new JPanel();
- convertPanel.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();
- convertPanel.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!");
- convertPanel.add(convertButton);
-
- convertButton.addActionListener(e -> this.presenter.convert());
- }
-
- { // output of conversion
- final JPanel outputPanel = new JPanel();
- convertPanel.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();
- convertPanel.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());
-
- { // panel for listing and searching
- final JPanel listPanel = new JPanel();
- unitLookupPanel.add(listPanel);
-
- listPanel.setLayout(new BorderLayout());
-
- { // unit search box
- listPanel.add(this.unitFilterEntry, BorderLayout.PAGE_START);
- this.unitFilterEntry.addCaretListener(e -> this.presenter.unitFilterUpdated());
- }
-
- { // a list of units
- listPanel.add(new JScrollPane(this.unitNameList), BorderLayout.CENTER);
- this.unitNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
- this.unitNameList.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
- final JPanel prefixListPanel = new JPanel();
- prefixLookupPanel.add(prefixListPanel);
-
- prefixListPanel.setLayout(new BorderLayout());
-
- { // prefix search box
- prefixListPanel.add(this.prefixFilterEntry, BorderLayout.PAGE_START);
- this.prefixFilterEntry.addCaretListener(e -> this.presenter.prefixFilterUpdated());
- }
-
- { // a list of prefixes
- prefixListPanel.add(new JScrollPane(this.prefixNameList), BorderLayout.CENTER);
- this.prefixNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
- this.prefixNameList.addListSelectionListener(e -> {
- this.presenter.prefixSelected();
- });
- }
- }
-
- { // the text box for prefix's toString
- prefixLookupPanel.add(this.prefixTextBox);
- this.unitTextBox.setEditable(false);
- }
- }
- }
- }
-
- /**
- * 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 setOutputText(final String text) {
- this.output.setText(text);
- }
-
- /**
- * Sets the text of the prefix text box.
- *
- * @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.
- *
- * @param t
- * text to set
- * @see javax.swing.text.JTextComponent#setText(java.lang.String)
- */
- public void setUnitTextBoxText(final String t) {
- this.unitTextBox.setText(t);
- }
-
- /**
- * 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/unitConverter/unit/BaseUnit.java b/src/unitConverter/unit/BaseUnit.java
deleted file mode 100755
index fe36c45..0000000
--- a/src/unitConverter/unit/BaseUnit.java
+++ /dev/null
@@ -1,180 +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 unitConverter.unit;
-
-import java.util.Objects;
-
-import 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.
- *
- * @author Adrien Hopkins
- * @since 2018-12-23
- * @since v0.1.0
- */
-public final class BaseUnit extends AbstractUnit {
- /**
- * 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);
- this.isFullBase = dimension.isBase();
- }
-
- /**
- * @return this unit as a {@code LinearUnit}
- * @since 2019-01-25
- * @since v0.1.0
- */
- public LinearUnit asLinearUnit() {
- return this.times(1);
- }
-
- @Override
- public double convertFromBase(final double value) {
- return value;
- }
-
- @Override
- public double convertToBase(final double value) {
- return value;
- }
-
- /**
- * Divides this unit by another unit.
- *
- * @param other
- * unit to divide by
- * @return quotient of two units
- * @throws IllegalArgumentException
- * if this unit's system is not other's system
- * @throws NullPointerException
- * if other is null
- * @since 2018-12-22
- * @since v0.1.0
- */
- public BaseUnit dividedBy(final BaseUnit other) {
- Objects.requireNonNull(other, "other must not be null.");
- if (!this.getSystem().equals(other.getSystem()))
- throw new IllegalArgumentException("Incompatible base units for division.");
- return new BaseUnit(this.getDimension().dividedBy(other.getDimension()), this.getSystem());
- }
-
- /**
- * Divides this unit by a divisor
- *
- * @param divisor
- * amount to divide by
- * @return quotient
- * @since 2018-12-23
- * @since v0.1.0
- */
- public LinearUnit dividedBy(final double divisor) {
- return new LinearUnit(this, 1 / divisor);
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (!(obj instanceof BaseUnit))
- return false;
- final BaseUnit other = (BaseUnit) obj;
- return Objects.equals(this.getSystem(), other.getSystem())
- && Objects.equals(this.getDimension(), other.getDimension());
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = result * prime + this.getSystem().hashCode();
- result = result * prime + this.getDimension().hashCode();
- return result;
- }
-
- /**
- * Multiplies this unit by another unit.
- *
- * @param other
- * unit to multiply by
- * @return product of two units
- * @throws IllegalArgumentException
- * if this unit's system is not other's system
- * @throws NullPointerException
- * if other is null
- * @since 2018-12-22
- * @since v0.1.0
- */
- public BaseUnit times(final BaseUnit other) {
- Objects.requireNonNull(other, "other must not be null.");
- if (!this.getSystem().equals(other.getSystem()))
- throw new IllegalArgumentException("Incompatible base units for multiplication.");
- return new BaseUnit(this.getDimension().times(other.getDimension()), this.getSystem());
- }
-
- /**
- * Multiplies this unit by a multiplier.
- *
- * @param multiplier
- * amount to multiply by
- * @return product
- * @since 2018-12-23
- * @since v0.1.0
- */
- public LinearUnit times(final double multiplier) {
- return new LinearUnit(this, multiplier);
- }
-
- /**
- * Returns this unit, but to an exponent.
- *
- * @param exponent
- * exponent
- * @return result of exponentiation
- * @since 2019-01-15
- * @since v0.1.0
- */
- 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/unitConverter/unit/LinearUnit.java b/src/unitConverter/unit/LinearUnit.java
deleted file mode 100644
index b786b3b..0000000
--- a/src/unitConverter/unit/LinearUnit.java
+++ /dev/null
@@ -1,184 +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 unitConverter.unit;
-
-import java.util.Objects;
-
-import unitConverter.dimension.UnitDimension;
-
-/**
- * A unit that is equal to a certain number multiplied by its base.
- *
- * @author Adrien Hopkins
- * @since 2018-12-22
- * @since v0.1.0
- */
-public final 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);
- }
-
- /**
- * Divides this unit by another unit.
- *
- * @param other
- * unit to divide by
- * @return quotient of two units
- * @throws NullPointerException
- * if other is null
- * @since 2018-12-22
- * @since v0.1.0
- */
- public LinearUnit dividedBy(final LinearUnit other) {
- Objects.requireNonNull(other, "other must not be null");
- final BaseUnit base = this.getBase().dividedBy(other.getBase());
- return new LinearUnit(base, this.getConversionFactor() / other.getConversionFactor());
- }
-
- @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())
- && Objects.equals(this.getConversionFactor(), other.getConversionFactor());
- }
-
- /**
- * @return conversionFactor
- * @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.getBase().hashCode();
- result = result * prime + Double.hashCode(this.getConversionFactor());
- return result;
- }
-
- /**
- * 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);
- }
-
- /**
- * Multiplies this unit by another unit.
- *
- * @param other
- * unit to multiply by=
- * @return product of two units
- * @throws NullPointerException
- * if other is null
- * @since 2018-12-22
- * @since v0.1.0
- */
- public LinearUnit times(final LinearUnit other) {
- Objects.requireNonNull(other, "other must not be null");
- final BaseUnit base = this.getBase().times(other.getBase());
- return new LinearUnit(base, this.getConversionFactor() * other.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());
- }
-}
diff --git a/src/unitConverter/unit/UnitTest.java b/src/unitConverter/unit/UnitTest.java
deleted file mode 100755
index c3237eb..0000000
--- a/src/unitConverter/unit/UnitTest.java
+++ /dev/null
@@ -1,47 +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 unitConverter.unit;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
-import unitConverter.dimension.StandardDimensions;
-
-/**
- * Testing the various Unit classes
- *
- * @author Adrien Hopkins
- * @since 2018-12-22
- */
-class UnitTest {
- @Test
- void testConversion() {
- final BaseUnit metre = SI.METRE;
- final Unit inch = metre.times(0.0254);
-
- assertEquals(1.9, inch.convertToBase(75), 0.01);
- }
-
- @Test
- void testEquals() {
- final BaseUnit metre = SI.METRE;
- final Unit meter = SI.SI.getBaseUnit(StandardDimensions.LENGTH);
-
- assertEquals(metre, meter);
- }
-}
diff --git a/unitsfile.txt b/unitsfile.txt
index 78e51f7..bda9b81 100755
--- a/unitsfile.txt
+++ b/unitsfile.txt
@@ -55,6 +55,7 @@ atto- 1e-18
zepto- 1e-21
yocto- 1e-24
+da- deca
D- deca
h- hecto
H- hecto
@@ -123,6 +124,9 @@ T tesla
hertz s^-1
Hz hertz
+gram millikg
+g gram
+
# Angle units and constants
# Tau is the circle constant, equal to a circle's diameter divided by its radius
@@ -134,9 +138,9 @@ radian m / m
rad radian
steradian m^2 / m^2
sr steradian
-degree 360 / tau * radian
+degree tau / 360 radian
deg degree
-° degree
+° degree
# Nonlinear units, which are not supported by the file reader and must be defined manually
# Use tempC(100) for 100 degrees Celsius
@@ -171,7 +175,7 @@ arcsecond 1 / 60 arcminute
arcsec arcsecond
# constants
-waterdensity 1 kilogram / litre
+waterdensity kilogram / litre
# Imperial length units
foot 0.3048 m
@@ -183,6 +187,10 @@ yd yard
mile 1760 yard
mi mile
+# Compressed notation
+kph km / hour
+mph mile / hour
+
# Imperial weight units
pound 0.45359237 kg
lb pound
@@ -231,4 +239,26 @@ metricpint 2 metriccup
pint metricpint
metricquart 2 metricpint
quart metricquart
-metricgallon 4 metricquart \ No newline at end of file
+metricgallon 4 metricquart
+
+# Energy units
+calorie 4.18 J
+cal calorie
+Calorie kilocalorie
+Cal Calorie
+Wh W h
+
+# Extra units to only include in the dimension-based converter
+km km
+cm cm
+mm mm
+mg mg
+mL mL
+ml ml
+kJ kJ
+MJ MJ
+kWh kWh
+m/s m / s
+km/h km / h
+ft/s foot / s
+mi/h mile / hour \ No newline at end of file