1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
#+TITLE: 7Units Design Document
#+SUBTITLE: For version 0.5.0-alpha.2
#+DATE: 2022 July 8
#+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry}
#+LaTeX_HEADER: \usepackage{xurl}
#+LaTeX: \newpage
* Introduction
7Units is a program that can convert between units. This document details the internal design of 7Units, intended to be used by current and future developers.
* System Overview
#+CAPTION: A big-picture diagram of 7Units, containing all of the major classes.
#+attr_latex: :height 144px
[[./diagrams/overview-diagram.plantuml.png]]
** Packages of 7Units
7Units splits its code into three main packages:
- ~sevenUnits.unit~ :: The [[*Unit System Design][unit system]]
- ~sevenUnits.utils~ :: Extra classes that aid the unit system.
- ~sevenUnitsGUI~ :: The [[*Front-End Design][front end]] code
~sevenUnits.unit~ depends on ~sevenUnits.utils~, while ~sevenUnitsGUI~ depends on both ~sevenUnits~ packages. There is only one class that isn't in any of these packages, ~sevenUnits.VersionInfo~.
** Major Classes of 7Units
- [[*Unit Classes][sevenUnits.unit.Unit]] :: The class representing a unit
- [[*The Unit Database][sevenUnits.unit.UnitDatabase]] :: A class that stores collections of units, prefixes and dimensions.
- [[*The View][sevenUnitsGUI.View]] :: The class that handles interaction between the user and the program.
- [[*The Presenter][sevenUnitsGUI.Presenter]] :: The class that handles communication between the ~View~ and the unit system.
#+LaTeX: \newpage
** Process of Unit Conversion
#+CAPTION: The process of converting units
[[./diagrams/convert-units.plantuml.png]]
1. The ~View~ triggers a unit conversion method from the ~Presenter~.
2. The ~Presenter~ gets raw input data from the ~View~'s public methods (from unit, to unit, from value).
3. The ~Presenter~ uses the ~UnitDatabase~'s methods to convert the raw data from the ~View~ into actual units.
4. The ~Presenter~ performs the unit conversion using the provided unit objects.
5. The ~Presenter~ calls the ~View~'s methods to show the result of the conversion.
#+LaTeX: \newpage
** Process of Expression Conversion
The process of expression conversion is similar to that of unit conversion.
#+CAPTION: The process of converting expressions
[[./diagrams/convert-expressions.plantuml.png]]
1. The ~View~ triggers a unit conversion method from the ~Presenter~.
2. The ~Presenter~ gets raw input data from the ~View~'s public methods (unit conversion: from unit, to unit, from value; expression conversion: input expression, output expression).
3. The ~Presenter~ uses the ~UnitDatabase~'s methods to parse the expressions, converting the input into a ~LinearUnitValue~ and the output into a ~Unit~.
4. The ~Presenter~ converts the provided value into the provided unit.
5. The ~Presenter~ calls the ~View~'s methods to show the result of the conversion.
#+LaTeX: \newpage
* Unit System Design
Any code related to the backend unit system is stored in the ~sevenUnits.unit~ package.
Here is a class diagram of the system. Unimportant methods, methods inherited from Object, getters and setters have been omitted.
#+CAPTION: Class diagram of sevenUnits.unit
[[./diagrams/units-class-diagram.plantuml.png]]
#+LaTeX: \newpage
** Dimensions
Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an [[*ObjectProduct][ObjectProduct]]<BaseDimension>, where ~BaseDimension~ is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions.
** Unit Classes
Units are internally represented by the abstract class ~Unit~. All units have an [[*ObjectProduct][ObjectProduct]]<BaseUnit> (referred to as the base) that they are based on, a dimension (ObjectProduct<BaseDimension>), one or more names and a symbol (these last two bits of data are contained in the ~NameSymbol~ class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make ~BaseUnit~ instances, and a protected one used to make general units (for other subclasses of ~Unit~). All unit classes are immutable.
Units also have two conversion functions - one which converts from a value expressed in this unit to its base unit, and another which converts from a value expressed in the base unit to this unit. In ~Unit~, they are defined as two abstract methods. This allows you to convert from any unit to any other (as long as they have the same base, i.e. you aren't converting metres to pounds). To convert from A to B, first convert from A to its base, then convert from the base to B.
~BaseUnit~ represents a unit that all other units are defined by. All of the units used by this system are defined by seven SI ~BaseUnit~ instances (metre, second, kilogram, ampere, kelvin, mole, candela; this is what 7Units is named after) and two non-SI ~BaseUnit~ instances (US dollar and bit). Because base units are themselves units (and should be able to be used as units), ~BaseUnit~ is a subclass of ~Unit~, using its own package-private constructor.
However, most units are instances of ~LinearUnit~, another subclass of ~Unit~. ~LinearUnit~ represents a unit that is /a product of a base unit and a constant called the *conversion factor*/. Most units you've ever used fall under this definition, the only common exceptions are degrees Celsius and Fahrenheit. This simplicity allows the ~LinearUnit~ to do many things:
- It can implement conversion to and from the base as multiplying and dividing by the conversion factor respectively
- You can easily create new units by multiplying or dividing a ~LinearUnit~ by a number (for example, kilometre = metre * 1000). This can be easily implemented as multiplying this unit's conversion factor by the multiplier and returning a new ~LinearUnit~ with that conversion factor factor.
- You can add or subtract two ~LinearUnit~ instances to create a third (as long as they have the same base) by adding or subtracting the conversion factor.
- You can multiply or divide any two ~LinearUnit~ instances to create a third by multiplying or dividing the bases and conversion factors.
- Note that any operations will return a unit without name(s) or a symbol. All unit classes have a ~withName~ method that returns a copy of them with different names and/or a different symbol (all of this info is contained in the ~NameSymbol~ class)
There are a few more classes which play small roles in the unit system:
- Unitlike :: A class that is like a unit, but its "value" can be any class. The only use of this class right now is to implement ~MultiUnit~, a combination of units (like "foot + inch", commonly used in North America for measuring height); its "value" is a list of numbers.
- FunctionalUnit :: A convenience class that implements the two conversion functions of ~Unit~ using ~DoubleUnaryOperator~ instances. This is used internally to implement degrees Celsius and Fahrenheit. There is also a version of this for ~Unitlike~, ~FunctionalUnitlike~.
- UnitValue :: A value expressed as a certain unit (such as "7 inches"). This class is used by the simple unit converter to represent units. You can convert them between units. There are also versions of this for ~LinearUnit~ and ~Unitlike~.
- Metric :: A static utility class with instances of all of the SI named units, the 9 base dimensions, SI prefixes, some common prefixed units like the kilometre, and a few non-SI units used commonly with them.
- BritishImperial :: A static utility class with instances of common units in the British Imperial system (not to be confused with the US Customary system, which is also called "Imperial"; it has the same unit names but the values of a few units are different). This class and the US Customary is divided into static classes for each dimension, such as ~BritishImperial.Length~.
- USCustomary :: A static utility class with instances of common units in the US Customary system (not to be confused with the British Imperial system; it has the same unit names but the values of a few units are different).
** Prefixes
A ~UnitPrefix~ is a simple object that can multiply a ~LinearUnit~ by a value. It can calculate a new name for the unit by combining its name and the unit's name (symbols are done similarly). It can do multiplication, division and exponentation with a number, as well as multiplication and division with another prefix; all of these work by changing the prefix's multiplier.
** The Unit Database
The ~UnitDatabase~ class stores all of the unit, prefix and dimension data used by this program. It is not a representation of an actual database, just a class that stores lots of data.
Units are stored using a custom ~Map~ implementation (~PrefixedUnitMap~) which maps unit names to units. It is backed by two maps: one for units (without prefixes) and one for prefixes. It is programmed to include prefixes (so if units includes "metre" and prefixes includes "kilo", this map will include "kilometre", mapping it to a unit representing a kilometre). It is immutable, but you can modify the underlying maps, which is reflected in the ~PrefixedUnitMap~. Other than that, it is a normal map implementation.
Prefixes and dimensions are stored in normal maps.
*** Parsing Expressions
Each ~UnitDatabase~ instance has four [[*ExpressionParser][ExpressionParser]] instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (~LinearUnit~, ~LinearUnitValue~, ~UnitPrefix~, ~ObjectProduct<BaseDimension>~). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating.
*** Parsing Files
There are two types of data files: unit and dimension.
Unit files contain data about units and prefixes. Each line contains the name of a unit or prefix (prefixes end in a dash, units don't) followed by an expression which defines it, separated by one or more space characters (this behaviour is defined by the static regular expression ~NAME_EXPRESSION~). Unit files are parsed line by line, each line being run through the ~addUnitOrPrefixFromLine~ method, which splits a line into name and expression, determines whether it's a unit or a prefix, and parses the expression. Because all units are defined by others, base units need to be defined with a special expression "!"; *these units should be added to the database before parsing the file*.
Dimension files are similar, only for dimensions instead of units and prefixes.
#+LaTeX: \newpage
* Front-End Design
The front-end of 7Units is based on an MVP model. There are two major frontend classes, the *View* and the *Presenter*.
** The View
The ~View~ is the part of the frontend code that directly interacts with the user. It handles input and output, but does not do any processing. Processing is handled by the presenter and the backend code.
The ~View~ is an interface, not a single class, so that I can easily create multiple views without having to rewrite any processing code. This allows me to easily prototype changes to the GUI without messing with existing code.
In addition, I have decided to move some functions of the ~View~ into two subinterfaces, ~UnitConversionView~ and ~ExpressionConversionView~. This is because 7Units supports two kinds of unit conversion: unit conversion (select two compatible units, specify a value then convert) and expression conversion (enter two expressions and convert the first to a multiple of the second). Putting these functions into subinterfaces allows a ~View~ to do one type of conversion without forcing it to support the other.
There are currently two implementations of the ~View~:
- TabbedView :: A Swing GUI implementation that uses tabs to separate the two types of conversion. The default GUI used by 7Units.
- ViewBot :: A simulated view that allows programs to set the output of its public methods (i.e. every getter in ~View~ has a setter in ~ViewBot~). Intended for testing, and is already used by ~PresenterTest~.
Both of these ~View~ implementations implement ~UnitConversionView~ and ~ExpressionConversionView~.
** The Presenter
The ~Presenter~ is an intermediary between the ~View~ and the backend code. It accepts the user's input and passes it to the backend, then accepts the backend's output and passes it to the frontend for user viewing. Its main functions do not have arguments or return values; instead it takes input from and provides output to the ~View~ via its public methods.
*** Rules
The ~Presenter~ has a set of function-object rules that determine some of its behaviours. Each corresponds to a setting in the ~View~, but they can be set to other values via the ~Presenter~'s setters (although nonstandard rules cannot be saved and loaded):
- numberDisplayRule :: A function that determines how numbers are displayed. This controls the rounding rules.
- prefixRepetitionRule :: A function that determines which combinations of prefixes are valid in unit expressions. This controls the unit prefix rules.
- searchRule :: A function that determines which prefixes are shown in the unit lists in unit conversion (which prefixed units are searchable).
These rules have been made this way to enable an incredible level of customization of these behaviours. Because any function object with the correct arguments and return type is accepted, these rules (especially the number display rule) can do much more than the default behaviours.
** Utility Classes
The frontend has many miscellaneous utility classes. Many of them are package-private. Here is a list of them, with a brief description of what they do and where they are used:
- DefaultPrefixRepetitionRule :: An enum containing the available rules determining when you can repeat prefixes. Used by the ~TabbedView~ for selecting the rule and by the ~Presenter~ for loading it from a file.
- DelegateListModel :: A ~javax.swing.ListModel~ implementation that delegates all of its methods to a ~List~. Implements ~List~ by also delegating to the underlying list. Used by the ~SearchBoxList~ to create an easily mutable ~ListModel~.
- FilterComparator :: A ~Comparator~ that sorts objects according to whether or not they match a filter. Used by the ~SearchBoxList~ for item sorting.
- GridBagBuilder :: A convenience class for generating ~GridBagConstraints~ objects for Swing's ~GridBagLayout~. Used by ~TabbedView~ for constructing the GUI.
- SearchBoxList :: A Swing component that combines a text field and a ~JList~ to create a searchable list. Used by the ~TabbedView~'s unit conversion mode.
- StandardDisplayRules :: A static utility class that allows you to generate display/rounding rules. Used by ~TabbedView~ for generating these rules and ~Presenter~ for loading them from a file.
- UnitConversionRecord :: A record-like object that contains the results of a unit or expression conversion. Used by ~UnitConversionView~ and ~ExpressionConversionView~ for accepting the results to be displayed.
#+LaTeX: \newpage
* Utility Classes
7Units has a few general "utility" classes. They aren't directly related to units, but are used in the units system.
** ObjectProduct
An ~ObjectProduct~ represents a "product" of elements of some type. The units system uses them to represent coherent units as a product of base units, and dimensions as a product of base dimensions.
Internally, it is represented using a map mapping objects to their exponents in the product. For example, the unit "kg m^2 / s^2" (i.e. a Joule) would be represented with a map like ~[kg: 1, m: 2, s: -2]~.
** ExpressionParser
The ~ExpressionParser~ class is used to parse the unit, prefix and dimension expressions that are used throughout 7Units. An expression is something like "(2 m + 30 J / N) * 8 s)". Each instance represents a type of expression, containing a way to obtain values (such as numbers or units) from the text and operations that can be done on these values (such as addition, subtraction or multiplication). Each operation also has a priority, which controls the order of operations (i.e. multiplication gets a higher priority than addition).
~ExpressionParser~ has a parameterized type ~T~, which represents the type of the value used in the expression. The expression parser currently only supports one type of value per expression; in the expressions used by 7Units numbers are treated as a kind of unit or prefix. Operators are represented by internal types; the system distinguishes between unary operators (those that take a single value, like negation) and binary operators (those that take 2 values, like +, -, * or /).
There is one exception to this rule - users are allowed to create "numeric operators" that take one argument of type ~T~ and one numeric argument. This is intended for exponentation, but could also be used for vector scaling.
Expressions are parsed in 2 steps:
1. Convert the expression to [[https://en.wikipedia.org/wiki/Reverse_Polish_notation][Reverse Polish Notation]], where operators come *after* the values they operate on, and brackets and the order of operations are not necessary. For example, "2 + 5" becomes "~2 5 +~", "(1 + 2) * 3" becomes "~1 2 + 3 *~" and the example expression earlier becomes "~2 m * 30 J * N / + 8 s * *~". This makes it simple to evaluate - early calculators used RPN for a good reason!
2. Evaluate the RPN expression. This can be done simply with a for loop and a stack. For each token in the expression, the progam does the following:
- if it is a number or unit, add it to the stack.
(the dimension parser adds numbers to a separate stack for type safety, as numbers cannot be stored as a dimension type like they can with units.)
- if it is a unary operator, take one value from the stack, apply the operator to it, and put the result into the stack.
- if it is a binary operator, take two values from the stack, apply the operator to them, and put the result into the stack.
- if it is a numeric operator, take one value from the stack and one number from the numeric stack, apply to the operator to the two, and put the result in the regular stack.
After evaluating the last token, there should be one value left in the stack - the answer. If there isn't, the original expression was malformed.
** Math Classes
There are two simple math classes in 7Units:
- ~UncertainDouble~ :: Like a ~double~, but with an uncertainty (e.g. \(2.0 \pm 0.4\)). The operations are like those of the regular Double, only they also calculate the uncertainty of the final value. They also have "exact" versions to help interoperation between ~double~ and ~UncertainDouble~. It is used by the converter's Scientific Precision setting.
- ~DecimalComparison~ :: A static utility class that contains a few alternate equals() methods for ~double~ and ~UncertainDouble~. These methods allow a slight (configurable) difference between values to still be considered equal, to fight roundoff error.
** Collection Classes
The ~ConditionalExistenceCollections~ class contains wrapper implementations of ~Collection~, ~Iterator~, ~Map~ and ~Set~. These implementations ignore elements that do not pass a certain condition - if an element fails the condition, ~contains~ will return false, the iterator will skip past it, it won't be counted in ~size~, etc. even if it exists in the original collection. Effectively, any element of the original collection that fails the test does not exist.
|