Tuesday, September 29, 2020

#1 2020-01-09 03:27:14 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

MeasurementUnitLib

Here in the UK, although we measure and picture distances in miles, presenters of television and radio documentaries persistently give distances in kilometres — usually mispronouncing them "kil-Ommitters" into the bargain. One soon tires of having to multiply by 5/8 in one's head every few minutes, while simultaneously recoiling from mispronunciations and trying not to loose track of what's being said, and it becomes almost more informative to listen with the sound off. So I set about writing a very simple script to do kilometres-to-miles conversions on demand. However, my scripting curiosity got the better of me and I ended up doing an in-depth exploration of the Foundation framework's "Units and Measurement" classes — which can't actually be used on the computer from which I can see my television!

I originally intended to follow this post with another containing a library I'd written using the "Units and Measurement" classes and methods. But it occurred to me a few days ago that, with all the custom units I'd had to define, and the AppleScript structures I'd devised to store and present them to the handlers, it wouldn't be too much extra bother to adapt the handlers to work with these structures anyway and ditch "Units and Measurement" altogether! So if you're not interested in my appraisal of "Units and Measurement", you can skip the rest of this post. The library — now "Units and Measurement"-less — is in the following post. It does work on the computer next to my television chair.  smile

Generally speaking, if you need to script a simple unit conversion, the best recourse is still to look up the conversion formula on Wikipedia or in Dictionary.app and write the math directly into the script. This is usually the least bother and the most interesting to do and it produces the fastest code and most accurate results:

Applescript:

set theMiles to theKilometres / 1.609344

AppleScript itself provides a handful of useful unit coercions, but they're limited to five categories of unit and you're stuck with the units defined in each. They do have the nice characteristic that they accept both English and American spellings for "metres" and "litres", although these compile the latter way. However, the unwary might not realise that "gallons" means US gallons, not the imperial kind! ASLG doesn't think to mention this obviously pertinent detail.

Applescript:

set litresPerGallon to 1 as gallons as liters as number
--> 3.785412 (not 4.54609 — or even the exact US equivalent: 3.785411784!)

Foundation's "Units and Measurement" classes were apparently introduced in macOS 10.12 Sierra. They're too much bother to use for simple one-off conversions, but are fairly interesting in themselves and show potential as the basis for a library. The documentation with Xcode 11.3 lists 21 unit category classes: NSUnitArea, NSUnitLength, NSUnitVolume, NSUnitAngle, NSUnitMass, NSUnitPressure, NSUnitAcceleration, NSUnitDuration, NSUnitFrequency, NSUnitSpeed, NSUnitEnergy, NSUnitPower, NSUnitTemperature, NSUnitIlluminance, NSUnitElectricCharge, NSUnitElectricCurrent, NSUnitElectricPotentialDifference, NSUnitElectricResistance, NSUnitConcentrationMass, NSUnitDispersion, and NSUnitFuelEfficiency. NSUnitVolume covers both solid and liquid measures, which are separate categories in AppleScript. Xcode 11.3's "NSUnit.h" file also has NSUnitInformationStorage (bytes, bits, kibibytes, etc.), credited as having been introduced in Catalina but not mentioned in the documentation. These classes are all subclasses of NSDimension, which is itself a subclass of NSUnit. Each provides predefined instances of units in its category and custom units are easy to create too. One predefined unit in each class is the class's "base unit" and every unit (including the base unit) has a 'converter' describing how to convert between it and the base unit. The NSUnitLength class, for example, has meters as its base unit and all its units' converters contain numbers indicating how many meters there are in those units.

Measurements are represented by instances of NSMeasurement, which consist of a number and a unit instance. They only have one number and one unit each, so they can't represent compound measurements like 5 feet 6 inches. This has to be either 5.5 feet or 66 inches (or 0.001041666667 miles or whatever). NSMeasurements can be "converted" to different units or they can be added to or subtracted from each other. In each case, the result is a new NSMeasurement instance, not a modification to an existing one.

As with the AppleScript coercions, conversions are only possible between units of the same class. The system performs conversions by converting from source units to base units and thence to target units. (Presumably this happens mainly mathematically, without the actual creation of an intermediate NSMeasurement instance in base units.) This is simpler than having math and logic paths covering every conceivable direct conversion between any two units in the class and it allows custom units simply to be created and used.

Scripting a conversion between predefined units goes something like this:

Applescript:

use AppleScript version "2.5" -- macOS 10.12 (Sierra) or later. NOT 10.11 (El Capitan).
use framework "Foundation"

(* How many yards in a mile? *)

-- Get predefined 'miles' and 'yards' instances of class "NSUnitLength".
set milesUnit to current application's class "NSUnitLength"'s |miles|()
set yardsUnit to current application's class "NSUnitLength"'s |yards|()
(* Alternatively:
   set milesUnit to current application's class "NSUnitLength"'s valueForKey:("miles")
   set yardsUnit to current application's class "NSUnitLength"'s valueForKey:("yards")
*)


-- Set up an NSMeasurement instance representing 1 mile.
set mileMeasurement to current application's class "NSMeasurement"'s alloc()'s initWithDoubleValue:(1) unit:(milesUnit)
-- Derive another from it representing the same distance in yards.
set yardsMeasurement to mileMeasurement's measurementByConvertingToUnit:(yardsUnit)

-- Read off the number of yards:
return yardsMeasurement's doubleValue()
--> 1759.99562554681 !!

The unsatisfactory result here isn't due to floating-point issues but to the fact that NSLength's preset miles unit is (at the time of writing) defined as 1609.34 metres, whereas the true figure is 1609.344 metres. It's only a four-millimetre difference, but it's the difference between accurate results and rubbish. In fact the converter coefficients for many of the other predefined units in these classes are mere approximations too, so their use isn't recommended if you're planning a Mars mission or drone strike. This imprecision's all the more puzzling for fact that the numbers themselves are doubles. However, custom units can be used in place of any unsatisfactory presets:

Applescript:

use AppleScript version "2.5" -- macOS 10.12 (Sierra) or later.
use framework "Foundation"

(* How many yards in a mile? *)

-- Set up an NSUnitConverterLinear instance with the correct number of metres for a mile.
set betterMilesConverter to current application's class "NSUnitConverterLinear"'s alloc()'s initWithCoefficient:(1609.344) |constant|:(0)
(* Or just:
   set betterMilesConverter to current application's class "NSUnitConverterLinear"'s alloc()'s initWithCoefficient:(1609.344)
*)

-- Create a custom NSUnitLength instance which uses this converter.
set betterMilesUnit to current application's class "NSUnitLength"'s alloc()'s initWithSymbol:("mi") converter:(betterMilesConverter)

-- Get a predefined 'yards' instance. Its converter's OK.
set yardsUnit to current application's class "NSUnitLength"'s |yards|()

-- Set up an NSMeasurement instance representing 1 mile.
set mileMeasurement to current application's class "NSMeasurement"'s alloc()'s initWithDoubleValue:(1) unit:(betterMilesUnit)
-- Derive another from it representing the same distance in yards.
set yardsMeasurement to mileMeasurement's measurementByConvertingToUnit:(yardsUnit)

-- Read off the number of yards:
return yardsMeasurement's doubleValue()
--> 1760.0 :)

The NSUnitConverterLinear class used above is the only NSUnitConverter subclass recommended (or even mentioned) in the Xcode documentation. Its constant property is always zero except in NSUnitTemperature's celsius and fahrenheit units, which have different zero points from the base unit kelvin. The documentation notwithstanding, NSUnitFuelEfficiency's milesPerGallon and milesPerImperialGallon units both have NSUnitConverterReciprocal converters. This is because these units are inversely proportional to the base unit, litersPer100Kilometers. (The more miles per gallon, the fewer litres per fixed distance.)

Besides the imprecision of many of the preset converter coefficients, the system has several other frown-inducing characteristics:

• Some of the classes and predefined units are either eccentrically named or are named in what many would regard as poor English. Examples are: NSUnitFuelEfficiency (whose units are actually for fuel consumption), NSUnitElectricPotentialDifference (most people talk about "voltage", which isn't powered by electricity ("electric") but is an aspect of it ("electrical")), metersPerSecondSquared (for "metersPerSecondPerSecond"), newtonsPerMetersSquared [sic] (for "newtonsPerSquareMeter"), gravity (uniquely singular), and lightyears (uniquely not dromedary case).
• Although nearly every unit has a plural label, it's a 'unit'.
fahrenheit, celsius, and kelvin are of course not units at all, but scales. The unit in all three cases is/are "degrees", but this word isn't used.
• Unqualified terms for liquid measures like gallons, pints, and teaspoons still parochially refer to the US versions without being noted as such in the documentation. However, imperial units are at least available, although these have to be specified explicitly.
• Three of the predefined units listed — pounds (mass), poundsPerSquareInch, and newtonsPerMetersSquared — don't actually exist. Well they do, but they're implemented as poundsMass, poundsForcePerSquareInch and newtonsPerMeterSquared respectively.
• When measurements with different units are either added or subtracted, the result is in base units, not in either of the originals.

The NSFormatter group of classes includes NSMeasurementFormatter, whose instances produce formatted NSStrings from NSUnit and NSMeasurement instances, but not vice versa. They're locale-aware to the extent that, unless set to do otherwise, they'll return what they think someone in your locale will want to see. So if your locale's a country which uses miles, and you do a miles-to-kilometers conversion, a formatted string from the result will show the distance in miles anyway! Measurement formatters may also turn out strings in units they consider more "convenient" than the originals, or even change the units for no apparent reason at all. Fortunately, they have a property which forces them to use the units of the measurements they're formatting, but it has to be set explicitly. Another property controls the "style" of the results: short (eg. "30km/h"), medium ("30 km/h"), or long ("30 kilometres per hour", impressively in the local spelling). But this only has any effect with system-predefined units. Measurements with custom units are rendered in the default, medium style. A third property gives access to the number formatter used, which by default is configured for decimal-style output and a maximum of three decimal places. The number of decimal places can be changed with an adjustment to this formatter.

That's all I have to report about the "Units and Measurement" system. The overall impression is one of a good design either hurriedly or shoddily implemented. It's fairly easy — if a lot a work! — to script round the shortcomings, but it shouldn't be necessary. Maybe things will improve as time goes by.

Meanwhile, the following post contains a library adapted from one I wrote recently to exploit the "Units and Measurement" classes. It uses the same principles as them. It just doesn't use themsmile


NG

Offline

 

#2 2020-01-09 03:29:16 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

The library script mentioned above is at the bottom of this post. As noted in its comments, it has five public handlers, of which the three which convert, add, and subtract measurements are the main ones. The other two are actually service handlers which interpret expressions passed in from client scripts and return instances of the library's own measurement and unit objects. They're made public for the benefit of client scripts which need to acquire such objects outside the context of the other three handlers.

The handlers are inherited by 22 script objects covering the various unit categories. They have to be called through the script object for the appropriate category so that they can use the data stored in that script object's properties. It's a bit of a pain having to code in the script object labels too, but since converting, adding, and subtracting can only be done where all the units are in the same category, it's perhaps not a bad idea to be conscious of the categories when scripting:

Edits (20th and 29th January 2020): The example scripts have been edited to show new labels for the three main public handlers and for the script objects themselves. The old labels do still work however if anyone's started using them!

Applescript:

use MULib : script "MeasurementUnitLib"

set distanceInMiles to MULib's LengthCategory's convertMeasurement:"100 kilometers" toUnits:"miles" resultType:"number"
--> 62.137119223733

set acreage to MULib's AreaCategory's convertMeasurement:"40 hectares" toUnits:"acres" resultType:"number"
--> 98.842152586866

tell MULib's VolumeCategory to set theResult to its addMeasurement:"94 imperial pints" toMeasurement:"22000 imperial gallons" resultType:"text"
--> "22,011.75 gal"

set angleStuff to script "MeasurementUnitLib"'s AngleCategory
set newAngle to angleStuff's subtractMeasurement:"217.5 radians" fromMeasurement:"90 degrees" resultType:"object"
--> {86.203908876912, {symbol:"°", coefficient:1.0, unitName:"degrees", baseline:0, converter:«script linearConverter»}}

As you can see, there are various ways in which the script objects can be addressed and a choice of forms in which results can be returned. It depends on what you find most convenient and what kind of results you need. The results of additions and subtractions are in the units of the second measurement parameter.

A "number" result is an AppleScript real denoting the number of units in a result. A "text" (or "string") or "NSString" result contains a number formatted as text in the local style, a space, and a unit symbol. A property at the top of the library code governs the maximum number of decimal places included. A "measurement object" (or "measurement" or "object") result is a list whose first item is a number and whose second item is a "unit object", which is a record containing the unit data. In most cases, you won't need to know these.

Measurement parameters can be passed to the handlers either as text (as above), or as lists containing a number and a unit name, or as full "measurement objects".

Unit parameters can be passed either as text representing the unit names or as "unit object" records. The names of the library's predefined units are all plural and are stored internally as dromedary-case strings with American spellings. But names can be passed to the handlers in any hotchpotch of cases, with spaces between words, with slashes instead of instances of "Per", and/or with English spellings. So "kilometersPerHour" can be entered as such or as "kilometersperhour", "kilometers per hour", "kilometers/hour", "kilometres/hour", etc. Either "amp" or "ampere" may be used in the names of the relevant electrical units and the word "degrees" can optionally be used in front of the names of the temperature scales. Some units have alternative names, since users may be more familiar with one than the other, eg. "metersPerSecondSquared" or "metersPerSecondPerSecond". Unqualified terms such as "gallons" and "pints" are interpreted as US or imperial units according to the locale set on the host machine, but can be explicitly prefixed with "US" or "imperial" if required. The units available in each category are listed in comments at the top the relevant script objects. Some of the script objects have alternative labels too.  smile

Edits (various dates): "gramme" can now be used an alternative to "gram". Units can be expressed in the singular if preferred — eg. "1 kilogramme", "1 mile per hour". "tons" and "hundredweights" are now interpreted as US or imperial units under the same criteria as for "gallons" and "pints", but can be explicitly prefixed with "short" or "long" for the US and imperial weights respectively.

If the library doesn't have a unit you need, you can define your own own. Suppose, for example, that for your own purposes you need a unit equivalent to a tenth of a mile, which you've decided to call "decimiles" and to give the symbol "dmi"  smile  :

Applescript:

use MULib : script "MeasurementUnitLib"

set LengthCategory to MULib's LengthCategory

-- LengthCategory's "base unit" is "meters". A tenth of a mile is 160.9344 meters.
-- The 'baseline' and 'converter' properties needn't be specified as the usual defaults (0 and MULib's linearConverter respectively) are required.
set decimiles to {unitName:"decimiles", symbol:"dmi", coefficient:160.9344}

-- Use this custom unit with any of the main public handlers:
LengthCategory's convertMeasurement:"4 kilometres" toUnits:decimiles resultType:"text"
--> "24.855 dmi"
LengthCategory's addMeasurement:{7.5, decimiles} toMeasurement:"47 miles" resultType:"number"
--> 47.75

Here's the library code itself:

Applescript:

property maxDecimalPlacesInFormattedStringResults : 3 -- Adjust as required or override in individual script objects.

(* MeasurementUnitLib by Nigel Garvey, December 2019-February 2020.

   Requires OS X 10.9.2 (Mavericks) or later.
   
   There are five public handlers in this incarnation …
       convertMeasurement:(measurement expression) toUnits:(unit expression) resultType:(result type expression) --> (measurement object, number, formatted text, or formatted NSString)
       addMeasurement:(measurement expression) toMeasurement:(measurement expression) resultType:(result type expression) --> (measurement object, number, formatted text, or formatted NSString)
       subtractMeasurement:(measurement expression) fromMeasurement:(measurement expression) resultType:(result type expression) --> (measurement object, number, formatted text, or formatted NSString)
       getMeasurementObject:(measurementExpression) --> (measurement object)
       getUnitObject:(unitExpression) --> (unit object)
           Key:
           'measurement object' : list containing number and unit object.
           'unit object' : record with properties for 'unitName', 'symbol', 'coefficient', 'baseline', and 'converter'.
           'measurement expression' : measurement object/list containing number and unit expression/text containing number and unit name.
           'unit expression' : unit object/text containing unit name.
           'result type expression' : text. One of "number"/("text" or "string")/"NSString"/("measurement object" or "measurement" or "object").
           Equivalent NSArrays, NSDictionaries, and NSStrings are also accepted.
   
   … and twenty-two unit category script objects through which to call them. Some have alternative labels which can be used if preferred:
       LengthCategory (or DistanceCategory)        PowerCategory
       AreaCategory                            TemperatureCategory
       VolumeCategory                        IlluminanceCategory
       AngleCategory                            ElectricalChargeCategory (or ElectricChargeCategory
       MassCategory                            ElectricalCurrentCategory (or ElectricCurrentCategory)
       PressureCategory                        VoltageCategory (or ElectricPotentialDifferenceCategory)
       AccelerationCategory                        ResistanceCategory (or ElectricResistanceCategory)
       DurationCategory                        MassConcentrationCategory (or ConcentrationMassCategory)
       FrequencyCategory                        VolumeConcentrationCategory (or DispersionCategory)
       SpeedCategory                            FuelConsumptionCategory (or FuelEfficiencyCategory)
       EnergyCategory                            InformationStorageCategory
       
   The library's handlers are inherited by all the above script objects and must be called through the one relevant to the unit category, eg.:
       set distanceInMiles to MULib's LengthCategory's convertMeasurement:"80 kilometres" toUnits:"miles" resultType:"number"
       set totalHours to MULib's DurationCategory's addMeasurement:{45, "minutes"} toMeasurement:"3 hours" resultType:"text"
   
   Passed unit names should largely match the dromedary-case names specified in the script objects, but some leeway is allowed with cases and spaces. English or American spellings may be used for "metres", "litres", and "grammes" and slashes can be used instead of the word "per". Additionally, "amp" is accepted for "ampere" and the names of the temperature scales may optionally be prefaced with the word "degrees" as in the AS unit coercions. Unqualified terms for liquid measures such as "gallons" and "pints" are interpreted as imperial or US units according to the locale set on the host machine, but either can be specified explicitly if required: eg. "US gallons", "imperial pints". Names can now also be passed in the singular. Formatted strings returned by the handlers aren't suitable as input for them.
*)


use AppleScript version "2.3.1" -- OS X 10.9.2 (Mavericks) or later.
use framework "Foundation"
property |⌘| : a reference to current application


(* CONVERTER SCRIPT OBJECTS *)
-- Can be specified in custom unit objects, but the contained handlers are private.
-- All inputs: (number of input units, unit object for the unit being converted to or from the base unit (including the base unit itself if that's the conversion))
-- All outputs: equivalent number of target units.

script linearConverter -- The norm.
   -- 'coefficient': number of base units in the given unit. 'baseline': where given-unit 0 comes on the base-unit scale (only relevant for temperatures).
   on unitsToBaseUnits(numberOfUnits, unitObject)
       return numberOfUnits * (unitObject's coefficient) + (unitObject's baseline)
   end unitsToBaseUnits
   
   on baseUnitsToTargetUnits(numberOfUnits, unitObject)
       if (numberOfUnits is "∞") then return numberOfUnits
       return (numberOfUnits - (unitObject's baseline)) / (unitObject's coefficient)
   end baseUnitsToTargetUnits
end script

script reciprocalConverter -- Only for conversions between 'milesPerGallon' or 'milesPerImperialGallon' and their base unit 'litersPer100Kilometers'.
   -- 'coefficient': reciprocal of the number of base units in the given unit.
   on unitsToBaseUnits(numberOfUnits, unitObject)
       if (numberOfUnits is 0.0) then return "∞"
       if (numberOfUnits is "∞") then return 0.0
       return (unitObject's coefficient) / numberOfUnits
   end unitsToBaseUnits
   
   property baseUnitsToTargetUnits : unitsToBaseUnits
end script


(* UNIT CATEGORY SCRIPT OBJECTS *)

script LengthCategory
   -- Base unit: meters.
   -- Units: megameters, kilometers, hectometers, decameters, meters, decimeters, centimeters, millimeters, micrometers, nanometers, picometers, inches, feet, yards, miles, scandinavianMiles, lightyears [sic], nauticalMiles, fathoms, furlongs, astronomicalUnits, parsecs, chains.
   property unitData : {{unitName:"megameters", symbol:"Mm", coefficient:1.0E+6}, {unitName:"kilometers", symbol:"km", coefficient:1000.0}, {unitName:"hectometers", symbol:"hm", coefficient:100.0}, {unitName:"decameters", symbol:"dam", coefficient:10.0}, {unitName:"meters", symbol:"m", coefficient:1.0}, {unitName:"decimeters", symbol:"dm", coefficient:0.1}, {unitName:"centimeters", symbol:"cm", coefficient:0.01}, {unitName:"millimeters", symbol:"mm", coefficient:1.0E-3}, {unitName:"micrometers", symbol:"µm", coefficient:1.0E-6}, {unitName:"nanometers", symbol:"nm", coefficient:1.0E-9}, {unitName:"picometers", symbol:"pm", coefficient:1.0E-12}, {unitName:"inches", symbol:"in", coefficient:0.0254}, {unitName:"feet", symbol:"ft", coefficient:0.3048}, {unitName:"yards", symbol:"yd", coefficient:0.9144}, {unitName:"miles", symbol:"mi", coefficient:1609.344}, {unitName:"scandinavianMiles", symbol:"smi", coefficient:1.0E+4}, {unitName:"lightyears", symbol:"ly", coefficient:9.4607304725808E+15}, {unitName:"nauticalMiles", symbol:"NM", coefficient:1852.0}, {unitName:"fathoms", symbol:"ftm", coefficient:1.8288}, {unitName:"furlongs", symbol:"fur", coefficient:201.168}, {unitName:"astronomicalUnits", symbol:"au", coefficient:1.495987707E+11}, {unitName:"parsecs", symbol:"pc", coefficient:3.08567758149137E+16}, {unitName:"chains", symbol:"ch", coefficient:20.1168}}
end script
property DistanceCategory : LengthCategory -- Alternative label.

script AreaCategory
   -- Base unit: squareMeters.
   -- Units: squareMegameters, squareKilometers, squareMeters, squareCentimeters, squareMillimeters, squareMicrometers, squareNanometers, squareInches, squareFeet, squareYards, squareMiles, acres, ares, hectares.
   property unitData : {{unitName:"squareMegameters", symbol:"Mm²", coefficient:1.0E+12}, {unitName:"squareKilometers", symbol:"km²", coefficient:1.0E+6}, {unitName:"squareMeters", symbol:"m²", coefficient:1.0}, {unitName:"squareCentimeters", symbol:"cm²", coefficient:1.0E-4}, {unitName:"squareMillimeters", symbol:"mm²", coefficient:1.0E-6}, {unitName:"squareMicrometers", symbol:"µm²", coefficient:1.0E-12}, {unitName:"squareNanometers", symbol:"nm²", coefficient:1.0E-18}, {unitName:"squareInches", symbol:"in²", coefficient:6.4516E-4}, {unitName:"squareFeet", symbol:"ft²", coefficient:0.09290304}, {unitName:"squareYards", symbol:"yd²", coefficient:0.83612736}, {unitName:"squareMiles", symbol:"mi²", coefficient:2.589988110336E+6}, {unitName:"acres", symbol:"ac", coefficient:4046.8564224}, {unitName:"ares", symbol:"a", coefficient:100.0}, {unitName:"hectares", symbol:"ha", coefficient:10000}}
end script

script VolumeCategory
   -- Base unit: liters.
   -- Units: megaliters, kiloliters, liters, deciliters, centiliters, milliliters, cubicKilometers, cubicMeters, cubicDecimeters, cubicCentimeters, cubicMillimeters, cubicInches, cubicFeet, cubicYards, cubicMiles, acreFeet, bushels, teaspoons, tablespoons, fluidOunces, cups, pints, quarts, gallons, imperialTeaspoons, imperialTablespoons, imperialFluidOunces, imperialPints, imperialQuarts, imperialGallons, metricCups, imperialCups, imperialBushels, metricTeaspoons, metricTablespoons, acreFeet.
   property unitData : {{unitName:"megaliters", symbol:"ML", coefficient:1.0E+6}, {unitName:"kiloliters", symbol:"kL", coefficient:1000.0}, {unitName:"liters", symbol:"L", coefficient:1.0}, {unitName:"deciliters", symbol:"dL", coefficient:0.1}, {unitName:"centiliters", symbol:"cL", coefficient:0.01}, {unitName:"milliliters", symbol:"mL", coefficient:1.0E-3}, {unitName:"cubicKilometers", symbol:"km³", coefficient:1.0E+12}, {unitName:"cubicMeters", symbol:"m³", coefficient:1000.0}, {unitName:"cubicDecimeters", symbol:"dm³", coefficient:1.0}, {unitName:"cubicCentimeters", symbol:"cm³", coefficient:1.0E-3}, {unitName:"cubicMillimeters", symbol:"mm³", coefficient:1.0E-6}, ¬
       {unitName:"cubicInches", symbol:"in³", coefficient:0.016387064}, {unitName:"cubicFeet", symbol:"ft³", coefficient:28.316846592}, {unitName:"cubicYards", symbol:"yd³", coefficient:764.554857984}, {unitName:"cubicMiles", symbol:"mi³", coefficient:4.16818182544058E+12}, {unitName:"acreFeet", symbol:"ac•ft", coefficient:1.23348183754752E+6}, ¬
       {unitName:"teaspoons", symbol:"tsp", coefficient:4.92892159375 / 1000}, {unitName:"tablespoons", symbol:"tbsp", coefficient:4.92892159375 * 3 / 1000}, {unitName:"fluidOunces", symbol:"fl oz", coefficient:0.029573529562}, {unitName:"cups", symbol:"cup", coefficient:0.2365882365}, {unitName:"pints", symbol:"pt", coefficient:0.473176473}, {unitName:"quarts", symbol:"qt", coefficient:0.946352946}, {unitName:"gallons", symbol:"gal", coefficient:3.785411784}, {unitName:"imperialTeaspoons", symbol:"tsp", coefficient:0.005}, {unitName:"imperialTablespoons", symbol:"tbsp", coefficient:0.015}, {unitName:"imperialFluidOunces", symbol:"fl oz", coefficient:0.028413062}, {unitName:"imperialPints", symbol:"pt", coefficient:0.56826125}, {unitName:"imperialQuarts", symbol:"qt", coefficient:1.1365225}, {unitName:"imperialGallons", symbol:"gal", coefficient:4.54609}, {unitName:"metricCups", symbol:"metric cup", coefficient:0.25}, ¬
       {unitName:"imperialCups", symbol:"cup", coefficient:0.284130625}, {unitName:"imperialBushels", symbol:"bsh", coefficient:36.36872}, {unitName:"metricTeaspoons", symbol:"tsb", coefficient:0.005}, {unitName:"metricTablespoons", symbol:"tbsp", coefficient:0.015}}
   
   on cleanUpUnitName(unitName) -- Input: AS text.
       set unitName to (continue cleanUpUnitName(unitName))
       if ((unitName is "teaspoons") or (unitName is "tablespoons") or (unitName is "fluidounces") or (unitName is "pints") or (unitName is "quarts") or (unitName is "gallons") or (unitName is "cups") or (unitName is "bushels")) then
           if (hostUsingNonUSGallonLocale()) then set unitName to "imperial" & unitName
       else if (unitName begins with "us") then
           set unitName to text 3 thru -1 of unitName
       end if
       return unitName
   end cleanUpUnitName
end script

script AngleCategory
   -- Base unit: degrees.
   -- Units: degrees, arcMinutes, arcSeconds, radians, gradians, revolutions, gons.
   property unitData : {{unitName:"degrees", symbol:"°", coefficient:1.0}, {unitName:"arcMinutes", symbol:"'", coefficient:1 / 60}, {unitName:"arcSeconds", symbol:"\"", coefficient:1 / 3600}, {unitName:"radians", symbol:"rad", coefficient:pi / 180}, {unitName:"gradians", symbol:"L", coefficient:1}, {unitName:"revolutions", symbol:"rev", coefficient:360.0}, {unitName:"gons", symbol:"gon", coefficient:0.9}}
end script

script MassCategory
   -- Base unit: kilograms.
   -- Units: kilograms, grams, decigrams, centigrams, milligrams, micrograms, nanograms, picograms, ounces, pounds, poundsMass, stones, metricTons, shortTons, carats, ouncesTroy, slugs, quarters, hundredweights, metricHundredweights, shortHundredweights, tons, tonnes, troyOunces.
   property unitData : {{unitName:"kilograms", symbol:"kg", coefficient:1.0}, {unitName:"grams", symbol:"g", coefficient:1.0E-3}, {unitName:"decigrams", symbol:"dg", coefficient:1.0E-4}, {unitName:"centigrams", symbol:"cg", coefficient:1.0E-5}, {unitName:"milligrams", symbol:"mg", coefficient:1.0E-6}, {unitName:"micrograms", symbol:"µg", coefficient:1.0E-9}, {unitName:"nanograms", symbol:"ng", coefficient:1.0E-12}, {unitName:"picograms", symbol:"pg", coefficient:1.0E-15}, {unitName:"ounces", symbol:"oz", coefficient:0.028349523125}, {unitName:"pounds", symbol:"lb", coefficient:0.45359237}, {unitName:"poundsMass", symbol:"lb", coefficient:0.45359237}, {unitName:"stones", symbol:"st", coefficient:6.35029318}, {unitName:"metricTons", symbol:"t", coefficient:1000}, {unitName:"shortTons", symbol:"ton", coefficient:907.18474}, {unitName:"carats", symbol:"ct", coefficient:2.0E-4}, {unitName:"ouncesTroy", symbol:"oz t", coefficient:0.0311034768}, {unitName:"quarters", symbol:"qr", coefficient:12.70058636}, {unitName:"hundredweights", symbol:"cwt", coefficient:50.80234544}, {unitName:"metricHundredweights", symbol:"cwt", coefficient:50.0}, {unitName:"shortHundredweights", symbol:"cwt", coefficient:45.359237}, {unitName:"tons", symbol:"ton", coefficient:1016.0469088}, {unitName:"tonnes", symbol:"t", coefficient:1000}, {unitName:"troyOunces", symbol:"oz t", coefficient:0.0311034768}}
   
   on cleanUpUnitName(unitName) -- Input: AS text.
       set unitName to (continue cleanUpUnitName(unitName))
       if ((unitName is "tons") or (unitName is "hundredweights")) then
           if (not hostUsingNonUSGallonLocale()) then set unitName to "short" & unitName -- Host IS using US-gallon locale.
       else if (unitName begins with "long") then
           set unitName to text 5 thru -1 of unitName
       end if
       return unitName
   end cleanUpUnitName
end script

script PressureCategory
   -- Base unit: newtonsPerMeterSquared (equivalent both to pascals and to joules per cubic meter).
   -- Units: newtonsPerMeterSquared, gigapascals, megapascals, kilopascals, hectopascals, inchesOfMercury, bars, millibars, millimetersOfMercury, poundsPerSquareInch, poundsForcePerSquareInch, pascals, newtonsPerSquareMeter, joulesPerCubicMeter, torrs, atmospheres.
   property unitData : {{unitName:"newtonsPerMeterSquared", symbol:"N/m²", coefficient:1.0}, {unitName:"gigapascals", symbol:"GPa", coefficient:1.0E+9}, {unitName:"megapascals", symbol:"MPa", coefficient:1.0E+6}, {unitName:"kilopascals", symbol:"kPa", coefficient:1000.0}, {unitName:"hectopascals", symbol:"hPa", coefficient:100.0}, {unitName:"bars", symbol:"bar", coefficient:1.0E+5}, {unitName:"millibars", symbol:"mbar", coefficient:100.0}, {unitName:"millimetersOfMercury", symbol:"mmHg", coefficient:133.322387415}, {unitName:"poundsPerSquareInch", symbol:"psi", coefficient:6894.757293168}, {unitName:"poundsForcePerSquareInch", symbol:"psi", coefficient:6894.757293168}, {unitName:"pascals", symbol:"Pa", coefficient:1.0}, {unitName:"newtonsPerSquareMeter", symbol:"N/m²", coefficient:1.0}, {unitName:"joulesPerCubicMeter", symbol:"J/m³", coefficient:1.0}, {unitName:"torrs", symbol:"Torr", coefficient:133.322368421053}, {unitName:"atmospheres", symbol:"atm", coefficient:101325}}
end script

script AccelerationCategory
   -- Base unit: metersPerSecondSquared.
   -- Units: metersPerSecondSquared, gravities, feetPerSecondPerSecond, feetPerSecondSquared, metersPerSecondPerSecond, milesPerHourPerSecond.
   property unitData : {{unitName:"metersPerSecondSquared", symbol:"m/s²", coefficient:1.0}, {unitName:"gravities", symbol:"g", coefficient:9.80665}, {unitName:"feetPerSecondPerSecond", symbol:"ft/s²", coefficient:0.3048}, {unitName:"feetPerSecondSquared", symbol:"ft/s²", coefficient:0.3048}, {unitName:"metersPerSecondPerSecond", symbol:"m/s²", coefficient:1.0}, {unitName:"milesPerHourPerSecond", symbol:"mph/s", coefficient:0.44704}}
end script

script DurationCategory
   -- Base unit: seconds.
   -- Units: seconds, minutes, hours, milliseconds, microseconds, nanoseconds, picoseconds.
   property unitData : {{unitName:"seconds", symbol:"sec", coefficient:1.0}, {unitName:"minutes", symbol:"min", coefficient:60.0}, {unitName:"hours", symbol:"hr", coefficient:3600.0}, {unitName:"milliseconds", symbol:"ms", coefficient:1.0E-3}, {unitName:"microseconds", symbol:"µs", coefficient:1.0E-6}, {unitName:"nanoseconds", symbol:"ns", coefficient:1.0E-9}, {unitName:"picoseconds", symbol:"ps", coefficient:1.0E-12}}
end script

script FrequencyCategory
   -- Base unit: hertz.
   -- Units: terahertz, gigahertz, megahertz, kilohertz, hertz, millihertz, microhertz, nanohertz, framesPerSecond.
   property unitData : {{unitName:"terahertz", symbol:"THz", coefficient:1.0E+12}, {unitName:"gigahertz", symbol:"GHz", coefficient:1.0E+9}, {unitName:"megahertz", symbol:"MHz", coefficient:1.0E+6}, {unitName:"kilohertz", symbol:"kHz", coefficient:1000.0}, {unitName:"hertz", symbol:"Hz", coefficient:1.0}, {unitName:"millihertz", symbol:"Hz", coefficient:1.0E-3}, {unitName:"microhertz", symbol:"µHz", coefficient:1.0E-6}, {unitName:"nanohertz", symbol:"nHz", coefficient:1.0E-9}, {unitName:"framesPerSecond", symbol:"FPS", coefficient:1.0}}
end script

script SpeedCategory
   -- Base unit: metersPerSecond.
   -- Units: metersPerSecond, kilometersPerHour, milesPerHour, knots, nauticalMilesPerHour (same as knots).
   property unitData : {{unitName:"metersPerSecond", symbol:"m/s", coefficient:1.0}, {unitName:"kilometersPerHour", symbol:"km/h", coefficient:1000 / 3600}, {unitName:"milesPerHour", symbol:"mph", coefficient:0.44704}, {unitName:"knots", symbol:"kn", coefficient:1852 / 3600}, {unitName:"nauticalMilesPerHour", symbol:"NM/h", coefficient:1852 / 3600}}
end script

script EnergyCategory
   -- Base unit: kilojoules.
   -- Units: kilojoules, joules, kilocalories, calories, kilowattHours, largeCalories, foodCalories, smallCalories (alternative terms for kilocalories and calories).
   property unitData : {{unitName:"kilojoules", symbol:"kJ", coefficient:1000.0}, {unitName:"joules", symbol:"J", coefficient:1.0}, {unitName:"kilocalories", symbol:"kcal", coefficient:4184.0}, {unitName:"calories", symbol:"cal", coefficient:4.184}, {unitName:"kilowattHours", symbol:"kWh", coefficient:3.6E+6}, {unitName:"largeCalories", symbol:"Cal", coefficient:4184.0}, {unitName:"foodCalories", symbol:"kcal", coefficient:4184.0}, {unitName:"smallCalories", symbol:"cal", coefficient:4.184}}
end script

script PowerCategory
   -- Base unit: watts.
   -- Units: terawatts, gigawatts, megawatts, kilowatts, watts, milliwatts, microwatts, nanowatts, picowatts, femtowatts, horsepower, mechanicalHorsepower, imperialHorsepower, hydraulicHorsepower, airHorsepower, metricHorsepower, electricalHorsepower, metricHorsepower.
   property unitData : {{unitName:"terawatts", symbol:"TW", coefficient:1.0E+12}, {unitName:"gigawatts", symbol:"GW", coefficient:1.0E+9}, {unitName:"megawatts", symbol:"MW", coefficient:1.0E+6}, {unitName:"kilowatts", symbol:"kW", coefficient:1000.0}, {unitName:"watts", symbol:"W", coefficient:1.0}, {unitName:"milliwatts", symbol:"mW", coefficient:1.0E-3}, {unitName:"nanowatts", symbol:"nW", coefficient:1.0E-9}, {unitName:"picowatts", symbol:"pW", coefficient:1.0E-12}, {unitName:"femtowatts", symbol:"fW", coefficient:1.0E-15}, {unitName:"horsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"mechanicalHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"imperialHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"hydraulicHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"airHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"metricHorsepower", symbol:"hp(M)", coefficient:735.49875}, {unitName:"electricalHorsepower", symbol:"hp(E)", coefficient:746}, {unitName:"boilerHorsepower", symbol:"hp(S)", coefficient:9812.5}}
end script

script TemperatureCategory
   -- Base unit: kelvin.
   -- "Units": kelvin, fahrenheit, celsius. (The word "degrees" is accepted before the scale names. "Centigrade" is accepted for "Celsius".)
   property unitData : {{unitName:"kelvin", symbol:"K", coefficient:1.0}, {unitName:"fahrenheit", symbol:"°F", coefficient:5 / 9, baseline:273.15 - 32 * 5 / 9}, {unitName:"celsius", symbol:"°C", coefficient:1.0, baseline:273.15}}
   
   on cleanUpUnitName(unitName) -- Input: AS text.
       set unitName to (continue cleanUpUnitName(unitName))
       set unitName to replaceText("degrees", "", unitName)
       set unitName to replaceText("centigrade", "celsius", unitName)
       return unitName
   end cleanUpUnitName
end script

script IlluminanceCategory
   -- Base unit: lux, lumensPerSquareMeter (same as lux).
   property unitData : {{unitName:"lux", symbol:"lx", coefficient:1.0}, {unitName:"lumensPerSquareMeter", symbol:"lm/m²", coefficient:1.0}}
end script

script ElectricalChargeCategory
   -- Base unit: coulombs (amperes per second).
   -- Units: coulombs, megaampereHours, kiloampereHours, ampereHours, milliampereHours, microampereHours. ("amp" is accepted for "ampere".)
   property unitData : {{unitName:"coulombs", symbol:"C", coefficient:1.0}, {unitName:"megaampereHours", symbol:"MAh", coefficient:3.6E+9}, {unitName:"kiloampereHours", symbol:"kAh", coefficient:3.6E+6}, {unitName:"ampereHours", symbol:"Ah", coefficient:3600.0}, {unitName:"milliampereHours", symbol:"mAh", coefficient:3.6}, {unitName:"microampereHours", symbol:"µAh", coefficient:0.0036}}
   
   on cleanUpUnitName(unitName) -- Input: AS text.
       set unitName to (continue cleanUpUnitName(unitName))
       if ((unitName contains "amp") and (unitName does not contain "ampere")) then set unitName to replaceText("amp", "ampere", unitName)
       return unitName
   end cleanUpUnitName
end script
property ElectricChargeCategory : ElectricalChargeCategory -- Alternative label.

script ElectricalCurrentCategory
   -- Base unit: amperes.
   -- Units: megaamperes, kiloamperes, amperes, milliamperes, microamperes. ("amp" is accepted for "ampere".)
   property unitData : {{unitName:"megaamperes", symbol:"MA", coefficient:1.0E+6}, {unitName:"kiloamperes", symbol:"kA", coefficient:1000.0}, {unitName:"amperes", symbol:"A", coefficient:1.0}, {unitName:"milliamperes", symbol:"mA", coefficient:1.0E-3}, {unitName:"microamperes", symbol:"µA", coefficient:1.0E-6}}
   
   on cleanUpUnitName(unitName) -- Input: AS text.
       set unitName to (continue cleanUpUnitName(unitName))
       return replaceText("amps", "amperes", unitName)
   end cleanUpUnitName
end script
property ElectricCurrentCategory : ElectricalCurrentCategory -- Alternative label.

script ElectricalVoltageCategory
   -- Base unit: volts.
   -- Units: megavolts, kilovolts, volts, millivolts, microvolts.
   property unitData : {{unitName:"megavolts", symbol:"MV", coefficient:1.0E+6}, {unitName:"kilovolts", symbol:"kV", coefficient:1000.0}, {unitName:"volts", symbol:"V", coefficient:1.0}, {unitName:"millivolts", symbol:"mV", coefficient:1.0E-3}, {unitName:"microvolts", symbol:"µV", coefficient:1.0E-6}}
end script
property ElectricPotentialDifferenceCategory : ElectricalVoltageCategory -- Alternative label.

script ElectricalResistanceCategory
   -- Base unit: ohms.
   -- Units: megaohms, kiloohms, ohms, milliohms, microohms.
   property unitData : {{unitName:"megaohms", symbol:"MΩ", coefficient:1.0E+6}, {unitName:"kiloohms", symbol:"kΩ", coefficient:1000.0}, {unitName:"ohms", symbol:"Ω", coefficient:1.0}, {unitName:"milliohms", symbol:"mΩ", coefficient:1.0E-3}, {unitName:"microohms", symbol:"µΩ", coefficient:1.0E-6}}
end script
property ElectricResistanceCategory : ElectricalResistanceCategory -- Alternative label.

script MassConcentrationCategory
   -- Base unit: gramsPerLiter.
   -- Predefined: gramsPerLiter, milligramsPerDeciliter.
   property unitData : {{unitName:"gramsPerLiter", symbol:"g/L", coefficient:1.0}, {unitName:"milligramsPerDeciliter", symbol:"mg/dL", coefficient:0.01}}
end script
property ConcentrationMassCategory : MassConcentrationCategory -- Alternative label.

script VolumeConcentrationCategory
   -- Base unit: partsPerMillion.
   -- Units: partsPerMillion.
   property unitData : {{unitName:"partsPerMillion", symbol:"ppm", coefficient:1.0}}
end script
property DispersionCategory : VolumeConcentrationCategory -- Alternative label.

script FuelConsumptionCategory
   -- Base unit: litersPer100Kilometers.
   -- Units: litersPer100Kilometers, milesPerGallon, milesPerImperialGallon.
   property unitData : {{unitName:"litersPer100Kilometers", symbol:"L/100km", coefficient:1}, {unitName:"milesPerGallon", symbol:"mpg", coefficient:3.785411784 * 62.137119223733, converter:reciprocalConverter}, {unitName:"milesPerImperialGallon", symbol:"mpg", coefficient:4.54609 * 62.137119223733, converter:reciprocalConverter}}
   
   on cleanUpUnitName(unitName) -- Input: AS text.
       set unitName to (continue cleanUpUnitName(unitName))
       if (unitName is "milespergallon") then
           if (hostUsingNonUSGallonLocale()) then set unitName to "milesperimperialgallon"
       else if (unitName is "milesperusgallon") then
           set unitName to "milespergallon"
       else if (unitName is "litersperhundredkilometers") then
           set unitName to "litersper100kilometers"
       end if
       return unitName
   end cleanUpUnitName
end script
property FuelEfficiencyCategory : FuelConsumptionCategory -- Alternative label.

script InformationStorageCategory
   -- Base unit: bytes.
   -- Units: bytes, bits, nibbles, yottabytes, zettabytes, exabytes, petabytes, terabytes, gigabytes, megabytes, kilobytes, yottabits, zettabits, exabits, petabits, terabits, gigabits, megabits, kilobits, yobibytes, zebibytes, exbibytes, pebibytes, tebibytes, gibibytes, mebibytes, kibibytes, yobibits, zebibits, exbibits, pebibits, tebibits, gibibits, mebibits, kibibits.
   property unitData : {{unitName:"bytes", symbol:"B", coefficient:1.0}, {unitName:"bits", symbol:"bit", coefficient:0.125}, {unitName:"nibbles", symbol:"nibble", coefficient:0.5}, ¬
       {unitName:"yottabytes", symbol:"YB", coefficient:1.0E+24}, {unitName:"zettabytes", symbol:"ZB", coefficient:1.0E+21}, {unitName:"exabytes", symbol:"EB", coefficient:1.0E+18}, {unitName:"petabytes", symbol:"PB", coefficient:1.0E+15}, {unitName:"terabytes", symbol:"TB", coefficient:1.0E+12}, {unitName:"gigabytes", symbol:"GB", coefficient:1.0E+9}, {unitName:"megabytes", symbol:"MB", coefficient:1.0E+6}, {unitName:"kilobytes", symbol:"kB", coefficient:1000.0}, ¬
       {unitName:"yottabits", symbol:"Ybit", coefficient:1.25E+23}, {unitName:"zettabits", symbol:"Zbit", coefficient:1.25E+20}, {unitName:"exabits", symbol:"Ebit", coefficient:1.25E+17}, {unitName:"petabits", symbol:"Pbit", coefficient:1.25E+14}, {unitName:"terabits", symbol:"Tbit", coefficient:1.25E+11}, {unitName:"gigabits", symbol:"Gbit", coefficient:1.25E+8}, {unitName:"megabits", symbol:"Mbit", coefficient:1.25E+5}, {unitName:"kilobits", symbol:"kbit", coefficient:125.0}, ¬
       {unitName:"yobibytes", symbol:"YiB", coefficient:1.20892581961463E+24}, {unitName:"zebibytes", symbol:"ZiB", coefficient:1.18059162071741E+21}, {unitName:"exbibytes", symbol:"EiB", coefficient:1.15292150460685E+18}, {unitName:"pebibytes", symbol:"PiB", coefficient:1.12589990684262E+15}, {unitName:"tebibytes", symbol:"TiB", coefficient:1.099511627776E+12}, {unitName:"gibibytes", symbol:"GiB", coefficient:1.073741824E+9}, {unitName:"mebibytes", symbol:"MiB", coefficient:1.048576E+6}, {unitName:"kibibytes", symbol:"kiB", coefficient:1024.0}, ¬
       {unitName:"yobibits", symbol:"Yibit", coefficient:1.51115727451829E+23}, {unitName:"zebibits", symbol:"Zibit", coefficient:1.47573952589676E+20}, {unitName:"exbibits", symbol:"Eibit", coefficient:1.44115188075856E+17}, {unitName:"pebibits", symbol:"Pibit", coefficient:1.40737488355328E+14}, {unitName:"tebibits", symbol:"Tibit", coefficient:1.37438953472E+11}, {unitName:"gibibits", symbol:"Gibit", coefficient:1.34217728E+8}, {unitName:"mebibits", symbol:"Mibit", coefficient:1.31072E+5}, {unitName:"kibibits", symbol:"Kibit", coefficient:128.0}}
end script


(* PUBLIC HANDLERS *)
-- These are inherited by the unit category script objects above and must be called as handlers of the relevant one.
-- See the comment at the top of this script for a parameter description key.

(* Convert a measurement to different units and return a result in the specified form. *)
-- Input: convertMeasurement:(measurement expression) toUnits:(unit expression) resultType:(result type expression).
-- Output: measurement object, number, text, or NSString.
on convertMeasurement:inputMeasurementExpression toUnits:outputUnitExpression resultType:resultTypeExpression
   set {numberOfInputUnits, inputUnitObject} to my getMeasurementObject:inputMeasurementExpression
   set outputUnitObject to my getUnitObject:outputUnitExpression
   
   set numberOfBaseUnits to inputUnitObject's converter's unitsToBaseUnits(numberOfInputUnits, inputUnitObject)
   set numberOfTargetUnits to outputUnitObject's converter's baseUnitsToTargetUnits(numberOfBaseUnits, outputUnitObject)
   set outputMeasurementObject to {numberOfTargetUnits, outputUnitObject}
   
   return measurementResult(outputMeasurementObject, resultTypeExpression)
end convertMeasurement:toUnits:resultType:

(* Add two measurements and return a result in the specified form in the units of the second measurement. *)
-- Input: addMeasurement:(measurement expression) toMeasurement:(measurement expression) resultType:(result type expression)
-- Output: measurement object, number, text, or NSString.
on addMeasurement:measurementExpression1 toMeasurement:measurementExpression2 resultType:resultTypeExpression
   return doArithmetic(1, measurementExpression1, measurementExpression2, resultTypeExpression)
end addMeasurement:toMeasurement:resultType:

(* Subtract one measurement from another and return a result in the specified form in the units of the second measurement. *)
-- Input: subtractMeasurement:(measurement expression) fromMeasurement:(measurement expression) resultType:(result type expression)
-- Output: measurement object, number, text, or NSString.
on subtractMeasurement:measurementExpression1 fromMeasurement:measurementExpression2 resultType:resultTypeExpression
   return doArithmetic(-1, measurementExpression1, measurementExpression2, resultTypeExpression)
end subtractMeasurement:fromMeasurement:resultType:

(* Derive a measurement object from the given expression. *)
-- Input: getMeasurementObject:(measurement expression)
-- Output: measurement object.
on getMeasurementObject:measurementExpression
   -- Get the measurement expression as an Objective-C object.
   set measurementExpression to (|⌘|'s class "NSArray"'s arrayWithObject:(measurementExpression))'s first item
   -- If it's an NSArray or NSString, extract the number and unit expression components. Otherwise error.
   if ((measurementExpression's isKindOfClass:(|⌘|'s class "NSArray")) as boolean) then
       if ((count measurementExpression) is not 2) then showError("Wrong number of items in measurement expression list parameter.")
       set {numberOfUnits, unitExpression} to measurementExpression as list
       try
           set numberOfUnits to numberOfUnits as real
       on error
           showError("Bad number in measurement expression list parameter.")
       end try
   else if ((measurementExpression's isKindOfClass:(|⌘|'s class "NSString")) as boolean) then
       set regexObject to |⌘|'s class "NSRegularExpression"'s regularExpressionWithPattern:("^\\s*+(-?[0-9][0-9.,]*+)\\s*+([a-zA-Z].++)$") options:(0) |error|:(missing value)
       set regexMatch to regexObject's firstMatchInString:(measurementExpression) options:(0) range:({0, measurementExpression's |length|()})
       if (regexMatch is missing value) then showError("Bad measurement expression string parameter:" & (linefeed & "“" & measurementExpression & "”."))
       set numberOfUnits to (regexObject's replacementStringForResult:(regexMatch) inString:(measurementExpression) offset:(0) template:("$1")) as text as real
       set unitExpression to regexObject's replacementStringForResult:(regexMatch) inString:(measurementExpression) offset:(0) template:("$2")
   else
       showError("Bad measurement expression parameter.")
   end if
   
   -- Get a "unit object" record matching the unit expression.
   set unitObject to my getUnitObject:unitExpression
   
   -- Return a "measurement object" list derived from what's been obtained.
   return {numberOfUnits, unitObject}
end getMeasurementObject:

(* Derive a unit object from the given expression. *)
-- Input: getUnitObject:(unit expression)
-- Output: unit object.
on getUnitObject:unitExpression
   -- Get the measurement expression as an Objective-C object.
   set unitExpression to (|⌘|'s class "NSArray"'s arrayWithObject:(unitExpression))'s first item
   if ((unitExpression's isKindOfClass:(|⌘|'s class "NSDictionary")) as boolean) then
       -- If it's an NSDictionary, the record form of this is what's needed.
       set unitObject to unitExpression as record
   else if ((unitExpression's isKindOfClass:(|⌘|'s class "NSString")) as boolean) then
       set unitName to unitExpression as text
       ignoring case
           -- If it's an NSString, ensure it's in a form which can be compared with the built-in unit names.
           set unitName to cleanUpUnitName(unitName)
           -- Search the current category script's 'unitData' list for a record containing the cleaned-up unit name.
           set unitObject to missing value
           repeat with i from 1 to (count my unitData)
               set theseData to item i of my unitData
               if ((theseData's unitName) = unitName) then
                   set unitObject to theseData
                   exit repeat
               end if
           end repeat
       end ignoring
       if (unitObject is missing value) then showError("No “" & unitExpression & "” unit in the specified category.")
   else
       showError("Bad unit expression parameter.")
   end if
   
   -- Add the default 'baseline' and 'converter' properties if not already included.
   return unitObject & {baseline:0, converter:linearConverter}
end getUnitObject:


(* PRIVATE HANDLERS *)
-- These are also inherited by the unit category script objects. Instances of 'my' refer to the script object through which a public handler has been called.

(* Do the business for the addMeasurement:toMeasurement:resultType: or subtractMeasurement:fromMeasurement:resultType: handlers above. *)
-- Input: (positive/negative multiplier (1 or -1), measurement expression, measurement expression, result type expression)
-- Output: measurement object, number, text, or NSString.
on doArithmetic(polarity, measurementExpression1, measurementExpression2, resultTypeExpression)
   -- Resolve the number of units and unit type from the second measurement expression.
   set {numberOfUnits2, unitObject2} to my getMeasurementObject:measurementExpression2
   -- To avoid a potential issue with reciprocal conversions, do the addition or subtraction in output units (those of measurement 2) rather than in base units.
   set numberOfUnits1 to my convertMeasurement:measurementExpression1 toUnits:unitObject2 resultType:"number"
   set totalNumberOfUnits to numberOfUnits2 + (numberOfUnits1 * polarity)
   -- Make an output measurement consisting of the result and the units of the second measurement.
   set outputMeasurementObject to {totalNumberOfUnits, unitObject2}
   
   return measurementResult(outputMeasurementObject, resultTypeExpression)
end doArithmetic

(* Clean up and return a possibly user-entered unit name. (Called from an 'ignoring case' condition in getUnitObject(), possibly via a 'continue' in a similarly named handler in a category script object.) *)
-- Input: Entered unit name as AS text.
-- Output: AS text.
on cleanUpUnitName(unitName)
   -- Remove any white space, Americanise any instances of "gramme", "metre", or "litre", and replace any instances of "/" with "per".
   set unitName to replaceText({space, tab, return, linefeed, character id 160}, "", unitName)
   set unitName to replaceText("gramme", "gram", unitName)
   set unitName to replaceText("litre", "liter", unitName)
   set unitName to replaceText("metre", "meter", unitName)
   set unitName to replaceText("/", "per", unitName)
   
   -- If the unit's given in the singular, pluralise it.
   -- Firstly get the part of the name which should be plural: ie. anything which comes before before "force", "per" (except in "imperial" or "ampere"), "mass", "troy" or one of the temperature scales. Otherwise the entire string.
   set astid to AppleScript's text item delimiters
   set AppleScript's text item delimiters to {"force", "per", "mass", "troy", "fahrenheit", "celsius", "centigrade", "kelvin"}
   set pluralPart to text item 1 of unitName
   set AppleScript's text item delimiters to astid
   if ((pluralPart is "") or (pluralPart is "im") or ((pluralPart ends with "am") and (pluralPart does not end with "gram"))) then set pluralPart to unitName
   -- If the plural part isn't already plural, and isn't not meant to be, deal with it.        
   if not ((pluralPart ends with "s") or (pluralPart ends with "feet") or (pluralPart ends with "hertz") or (pluralPart ends with "horsepower") or (pluralPart ends with "lux") or (pluralPart is "fahrenheit") or (pluralPart is "centigrade") or (pluralPart is "kelvin")) then
       set startOfTheRest to (count pluralPart) + 1
       -- Special-case "foot", "inch", or "y" preceded by a non-vowel. Otherwise simply add "s".
       if (pluralPart ends with "foot") then
           set pluralPart to text 1 thru -4 of pluralPart & "eet"
       else if (pluralPart ends with "inch") then
           set pluralPart to pluralPart & "es"
       else if ((pluralPart ends with "y") and (character -2 of pluralPart is not in "aeiou")) then
           set pluralPart to text 1 thru -2 of pluralPart & "ies"
       else
           set pluralPart to pluralPart & "s"
       end if
       -- If the pluralised part isn't the entire string, append the rest.
       if (startOfTheRest > (count unitName)) then
           set unitName to pluralPart
       else
           set unitName to pluralPart & text startOfTheRest thru -1 of unitName
       end if
   end if
   
   return unitName
end cleanUpUnitName

(* Replace instances of a particular substring in an AS text. *)
-- Input: (text to be replaced, replacement text, containing text)
-- Output: text.
on replaceText(textToGo, replacementText, containingText)
   set astid to AppleScript's text item delimiters
   set AppleScript's text item delimiters to textToGo
   set textItems to containingText's text items
   set AppleScript's text item delimiters to replacementText
   set containingText to textItems as text
   set AppleScript's text item delimiters to astid
   
   return containingText
end replaceText

(* Return whether or not the host computer's using a non-US gallon locale. *)
-- Output: boolean.
on hostUsingNonUSGallonLocale()
   -- Pre-Sierra compatible NSLocale code.
   set hostLocaleIdentifier to |⌘|'s class "NSLocale"'s currentLocale()'s localeIdentifier()
   set localCountryCode to ((|⌘|'s class "NSLocale"'s componentsFromLocaleIdentifier:(hostLocaleIdentifier))'s valueForKey:(|⌘|'s NSLocaleCountryCode)) as text
   -- "Other than the United States, the US gallon is still used in Belize, Colombia, The Dominican Republic, Ecuador, El Salvador, Guatemala, Haiti, Honduras, Liberia, Nicaragua, and Peru, but only for the sale of gasoline; all other products in these countries are sold in litres, or multiples and submultiples of a litre." — Wikipedia.
   considering case
       return (localCountryCode is not in {"US", "BZ", "CO", "DO", "EC", "SV", "GT", "HT", "HN", "LR", "NI", "PE"})
   end considering
end hostUsingNonUSGallonLocale

(* Return a measurement value either as itself, a number, text, or NSString, as indicated by the 'resultTypeExpression' parameter. *)
-- Input: (measurement object, result type expression as text or NSString)
-- Output: measurement object, number, text, or NSString.
on measurementResult(measurementObject, resultTypeExpression)
   -- If "number", "text", "string", "NSString","object", "measurement", or "measurement object" is specified, return the appropriate result. Otherwise error.
   set resultTypeExpressionOriginal to (|⌘|'s class "NSArray"'s arrayWithObject:(resultTypeExpression))'s firstObject()
   if (not ((resultTypeExpressionOriginal's isKindOfClass:(|⌘|'s class "NSString")) as boolean)) then showError("Bad result type parameter.")
   set resultTypeExpression to (resultTypeExpressionOriginal's stringByReplacingOccurrencesOfString:("\\s++") withString:("") options:(|⌘|'s NSRegularExpressionSearch) range:({0, resultTypeExpressionOriginal's |length|()})) as text
   ignoring case
       if (resultTypeExpression is "number") then return measurementObject's beginning
       if (resultTypeExpression is in {"text", "string", "NSString"}) then
           set {numberOfUnits, unitObject} to measurementObject
           if (numberOfUnits is "∞") then
               set formattedNumber to |⌘|'s class "NSString"'s stringWithString:(numberOfUnits)
           else
               set numberFormatter to |⌘|'s class "NSNumberFormatter"'s new()
               tell numberFormatter to setNumberStyle:(|⌘|'s NSNumberFormatterDecimalStyle)
               tell numberFormatter to setMaximumFractionDigits:(my maxDecimalPlacesInFormattedStringResults)
               tell numberFormatter to setLocalizesFormat:(true) -- The default?
               set formattedNumber to numberFormatter's stringFromNumber:(numberOfUnits)
           end if
           set stringResult to formattedNumber's stringByAppendingFormat_(" %@", unitObject's symbol)
           
           if (resultTypeExpression is "NSString") then return stringResult
           return stringResult as text
       end if
       if (resultTypeExpression is in {"object", "measurement", "measurementobject"}) then return measurementObject
   end ignoring
   showError("Unrecognised result type parameter:" & (linefeed & "“" & resultTypeExpressionOriginal & "”."))
end measurementResult

(* Display an error message and stop the script. *)
-- Input: error message.
-- Output: error number -128.
on showError(message)
   using terms from scripting additions
       display dialog message with title "MeasurementUnitLib:" with icon stop buttons {"OK"} default button 1 cancel button 1
   end using terms from
end showError

(* Deprecated labels for the unit category script objects. *)
property LengthScript : LengthCategory
property DistanceScript : LengthCategory
property AreaScript : AreaCategory
property VolumeScript : VolumeCategory
property AngleScript : AngleCategory
property MassScript : MassCategory
property PressureScript : PressureCategory
property AccelerationScript : AccelerationCategory
property DurationScript : DurationCategory
property FrequencyScript : FrequencyCategory
property SpeedScript : SpeedCategory
property EnergyScript : EnergyCategory
property PowerScript : PowerCategory
property TemperatureScript : TemperatureCategory
property IlluminanceScript : IlluminanceCategory
property ElectricalChargeScript : ElectricalChargeCategory
property ElectricChargeScript : ElectricalChargeCategory
property ElectricalCurrentScript : ElectricalCurrentCategory
property ElectricCurrentScript : ElectricalCurrentCategory
property ElectricalVoltageScript : ElectricalVoltageCategory
property ElectricPotentialDifferenceScript : ElectricalVoltageCategory
property ElectricalResistanceScript : ElectricalResistanceCategory
property ElectricResistanceScript : ElectricalResistanceCategory
property MassConcentrationScript : MassConcentrationCategory
property ConcentrationMassScript : MassConcentrationCategory
property VolumeConcentrationScript : VolumeConcentrationCategory
property DispersionScript : VolumeConcentrationCategory
property FuelConsumptionScript : FuelConsumptionCategory
property FuelEfficiencyScript : FuelConsumptionCategory
property InformationStorageScript : InformationStorageCategory

(* Deprecated labels for the three main public handlers. *)
property convert_to_withResultAs_ : convertMeasurement_toUnits_resultType_
property add_to_withResultAs_ : addMeasurement_toMeasurement_resultType_
property subtract_from_withResultAs_ : subtractMeasurement_fromMeasurement_resultType_

Last edited by Nigel Garvey (2020-02-10 06:29:32 am)


NG

Offline

 

#3 2020-01-09 05:54:09 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6461

Re: MeasurementUnitLib

Nigel Garvey wrote:

The overall impression is one of a good design either hurriedly or shoddily implemented.



Fair comment — I'd also add unevenly implemented.

You mention in passing that they're locale aware, but it's more than that. Many of these classes were introduced in iOS first, and their real goal is not to perform relatively trivial calculations, but to simplify — and encourage — localization. The idea is that a developer can deal with values in a way recognizable to any user without having to know what units are appropriate.

The unsatisfactory result here isn't due to floating-point issues but to the fact that NSLength's preset miles unit is (at the time of writing) defined as 1609.34 metres, whereas the true figure is 1609.344 metres. It's only a four-millimetre difference, but it's the difference between accurate results and rubbish.



What version of the OS did you test under? When I run your first script here (10.15.2) it does return 1760.0, not 1759.99562554681.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#4 2020-01-10 03:32:35 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

Shane Stanley wrote:

What version of the OS did you test under? When I run your first script here (10.15.2) it does return 1760.0, not 1759.99562554681.


Hi Shane.

Thanks for your interest.  smile

My results are from running in Mojave, 10.14.6, "en_GB" locale. On this system, the coefficient for the predefined miles unit is 1609.34, which is what latest Xcode documentation says it is, so I was assuming this was still the case in Catalina. Your result suggests it may have been corrected now — discounting some localisation for Australian miles or an incorrect yards coefficient in Catalina.  wink  The numbers should of course have been correct from the moment the classes were introduced to macOS in Sierra 10.12.


NG

Offline

 

#5 2020-01-10 05:40:34 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6461

Re: MeasurementUnitLib

Nigel Garvey wrote:

My results are from running in Mojave, 10.14.6, "en_GB" locale.



I don't see how the locale can have any affect — that would rather defeat the purpose — so maybe this is the first good thing I've found to say for Catalina neutral.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#6 2020-01-10 06:48:58 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

Shane Stanley wrote:
Nigel Garvey wrote:

My results are from running in Mojave, 10.14.6, "en_GB" locale.



I don't see how the locale can have any affect


Sorry. I meant "en_GB" to be understood in the context of my findings generally. As far as I know, only the formatters are locale-aware. The predefined measurement units are just what they are.

so maybe this is the first good thing I've found to say for Catalina neutral.


The prospect of a more accurate mile isn't enough to tempt me to upgrade.  wink


NG

Offline

 

#7 2020-01-10 07:09:33 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6461

Re: MeasurementUnitLib

Nigel Garvey wrote:

The prospect of a more accurate mile isn't enough to tempt me to upgrade.  wink



I'm shocked. wink


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#8 2020-01-13 04:30:01 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

I've added a couple of minor tweaks to the library code: "grammes" and derivatives are now accepted as alternative spellings for "grams" etc. and unit names can be expressed in the singular — eg. "1 mile per hour" rather than "1 miles per hour". The unit names themselves remain what they are. It's just the string input which gets interpreted.


NG

Offline

 

#9 2020-01-20 07:15:54 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

Further updates in post #2, principally:
• I've changed the labels of the three main handlers to make them look more ASObjC-like. But the old labels still work if anyone's actually using them.
• "tons" and "hundredweights" are now interpreted as the US or imperial units under the same criteria as for "gallons" and "pints", but can be explicitly specified as "short" or "long" for the US or imperial versions respectively.
• A couple of bug fixes.


NG

Offline

 

#10 2020-01-26 04:05:55 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

Here for the exercise is a JXA version of the library. It's taken a while to do — not just because I've had to learn enough JavaScript to do it, but because several working versions of it failed to work as libraries. It turns out that some kinds of data, such as Objective-C objects and functions within JavaScript objects, can't be accessed from opposite sides of the library/client interface. So the names of unit categories have to be passed as text parameters to the public functions and it's then up to the library to sort out the inheritance on its side of the divide. Similarly, returned "object" results now contain the names of the converters used — "linearConverter" or "reciprocalConverter" — instead of dead function references. These names can likewise be passed as strings if required. Custom converters can be specified, but it's extremely unlikely they'd be needed. Objective-C parameters and results still aren't possible.

Note that this version of the library is in JXA (Apple's JavaScript for Automation language), not AppleScript. It should be opened in Script Editor and compiled and saved with "JavaScript" selected in the window's "navigation bar" menu instead of "AppleScript". It can only be used with JXA client scripts. Here's some example client code:

Applescript:

/* This is a JXA script. */

var MULib = Library('MeasurementUnitLib_JXA'); // Equivalent to 'use MULib: script "MeasurementUnitLib"' in AppleScript.

MULib.convertMeasurementToUnitsCategoryResultType("1 gallon", "litres", "volume", "number");
// --> 4.54609 in imperial gallons locales. 3.785411784 in US gallon locales.

// Addition and subtraction results are in the units of the second measurement parameter.
MULib.addMeasurementToMeasurementCategoryResultType([2, "chains"], "20 miles", "distance", "text");
// --> "20.025 mi"
MULib.subtractMeasurementFromMeasurementCategoryResultType("250 milliseconds", "60 seconds", "duration", "text");
// --> "59.75 sec"

// Custom units can be used, usually without needing to specify the 'baseline' and 'converter' values (defaults: 0 and "linearConverter" respectively).
let rods = {unitName:"rods", symbol:"rod", coefficient: 5.0292}; // 1 rod, pole, or perch = 1/4 chains.
MULib.addMeasurementToMeasurementCategoryResultType([3, rods], "20 chains", "distance", "text");
// --> "20.75 ch"

// Returned "object" results are fully specified and can be used as parameters themselves.
chainsPlusRods = MULib.addMeasurementToMeasurementCategoryResultType([3, rods], "20 chains", "distance", "measurement object");
// --> [20.75, {"unitName":"chains", "symbol":"ch", "coefficient":20.1168, "baseline":0, "converter":"linearConverter"}]
milesPlusChainsPlusRods = MULib.addMeasurementToMeasurementCategoryResultType(chainsPlusRods, "5 miles", "distance", "text");
// --> "5.259 mi"

The library code:

Applescript:

/* MeasurementUnitLib_JXA by Nigel Garvey, December 2019-February 2020.

   Requires OS X 10.10 (Yosemite) or later.
   
   There are five public functions in this incarnation …
       convertMeasurementToUnitsCategoryResultType(measurement expression, unit expression, category expression, result type expression) --> (measurement object, number, or formatted text)
       addMeasurementToMeasurementCategoryResultType(measurement expression, measurement expression, category expression, result type expression) --> (measurement object, number, or formatted text)
       subtractMeasurementFromMeasurementCategoryResultType(measurement expression, measurement expression, category expression, result type expression) --> (measurement object, number, or formatted text)
       getMeasurementObjectCategory(measurementExpression, category expression) --> (measurement object)
       getUnitObjectCategort(unitExpression, category expression) --> (unit object)
           Key:
           'measurement object' : array containing number and unit object.
           'unit object' : object with properties for 'unitName', 'symbol', 'coefficient', 'baseline', and 'converter'.
           'measurement expression' : Array containing number and unit expression/text containing number and unit name.
           'unit expression' : Text containing unit name.
           'category expression' : text. One of the unit category names below. Case insensitive
           'result type expression' : text. One of "number"/("text" or "string")/("measurement object" or "measurement" or "object"). Case insenstive.
   
   … and twenty-two unit categories to which to apply them. Some have alternative names which can be used if preferred:
       "Length" (or "Distance")        "Power"
       "Area"                            "Temperature"
       "Volume"                        "Illuminance"
       "Angle"                            "ElectricalCharge" (or "ElectricCharge"
       "Mass"                            "ElectricalCurrent" (or "ElectricCurrent")
       "Pressure"                        "Voltage" (or "ElectricPotentialDifference")
       "Acceleration"                    "Resistance" (or "ElectricResistance")
       "Duration"                        "MassConcentration" (or "ConcentrationMass")
       "Frequency"                        "VolumeConcentration" (or "Dispersion")
       "Speed"                            "FuelConsumption" (or "FuelEfficiency")
       "Energy"                        "InformationStorage"
       
   Passed unit names should largely match the dromedary-case names specified in the script objects, but some leeway is allowed with cases and spaces. English or American spellings may be used for "metres", "litres", and "grammes" and slashes can be used instead of the word "per". Additionally, "amp" is accepted for "ampere" and the names of the temperature scales may optionally be prefaced with the word "degrees" as in the AS unit coercions. Unqualified terms for liquid measures such as "gallons" and "pints" are interpreted as imperial or US units according to the locale set on the host machine, but either can be specified explicitly if required: eg. "US gallons", "imperial pints". Unit names can also be passed in the singular. Formatted strings returned by the functions aren't suitable as input for them.
*/

var app = Application.currentApplication();
app.includeStandardAdditions = true;
// JXA in El Capitan (and probably other systems) doesn't understand 'let', so all the variables here are 'var's.


/* PUBLIC FUNCTIONS */

/* Convert a measurement to different units and return a result in the specified form. */
// Input: (measurement expression, unit expression, category expression, result type xpression).
// Output: measurement object, number, or text.
function convertMeasurementToUnitsCategoryResultType(measurementExpression, unitExpression, categoryExpression, resultTypeExpression) {
   return category(categoryExpression).convertMeasurementToUnitsResultType(measurementExpression, unitExpression, resultTypeExpression);
};

/* Add two measurements and return a result in the specified form in the units of the second measurement. */
// Input: (measurement expression, measurement expression, category expression, result type xpression).
// Output: measurement object, number, or text.
function addMeasurementToMeasurementCategoryResultType(measurementExpression1, measurementExpression2, categoryExpression, resultTypeExpression) {
   return category(categoryExpression).doArithmetic(1, measurementExpression1, measurementExpression2, resultTypeExpression);
};

/* Subtract one measurement from another and return a result in the specified form in the units of the second measurement. */
// Input: (measurement expression, measurement expression, category expression, result type xpression).
// Output: measurement object, number, or text.
function subtractMeasurementFromMeasurementCategoryResultType(measurementExpression1, measurementExpression2, categoryExpression, resultTypeExpression) {
   return category(categoryExpression).doArithmetic(-1, measurementExpression1, measurementExpression2, resultTypeExpression);
};

/* Derive a measurement object from the given measurement expression. */
// Input: (measurement expression, category expression)
// Output: measurement object.
function getMeasurementObjectCategory(measurementExpression, categoryExpression) {
   var categoryObject = category(categoryExpression);
   var measurementObject = categoryObject.getMeasurementObject(measurementExpression);
   return categoryObject.measurementResult(measurementObject, "object");
};

/* Derive a unit object from the given unit expression. */
// Input: (unit expression, category expression)
// Output: unit object.
function getUnitObjectCategory(unitExpression, categoryExpression) {
   var categoryObject = category(categoryExpression);
   var unitObject = categoryObject.getUnitObject(unitExpression);
   return categoryObject.measurementResult([0, unitObject], "object")[1];
};

/* PRIVATE FUNCTIONS */

/* Given a unit-category name, return a matching category object. */
// Input: gategory expression (text).
// Output: category object.
function category(categoryExpression) {
   var unitData, anythingElse = function(unitName) { return unitName; }; // Variables to be used at the end, with default value for 'anythingElse'.
   
   // Use a despaced, upper-case version of the input to locate the category code. (Upper-casing for "case-insensitive" comparisons recommended by Mozilla.)
   switch(categoryExpression.replace(/\s+/g, "").toUpperCase()) {
       case "LENGTH":
       case "DISTANCE":
           // Base unit: meters.
           // Units: megameters, kilometers, hectometers, decameters, meters, decimeters, centimeters, millimeters, micrometers, nanometers, picometers, inches, feet, yards, miles, scandinavianMiles, lightyears [sic], nauticalMiles, fathoms, furlongs, astronomicalUnits, parsecs, chains.
           unitData = [{unitName:"megameters", symbol:"Mm", coefficient:1.0E+6}, {unitName:"kilometers", symbol:"km", coefficient:1000.0}, {unitName:"hectometers", symbol:"hm", coefficient:100.0}, {unitName:"decameters", symbol:"dam", coefficient:10.0}, {unitName:"meters", symbol:"m", coefficient:1.0}, {unitName:"decimeters", symbol:"dm", coefficient:0.1}, {unitName:"centimeters", symbol:"cm", coefficient:0.01}, {unitName:"millimeters", symbol:"mm", coefficient:1.0E-3}, {unitName:"micrometers", symbol:"µm", coefficient:1.0E-6}, {unitName:"nanometers", symbol:"nm", coefficient:1.0E-9}, {unitName:"picometers", symbol:"pm", coefficient:1.0E-12}, {unitName:"inches", symbol:"in", coefficient:0.0254}, {unitName:"feet", symbol:"ft", coefficient:0.3048}, {unitName:"yards", symbol:"yd", coefficient:0.9144}, {unitName:"miles", symbol:"mi", coefficient:1609.344}, {unitName:"scandinavianMiles", symbol:"smi", coefficient:1.0E+4}, {unitName:"lightyears", symbol:"ly", coefficient:9.4607304725808E+15}, {unitName:"nauticalMiles", symbol:"NM", coefficient:1852.0}, {unitName:"fathoms", symbol:"ftm", coefficient:1.8288}, {unitName:"furlongs", symbol:"fur", coefficient:201.168}, {unitName:"astronomicalUnits", symbol:"au", coefficient:1.495987707E+11}, {unitName:"parsecs", symbol:"pc", coefficient:3.08567758149137E+16}, {unitName:"chains", symbol:"ch", coefficient:20.1168}];
           break;
           
       case "AREA":
           // Base unit: squareMeters.
           // Units: squareMegameters, squareKilometers, squareMeters, squareCentimeters, squareMillimeters, squareMicrometers, squareNanometers, squareInches, squareFeet, squareYards, squareMiles, acres, ares, hectares.
           unitData = [{unitName:"squareMegameters", symbol:"Mm²", coefficient:1.0E+12}, {unitName:"squareKilometers", symbol:"km²", coefficient:1.0E+6}, {unitName:"squareMeters", symbol:"m²", coefficient:1.0}, {unitName:"squareCentimeters", symbol:"cm²", coefficient:1.0E-4}, {unitName:"squareMillimeters", symbol:"mm²", coefficient:1.0E-6}, {unitName:"squareMicrometers", symbol:"µm²", coefficient:1.0E-12}, {unitName:"squareNanometers", symbol:"nm²", coefficient:1.0E-18}, {unitName:"squareInches", symbol:"in²", coefficient:6.4516E-4}, {unitName:"squareFeet", symbol:"ft²", coefficient:0.09290304}, {unitName:"squareYards", symbol:"yd²", coefficient:0.83612736}, {unitName:"squareMiles", symbol:"mi²", coefficient:2.589988110336E+6}, {unitName:"acres", symbol:"ac", coefficient:4046.8564224}, {unitName:"ares", symbol:"a", coefficient:100.0}, {unitName:"hectares", symbol:"ha", coefficient:10000}];
           break;
           
       case "VOLUME":
           // Base unit: liters.
           // Units: megaliters, kiloliters, liters, deciliters, centiliters, milliliters, cubicKilometers, cubicMeters, cubicDecimeters, cubicCentimeters, cubicMillimeters, cubicInches, cubicFeet, cubicYards, cubicMiles, acreFeet, bushels, teaspoons, tablespoons, fluidOunces, cups, pints, quarts, gallons, imperialTeaspoons, imperialTablespoons, imperialFluidOunces, imperialPints, imperialQuarts, imperialGallons, metricCups, imperialCups, imperialBushels, metricTeaspoons, metricTablespoons, acreFeet.
           unitData = [{unitName:"megaliters", symbol:"ML", coefficient:1.0E+6}, {unitName:"kiloliters", symbol:"kL", coefficient:1000.0}, {unitName:"liters", symbol:"L", coefficient:1.0}, {unitName:"deciliters", symbol:"dL", coefficient:0.1}, {unitName:"centiliters", symbol:"cL", coefficient:0.01}, {unitName:"milliliters", symbol:"mL", coefficient:1.0E-3}, {unitName:"cubicKilometers", symbol:"km³", coefficient:1.0E+12}, {unitName:"cubicMeters", symbol:"m³", coefficient:1000.0}, {unitName:"cubicDecimeters", symbol:"dm³", coefficient:1.0}, {unitName:"cubicCentimeters", symbol:"cm³", coefficient:1.0E-3}, {unitName:"cubicMillimeters", symbol:"mm³", coefficient:1.0E-6},
           {unitName:"cubicInches", symbol:"in³", coefficient:0.016387064}, {unitName:"cubicFeet", symbol:"ft³", coefficient:28.316846592}, {unitName:"cubicYards", symbol:"yd³", coefficient:764.554857984}, {unitName:"cubicMiles", symbol:"mi³", coefficient:4.16818182544058E+12}, {unitName:"acreFeet", symbol:"ac•ft", coefficient:1.23348183754752E+6},
           {unitName:"teaspoons", symbol:"tsp", coefficient:4.92892159375 / 1000}, {unitName:"tablespoons", symbol:"tbsp", coefficient:4.92892159375 * 3 / 1000}, {unitName:"fluidOunces", symbol:"fl oz", coefficient:0.029573529562}, {unitName:"cups", symbol:"cup", coefficient:0.2365882365}, {unitName:"pints", symbol:"pt", coefficient:0.473176473}, {unitName:"quarts", symbol:"qt", coefficient:0.946352946}, {unitName:"gallons", symbol:"gal", coefficient:3.785411784}, {unitName:"imperialTeaspoons", symbol:"tsp", coefficient:0.005}, {unitName:"imperialTablespoons", symbol:"tbsp", coefficient:0.015}, {unitName:"imperialFluidOunces", symbol:"fl oz", coefficient:0.028413062}, {unitName:"imperialPints", symbol:"pt", coefficient:0.56826125}, {unitName:"imperialQuarts", symbol:"qt", coefficient:1.1365225}, {unitName:"imperialGallons", symbol:"gal", coefficient:4.54609}, {unitName:"metricCups", symbol:"metric cup", coefficient:0.25},
           {unitName:"imperialCups", symbol:"cup", coefficient:0.284130625}, {unitName:"imperialBushels", symbol:"bsh", coefficient:36.36872}, {unitName:"metricTeaspoons", symbol:"tsb", coefficient:0.005}, {unitName:"metricTablespoons", symbol:"tbsp", coefficient:0.015}];
           anythingElse = function(unitName) { // Input: upper-case string.
               if ((unitName === "TEASPOONS") || (unitName === "TABLESPOONS") || (unitName === "FLUIDOUNCES") || (unitName === "PINTS") || (unitName === "QUARTS") || (unitName === "GALLONS") || (unitName === "CUPS") || (unitName === "BUSHELS")) {
                   if (this.hostUsingNonUSGallonLocale()) { unitName = "IMPERIAL" + unitName; };
               } else if (unitName.startsWith("US")) {
                   unitName = unitName.substring(2, unitName.length);
               };
               return unitName;
           };
           break;
           
       case "ANGLE":
           // Base unit: degrees.
           // Units: degrees, arcMinutes, arcSeconds, radians, gradians, revolutions, gons.
           unitData = [{unitName:"degrees", symbol:"°", coefficient:1.0}, {unitName:"arcMinutes", symbol:"'", coefficient:1 / 60}, {unitName:"arcSeconds", symbol:"\"", coefficient:1 / 3600}, {unitName:"radians", symbol:"rad", coefficient:245850922 / 78256779 / 180}, {unitName:"gradians", symbol:"L", coefficient:1}, {unitName:"revolutions", symbol:"rev", coefficient:360.0}, {unitName:"gons", symbol:"gon", coefficient:0.9}];
           break;
           
       case "MASS":
           // Base unit: kilograms.
           // Units: kilograms, grams, decigrams, centigrams, milligrams, micrograms, nanograms, picograms, ounces, pounds, poundsMass, stones, metricTons, shortTons, carats, ouncesTroy, slugs, quarters, hundredweights, metricHundredweights, shortHundredweights, tons, tonnes, troyOunces.
           unitData = [{unitName:"kilograms", symbol:"kg", coefficient:1.0}, {unitName:"grams", symbol:"g", coefficient:1.0E-3}, {unitName:"decigrams", symbol:"dg", coefficient:1.0E-4}, {unitName:"centigrams", symbol:"cg", coefficient:1.0E-5}, {unitName:"milligrams", symbol:"mg", coefficient:1.0E-6}, {unitName:"micrograms", symbol:"µg", coefficient:1.0E-9}, {unitName:"nanograms", symbol:"ng", coefficient:1.0E-12}, {unitName:"picograms", symbol:"pg", coefficient:1.0E-15}, {unitName:"ounces", symbol:"oz", coefficient:0.028349523125}, {unitName:"pounds", symbol:"lb", coefficient:0.45359237}, {unitName:"poundsMass", symbol:"lb", coefficient:0.45359237}, {unitName:"stones", symbol:"st", coefficient:6.35029318}, {unitName:"metricTons", symbol:"t", coefficient:1000}, {unitName:"shortTons", symbol:"ton", coefficient:907.18474}, {unitName:"carats", symbol:"ct", coefficient:2.0E-4}, {unitName:"ouncesTroy", symbol:"oz t", coefficient:0.0311034768}, {unitName:"quarters", symbol:"qr", coefficient:12.70058636}, {unitName:"hundredweights", symbol:"cwt", coefficient:50.80234544}, {unitName:"metricHundredweights", symbol:"cwt", coefficient:50.0}, {unitName:"shortHundredweights", symbol:"cwt", coefficient:45.359237}, {unitName:"tons", symbol:"ton", coefficient:1016.0469088}, {unitName:"tonnes", symbol:"t", coefficient:1000}, {unitName:"troyOunces", symbol:"oz t", coefficient:0.0311034768}];
           anythingElse = function(unitName) { // Input: upper-case string.
               if ((unitName === "TONS") || (unitName === "HUNDREDWEIGHTS")) {
                   if (! this.hostUsingNonUSGallonLocale()) { unitName = "SHORT" + unitName; }; // Host IS using US-gallon locale.
               } else if (unitName.startsWith("LONG")) {
                   unitName = unitName.substring(4, unitName.length);
               };
               return unitName;
           };
           break;
           
       case "PRESSURE":
           // Base unit: newtonsPerMeterSquared (equivalent both to pascals and to joules per cubic meter.
           // Units: newtonsPerMeterSquared, gigapascals, megapascals, kilopascals, hectopascals, inchesOfMercury, bars, millibars, millimetersOfMercury, poundsPerSquareInch, poundsForcePerSquareInch, pascals, newtonsPerSquareMeter, joulesPerCubicMeter, torrs, atmospheres.
           unitData = [{unitName:"newtonsPerMeterSquared", symbol:"N/m²", coefficient:1.0}, {unitName:"gigapascals", symbol:"GPa", coefficient:1.0E+9}, {unitName:"megapascals", symbol:"MPa", coefficient:1.0E+6}, {unitName:"kilopascals", symbol:"kPa", coefficient:1000.0}, {unitName:"hectopascals", symbol:"hPa", coefficient:100.0}, {unitName:"bars", symbol:"bar", coefficient:1.0E+5}, {unitName:"millibars", symbol:"mbar", coefficient:100.0}, {unitName:"millimetersOfMercury", symbol:"mmHg", coefficient:133.322387415}, {unitName:"poundsPerSquareInch", symbol:"psi", coefficient:6894.757293168}, {unitName:"poundsForcePerSquareInch", symbol:"psi", coefficient:6894.757293168}, {unitName:"pascals", symbol:"Pa", coefficient:1.0}, {unitName:"newtonsPerSquareMeter", symbol:"N/m²", coefficient:1.0}, {unitName:"joulesPerCubicMeter", symbol:"J/m³", coefficient:1.0}, {unitName:"torrs", symbol:"Torr", coefficient:133.322368421053}, {unitName:"atmospheres", symbol:"atm", coefficient:101325}];
           break;
           
       case "ACCELERATION":
           // Base unit: metersPerSecondSquared.
           // Units: metersPerSecondSquared, gravities, feetPerSecondPerSecond, feetPerSecondSquared, metersPerSecondPerSecond, milesPerHourPerSecond.
           unitData = [{unitName:"metersPerSecondSquared", symbol:"m/s²", coefficient:1.0}, {unitName:"gravities", symbol:"g", coefficient:9.80665}, {unitName:"feetPerSecondPerSecond", symbol:"ft/s²", coefficient:0.3048}, {unitName:"feetPerSecondSquared", symbol:"ft/s²", coefficient:0.3048}, {unitName:"metersPerSecondPerSecond", symbol:"m/s²", coefficient:1.0}, {unitName:"milesPerHourPerSecond", symbol:"mph/s", coefficient:0.44704}];
           break;
           
       case "DURATION":
           // Base unit: seconds.
           // Units: seconds, minutes, hours, milliseconds, microseconds, nanoseconds, picoseconds.
           unitData = [{unitName:"seconds", symbol:"sec", coefficient:1.0}, {unitName:"minutes", symbol:"min", coefficient:60.0}, {unitName:"hours", symbol:"hr", coefficient:3600.0}, {unitName:"milliseconds", symbol:"ms", coefficient:1.0E-3}, {unitName:"microseconds", symbol:"µs", coefficient:1.0E-6}, {unitName:"nanoseconds", symbol:"ns", coefficient:1.0E-9}, {unitName:"picoseconds", symbol:"ps", coefficient:1.0E-12}];
           break;
           
       case "FREQUENCY":
           // Base unit: hertz.
           // Units: terahertz, gigahertz, megahertz, kilohertz, hertz, millihertz, microhertz, nanohertz, framesPerSecond.
           unitData = [{unitName:"terahertz", symbol:"THz", coefficient:1.0E+12}, {unitName:"gigahertz", symbol:"GHz", coefficient:1.0E+9}, {unitName:"megahertz", symbol:"MHz", coefficient:1.0E+6}, {unitName:"kilohertz", symbol:"kHz", coefficient:1000.0}, {unitName:"hertz", symbol:"Hz", coefficient:1.0}, {unitName:"millihertz", symbol:"Hz", coefficient:1.0E-3}, {unitName:"microhertz", symbol:"µHz", coefficient:1.0E-6}, {unitName:"nanohertz", symbol:"nHz", coefficient:1.0E-9}, {unitName:"framesPerSecond", symbol:"FPS", coefficient:1.0}];
           break;
           
       case "SPEED":
           // Base unit: metersPerSecond.
           // Units: metersPerSecond, kilometersPerHour, milesPerHour, knots, nauticalMilesPerHour (same as knots).
           unitData = [{unitName:"metersPerSecond", symbol:"m/s", coefficient:1.0}, {unitName:"kilometersPerHour", symbol:"km/h", coefficient:1000 / 3600}, {unitName:"milesPerHour", symbol:"mph", coefficient:0.44704}, {unitName:"knots", symbol:"kn", coefficient:1852 / 3600}, {unitName:"nauticalMilesPerHour", symbol:"NM/h", coefficient:1852 / 3600}];
           break;
           
       case "ENERGY":
           // Base unit: kilojoules.
           // Units: kilojoules, joules, kilocalories, calories, kilowattHours, largeCalories, foodCalories, smallCalories (alternative terms for kilocalories and calories).
           unitData = [{unitName:"kilojoules", symbol:"kJ", coefficient:1000.0}, {unitName:"joules", symbol:"J", coefficient:1.0}, {unitName:"kilocalories", symbol:"kcal", coefficient:4184.0}, {unitName:"calories", symbol:"cal", coefficient:4.184}, {unitName:"kilowattHours", symbol:"kWh", coefficient:3.6E+6}, {unitName:"largeCalories", symbol:"Cal", coefficient:4184.0}, {unitName:"foodCalories", symbol:"kcal", coefficient:4184.0}, {unitName:"smallCalories", symbol:"cal", coefficient:4.184}];
           break;
           
       case "POWER":
           // Base unit: watts.
           // Units: terawatts, gigawatts, megawatts, kilowatts, watts, milliwatts, microwatts, nanowatts, picowatts, femtowatts, horsepower, mechanicalHorsepower, imperialHorsepower, hydraulicHorsepower, airHorsepower, metricHorsepower, electricalHorsepower, metricHorsepower.
           initDate = [{unitName:"terawatts", symbol:"TW", coefficient:1.0E+12}, {unitName:"gigawatts", symbol:"GW", coefficient:1.0E+9}, {unitName:"megawatts", symbol:"MW", coefficient:1.0E+6}, {unitName:"kilowatts", symbol:"kW", coefficient:1000.0}, {unitName:"watts", symbol:"W", coefficient:1.0}, {unitName:"milliwatts", symbol:"mW", coefficient:1.0E-3}, {unitName:"nanowatts", symbol:"nW", coefficient:1.0E-9}, {unitName:"picowatts", symbol:"pW", coefficient:1.0E-12}, {unitName:"femtowatts", symbol:"fW", coefficient:1.0E-15}, {unitName:"horsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"mechanicalHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"imperialHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"hydraulicHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"airHorsepower", symbol:"hp(I)", coefficient:745.69987158227}, {unitName:"metricHorsepower", symbol:"hp(M)", coefficient:735.49875}, {unitName:"electricalHorsepower", symbol:"hp(E)", coefficient:746}, {unitName:"boilerHorsepower", symbol:"hp(S)", coefficient:9812.5}];
           break;
           
       case "TEMPERATURE":
           // Base unit: kelvin.
           // "Units": kelvin, fahrenheit, celsius. (The word "degrees" is accepted before the scale names. "Centigrade" is accepted for "Celsius".)
           unitData = [{unitName:"kelvin", symbol:"K", coefficient:1.0}, {unitName:"fahrenheit", symbol:"°F", coefficient:5 / 9, baseline:273.15 - 32 * 5 / 9}, {unitName:"celsius", symbol:"°C", coefficient:1.0, baseline:273.15}];
           anythingElse = function(unitName) { // Input: upper-case string.
               return unitName.replace("DEGREES", "").replace("CENTIGRADE", "CELSIUS");
           };
           break;
           
       case "ILLUMINANCE":
           // Base unit: lux.
           // Units: lux, lumensPerSquareMeter (same as lux).
           unitData = [{unitName:"lux", symbol:"lx", coefficient:1.0}, {unitName:"lumensPerSquareMeter", symbol:"lm/m²", coefficient:1.0}];
           break;
           
       case "ELECTRICALCHARGE":
       case "ELECTRICCHARGE":
           // Base unit: coulombs (amperes per second).
           // Units: coulombs, megaampereHours, kiloampereHours, ampereHours, milliampereHours, microampereHours. ("amp" is accepted for "ampere".)
           unitData = [{unitName:"coulombs", symbol:"C", coefficient:1.0}, {unitName:"megaampereHours", symbol:"MAh", coefficient:3.6E+9}, {unitName:"kiloampereHours", symbol:"kAh", coefficient:3.6E+6}, {unitName:"ampereHours", symbol:"Ah", coefficient:3600.0}, {unitName:"milliampereHours", symbol:"mAh", coefficient:3.6}, {unitName:"microampereHours", symbol:"µAh", coefficient:0.0036}];
           anythingElse = function(unitName) { // Input: upper-case string.
               return unitName.replace(/AMP(?!ERE)/, "AMPERE");
           };
           break;
           
       case "ELECTRICALCURRENT":
       case "ELECTRICCURRENT":
           // Base unit: amperes.
           // Units: megaamperes, kiloamperes, amperes, milliamperes, microamperes. ("amp" is accepted for "ampere".)
           unitData = [{unitName:"megaamperes", symbol:"MA", coefficient:1.0E+6}, {unitName:"kiloamperes", symbol:"kA", coefficient:1000.0}, {unitName:"amperes", symbol:"A", coefficient:1.0}, {unitName:"milliamperes", symbol:"mA", coefficient:1.0E-3}, {unitName:"microamperes", symbol:"µA", coefficient:1.0E-6}];
           anythingElse = function(unitName) { // Input: upper-case string.
               return unitName.replace("AMPS", "AMPERES");
           };
           break;
           
       case "ELECTRICALVOLTAGE":
       case "ELECTRICPOTENTIALDIFFERENCE":
           // Base unit: volts.
           // Units: megavolts, kilovolts, volts, millivolts, microvolts.
           unitData = [{unitName:"megavolts", symbol:"MV", coefficient:1.0E+6}, {unitName:"kilovolts", symbol:"kV", coefficient:1000.0}, {unitName:"volts", symbol:"V", coefficient:1.0}, {unitName:"millivolts", symbol:"mV", coefficient:1.0E-3}, {unitName:"microvolts", symbol:"µV", coefficient:1.0E-6}];
           break;
           
       case "ELECTRICALRESISTANCE":
       case "ELECTRICRESISTANCE":
           // Base unit: ohms.
           // Units: megaohms, kiloohms, ohms, milliohms, microohms.
           unitData = [{unitName:"megaohms", symbol:"MΩ", coefficient:1.0E+6}, {unitName:"kiloohms", symbol:"kΩ", coefficient:1000.0}, {unitName:"ohms", symbol:"Ω", coefficient:1.0}, {unitName:"milliohms", symbol:"mΩ", coefficient:1.0E-3}, {unitName:"microohms", symbol:"µΩ", coefficient:1.0E-6}];
           break;
           
       case "MASSCONCENTRATION":
       case "CONCENTRATIONMASS":
           // Base unit: gramsPerLiter.
           // Units: gramsPerLiter, milligramsPerDeciliter.
           unitData = [{unitName:"gramsPerLiter", symbol:"g/L", coefficient:1.0}, {unitName:"milligramsPerDeciliter", symbol:"mg/dL", coefficient:0.01}];
           break;
           
       case "VOLUMECONCENTRATION":
       case "DISPERSION":
           // Base unit: partsPerMillion.
           // Units: partsPerMillion.
           unitData = [{unitName:"partsPerMillion", symbol:"ppm", coefficient:1.0}];
           break;
           
       case "FUELCONSUMPTION":
       case "FUELEFFICIENCY":
           // Base unit: litersPer100kilometers.
           // Units: litersPer100kilometers, milesPerGallon, milesPerImperialGallon.
           var reciprocalFunction = function(numberOfUnits, unitObject) { return unitObject.coefficient / numberOfUnits }; // JavaScript copes with dividing by 0 or Infinity by itself!
           var reciprocalConverter = makeConverterObject("reciprocalConverter", reciprocalFunction, reciprocalFunction);
           unitData = [{unitName:"litersPer100Kilometers", symbol:"L/100km", coefficient:1}, {unitName:"milesPerGallon", symbol:"mpg", coefficient:3.785411784 * 62.137119223733, converter:reciprocalConverter}, {unitName:"milesPerImperialGallon", symbol:"mpg", coefficient:4.54609 * 62.137119223733, converter:reciprocalConverter}];
           anythingElse = function(unitName) { // Input: upper-case string.
               if (unitName === "MILESPERGALLON") {
                   if (this.hostUsingNonUSGallonLocale()) { unitName = "MILESPERIMPERIALGALLON"; };
               } else if (unitName === "MILESPERUSGALLON") {
                   unitName = "MILESPERGALLON";
               } else if (unitName === ("LITERSPERHUNDREDKILOMETERS")) {
                   unitName = "LITERSPER100KILOMETERS";
               };
               return unitName;
           };
           break;
           
       case "INFORMATIONSTORAGE":
           // Base unit: bytes.
           // Units: bytes, bits, nibbles, yottabytes, zettabytes, exabytes, petabytes, terabytes, gigabytes, megabytes, kilobytes, yottabits, zettabits, exabits, petabits, terabits, gigabits, megabits, kilobits, yobibytes, zebibytes, exbibytes, pebibytes, tebibytes, gibibytes, mebibytes, kibibytes, yobibits, zebibits, exbibits, pebibits, tebibits, gibibits, mebibits, kibibits.
           unitData = [{unitName:"bytes", symbol:"B", coefficient:1.0}, {unitName:"bits", symbol:"bit", coefficient:0.125}, {unitName:"nibbles", symbol:"nibble", coefficient:0.5},
           {unitName:"yottabytes", symbol:"YB", coefficient:1.0E+24}, {unitName:"zettabytes", symbol:"ZB", coefficient:1.0E+21}, {unitName:"exabytes", symbol:"EB", coefficient:1.0E+18}, {unitName:"petabytes", symbol:"PB", coefficient:1.0E+15}, {unitName:"terabytes", symbol:"TB", coefficient:1.0E+12}, {unitName:"gigabytes", symbol:"GB", coefficient:1.0E+9}, {unitName:"megabytes", symbol:"MB", coefficient:1.0E+6}, {unitName:"kilobytes", symbol:"kB", coefficient:1000.0},
           {unitName:"yottabits", symbol:"Ybit", coefficient:1.25E+23}, {unitName:"zettabits", symbol:"Zbit", coefficient:1.25E+20}, {unitName:"exabits", symbol:"Ebit", coefficient:1.25E+17}, {unitName:"petabits", symbol:"Pbit", coefficient:1.25E+14}, {unitName:"terabits", symbol:"Tbit", coefficient:1.25E+11}, {unitName:"gigabits", symbol:"Gbit", coefficient:1.25E+8}, {unitName:"megabits", symbol:"Mbit", coefficient:1.25E+5}, {unitName:"kilobits", symbol:"kbit", coefficient:125.0},
           {unitName:"yobibytes", symbol:"YiB", coefficient:1.20892581961463E+24}, {unitName:"zebibytes", symbol:"ZiB", coefficient:1.18059162071741E+21}, {unitName:"exbibytes", symbol:"EiB", coefficient:1.15292150460685E+18}, {unitName:"pebibytes", symbol:"PiB", coefficient:1.12589990684262E+15}, {unitName:"tebibytes", symbol:"TiB", coefficient:1.099511627776E+12}, {unitName:"gibibytes", symbol:"GiB", coefficient:1.073741824E+9}, {unitName:"mebibytes", symbol:"MiB", coefficient:1.048576E+6}, {unitName:"kibibytes", symbol:"kiB", coefficient:1024.0},
           {unitName:"yobibits", symbol:"Yibit", coefficient:1.51115727451829E+23}, {unitName:"zebibits", symbol:"Zibit", coefficient:1.47573952589676E+20}, {unitName:"exbibits", symbol:"Eibit", coefficient:1.44115188075856E+17}, {unitName:"pebibits", symbol:"Pibit", coefficient:1.40737488355328E+14}, {unitName:"tebibits", symbol:"Tibit", coefficient:1.37438953472E+11}, {unitName:"gibibits", symbol:"Gibit", coefficient:1.34217728E+8}, {unitName:"mebibits", symbol:"Mibit", coefficient:1.31072E+5}, {unitName:"kibibits", symbol:"Kibit", coefficient:128.0}];
           break;
           
       default:
           showError("Unrecognised category name parameter:\n“" + categoryExpression + "”.");
   };
   
   return makeCategoryObject(unitData, anythingElse);
}; // end category

// The following two functions are simply wrappers to keep the 'class' variables they contain local and non-persistent. It makes no difference when the script's used as a library, but it saves having to recompile between successive test runs of the code in Script Editor!

/* Construct a unit-category object. */
// Input: unit data and anythingElse() function selected by the category() function above.
// Output: category object of the required type with appropriate data and functions.
function makeCategoryObject(unitData, anythingElse){
   class UnitCategory {
       constructor(unitData, anythingElse) {
           this.maxDecimalPlacesInFormattedStringResults = 3;
           this.unitData = unitData;
           this.anythingElse = anythingElse;
           this.linearConverter = makeConverterObject(
               "linearConverter",
               function(numberOfUnits, unitObject) { return numberOfUnits * unitObject.coefficient + unitObject.baseline; },
               function(numberOfUnits, unitObject) { return (numberOfUnits - unitObject.baseline) / unitObject.coefficient; }
           );
       };
       
       /* Convert a measurement to different units and return a result in the specified form. */
       // Input: convertMeasurement:(measurement expression) toUnits:(unit expression) resultType:(result type expression).
       // Output: measurement object, number, or text.
       convertMeasurementToUnitsResultType(inputMeasurementExpression, outputUnitExpression, resultTypeExpression) {
           var [numberOfInputUnits, inputUnitObject] = this.getMeasurementObject(inputMeasurementExpression);
           var outputUnitObject = this.getUnitObject(outputUnitExpression);
           
           var numberOfBaseUnits = inputUnitObject.converter.unitsToBaseUnits(numberOfInputUnits, inputUnitObject);
           var numberOfTargetUnits = outputUnitObject.converter.baseUnitsToTargetUnits(numberOfBaseUnits, outputUnitObject);
           var outputMeasurementObject = [numberOfTargetUnits, outputUnitObject];
           
           return this.measurementResult(outputMeasurementObject, resultTypeExpression);
       };
       
       /* Derive a measurement object from the given expression. */
       // Input: measurement expression.
       // Output: measurement object.
       getMeasurementObject(measurementExpression) {
           // If it's an array or a string, extract the number and unit expression components. Otherwise error.
           var className = measurementExpression.constructor.name;
           if (className === "Array") {
               if (measurementExpression.length !== 2) { showError("Wrong number of items in measurement expression list parameter."); };
               var numberOfUnits = Number(measurementExpression[0]);
               if (numberOfUnits === NaN) { showError("Bad number in measurement expression list parameter."); };
               var unitExpression = measurementExpression[1];
           } else if (className = "String") {
               var regexPattern = /^\s*(-?[0-9][0-9.,]*)\s*([a-zA-Z].+)$/;
               var regexMatch = regexPattern.exec(measurementExpression);
               if (regexMatch === null) { showError("Bad measurement expression string parameter:\n“" + measurementExpression + "”."); };
               var numberOfUnits = Number(regexMatch[1]);
               var unitExpression =regexMatch[2];
           } else {
               showError("Bad measurement expression parameter.");
           };
           
           // Get a "unit object" matching the unit expression.
           var unitObject = this.getUnitObject(unitExpression);
           
           return [numberOfUnits, unitObject];
       };
       
       /* Derive a unit object from the given expression. */
       // Input: getUnitObject
       getUnitObject(unitExpression) {
           var className = unitExpression.constructor.name;
           if (className === "Object") {
               // If it's already an "Object", this is what's needed.
               var unitObject = unitExpression;
           } else if (className === "String") {        
               // If it's a string, ensure it's in a form which can be compared with the built-in unit names.
               var unitName = this.cleanUpUnitName(unitExpression); // Upper-case string, all one word.
               // Search the current category object's 'unitData' array for a unit object containing the cleaned-up unit name.
               for (var theseData of this.unitData) {
                   if (theseData.unitName.toUpperCase() === unitName) {
                       var unitObject = theseData;
                       break;
                   };
               };
               if (unitObject === undefined) { showError("No “" + unitExpression + "” unit in the specified category."); };
           } else {
               showError("Bad unit expression parameter.");
           };
           
           // Add the default 'baseline' property if not already included.
           if (unitObject.baseline === undefined) { unitObject.baseline = 0; };
           // Take appropriate action over the 'converter' property.
           var converterExpression = unitObject.converter;
           if (converterExpression === undefined) {
               // The converter's not defined. Use the default linear converter.
               unitObject.converter = this.linearConverter;
           } else {
               var className = converterExpression.constructor.name;
               if (className === "Converter") {
                   // The unit's a preset with a reciprocal converter. No need to do anything.
               } else if (className === "String") {
                   // The client script has specified the converter as a string parameter.
                   var upperCaseConverterExpression = converterExpression.replace(/\s+/g, "").toUpperCase()
                   if (upperCaseConverterExpression === "LINEARCONVERTER") {
                       unitObject.converter = this.linearConverter;
                   } else if (upperCaseConverterExpression === "RECIPROCALCONVERTER") {
                       var reciprocalFunction = function(numberOfUnits, unitObject) { return unitObject.coefficient / numberOfUnits; };
                       unitObject.converter = makeConverterObject("reciprocalConverter", reciprocalFunction, reciprocalFunction);
                   } else {
                       // Interpret a string representation of the required converter object.
                       var converterObject = Function('"use strict";return (' + converterExpression.toString() + ')')();
                       if (converterObject.name === undefined) converterObject.name = "(customConverter)";
                       // The object works as is, but a formally constructed one includes a toString() function to return the source code if required.
                       unitObject.converter = makeConverterObject(converterObject.name, converterObject.unitsToBaseUnits, converterObject.baseUnitsToTargetUnits);
                   };
               } else {
                   showError("Bad converter expression parameter.");
               };
           };
                       
           return unitObject;
       };
       
       /* Do the business for the addMeasurementToMeasurementCategoryResultType() and subtractMeasurementFomMeasurementCategoryResultType() functions. */
       // Input: (positive/negative multiplier (1 or -1), measurement expression, measurement expression, result type expression)
       // Output: measurement object, number, or text.
       doArithmetic(polarity, measurementExpression1, measurementExpression2, resultTypeExpression) {
           // Resolve the number of units and unit type from the second measurement expression.

           var [numberOfUnits2, unitObject2] = this.getMeasurementObject(measurementExpression2);
           // To avoid a potential issue with reciprocal conversions, do the addition or subtraction in output units (those of measurement 2) rather than in base units.
           var numberOfUnits1 = this.convertMeasurementToUnitsResultType(measurementExpression1, unitObject2, "number");
           var totalNumberOfUnits = numberOfUnits2 + (numberOfUnits1 * polarity);
           // Make an output measurement consisting of the result and the units of the second measurement.
           var outputMeasurementObject = [totalNumberOfUnits, unitObject2];
   
       return this.measurementResult(outputMeasurementObject, resultTypeExpression);
       };
       
       /* Clean up a possibly user-entered unit name and return it as lower-case text. */
       // Input: Entered unit name.
       // Output: Doctored unit name (lower case throughout).
       cleanUpUnitName(unitName) {
           // Get the input as an upper-case string.
           unitName = unitName.toUpperCase();
           // Remove any white space, Americanise any instances of "GRAMME", "METRE", or "LITRE", and replace any instances of "/" with "PER".
           unitName = unitName.replace(/[\s-]+/g, "");
           unitName = unitName.replace(/GRAMME/g, "GRAM");
           unitName = unitName.replace(/(MET|LIT)RE/g, "$1ER");
           unitName = unitName.replace(/\//g, "PER");
           
           // If the unit's given in the singular, pluralise it.
           // Firstly get the part of the name which should be plural: ie. anything which comes before before "FORCE", "PER" (except in "IMPERIAL" or "AMPERE"), "MASS", "TROY" or one of the temperature scales. Otherwise the entire string.
           var pluralPart = unitName.replace(/\B(?:FORCE|PER|MASS|TROY|FAHRENHEIT|CELSIUS|CENTIGRADE|KELVIN).*/, "");
           if ((pluralPart === "IM") || ((pluralPart.endsWith("AM")) && (!pluralPart.endsWith("GRAM")))) { pluralPart = unitName; };
           // If the plural part isn't already plural, and isn't not meant to be, deal with it.
           if (!/(S|FEET|FAHRENHEIT|CENTIGRADE|KELVIN|HERTZ|HORSEPOWER|LUX)$/.test(pluralPart)) {
               var startOfTheRest = pluralPart.length;
               // Special-case "FOOT", "INCH", or "Y" preceded by a non-vowel. Otherwise simply add "S".
               if (pluralPart.endsWith("FOOT")) {
                   pluralPart = pluralPart.replace(/FOOT$/, "FEET");
               } else if (pluralPart.endsWith("INCH")) {
                   pluralPart = pluralPart + "ES";
               } else if (/[^AEIOU]Y$/.test(pluralPart)) {
                   pluralPart = pluralPart.replace(/Y$/, "IES");
               } else {
                   pluralPart = pluralPart + "S";
               };
               // If the pluralised part isn't the entire string, append the rest.
               unitName = pluralPart + unitName.substring(startOfTheRest);
           };
   
           // Do any further edits required by the category currently inheriting these functions and return the edited string.
           return this.anythingElse(unitName);
       };
               
       /* Return whether or not the host computer's using a non-US gallon locale. */
       // Output: boolean.
       hostUsingNonUSGallonLocale() {
           // Pre-Sierra compatible NSLocale code.
           var hostLocaleIdentifier = $.NSLocale.currentLocale.localeIdentifier;
           var localCountryCode = $.NSLocale.componentsFromLocaleIdentifier(hostLocaleIdentifier).valueForKey($.NSLocaleCountryCode).js;
           // "Other than the United States, the US gallon is still used in Belize, Colombia, The Dominican Republic, Ecuador, El Salvador, Guatemala, Haiti, Honduras, Liberia, Nicaragua, and Peru, but only for the sale of gasoline; all other products in these countries are sold in litres, or multiples and submultiples of a litre." — Wikipedia.
           return (! ["US", "BZ", "CO", "DO", "EC", "SV", "GT", "HT", "HN", "LR", "NI", "PE"].includes(localCountryCode));
       };
       
       /* Return a measurement value either as itself, a number, or text, as indicated by the 'resultTypeExpression' parameter. */
       // Input: (measurement object, result type expression as text)
       // Output: measurement object, number, text, or NSString.
       measurementResult(measurementObject, resultTypeExpression) {
           // If "number", "text", "string", "object", "measurement", or "measurement object" is specified, return the appropriate result. Otherwise error.
           var resultTypeExpressionOriginal = ObjC.unwrap(resultTypeExpression)
           if (resultTypeExpression.constructor.name !== "String") { showError("Bad result type parameter."); };
           resultTypeExpression = resultTypeExpressionOriginal.replace(/\s+/g, "").toUpperCase(); // Mozilla recommends upper case rather than lower.
           if (resultTypeExpression === "NUMBER") { return measurementObject[0]; };
           if ((resultTypeExpression === "TEXT") || (resultTypeExpression === "STRING")) {
               var [numberOfUnits, unitObject] = measurementObject;
               if (numberOfUnits === Infinity) {
                   var formattedNumber = "∞";
               } else {
                   var numberFormatter = $.NSNumberFormatter.new;
                   numberFormatter.setNumberStyle($.NSNumberFormatterDecimalStyle);
                   numberFormatter.setMaximumFractionDigits(this.maxDecimalPlacesInFormattedStringResults);
                   numberFormatter.setLocalizesFormat(true); // The default?
                   var formattedNumber = numberFormatter.stringFromNumber(numberOfUnits).js;
               };
       
               return formattedNumber + " " + unitObject.symbol;
           } else if (["OBJECT", "MEASUREMENT", "MEASUREMENTOBJECT"].includes(resultTypeExpression)) {
               // Convert the 'converter' entry in the measurement object's unit component into a string which can be passed back to the client script.
               // If the converter's 'name' is of one of the two built-in converters, use that. Otherwise a string representation of the convertor object.
               var converterName = measurementObject[1].converter.name;
               measurementObject[1].converter = ((converterName === "linearConverter") || (converterName === "reciprocalConverter")) ? converterName : measurementObject[1].converter.toString();
               return measurementObject;
           };
           showError("Unrecognised result type parameter:\n“" + resultTypeExpressionOriginal + "”.");
       };
   };
   
   return new UnitCategory(unitData, anythingElse);
}; // end makeCategoryObject

/* Construct a converter object. */
// Input: (converter name, unitsToBaseUnits() function, baseUnitsToTargetUnits() function)
// Output: Converter object.
function makeConverterObject(name, unitsToBaseUnits, baseUnitsToTargetUnits) {
   class Converter {
       constructor(name, unitsToBaseUnits, baseUnitsToTargetUnits) {
           this.name = name;
           this.unitsToBaseUnits = unitsToBaseUnits;
           this.baseUnitsToTargetUnits = baseUnitsToTargetUnits;
       };
       toString() {
           return "{name:'" + name + "', unitsToBaseUnits:" + unitsToBaseUnits + ", baseUnitsToTargetUnits:" + baseUnitsToTargetUnits + "}";
       }
   };

   return new Converter(name, unitsToBaseUnits, baseUnitsToTargetUnits);
};

/* Display an error message and stop the script. */
// Input: error message.
// Output: error number -128.
function showError(message) {
   app.displayDialog(message, {withTitle:"MeasurementUnitLib_JXA:", withIcon:"stop", buttons:["OK"], defaultButton:1, cancelButton:1});
};

Last edited by Nigel Garvey (2020-02-10 06:31:53 am)


NG

Offline

 

#11 2020-01-26 04:57:22 pm

Fredrik71
Member
Registered: 2019-10-23
Posts: 397

Re: MeasurementUnitLib

Nigel Garvey wrote:



Nigel are you on steroid to convince Americans to use real European measure.
If you ask me and real bakers also in America they always use grams. So no CUP of tea to me smile

Very impressed... and you did all that in your sleep.

Last edited by Fredrik71 (2020-01-26 04:58:38 pm)


I could teach you to cook but I couldn't do anything if you do not have desire or commitment for it.

Offline

 

#12 2020-01-26 05:35:16 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

Hi Frederik71

Fredrik71 wrote:

Nigel are you on steroid to convince Americans to use real European measure.


No, Not really.  smile  The idea was to produce something which would work with users' own assumptions (as long as they're English speakers  wink ) rather than those of Apple's software writers. I doubt the scripts will actually see a lot of use, but it's been interesting writing them and I've learnt some JavaScript in the process! I suppose I should sort out some proper error trapping for them. At the moment, if anything goes wrong, they just report the error without giving much of a clue what caused it.

... and you did all that in your sleep.


Well. Instead of my sleep anyway. I have a bit of cold at the moment.  sad


NG

Offline

 

#13 2020-01-27 09:51:52 am

Fredrik71
Member
Registered: 2019-10-23
Posts: 397

Re: MeasurementUnitLib

Nigel...

Get well soon or in Swedish: Krya på dig



Have you check if HealthKit, could be used in AppleScript smile

Apple HealthKit'
https://developer.apple.com/documentati … dentifiers

ex.

get nutrition of (250 grams of "cow meat") smile

or

Array of ingredients with properties of name, weight (1 item per line)
for every item get nutrition at api database
total amount of.... fat, sugar...

if the total fat and sugar is to high... beep 3 times... smile

I had in paste a measure scale that gave me all this, but the problem was the quality of exact measure was so different, so I return it... money back.

Now I use request api to web url to get data.

I'm a strong believer that nutrition knowledge are benefit for the health.

Last edited by Fredrik71 (2020-01-27 09:55:14 am)


I could teach you to cook but I couldn't do anything if you do not have desire or commitment for it.

Offline

 

#14 2020-01-27 05:57:10 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

I wrote:

I suppose I should sort out some proper error trapping for them. At the moment, if anything goes wrong, they just report the error without giving much of a clue what caused it.


Now done. It's input checking rather than error trapping, but hey.

Fredrik71 wrote:

Get well soon or in Swedish: Krya på dig


Tack.  smile


That looks like one of the first things I'd turn off if I ever got an iPhone!  wink  I'm old enough to remember when phones were for speaking to people far away and watches were for telling the time. I saw a review of the Apple Watch 2 on YouTube a few years ago where the reviewer — apparently not realising what he was saying — remarked that: "The new processor inside the watch makes it run much faster."  lol


NG

Offline

 

#15 2020-01-27 09:48:18 pm

Fredrik71
Member
Registered: 2019-10-23
Posts: 397

Re: MeasurementUnitLib

Nigel Garvey wrote:


"The new processor inside the watch makes it run much faster."  lol



haha... I'm also old enough to know my father first mobile was Storno 9000 with battery
I think the weight was 12kg, and the battery was more heavier and the phone. smile

And nobody complain...


I could teach you to cook but I couldn't do anything if you do not have desire or commitment for it.

Offline

 

#16 2020-01-29 05:09:44 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

To add to the confusion, I've now decided that the names of the unit category script objects in the post #2 version should end with "Category" rather than "Script" — LengthCategory, AreaCategory, etc. However, the old labels still work and I've no immediate plans to remove them. For consistency, a "string" result is now AS text rather than an NSString. Both scripts have received minor cosmetic tweaks.


NG

Offline

 

#17 2020-02-01 06:53:16 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

I've now worked out what to do about getting usable JavaScript functions through the library/client barrier and have updated post #10 accordingly. The JXA version of the library now also has public functions matching the ASObjC version's getMeasurementObject() and getUnitObject() handlers.


NG

Offline

 

#18 2020-02-10 06:36:36 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5284

Re: MeasurementUnitLib

A couple of minor errors and omissions fixed. Both scripts now use "vanilla" methods instead of the original NSString stuff to massage and match the input text, which is slightly faster. In the AS script (post #2), the anythingElse() handler used in conjunction with cleanUpUnitName() has been replaced with a more elegant continue approach. This leaves open the possibilty of limiting the "gramme", "litre", "metre", and "/" edits  to the script objects where they're relevant; but at the moment, I prefer them to be all in one place.

Nothing which affects how the scripts are used, though.


NG

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)