Tuesday, January 21, 2020

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

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

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: 5147

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 via 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:

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

Applescript:

use MULib : script "MeasurementUnitLib"

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

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

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

set AngleScript to MULib's AngleScript
set newAngle to AngleScript'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 want. 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 "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 some variant of "kilometersperhour", "kilometers per hour", "kilometers/hour", "kilometres/hour", etc. "amp" or "ampere" are both accepted in the names of 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" is now also accepted as an alternative spelling for "gram". Units can be expressed in the singular if preferred — eg. "1 kilogramme" instead of "1 kilogrammes", "1 mile per hour", etc. "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 US and imperial weights respectively.

If the library doesn't have a unit you require, 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 LengthScript to MULib's LengthScript

-- LengthScript'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 public handlers:

LengthScript's convertMeasurement:"4 kilometres" toUnits:decimiles resultType:"text"
--> "24.855 dmi"

LengthScript'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/January 2020.

   Requires macOS 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 ("measurement object"/"measurement"/"object")/"number"/"text"/("NSString"/"string").
           Equivalent NSArrays, NSDictionaries, and NSStrings are also accepted.
           *)

   
   … but twenty-two unit-category script objects through which to call them. Some have alternative labels which can be used if preferred:
       LengthScript (or DistanceScript)
       AreaScript
       VolumeScript
       AngleScript
       MassScript
       PressureScript
       AccelerationScript
       DurationScript
       FrequencyScript
       SpeedScript
       EnergyScript
       PowerScript
       TemperatureScript
       IlluminanceScript
       ElectricalChargeScript (or ElectricChargeScript)
       ElectricalCurrentScript (or ElectricCurrentScript)
       ElectricalVoltageScript (or ElectricPotentialDifferenceScript)
       ElectricalResistanceScript (or ElectricResistanceScript)
       MassConcentrationScript (or ConcentrationMassScript)
       VolumeConcentrationScript (or DispersionScript)
       FuelConsumptionScript (or FuelEfficiencyScript)
       InformationStorageScript
       
   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 LengthScript's convertMeasurement:"80 kilometres" toUnits:"miles" resultType:"number"
       set totalHours to MULib's DurationScript'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" -- Mavericks (10.9) 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 *)

(* LENGTH *)
script LengthScript
   -- 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}}
   property preCapitalRegexGroup : "(ical|scandinavian)"
end script
property DistanceScript : LengthScript -- Alternative label.

(* AREA *)
script AreaScript
   -- 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}}
   property preCapitalRegexGroup : "(square)"
end script

(* VOLUME *)
script VolumeScript
   -- Base unit: liters.
   -- Units: megaliters, kiloliters, liters, deciliters, centiliters, millilitres, cubicKilometers, cubicMeters, cubicDecimeters, 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:"millilitres", 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:"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}}
   property preCapitalRegexGroup : "(cubic|acre|fluid|^us(?=gal|tea|tab|flu|pin|qua)|imperial|metric)"
   
   on anythingElse(unitName) -- Input: NSMutableString.
       set ambiguousUnitNames to |⌘|'s class "NSArray"'s arrayWithArray:({"teaspoons", "tablespoons", "fluidOunces", "pints", "quarts", "gallons", "cups", "bushels"})
       if ((ambiguousUnitNames's containsObject:(unitName)) as boolean) then
           if (hostUsingNonUSGallonLocale()) then
               set imperialUnitNames to |⌘|'s class "NSArray"'s arrayWithArray:({"imperialTeaspoons", "imperialTablespoons", "imperialFluidOunces", "imperialPints", "imperialQuarts", "imperialGallons", "imperialCups", "imperialBushels"})
               tell unitName to setString:(imperialUnitNames's objectAtIndex:(ambiguousUnitNames's indexOfObject:(unitName)))
           end if
       else
           set USUnitNames to |⌘|'s class "NSArray"'s arrayWithArray:({"usTeaspoons", "usTablespoons", "usFluidOunces", "usPints", "usQuarts", "usGallons", "usCups", "usBushels"})
           if ((USUnitNames's containsObject:(unitName)) as boolean) then tell unitName to setString:(ambiguousUnitNames's objectAtIndex:(USUnitNames's indexOfObject:(unitName)))
       end if
   end anythingElse
end script

(* ANGLE *)
script AngleScript
   -- 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}}
   property preCapitalRegexGroup : "(arc)"
end script

(* MASS *)
script MassScript
   -- 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}}
   property preCapitalRegexGroup : "(metric|short|long|ounces|troy)"
   
   on anythingElse(unitName) -- Input: NSMutableString.
       set ambiguousUnitNames to |⌘|'s class "NSArray"'s arrayWithArray:({"tons", "hundredweights"})
       if ((ambiguousUnitNames's containsObject:(unitName)) as boolean) then
           if (not hostUsingNonUSGallonLocale()) then -- Host using US-gallon locale.
               set USUnitNames to |⌘|'s class "NSArray"'s arrayWithArray:({"shortTons", "shortHundredweights"})
               tell unitName to setString:(imperialUnitNames's objectAtIndex:(ambiguousUnitNames's indexOfObject:(unitName)))
           end if
       else
           set imperialUnitNames to |⌘|'s class "NSArray"'s arrayWithArray:({"longTons", "longHundredweights"})
           if ((imperialUnitNames's containsObject:(unitName)) as boolean) then tell unitName to setString:(ambiguousUnitNames's objectAtIndex:(imperialUnitNames's indexOfObject:(unitName)))
       end if
   end anythingElse
end script

(* PRESSURE *)
script PressureScript
   -- Base unit: newtonsPerMeterSquared (equivalent both to pascals and to joules per cubic metre).
   -- 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}}
   property preCapitalRegexGroup : "(s(?=of|per)|per|meter(?=sq)|of|square(?!d$)|cubic)"
end script

(* ACCELERATION *)
script AccelerationScript
   -- 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}}
   property preCapitalRegexGroup : "(meters|feet|miles|per|second|hour)"
end script

(* DURATION *)
script DurationScript
   -- 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}}
   property preCapitalRegexGroup : missing value
end script

(* FREQUENCY *)
script FrequencyScript
   -- 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}}
   property preCapitalRegexGroup : "(frames|per)"
end script

(* SPEED *)
script SpeedScript
   -- 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}}
   property preCapitalRegexGroup : "(ical|miles|meters|(?<=s)per)"
end script

(* ENERGY *)
script EnergyScript
   -- Base unit: kilojoules.
   -- Predefined: kilojoules, joules, kilocalories, calories, kilowattHours.
   -- Added here: largeCalorie, foodCalorie, smallCalorie (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:"largeCalorie", symbol:"KCal", coefficient:4184.0}, {unitName:"foodCalorie", symbol:"KCal", coefficient:4184.0}, {unitName:"smallCalorie", symbol:"cal", coefficient:4.184}}
   property preCapitalRegexGroup : "(watt)"
end script

(* POWER *)
script PowerScript
   -- 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}}
   property preCapitalRegexGroup : "(mechanical|imperial|hydraulic|air|metric|electrical|boiler)"
end script

(* TEMPERATURE *)
script TemperatureScript
   -- Base unit: kelvin.
   -- "Units": kelvin, fahrenheit, celsius, centigrade (alternative to celsius). (The word "degrees" is accepted before the scale names.)
   property unitData : {{unitName:"kelvin", symbol:"K", coefficient:1.0, baseline:0}, {unitName:"fahrenheit", symbol:"°F", coefficient:5 / 9, baseline:273.15 - 32 * 5 / 9}, {unitName:"celsius", symbol:"°C", coefficient:1.0, baseline:273.15}, {unitName:"centigrade", symbol:"°C", coefficient:1.0, baseline:273.15}}
   property preCapitalRegexGroup : missing value
   
   on anythingElse(unitName) -- Input: NSMutableString.
       tell unitName to replaceOccurrencesOfString:("degrees") withString:("") options:(0) range:({0, its |length|()})
   end anythingElse
end script

(* ILLUMINANCE *)
script IlluminanceScript
   -- Base unit: lux, lumensPerSquareMeter (same as lux).
   property unitData : {{unitName:"lux", symbol:"lx", coefficient:1.0}, {unitName:"lumensPerSquareMeter", symbol:"lm/m²", coefficient:1.0}}
   property preCapitalRegexGroup : "(lumens|per|square)"
end script

(* ELECTRICAL CHARGE *)
script ElectricalChargeScript
   -- 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}}
   property preCapitalRegexGroup : "(amp(?:ere)?)"
   
   on anythingElse(unitName) -- Input: NSMutableString.
       tell unitName to replaceOccurrencesOfString:("amp(?!ere)") withString:("ampere") options:(|⌘|'s NSRegularExpressionSearch) range:({0, its |length|()})
   end anythingElse
end script
property ElectricChargeScript : ElectricalChargeScript -- Alternative label.

(* ELECTRICAL CURRENT *)
script ElectricalCurrentScript
   -- 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}}
   property preCapitalRegexGroup : missing value
   
   on anythingElse(unitName) -- Input: NSMutableString.
       tell unitName to replaceOccurrencesOfString:("amps") withString:("amperes") options:(0) range:({0, its |length|()})
   end anythingElse
end script
property ElectricCurrentScript : ElectricalCurrentScript -- Alternative label.

(* ELECTRICAL VOLTAGE *)
script ElectricalVoltageScript
   -- 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}}
   property preCapitalRegexGroup : missing value
end script
property ElectricPotentialDifferenceScript : ElectricalVoltageScript -- Alternative label.

(* ELECTRICAL RESISTANCE *)
script ElectricalResistanceScript
   -- 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}}
   property preCapitalRegexGroup : missing value
end script
property ElectricResistanceScript : ElectricalResistanceScript -- Alternative label.

(* MASS CONCENTRATION *)
script MassConcentrationScript
   -- Base unit: gramsPerLiter.
   -- Predefined: gramsPerLiter, milligramsPerDeciliter.
   property unitData : {{unitName:"gramsPerLiter", symbol:"g/L", coefficient:1.0}, {unitName:"milligramsPerDeciliter", symbol:"mg/dL", coefficient:0.01}}
   property preCapitalRegexGroup : "(grams|per)"
end script
property ConcentrationMassScript : MassConcentrationScript -- Alternative label.

(* VOLUME CONCENTRATION *)
script VolumeConcentrationScript
   -- Base unit: partsPerMillion.
   -- Units: partsPerMillion.
   property unitData : {{unitName:"partsPerMillion", symbol:"ppm", coefficient:1.0}}
   property preCapitalRegexGroup : "(parts|per)"
end script
property DispersionScript : VolumeConcentrationScript -- Alternative label.

(* FUEL CONSUMPTION *)
script FuelConsumptionScript
   -- Base unit: litersPer100Kilometers.
   -- Units: litresPer100Kilometres, 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}}
   property preCapitalRegexGroup : "(miles|liters|(?<=s)per|us|imperial)"
   
   on anythingElse(unitName) -- Input: NSMutableString.
       if (((unitName's isEqualToString:("milesPerGallon")) as boolean) and (hostUsingNonUSGallonLocale())) then
           tell unitName to setString:("milesPerImperialGallon")
       else if ((unitName's isEqualToString:("milesPerUsGallon")) as boolean) then
           tell unitName to setString:("milesPerGallon")
       end if
   end anythingElse
end script
property FuelEfficiencyScript : FuelConsumptionScript -- Alternative label.

(* INFORMATION STORAGE *)
script InformationStorageScript
   -- 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:"B", 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}}
   property preCapitalRegexGroup : missing value
end script


(* PUBLIC HANDLERS *)
-- These are inherited by the 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
   if (measurementExpression's class is list) then
       set {numberOfUnits, unitExpression} to measurementExpression
   else if (measurementExpression's class is text) then
       set {numberOfUnits, unitExpression} to {(text 1 thru word 1 of measurementExpression) as number, text from word 2 to end of measurementExpression}
   else if ((((measurementExpression's isKindOfClass:(|⌘|'s class "NSArray")) as boolean) and (count measurementExpression) is 2)) then
       set {numberOfUnits, unitExpression} to measurementExpression
   else if ((measurementExpression's isKindOfClass:(|⌘|'s class "NSString")) as boolean) then
       set measurementExpression to measurementExpression as text
       set {numberOfUnits, unitExpression} to {(text 1 thru word 1 of measurementExpression) as number, text from word 2 to end of measurementExpression}
   else
       error
   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
   if (unitExpression's class is record) then
       return unitExpression & {baseline:0, converter:linearConverter}
       -- If the input's text or NSString, get and return the indicated unit. Otherwise, if the input IS such an unit, return it as is.
   else if ((unitExpression's class is text) or ((unitExpression's isKindOfClass:(|⌘|'s class "NSString")) as boolean)) then -- Will error if 'unit' is an AS class other than text.
       set unitName to cleanUpUnitName(unitExpression)
       -- Get an NSMutableArray version of the current script object's unitData list and filter for a dictionary containing the cleaned-up unit name.
       set unitData to |⌘|'s class "NSMutableArray"'s arrayWithArray:(my unitData)
       set filter to |⌘|'s class "NSPredicate"'s predicateWithFormat_("unitName == %@", unitName)
       tell unitData to filterUsingPredicate:(filter)
       -- If such a dictionary exists, derive a "unit object" record from it, concatenating in the default baseline and converter settings if none specified.
       if ((count unitData) > 0) then return ((unitData's firstObject()) as record) & {baseline:0, converter:linearConverter}
   else if ((unitExpression's isKindOfClass:(|⌘|'s class "NSDictionary")) as boolean) then
       return (unitExpression as record) & {baseline:0, converter:linearConverter}
   end if
   
   error
end getUnitObject:


(* PRIVATE HANDLERS *)
-- Like the public handlers, these are inherited by the script objects above. Instances of 'my' refer to the script object through which a handler's 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 a possibly user-entered unit name and derive a dromedary-case NS(Mutable)String from it. *)
-- Input: (Unit name as text or NSString, regex group indicating matches in the name which will need to be followed by capitals (or missing value to keep everything lower-cased)).
-- Output: NSMutableString.
on cleanUpUnitName(unitName)
   -- Firstly get the input as a lower-case NSMutableString. The lowercaseString of an NSMutableString is itself mutable on my machines. Uncomment "'s mutableCopy()" if this isn't universal!
   set unitName to (|⌘|'s class "NSMutableString"'s stringWithString:(unitName))'s lowercaseString() --'s mutableCopy()
   -- Americanise any English spellings.
   set regexSearch to |⌘|'s NSRegularExpressionSearch
   tell unitName to replaceOccurrencesOfString:("(?<=met|lit)re") withString:("er") options:(regexSearch) range:({0, its |length|()})
   -- Replace any instances of "/" with "per".
   tell unitName to replaceOccurrencesOfString:("/") withString:("per") options:(0) range:({0, unitName's |length|()})
   -- Remove any existing white space or hyphens. Convert any instances of "gramme" to "gram".
   tell unitName to replaceOccurrencesOfString:("[\\s-]++|(?<=gram)me") withString:("") options:(regexSearch) range:({0, its |length|()})
   
   -- If the unit's given in the singular, pluralise it. Firstly special-case "foot" to "feet" and "inch" to "inches" where appropriate.
   tell unitName to replaceOccurrencesOfString:("(?<=^(?:cubic|square|acre)?)foot(?=per|$)") withString:("feet") options:(regexSearch) range:({0, its |length|()})
   tell unitName to replaceOccurrencesOfString:("(?<=^(?:cubic|square)?)inch(?=per|$)") withString:("inches") options:(regexSearch) range:({0, its |length|()})
   -- Locate the first occurrence of the word "per" which isn't at the beginning of the string and doesn't follow either "am" (as in "ampere", unless it's in "gram") or "im" (as in "imperial").
   set perRange to unitName's rangeOfString:("(?<!^|(?:(?<!gr)a|i)m)per") options:(regexSearch) range:({0, unitName's |length|()})
   -- If there is such an occurrence, the plural ending should be immediately before it. Otherwise at the end of the string.
   if (perRange's |length|() > 0) then
       set pluralEndingLocation to perRange's location()
   else
       set pluralEndingLocation to unitName's |length|()
   end if
   -- Is there a "y" in that location, not preceded by a vowel?
   set singularYRange to unitName's rangeOfString:("(?<=[^aeiou])y$") options:(regexSearch) range:({0, pluralEndingLocation})
   -- If so, change it to "ies". Otherwise, unless there's already an "s", or "feet", one of the temperature scales, "hertz", "horsepower", or "lux", insert "s".
   if (singularYRange's |length|() > 0) then
       tell unitName to replaceCharactersInRange:(singularYRange) withString:("ies")
   else
       tell unitName to replaceOccurrencesOfString:("(?<!s|feet|fahrenheit|celsius|centigrade|kelvin|hertz|horsepower|lux)$") withString:("s") options:(regexSearch) range:({0, pluralEndingLocation})
   end if
   
   -- Insert spaces after any matches to the script object's 'preCapitalRegexGroup' regex psttern.
   if (my preCapitalRegexGroup is not missing value) then tell unitName to replaceOccurrencesOfString:(my preCapitalRegexGroup) withString:("$1 ") options:(regexSearch) range:({0, its |length|()})
   -- Any spaces?
   set spaceRange to unitName's rangeOfString:(space)
   if (spaceRange's |length|() > 0) then
       -- If so, extract and capitalise (ie. title-case) everything after the first one.
       set capitalisationRange to {spaceRange's location(), (unitName's |length|()) - (spaceRange's location())}
       set capitalisedWords to (unitName's substringWithRange:(capitalisationRange))'s capitalizedString()
       -- Put the capitalised stuff back into the string and delete all the spaces.
       tell unitName to replaceCharactersInRange:(capitalisationRange) withString:(capitalisedWords)
       tell unitName to replaceOccurrencesOfString:(space) withString:("") options:(0) range:({0, its |length|()})
   end if
   -- Do any further edits required by the script object currently exercising inheritance of these handlers.
   anythingElse(unitName)
   
   return unitName
end cleanUpUnitName

(* Blank anythingElse() handler for script objects not having their own. *)
on anythingElse(unitName)
end anythingElse

(* 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.
   set USGallonCountryCodes to {"US", "BZ", "CO", "DO", "EC", "SV", "GT", "HT", "HN", "LR", "NI", "PE"}
   
   considering case
       return (localCountryCode is not in USGallonCountryCodes)
   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)
   set resultTypeExpression to resultTypeExpression as text
   ignoring case
       -- If "object", "measurement" "measurement object", or "number" is specified, return the appropriate result.
       if ((resultTypeExpression ends with "object") or (resultTypeExpression begins with "measurement")) then return measurementObject
       if (resultTypeExpression is "number") then return measurementObject's beginning
       
       -- Otherwise format a string containing the measurement's number and unit symbol.
       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 "text") then return stringResult as text
       
       return stringResult
   end ignoring
end measurementResult

(* 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 (Yesterday 07:12:00 am)


NG

Offline

 

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

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

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: 5147

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: 6143

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: 5147

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: 6143

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: 5147

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 Yesterday 07:15:54 am

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

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

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)