The purpose of Units implementation in vCalc is to allow equation authors to designate certain variables to be associated with units. This is intended to allow users of this to select their desired input units from a list of compatible units, while the equation remains operable for these new units. Similarly, equations are intended to have the option to output results in units, and these units will be displayed. Again, users can override the display of the output result by choosing compatible units, at which point the system automatically converts.
Current Unit Set
vCalc currently handles 614 units across 65 unit categories as follows:
- Acceleration [m/s², ft/s², grav]
- Amount Of Substance [mol, atom, osmole]
- Angle [rad, centiradian, °, grade, ', rev, "]
- Angular Acceleration [rad/s², rad/h², rad/min², °/h², °/min², °/s²]
- Angular Velocity [rad/s, rad/day, rad/h, rad/min, °/day, °/h, °/min, °/s, rev/s, rpm, rev/h, rev/day]
- Area [m², µm², mm², cm², km², mil², in², ft², yd², mi², acre, a, ha]
- Catalytic Activity [kat]
- Concentration [mol/m³, mol/L]
- Data Amount [bit, Mb, byte, KB, MB, GB, TB, PB]
- Data Rate [bit/s, bit/min, bit/h, bit/day, Mb/s, Mb/min, Mb/h, Mb/day, byte/s, byte/min, byte/h, byte/day, KB/s, KB/min, KB/h, KB/day, MB/s, MB/min, MB/h, MB/day, GB/s, GB/min, GB/h, GB/day, TB/s, TB/min, TB/h, TB/day, PB/s, PB/min, PB/h, PB/day]
- Dimensionless [real, %, dB]
- Duration [s, ns, µs, ms, min, h, day, week, month, year, decade, century, day_sidereal, year_calendar, year_sidereal]
- Dynamic Viscosity [kg/(m·s), mPa·s, g/(cm·s)]
- Electric Capacitance [F, pF, µF, mF, kF]
- Electric Charge [C, kC, mC, µC, pC, Fd, e, Fr, mAh, Ah]
- Electric Conductance [S]
- Electric Current [A, mA, μA, nA, Gi]
- Electric Field [N/C, kgf/C, lbf/C, V/m, V/ft, V/in]
- Electric Inductance [H, mH, nH]
- Electric Potential [V, mV]
- Electric Resistance [Ω, mΩ, kΩ]
- Energy [J, ft_lbf, mJ, W_s, N_m, kWh, MWh, GWh, TWh, feV, neV, μeV, meV, eV, keV, MeV, peV, hartree, erg, cal, kcal, Mcal, BTU]
- Enthalpy [J/mol, kJ/mol, BTU/mol, eV/mol, erg/mol]
- Fluid Resistance [Pa·s/L, Pa·min/L, Pa·s/mL, Pa·min/mL, Pa·s/m³, Pa·min/m³, Pa·s/cm³, Pa·min/cm³, mmHg·s/L, mmHg·min/L, mmHg·s/mL, mmHg·min/mL, mmHg·s/cm³, mmHg·min/cm³]
- Force [N, dyn, kgf, lbf, tonne_force, short_ton_force, long_ton_force, kip]
- Frequency [Hz, kHz, MHz, GHz, THz, PHz, EHz, perSec, perMin, perHr, perDay, perWk, perYr]
- Illuminance [lx, La, W/cm², W/m²]
- Kinematic Viscosity [cm²/s]
- Length [m, nm, µm, mm, cm, km, in, ft, yd, mi, nmi, Å, mil, pixel, pt, foot_survey_us, fathom, au, light_second, light_minute, light_hour, light_day, ly, kly, pc, Mpc]
- Length Per Volume [mpg, km/L]
- Luminous Flux [lm]
- Luminous Intensity [cd]
- Magnetic Field Strength [A/m]
- Magnetic Flux [Wb, Mx]
- Magnetic Flux Density [T, G]
- Mass [kg, mg, g, oz, lb, ton_us, t, ton_uk, me, u, gr, ct, dwt, troy_ounce, Earth_Mass, Jupiter_Mass, Solar_Mass]
- Mass Flow Rate [kg/s, kg/min, kg/h, g/s, lb/s, lb/min, lb/h]
- Molar Mass [g/mol, kg/mol, kg/kmol, mg/mol, oz/mol, lb/mol]
- Moment of Inertia [kg·m², g·m², lb·ft², oz·in²]
- Momentum [kg·m/s]
- Money [USD, AUD, BRL, CAD, CHF, CNY, EUR, GBP, INR, JPY, MXN, RUB, ZAR]
- Money Per Area [USD/in², USD/ft², USD/yd², USD/mi², EUR/cm², EUR/m², EUR/km², GBP/cm², GBP/m², GBP/km², JPY/cm², JPY/m², JPY/km², INR/cm², INR/m², INR/km², CNY/cm², CNY/m², CNY/km², RUB/cm², RUB/m², RUB/km²]
- Money Per Energy [USD/J, USD/kWh, USD/MWh, USD/GWh, USD/TWh, EUR/kWh, EUR/MWh, EUR/GWh, EUR/TWh, INR/kWh, INR/MWh, INR/GWh, INR/TWh, RUB/kWh, RUB/MWh, RUB/GWh, RUB/TWh]
- Money Per Length [USD/in, USD/ft, USD/mi, USD/nmi, EUR/cm, EUR/m, EUR/km, GBP/cm, GBP/m, GBP/km, JPY/cm, JPY/m, JPY/km, INR/cm, INR/m, INR/km, CNY/cm, CNY/m, CNY/km, RUB/cm, RUB/m, RUB/km]
- Money Per Mass [USD/g, USD/oz, USD/troy_ounce, USD/lb, USD/kg, USD/ton_us, EUR/g, EUR/kg, EUR/t, GBP/g, GBP/kg, GBP/t, CAD/g, CAD/kg, JPY/g, JPY/kg, JPY/t, INR/g, INR/kg, INR/t, CNY/g, CNY/kg, CNY/t, RUB/g, RUB/kg, RUB/t]
- Money Per Time [USD/s, USD/min, USD/h, USD/day, USD/week, USD/month, USD/year, CAD/s, CAD/min, CAD/h, CAD/day, EUR/s, EUR/min, EUR/h, EUR/day, GBP/s, GBP/min, GBP/h, GBP/day, MXN/s, MXN/min, MXN/h, MXN/day, JPY/s, JPY/min, JPY/h, JPY/day, INR/s, INR/min, INR/h, INR/day, CNY/s, CNY/min, CNY/h, CNY/day, RUB/s, RUB/min, RUB/h, RUB/day]
- Money Per Volume [USD/oz_liquid, USD/qt, USD/gal, USD/ft³, USD/yd³, USD/bbl, CAD/cL, CAD/L, CAD/m³, CAD/bbl, EUR/cL, EUR/L, EUR/m³, EUR/bbl, GBP/cL, GBP/L, GBP/m³, GBP/bbl, MXN/cL, MXN/L, MXN/m³, MXN/bbl, JPY/cL, JPY/L, JPY/m³, JPY/bbl, INR/cL, INR/L, INR/m³, INR/bbl, CNY/cL, CNY/L, CNY/m³, CNY/bbl, RUB/cL, RUB/L, RUB/m³, RUB/bbl]
- Permeability [d, md, µd]
- Power [W, mW, kW, MW, GW, TW, BTU/h, hp]
- Pressure [Pa, mPa, kPa, MPa, mmHg, inHg, mbar, bar, atm, N/mm², N/cm², N/m², psi, Torr]
- Radiation Dose Absorbed [Sv, Gy, rd, rem]
- Radiation Dose Effective [Sv, Gy, rd, rem]
- Radioactive Activity [Bq, Ci]
- Solid Angle [sr, sphere]
- Stress [N/m², N/cm², N/mm², kN/cm², kN/mm², psi_stress, lbf/ft², short_ton_force/in², short_ton_force/ft², long_ton_force/in², long_ton_force/ft²]
- Surface Energy [N/m, lbf/in, lbf/ft, kip/ft]
- Temperature [K, ℃, °F, °R]
- Torque [J, ft_lbf, mJ, W_s, N_m, kWh, MWh, GWh, TWh, feV, neV, μeV, meV, eV, keV, MeV, peV, hartree, erg, cal, kcal, Mcal, BTU]
- Velocity [m/s, m/min, mm/s, cm/s, km/s, km/min, km/h, mph, in/s, in/min, in/h, ft/s, ft/min, ft/h, kn, Mach, c]
- Volume [m³, mm³, cm³, mL, daL, L, km³, in³, ft³, yd³, mi³, ac⋅ft, oz_liquid_uk, oz_liquid, gal, gallon_dry_us, gallon_uk, bu, smidgen, pinch, dash, tsp, tbsp, cup, pint, qt, drop, bbl]
- Volumetric Density [g/mL, g/daL, kg/L, kg/m³, lb/ft³, g/cm³, g/m³, t/m³, ton_us/yd³, oz/in³]
- Volumetric Flow Rate [L/s, L/min, L/h, L/day, mL/s, mL/min, mL/h, qt/s, qt/min, qt/h, gal/s, gal/min, gal/h, drop/min, bbl/min, bbl/h, bbl/day, ft³/min, ft³/h, ft³/day, m³/s, m³/min, m³/h, m³/day]
Implementation
The Units implementation provides
- additional groovy libraries and shortcuts for dealing with units
- UI features to allow equation authors to designate variables as "Amounts"
- UI features to allow users to input variable substitutions with acceptable units
- UI features to allow users to view and convert calculator result units
Designating Variables as Units
From the equation editor "Algorithm Tab", variables now have an additional column "Units". If the variable datatype is "Decimal," then the Unit selection menu becomes enabled. Leaving the unit selection as "None" leaves the variable as a regular Decimal datatype. However, choosing any other item from the menu causes the variable to become an Amount.
The menu organizes units by their Quantity Type. Choosing one of the items prefixed by "Any", e.g. "Any Duration," "Any Length" will cause the variable to accept any Amount of such Quantity Type without conversion. If instead, a specific Unit is chosen, then the Amount will automatically be converted into that unit before the equation receives it.
'''NB''' Choosing a specific unit as opposed to a "Any ..." unit has no effect on whether or not a user may choose compatible input units. In all cases, the calculator-user may override input unit with any compatible unit. This option primarily affects whether the substitution value is pre-converted for the equation. One other minor affect is in the display of the markup. If a specific unit has been designated in the formula tab, the substitution of user input values in the VAL calculator mode will be converted to those designated in the formula tab before being displayed. Otherwise, the displayed values are in the units entered by the user.
Inputting Units in Variable Substitution
When the calculator-user adds an equation into the pallet the user is prompted for variables via the right-hand grid as before. However, variables with units will have an additional text box to choose the input units. If the equation author has specified a specific unit to be used, then this is shown as the default. If the equation author has specified "Any" unit of a given dimension, then the vCalc standard unit (usually SI unit) will be the initial default. In either case, the user can override the input unit from a dropdown box.
Calculator Result Units
When a final calculator result has units information, then the unit description is displayed to the right of the numeric result. If compatible units are found, then the unit description becomes a drop down box from which the user may choose an override. If a user selects a compatible unit from the drop down box then the output is converted into that unit.
If the displayed unit is a ''Product Unit'' (i.e. a result of multiplication or division of other units) then the output is displayed in those terms '''unless''' the product unit is found to be equivalent to a named unit, in which case the label for that unit is shown.
Example: Returning 3 Joules divided by 3 cubic meters (e.g. 3.J/3.to("m^3"))) will return 1 Pascal since J/m^3^ = (kg * m^2^)/(s^2^ * m^3^) = kg/(s^2^ * m) = N/m^2^ = Pa
Note: this simplification to known units only occurs on the final display on the calculator. When using referencing equations in equations, the output from the referenced equation is taken as-is such that the groovy author may handle it as desired.
Groovy
Basic unit support within Groovy is provided by the JScience library. The JScience API is available within groovy, but vCalc has also implemented some changes (hereafter, the "vCalc Domain Specific Language (DSL)") to Groovy to allow more convenient and seamless use of units.
The implementation is based on the data type Amount provided by JScience. Unlike regular numerics, an Amount has a Unit associated with it. By declaring a variable as an Amount from within the GUI, an equation will automatically receive an argument of type Amount.
Following Groovy Math's approach of attempting to provide the 'least surprising' result, the vCalc DSL simplifies some JScience syntax and expands interoperability with other datatypes. Therefore, in many (but not all) cases, Amounts can be used interchangeably with regular numerical types and in many cases equations do not need to be modified in order to accurately calculate unit conversions.
The DSL
The vCalc DSL provides the following shortcuts for working with units:
- Declaring Amounts
- By appending .<unit_abbrev>, ."<unit_abbrev"(this should be used if the units contains ambiguous characters such as /,^,-1, etc), or .to("<unit_abbrev>")to a numeric value, the number is transformed into an amount of units <unit_abbrev>. Example: def dist = 35.223.mis equivalent to def dist = Amount.valueOf(35.223, Units.valueOf("m"));
- Converting Amounts
- Calling to("<unit_abbrev>")method on any amount will convert it to units <unit_abbrev> if such conversion is possible. Example: dist.to("mi")is equivalent to dist.to(Unit.valueOf("mi"))
- Basic Operations involving Amounts and Numbers
- Per "least surprising" approach, the vCalc DSL allows the use of operators +,-,*,/ among Numbers as Amounts as one would expect, with the following noteworthy adjustments
- JScience does not natively provide methods for Addition or Subtraction between Amounts and Numbers, as an operation such as "3 meters + 10" does not make physical sense. However, to make the calculator and equations useful, I have changed this behaviour by providing such Addition and Subtraction that the quantities are added/subtracted and the result is output in the Amount's original units. E.g. 3.m + 10 = 13.m
- JScience does not natively simplify compatible units combined by scalar operations. I have modified multiply and divide operators to convert the second argument's units into the first (if compatible), and output the resulting units in terms of the first argument. E.g. Jscience would report 1.kg * 1.lb as 1.0 kg*lb, but I have adjusted this same operation to report 0.45359237 kg^2^, while 1.lb * 1.kg will result in 2.204 lb^2^
- The one exception to this is percent times percent, which under the above rules would normally yield %^2^. I have modified the multiplication rules to treat n."%" * n."%"as n."%" * n."%".to("real") such that the final answer is also in units of percent
- the **power operator only works with integer exponents
- java.lang.Math functions: Compatibility with Amounts for the following java.lang.Math functions is provided: abs, cos, cosh, round, signum, sin, sqrt, sinh, tan, tanh.
Additional Notes
Compatibility of non-units aware equations with units
Equations using variables that rely only +, -, *, /, abs, cos, cosh, round, signum, sin, sqrt, sinh, tan, and/or tanh should automatically track units accurately. Pow also works, but only for integer arguments. Comparison operators (<,>,==,etc) need to be used with caution. The statement of an amount (e.g. x > 1.0."m" for one meter) compared to an amount works. However, x > 1 will fail if x is an amount.
There are many equations explicitly casting variables to double via code such as "double myDouble = #myVar". This works ok with BigIntegers and BigDecimals as groovy provides a built in casting. However, this does not work with Amounts. Hence, the current "Inverse" function works correctly for Amounts but the "Reciprocal" function does not. If for some reason one needs to specifically cast types to Double, it is much safer to use the "as" operator, e.g. "Double myDouble = #myVar as Double". Doing this with an Amount will allow the equation to work, however units information will of course be discarded.
Named units vs derived unit
Named units are those that can be chosen by the user to convert to (in the case of output results) or convert from (in the case of variable inputs). Also, the label for named can be used by equation authors as a shortcut if it's easier, e.g. 3.0.to("m/h")is equivalent to 3.0.mph.
However, the system is not limited to working only in named units. Additional derived units can be created and used as well. For example, there is no named unit for inch per minute, but def v = 3.inch/3.mincan be used within equations like any other velocity, despite not having a system name. Output in this unit can be freely converted to m/s, mph, etc.
SI Prefixes
SI prefixes (''mega'', ''kilo'', ''yotta'', etc) can be used to declare/convert variables within groovy for SI units (and only SI units). For example, all the following are equivalent measures
def m = 3.m def cm = 300.cm def ym = 1.0E-24.Ym def microm = 1000000.microm;
The prefix characters are those defined as the SI symbol, '''except''' the micro prefix "μ" which should be written as "micro" (as in example above).
Some hints:
Coercing plain numbers into units works fine so the following can be used to convert an existing amount or plain number into the desired units.
def foo = #myvar.to("m"); //works ok with Amounts and Numbers
But, if you don't want to automatically convert to a specific unit, but still want to work nicely with numbers on the calculator, you'll need to type check your arguments. The following will accept an argument as-is if the argument already has units, but will convert to Meters if the arg is a plain number.
def foo = #myVar; if (!VUnitUtils.isAmount(foo)) { //if not an amount foo = foo.to("m"); //treat as meters }
Known Issues
- JScience does internal math with doubles, so BigInteger and BigDecimal precision are not supported.
- Calculator users cannot convert among SI prefixes
- when determining if a named unit is equiv to a computed unit, the dimensions are checked then a test conversion is made. If the conversion result is identical to the original, then it assumed to be the same. This could potentially a problem when different unit happen to yeild the same result. E.g. -40 deg F = -40 deg C.
- oz is not compatible with kilogram
- Comparison operators among Amounts do not work in groovy. Must use the to Amount.compareTo() method instead
- Luminous intensity standard unit is lm instead of cd
- The calculator result is first computed in whatever units are produced naturally and only the final answer is converted. This may be confusing at times when the user is converting output units and adding numbers from the keypad. For example if the calculator outputs 10 meters and the user adds 10 from the keypad, the answer will show "20 m". If the user then converts this to ft, the answer is 65.62 ft. IF the user then adds 10, the answer is then 98.42 ft. This is because the stack is outputting the result of 10m + 10 + 10 and converting to feet. There may be a way in the future to consider numerics entered from the keypad to be in whatever units currently designated by the output conversion.
- No way to specify units to input variables in equation tests (default will be used). Output units are however included as text in the results textbox.