Extension:UnitsFormatter/0.1
From MediaWiki.org
<?php
/*
* Unit formatting extension.
*
* This extension installs a new tag, <unit> ... </unit>. This tag allows
* the user to display a quantity with units easily and flexibly. The two
* key benefits are:
*
* * Allows authors to write quantities easily
* * Allows readers to see units formatted according to their preferences
*
* Usage
* =====
*
* <unit>4.00 usgal</unit>
*
* This displays the volume 4 U.S. gallons, formatted
* according to user preferences. For example:
* 4.00 US gallons
* But if the user prefers metric units, he/she will see:
* 15.14 litres
* A British user could elect to see units in British measures:
* 3.33 British gallons
* The user can opt to see both metric and non-metric units; for example:
* 15.14 litres (3.33 Br gal)
* 4.00 US gallons (15.14 l)
*
* The output is formatted according to Wikipedia standards; the unit names
* link to the appropriate article the first time they are used, and non-
* breaking spaces are used where appropriate.
*
* The number of decimal places is taken from the input; 2 in the above
* example. It can be specified explicitly using the "digits" parameter:
*
* <unit digits=3>1 m</unit> => 1.000 meter (1.094 yd)
*
* The user can specify the realm of interest, such as "nautical", with the
* "realm" parameter; units with a matching realm will then be preferred:
*
* <unit>12 km/h</unit> => 12 kilometres per hour (7 mph)
* <unit realm=naut>12 km/h</unit> => 12 kilometres per hour (6 kt)
*
* An additional unit to display can be specified with the "alt" tag;
* this could be useful to force Kelvin output for temperatures:
*
* <unit alt="K">-40.0 C</unit> => -40.0 °F (-40.0 °C, 233.2 K)
*
* Units are specified using identifiers based on their standard symbols.
* The <showunits> tag displays a table of all known units:
*
* <showunits />
*/
if ( !defined( 'MEDIAWIKI' ) ) {
die( 'This file is a MediaWiki extension, it is not a valid entry point' );
}
$wgExtensionFunctions[] = 'wfSetupUnitsFormatting';
$wgExtensionCredits['parserhook'][] = array(
'name' => 'UnitsFormatting',
'author' => 'Johan the Ghost',
'description' => 'The <unit> tag displays measures according to user preferences',
);
$wgHooks['LanguageGetMagic'][] = 'wfUnitsFormattingLanguageGetMagic';
//////////////////////////////////////////////////////////////////////////
// Setup Functions
//////////////////////////////////////////////////////////////////////////
function wfSetupUnitsFormatting() {
new UnitsFormattingExtension();
}
function wfUnitsFormattingLanguageGetMagic(&$magicWords, $langCode) {
$magicWords['unit'] = array(0, 'unit');
$magicWords['decimals'] = array(0, 'decimals');
$magicWords['sigfigs'] = array(0, 'sigfigs');
$magicWords['roundsig'] = array(0, 'roundsig');
return true;
}
//////////////////////////////////////////////////////////////////////////
// Extension Class
//////////////////////////////////////////////////////////////////////////
class UnitsFormattingExtension {
//////////////////////////////////////////////////////////////////////////
// Units Configuration
//////////////////////////////////////////////////////////////////////////
/*
* The table of all known units. This is an associative array, where
* the key is the unit identifier as used by this extension, and the
* value is an array of conversion data for the unit. Note that all
* identifiers are lower-case -- the identifier supplied by the user
* is converted to lower-case.
*
* The table is divided into sections by entries whose value is a scalar.
* These entries provide the name of a type of unit, and the ID of the
* base unit of that type -- for example:
* 'Length' => 'm',
* begins the set of length units, and says that the base unit for
* conversions is metres. The base unit itself should be the very next
* entry. These entries are used to format the units table, and
* otherwise ignored.
*
* The conversion data specifies how to convert the unit *to* the base
* SI unit for that measure (this unit is named in the comments below).
* It consists of:
* * The unit's locale: 'm' if a metric unit; 'b' if British; 'u'
* if US. The pseudo-locale 'o' means it is both British and US;
* 'a' that it is universal.
* * The unit's realm, if applicable, else null. This is a hint for
* the preferred units; eg. nautical over statute. Existing realms:
* 'naut' for nautical.
* * The multiplier to multiply the result by to get the
* SI value. If an array, it contains the offset to add to the unit,
* and the multiplier (for temperatures).
* * The name of the "alternate" unit; ie. the unit to show the
* imperial measure in, if this is metric, or vice versa; also
* British vs. US equivalents. This can be an array, in which
* case a unit will be chosen depending on the users' preferences.
* Use array() if there is no alternate (for an 'a' unit).
*
* The ordering of the table is significant -- where there are multiple
* alternatives, the first found is preferred. This is rare, though; it
* only happens where a unit specifies multiple alternates of the same
* locale; for example, km/h converting to both mph and knots, both of
* which have locale='o'. The first unit will be used, unless a specific
* realm was requested and a later unit's realm matches.
*
* Note that offsets only apply to temperatures. The conversion of any
* unit to the SI base unit is given by:
* $si = ($x + $offset) * $multiplier;
*
* Note: the line
* 'in2' => array('o', 0, 0.000645, 'cm2'),
* does *not* mean that a square inch is 0.000645 cm2 -- it means that
* a square inch is 0.000645 of the base SI unit (m2), and that the
* alternate display for square inches is cm2.
*
* Note that the alternates for US/British units *must* include their
* British/US alternatives -- so if we start from U.S. gallons, we get
* UK gallons, if the user wants to see British units, as well as litres
* for metric.
*/
private $conversions = array(
// Length: convert to metres.
'Length' => 'm',
'm' => array('m', null, 1, array('yd', 'fath')),
'km' => array('m', null, 1000, array('mi', 'nm')),
'cm' => array('m', null, 0.01, 'in'),
'mm' => array('m', null, 0.001, 'in'),
'yd' => array('o', null, 0.914, 'm'),
'ft' => array('o', null, 0.3048, 'm'),
'in' => array('o', null, 0.0254, 'cm'),
'mi' => array('o', null, 1609.344, 'km'),
'nm' => array('o', 'naut', 1852, 'km'), // International nm.
'chain' =>array('o', null, 20.1168, 'm'),
'fath' => array('o', 'naut', 1.8288, 'm'),
'furl' => array('o', null, 201.168, 'km'),
// Area: convert to metres^2.
'Area' => 'm2',
'm2' => array('m', null, 1, 'yd2'),
'cm2' => array('m', null, 0.0001, 'in2'),
'a' => array('m', null, 100, 'yd2'),
'ha' => array('m', null, 10000, 'acre'), // Hectare
'km2' => array('m', null, 1000000, 'mi2'),
'in2' => array('o', null, 0.000645, 'cm2'),
'ft2' => array('o', null, 0.092903, 'm2'),
'yd2' => array('o', null, 0.83612736, 'm2'),
'acre' => array('o', null, 4046.8564224, 'ha'),
'mi2' => array('o', null, 2589988.110336, 'km2'),
// Volume: convert to litres.
'Volume' => 'l',
'l' => array('m', null, 1, array('brpt', 'uspt', 'oil')),
'ml' => array('m', null, 0.001, array('brfloz', 'usfloz')),
'cc' => array('m', null, 0.001, 'in3'), // Alt. name for ml
'm3' => array('m', null, 1000, 'yd3'),
'in3' => array('o', null, 0.016387, 'ml'),
'ft3' => array('o', null, 28.316847, 'm3'),
'yd3' => array('o', null, 764.554858, 'm3'),
'brfloz'=>array('b', null, 0.028413, array('ml', 'usfloz')),
'brpt' => array('b', null, 0.568261, array('l', 'uspt')),
'brgal' =>array('b', null, 4.54609, array('l', 'usgal')),
// These are the liquid US measures:
'usfloz'=>array('u', null, 0.029574, array('ml', 'brfloz')),
'uspt' => array('u', null, 0.473176, array('l', 'brpt')),
'usgal' =>array('u', null, 3.785412, array('l', 'brgal')),
'oil' => array('o', 'oil', 158.987295, 'l'), // Oil barrel
// Mass: convert to kg. ozt and lbt are Troy ounces and pounds
// (the regular kind are Avoirdupois). st is a stone. scwt and
// lcwt are short (100lb) and long (110lb) hundredweights. ston
// and lton are short and long tons. mt is a metric ton.
'Mass' => 'kg',
'kg' => array('m', null, 1, 'lb'),
'carat' =>array('m', null, 0.000200, 'gr'),
'g' => array('m', null, 0.001, 'oz'),
'mt' => array('m', null, 1000, array('ston', 'lton')),
'gr' => array('o', null,0.00006479891, 'g'),
'oz' => array('o', null, 0.02835, 'g'),
'lb' => array('o', null, 0.453592, 'kg'),
'ozt' => array('o', null, 0.031103, 'g'),
'lbt' => array('o', null, 0.373242, 'kg'),
'st' => array('b', null, 6.350293, array('kg', 'lb')),
'scwt' => array('u', null, 45.359237, array('kg', 'lcwt')),
'lcwt' => array('b', null, 50.802396, array('kg', 'scwt')),
'ston' => array('u', null, 907.18474, array('mt', 'lton')),
'lton' => array('b', null, 1016.0469088, array('mt', 'ston')),
// Energy: convert to joules.
// Calories are about the most obscene example of why any
// self-respecting scientist or engineer would use SI units and
// nothing else. Not only are there at least 8 different definitions
// of a calorie, but 1000 calories is also called "1 calorie".
// We provide the following:
// cal-th Thermochemical calorie
// cal-m Mean calorie
// cal-15 15 deg C calorie
// cal-20 20 deg C calorie
// cal-ns International Union of Nutritional Sciences (IUNS)
// calorie
// cal-it International Steam Table Calorie (1956) (as
// distinct from the International Steam Table
// Calorie (1929))
// kcal-ns IUNS calorie * 1000 (presumably what's meant
// by "calorie" in food)
// BTUs are as bad, maybe even worse. We have:
// btu-th Uses the thermochemical calorie
// btu-m Uses the mean calorie
// btu-15 (based on 59 deg F) uses the 15 deg C calorie
// btu-it Uses the International Steam Table calorie
// btu-is ISO BTU, rounded version of btu-it
// See Wikipedia for more on this garbage.
'Energy' => 'j',
'j' => array('m', null, 1, 'cal-th'),
'kwh' => array('m', null, 3600000, 'btu-th'),
'cal-th'=>array('o', null, 4.184, 'j'),
'cal-m' =>array('o', null, 4.19002, 'j'),
'cal-15'=>array('o', null, 4.1858, 'j'),
'cal-20'=>array('o', null, 4.1819, 'j'),
'cal-ns'=>array('o', null, 4.182, 'j'),
'cal-it'=>array('o', null, 4.1868, 'j'),
'kcal-ns'=>array('o',null, 4182, 'j'),
'btu-th'=>array('o', null, 1054.35026444, 'j'),
'btu-m' =>array('o', null, 1055.87, 'j'),
'btu-15'=>array('o', null, 1054.804, 'j'),
'btu-it'=>array('o', null, 1055.05585262, 'j'),
'btu-is'=>array('o', null, 1055.056, 'j'),
// Force: convert to newtons.
'Force' => 'n',
'n' => array('m', null, 1, 'lbf'),
'dyn' => array('m', null, 0.00001, 'lbf'),
'kn' => array('m', null, 1000, 'lbf'),
'lbf' => array('o', null, 4.448222, 'n'),
// Temp: convert to kelvin. These are the only units with an offset,
// which is indicated by a conversion factor which is an array
// of (offset, multiplier).
'Temp' => 'k',
'k' => array('m', null, array( 0, 1), array('c', 'f')),
'c' => array('m', null, array(273.15, 1), 'f'),
'f' => array('o', null, array(459.67, 0.55555555555555555555555555555556), 'c'),
// Time: convert to seconds.
'Time' => 's',
's' => array('a', null, 1, array()),
'min' => array('a', null, 60, array()),
'hr' => array('a', null, 3600, array()),
// Speed: convert to metres/second. This is an example where
// generalised compound unit handling would be cool, but also a
// problem -- it would convert m/s into yards/second, because metres
// convert to yards (see 'm' above). ft/s is far more common for
// non-metric speeds.
'Speed' => 'm/s',
'm/s' => array('m', null, 1, 'ft/s'),
'cm/s' => array('m', null, 0.01, 'in/s'),
'km/h' => array('m', null, 0.27777777777777777777777777777778, array('mph', 'kt')),
'light' =>array('m', null, 299792458, array('km/h', 'mph')),
'in/s' => array('o', null, 0.0254, 'cm/s'),
'ft/s' => array('o', null, 0.3048, 'm/s'),
'mph' => array('o', null, 0.44704, array('km/h', 'kt')),
'kt' => array('o', 'naut', 0.51444444444444444444444444444444, array('km/h', 'mph')),
);
/*
* Set up the wiki messages for the names of the units. This array
* has a sub-array per language; each subarray is indexed by unit identifier,
* and the values are arrays containing the names of the unit:
* * The full singular name
* * The full plural name
* * The abbreviation
* * (optional) A wikilink to the article defining this unit,
* not including any interwiki part. The content of the wiki message
* 'unit-link-prefix' will be prepended to this.
*
* For each unit U, the following wiki messages are created, initialised
* to the values in the array, and can be customised in the MediaWiki:
* namespace:
* * unit-U-name: the singular name
* * unit-U-names: the plural name
* * unit-U-ab: the abbreviation
* * unit-U-link: the definition wikilink
*
* If a wikilink is not given, the unit will be displayed without a link.
*/
private $unitMsgList = array(
'en' => array(
// Length.
'm' => array('metre', 'metres', 'm', 'metre'),
'km' => array('kilometre', 'kilometres', 'km', 'kilometre'),
'cm' => array('centimetre', 'centimetres', 'cm', 'centimetre'),
'mm' => array('millimetre', 'millimetres', 'mm', 'millimetre'),
'yd' => array('yard', 'yards', 'yd', 'yard'),
'ft' => array('foot', 'feet', 'ft', 'foot (unit of length)'),
'in' => array('inch', 'inches', 'in', 'inch'),
'mi' => array('mile', 'miles', 'mi', 'mile'),
'nm' => array('nautical mile', 'nautical miles', 'nm', 'nautical mile'),
'chain' => array('chain', 'chains', 'ch', 'chain (unit)'),
'fath' => array('fathom', 'fathoms', 'fath', 'fathom'),
'furl' => array('furlong', 'furlongs', 'furl', 'furlong'),
// Area.
'm2' => array('square metre', 'square metres', 'm²', 'square metre'),
'cm2' => array('square centimetre', 'square centimetres', 'cm²', 'square metre'),
'a' => array('are', 'ares', 'a', 'are'),
'ha' => array('hectare', 'hectares', 'ha', 'hectare'),
'km2' => array('square kilometre', 'square kilometres', 'km²', 'square kilometre'),
'in2' => array('square inch', 'square inches', 'in²', 'square inch'),
'ft2' => array('square foot', 'square feet', 'ft²', 'square foot'),
'yd2' => array('square yard', 'square yards', 'yd²', 'square yard'),
'acre' => array('acre', 'acres', 'acre', 'acre'),
'mi2' => array('square mile', 'square miles', 'mi²', 'square mile'),
// Volume.
'l' => array('litre', 'litres', 'l', 'litre'),
'ml' => array('millilitre', 'millilitres', 'ml', 'litre'),
'cc' => array('cubic centimetre', 'cubic centimetres', 'cc', 'cubic centimetre'),
'm3' => array('cubic metre', 'cubic metres', 'm³', 'cubic metre'),
'in3' => array('cubic inch', 'cubic inches', 'in³', 'cubic inch'),
'ft3' => array('cubic foot', 'cubic feet', 'ft³', 'cubic foot'),
'yd3' => array('cubic yard', 'cubic yards', 'yd³', 'cubic yard'),
'brfloz'=>array('British fluid ounce', 'British fluid ounces', 'Br fl oz', 'fluid ounce'),
'brpt' => array('British pint', 'British pints', 'Br pt', 'pint'),
'brgal' =>array('British gallon', 'British gallons', 'Br gal', 'gallon'),
'usfloz'=>array('US fluid ounce', 'US fluid ounces', 'US fl oz', 'fluid ounce'),
'uspt' => array('US pint', 'US pints', 'US pt', 'pint'),
'usgal' =>array('US gallon', 'US gallons', 'US gal', 'gallon'),
'oil' => array('oil barrel', 'oil barrels', 'bbl', 'barrel (unit)'),
// Mass.
'kg' => array('kilogram', 'kilograms', 'kg', 'kilogram'),
'carat' => array('carat', 'carats', 'carat', 'carat'),
'g' => array('gram', 'grams', 'g', 'gram'),
'mt' => array('tonne', 'tonnes', 'mt', 'tonne'),
'gr' => array('grain', 'grains', 'gr', 'grain (measure)'),
'oz' => array('ounce', 'ounces', 'oz', 'ounce'),
'lb' => array('pound', 'pounds', 'lb', 'pound (mass)'),
'ozt' => array('troy ounce', 'troy ounces', 'ozt', 'ounce'),
'lbt' => array('troy pound', 'troy pounds', 'lbt', 'pound (mass)'),
// Note that the plural of "stone" is correctly "stone".
'st' => array('stone', 'stone', 'st', 'stone (weight)'),
'scwt' => array('short hundredweight', 'short hundredweights', 'scwt', 'hundredweight'),
'lcwt' => array('long hundredweight', 'long hundredweights', 'lcwt', 'hundredweight'),
'ston' => array('short ton', 'short tons', 'ston', 'short ton'),
'lton' => array('long ton', 'long tons', 'lton', 'long ton'),
// Energy.
'j' => array('joule', 'joules', 'J', 'joule'),
'kwh' => array('kilowatt-hour', 'kilowatt-hours', 'kWh', 'watt-hour'),
'cal-th'=>array('thermochemical calorie', 'thermochemical calories', 'cal<sub>th</sub>', 'calorie'),
'cal-m' =>array('mean calorie', 'mean calories', 'cal<sub>mean</sub>', 'calorie'),
'cal-15'=>array('15 °C calorie', '15 °C calories', 'cal<sub>15</sub>', 'calorie'),
'cal-20'=>array('20 °C calorie', '20 °C calories', 'cal<sub>20</sub>', 'calorie'),
'cal-ns'=>array('IUNS calorie', 'IUNS calories', 'cal<sub>IUNS</sub>', 'calorie'),
'cal-it'=>array('international calorie', 'international calories', 'cal<sub>INT</sub>', 'calorie'),
'kcal-ns'=>array('IUNS kilocalorie', 'IUNS kilocalories', 'kcal<sub>IUNS</sub>', 'calorie'),
'btu-th'=>array('thermochemical BTU', 'thermochemical BTUs', 'BTU<sub>th</sub>', 'British thermal unit'),
'btu-m' =>array('mean BTU', 'mean BTUs', 'BTU<sub>mean</sub>', 'British thermal unit'),
'btu-15'=>array('15 °C BTU', '15 °C BTUs', 'BTU<sub>15</sub>', 'British thermal unit'),
'btu-it'=>array('international BTU', 'international BTUs', 'BTU<sub>INT</sub>', 'British thermal unit'),
'btu-is'=>array('ISO BTU', 'ISO BTUs', 'BTU<sub>ISO</sub>', 'British thermal unit'),
// Force.
'n' => array('newton', 'newtons', 'N', 'newton'),
'dyn' => array('dyne', 'dynes', 'dyn', 'dyne'),
'kn' => array('kilonewton', 'kilonewtons', 'kN', 'newton'),
'lbf' => array('pound force', 'pounds force', 'lbf', 'pound-force'),
// Temperature.
'k' => array('K', 'K', 'K', 'kelvin'),
'c' => array('°C', '°C', '°C', 'celsius'),
'f' => array('°F', '°F', '°F', 'fahrenheit'),
// Time.
's' => array('second', 'seconds', 's', 'second'),
'min' => array('minute', 'minutes', 'min', 'minute'),
'hr' => array('hour', 'hours', 'hr', 'hour'),
// Speed.
'm/s' => array('metre per second', 'metres per second', 'm/s', 'metre per second'),
'cm/s' => array('centimetre per second', 'centimetres per second', 'cm/s', 'metre per second'),
'km/h' => array('kilometre per hour', 'kilometres per hour', 'km/h', 'kilometres per hour'),
'light' =>array('speed of light', 'speed of light', 'c', 'speed of light'),
'in/s' => array('inch per second', 'inches per second', 'in/s', 'inches per second'),
'ft/s' => array('foot per second', 'feet per second', 'ft/s', 'feet per second'),
'mph' => array('mile per hour', 'miles per hour', 'mph', 'miles per hour'),
'kt' => array('knot', 'knots', 'kt', 'knot (speed)'),
),
);
//////////////////////////////////////////////////////////////////////////
// General Configuration
//////////////////////////////////////////////////////////////////////////
/*
* This array sets up our messages for things other than units.
*/
private $messageList = array(
'en' => array(
// The messages for the toggle buttons that we create in
// userToggles() below.
'tog-preferinput' => 'Prefer the article\'s units',
'tog-prefermetric' => 'Prefer metric units',
'tog-preferbritish' => 'For non-metric, prefer British over US units',
'tog-dualunits' => 'Show both metric and non-metric units',
// The interwiki prefix which is prepended to all links.
'unit-link-prefix' => 'wikipedia:',
),
);
//////////////////////////////////////////////////////////////////////////
// Private Data
//////////////////////////////////////////////////////////////////////////
/*
* The user's preferred unit styles; an array of the
* user's preferred unit types in preferred order. For example,
* array('m', 'b', 'o') means show metric, then British or
* general non-metric as available.
*/
private $prefStyles = null;
/*
* Array of units that we've see in this run (ie. the current page).
* This is used to let us to hyperlinks on the first use only.
*/
private $seenUnits = array();
//////////////////////////////////////////////////////////////////////////
// Constructors and Setup
//////////////////////////////////////////////////////////////////////////
/*
* Construct the extension and install it as a parser hook.
*/
public function __construct() {
global $wgHooks, $wgParser;
$this->setupMessages();
// Install the "unit" markup tag.
$wgParser->setHook('unit', array(&$this, 'unitTag'));
$wgParser->setHook('showunits', array(&$this, 'showunitsTag'));
// For debugging, install the "unitprefs" tag, which allows
// user preferences to be manipulated in a test page.
$wgParser->setHook('unitprefs', array(&$this, 'unitprefsTag'));
// Install a hook in to the preferences page to set up additional
// checkbox preferences items.
$wgHooks['UserToggles'][] = array(&$this, 'userToggles');
// Cacheing of pages is based on user preferences; since we're
// creating new preferences, we must add them to this hash.
$wgHooks['PageRenderingHash'][] = array(&$this, 'hashHook');
// Check the user's preferences for his/her preferred unit styles.
$this->prefStyles = $this->getPreferredStyles();
}
/*
* Set up the system messages used by this extension.
*/
private function setupMessages() {
global $wgMessageCache;
// Add our basic UI messages.
foreach ($this->messageList as $lang => $messages)
$wgMessageCache->addMessages($messages, $lang);
// Add per-unit messages.
foreach ($this->unitMsgList as $lang => $messages) {
foreach ($messages as $unit => $msgs) {
$n = 'unit-' . $unit;
$wgMessageCache->addMessage($n . '-name', $msgs[0], $lang);
$wgMessageCache->addMessage($n . '-names', $msgs[1], $lang);
$wgMessageCache->addMessage($n . '-ab', $msgs[2], $lang);
if (array_key_exists(3, $msgs))
$wgMessageCache->addMessage($n . '-link', $msgs[3], $lang);
}
}
}
/*
* This hook is invoked by the preferences page to set up additional
* checkbox preferences items. We use this to add our user options.
* These go into the "Misc" tab.
*/
public function userToggles(&$extraToggles) {
$extraToggles[] = 'preferinput';
$extraToggles[] = 'prefermetric';
$extraToggles[] = 'preferbritish';
$extraToggles[] = 'dualunits';
return true;
}
/*
* This hook is invoked to add to the user's page rendering hash code.
*
* Pages are cached, which would defeat per-user configurable rendering
* such as this extension. To avoid this, each page is cached not just
* by name, but also by a hash generated from the user's preferences.
* Since we're creating new preferences, we have to add them to the hash.
* This hook is invoked to do that.
*/
public function hashHook(&$confstr) {
global $wgUser;
$confstr .= '!' . $wgUser->getOption('preferinput');
$confstr .= '!' . $wgUser->getOption('prefermetric');
$confstr .= '!' . $wgUser->getOption('preferbritish');
$confstr .= '!' . $wgUser->getOption('dualunits');
return true;
}
/*
* What kind of units does the user prefer? Returns an array of the
* user's preferred unit types in preferred order. For example,
* array('m', 'b') meaning show metric, then British. (For British or
* US, unit() will automatically substitute general non-metric or
* universal units if necessary.)
*/
private function getPreferredStyles($inunit=null, $met=null, $brit=null, $dual=null) {
global $wgUser;
// Get the user's prefs.
if ($inunit === null)
$inunit = $wgUser->getOption('preferinput');
if ($met === null)
$met = $wgUser->getOption('prefermetric');
if ($brit === null)
$brit = $wgUser->getOption('preferbritish');
if ($dual === null)
$dual = $wgUser->getOption('dualunits');
$style = array();
// If the user wants to see the input units first, then place 'i'
// at the front.
if ($inunit)
$style[] = 'i';
// Set up the user's primary style. If this is non-metric,
// look for British/US first, then general non-metric.
if ($met)
$style[] = 'm';
else
$style[] = $brit ? 'b' : 'u';
// If the user wants dual display, set up the secondary style.
if ($dual) {
if ($met)
$style[] = $brit ? 'b' : 'u';
else
$style[] = 'm';
}
return $style;
}
//////////////////////////////////////////////////////////////////////////
// Unit Formatting
//////////////////////////////////////////////////////////////////////////
/*
* This is the handler for the "<unitprefs />" wiki tag. Set
* the user preferences for this page.
*/
public function unitprefsTag($text, $argv, &$parser) {
$res = array();
$inunit = null;
if (array_key_exists('inunit', $argv)) {
$inunit = $argv['inunit'];
$res[] = ($inunit ? "input first" : "my units");
}
$met = null;
if (array_key_exists('met', $argv)) {
$met = $argv['met'];
$res[] = ($met ? "metric" : "non-metric");
}
$brit = null;
if (array_key_exists('brit', $argv)) {
$brit = $argv['brit'];
$res[] = ($brit ? "British" : "US");
}
$dual = null;
if (array_key_exists('dual', $argv)) {
$dual = $argv['dual'];
$res[] = ($dual ? "dual" : "single");
}
$this->prefStyles = $this->getPreferredStyles($inunit, $met, $brit, $dual);
return "Preferences: " . implode(", ", $res) . ".";
}
/*
* This is the handler for the "<unit>...</unit>" wiki tag. Format a unit
* as per the given parameters and the user's preferences, and return
* the HTML text.
*/
public function unitTag($text, $argv, &$parser) {
list($mag, $name) = explode(" ", trim($text));
// Get the precision, if specified.
$digits = '';
if (array_key_exists('digits', $argv))
$digits = $argv['digits'];
// Get the realm, if specified.
$realm = null;
if (array_key_exists('realm', $argv))
$realm = $argv['realm'];
// Get the alternate unit, if specified.
$alt = '';
if (array_key_exists('alt', $argv))
$alt = $argv['alt'];
$result = $this->unit($mag, $name, $realm, $digits, $alt);
// Done. Parse the result to expand wiki markup.
$ret = $parser->parse($result,
$parser->mTitle,
$parser->mOptions,
false,
false);
return $ret->getText();
}
/**
* Perform unit conversion.
*
* $mag The numeric value to format.
* $name The units of $mag; must be a unit ID.
* $realm The page's requested realm, or null.
* $digits The number of decimal digits to display; will be
* scaled appropriately for each conversion.
* $third If not null, additional units to display.
*
* Returns the formatted wiki text.
*/
private function unit($mag, $name, $realm, $digits='', $third='') {
try {
// See if the page has requested that the input units be fixed
// (by appending a "!").
$fix = 0;
if ($name[strlen($name) - 1] == '!') {
$name = substr($name, 0, strlen($name) - 1);
$fix = 1;
}
// How many decimal places do we want?
// If not specified, count the decimals in the input.
if ($digits === '')
$digits = $this->decimals($mag);
// Get the conversion data for this unit. We set maxdepth
// to 1, so we get this unit and its alternates.
$converters = array();
$conv = $this->getConverters($name, $realm, $converters, 1);
// If no realm is specified, but this unit has a realm, use that
// realm.
if (!$realm && $conv[1])
$realm = $conv[1];
// Now build a list of the units we are going to convert to. This
// is an ordered array of conversion data. It may contain dupes,
// in which case formatUnits() will filter them out.
$units = array();
// If we were asked to fix the named unit, put it at the beginning
// of the units list.
if ($fix)
$units[] = $conv;
// Add the conversion data for the user's preferred locale(s).
// 'i' means use the input units.
foreach ($this->prefStyles as $s) {
if ($s == 'i')
$units[] = $conv;
else if (array_key_exists($s, $converters))
$units[] = $converters[$s];
}
// If we were asked for a third unit, find the conversion and
// add it on.
if ($third)
$units[] = $this->getConverters($third, $realm, $converters, 0);
// Finally, format it according to the given conversions.
return $this->formatUnits($mag, $conv, $units, $digits);
} catch (Exception $e) {
return $e->getMessage();
}
}
/*
* Given one or more unit IDs in $names, try to find conversion data
* for them. Place the results in $converters, which is indexed by the
* unit locale: 'm' for metric, 'b' for British, 'u' for US. The special
* locales 'o' and 'a' are expanded here.
*
* If $converters already has an entry for a
* locale, then don't overwrite it -- so the first metric unit found is
* the one we will use for metric, etc. However, a unit whose non-null
* realm matches $realm will overwrite previous units for that locale.
*
* If $maxdepth > 0, then recurse -- for each set of conversion data
* we find, call getConverters() for its alternate unit(s). Keep recursing
* up to $maxdepth.
*
* We return the last set of conversion data found in this invocation
* (not the data from recursive invocations). This is most useful
* if $names is a single name.
*/
private function getConverters($names, $realm, &$converters, $maxdepth=0) {
// We can be passed a single name or an array.
if (!is_array($names))
$names = array($names);
// Get the conversion data for each unit. Set $conv to the last
// set of data found.
foreach ($names as $name) {
// Get the conversion for this unit.
$name = strtolower($name);
if (!array_key_exists($name, $this->conversions))
throw new Exception("unknown unit:" . $name);
$conv = $this->conversions[$name];
if (!is_array($conv))
throw new Exception("invalid unit:" . $name);
// Set 'key' in the conversion data to its ID for later use.
$conv['key'] = $name;
// Save the conversion to $converters if we don't have a
// conversion of this locale, OR if this conversion is a match
// for the realm. Locale 'o' means British and US ('b' and 'u');
// 'a' means all.
if ($conv[0] == 'a')
$locs = array('m', 'b', 'u');
else if ($conv[0] == 'o')
$locs = array('b', 'u');
else
$locs = array($conv[0]);
foreach ($locs as $loc) {
if (!array_key_exists($loc, $converters) ||
($realm && $conv[1] == $realm))
$converters[$loc] = $conv;
}
// Get the conversion data for the unit's metric/imperial
// equivalent unit(s), if requested.
if ($maxdepth > 0) {
$altunits = $conv[3];
$this->getConverters($altunits, $realm, $converters, $maxdepth - 1);
}
}
// Return the last set of conversion data for $names.
return @$conv;
}
/*
* Format the given value according to a given list of conversions.
*
* $mag The numeric value to format.
* $siConv Conversion data to convert $mag to the base SI unit.
* $conversions The list of conversions to do. This is an array
* of conversion data. Each conversion data set has
* its 'key' field set to its unit ID.
* $digits The number of decimal digits to display; will be
* scaled appropriately for each conversion.
*
* Returns the text of the converted units.
*
* The first unit is output in full; subsequent ones are in parentheses,
* and use abbreviated unit names.
*
* No unit will be output more than once, even if it appears multiple
* times in $conversions, which can happen.
*/
private function formatUnits($mag, $siConv, $conversions, $digits) {
$res = '';
$count = 0;
$seenHere = array();
foreach ($conversions as $conv) {
$key = $conv['key'];
// Don't do the same unit twice in one conversion.
if (array_key_exists($key, $seenHere))
continue;
$seenHere[$key] = 1;
// See whether we've seen this unit before in this page.
$first = !array_key_exists($key, $this->seenUnits);
if ($first)
$this->seenUnits[$key] = 1;
// Get the conversion from the input to SI units, and the
// conversion from this unit to SI. Get both conversions into
// the format (offset, multiplier).
$siFact = is_array($siConv[2]) ? $siConv[2] : array(0, $siConv[2]);
$uFact = is_array($conv[2]) ? $conv[2] : array(0, $conv[2]);
// Convert the value. Calculate the total multiplier.
$val = (($mag + $siFact[0]) * $siFact[1]) / $uFact[1] - $uFact[0];
$mult = $siFact[1] / $uFact[1];
// Round and format the value. First, if the conversion involves
// order-of-magnitude changes, auto-adjust the number of decimals;
// so, for example, 3mm converts to 0.12 inches, not 0 inches
// (avoid loss of precision), and 1.000 acre converts to 4,047 m²,
// not 4,046.856 m² (avoid surious precision). Note that round()
// with a negative precision rounds before the point, so we
// correctly convert 1.000 square mile to 2,590,000 m², rather
// than 2,589,988 m² (if you force conversion to m²). We need to
// push pretty hard to the side of displaying more, not fewer digits,
// to get 1mm to convert to 0.04 inches, as opposed to 0.0 inches.
$dig = $digits;
while ($mult < 0.5) { // Be conservative and increase early
++$dig;
$mult *= 10;
}
while ($mult >= 50) { // Be conservative and reduce late
--$dig;
$mult /= 10;
}
$val = number_format(round($val, $dig), $dig < 0 ? 0 : $dig);
// Get the system message for the unit name.
if ($count == 0)
$msgid = 'unit-' . $key . ($val == 1 ? '-name' : '-names');
else
$msgid = 'unit-' . $key . '-ab';
$msg = wfMsg($msgid);
// Make the name into a link the first time we see the unit.
if ($first) {
// Link prefix.
$prefix = wfMsg('unit-link-prefix');
if (wfEmptyMsg('unit-link-prefix', $prefix))
$prefix = '';
$linkid = 'unit-' . $key . '-link';
$link = wfMsg($linkid);
if (!wfEmptyMsg($linkid, $link))
$msg = sprintf("[[%s%s|%s]]", $prefix, $link, $msg);
}
// Generate the formatted unit.
if ($count == 1)
$res .= ' (';
else if ($count > 1)
$res .= ', ';
$res .= sprintf("%s %s", $val, $msg);
++$count;
}
if ($count > 1)
$res .= ')';
return $res;
}
//////////////////////////////////////////////////////////////////////////
// Units Table Output
//////////////////////////////////////////////////////////////////////////
/*
* This is the handler for the "<showunits />" wiki tag. Display
* the table of known units.
*/
public function showunitsTag($text, $argv, &$parser) {
$table = '';
// Output the table heading.
$table .= "{| class=wikitable\n";
$table .= "! Type\n";
$table .= "! ID\n";
$table .= "! Name\n";
$table .= "! Ab\n";
$table .= "! Link\n";
$table .= "! Realm\n";
$table .= "! Locale\n";
$table .= "! Conversion\n";
$table .= "! Alternates\n";
$type = null;
$si = '?';
// Get the unit link prefix.
$prefix = wfMsg('unit-link-prefix');
if (wfEmptyMsg('unit-link-prefix', $prefix))
$prefix = '';
// Output each conversion we have configured.
foreach ($this->conversions as $key => $def) {
// If this is a unit class heading, display it.
if (!is_array($def)) {
$type = $key;
$si = $def;
$table .= "|-\n";
$table .= "| colspan=8 |\n";
continue;
}
// Start the row for this unit, and put out the unit ID.
$table .= "|-\n";
if ($type) {
$table .= "! " . $type . "\n";
$type = null;
} else
$table .= "|\n";
$table .= "| " . $key . "\n";
// Show the unit messages 'unit-xx-name', etc.
foreach (array('name', 'ab', 'link') as $m) {
// Get the message.
$msgid = "unit-$key-$m";
$msg = wfMsg($msgid);
if (wfEmptyMsg($msgid, $msg))
$msg = "--";
// If it's the full name, get the plural name too. If we find
// one, try to abbreviate the result like "inch(es)"; else
// display both, like "foot/feet".
if ($m == 'name') {
$m2id = "unit-$key-names";
$m2 = wfMsg($m2id);
if (!wfEmptyMsg($m2id, $m2)) {
if (strlen($m2) > strlen($msg) && strstr($m2, $msg) == $m2)
$msg = $msg . "(" . substr($m2, strlen($msg)) . ")";
else
$msg .= "/" . $m2;
}
} else if ($m == 'link' && $msg != "--")
$msg = sprintf("[[%s%s|%s]]", $prefix, $msg, $msg);
// Show the message(s).
$table .= "| " . $msg . "\n";
}
// Show the unit realm if present.
$table .= "| " . ($def[1] ? $def[1] : "") . "\n";
// Show the unit locale.
switch ($def[0]) {
case 'm':
$table .= "| metric\n";
break;
case 'o':
$table .= "| US/UK\n";
break;
case 'b':
$table .= "| UK\n";
break;
case 'u':
$table .= "| USA\n";
break;
case 'a':
$table .= "| univ.\n";
break;
default:
$table .= "| ???\n";
break;
}
// Show the conversion formula.
$off = is_array($def[2]) ? $def[2][0] : 0;
$mul = is_array($def[2]) ? $def[2][1] : $def[2];
if ($key == $si)
$table .= "| base\n";
else if ($off == 0)
$table .= "| 1 $key = $mul $si\n";
else if ($mul == 1)
$table .= "| $key = $si - $off\n";
else
$table .= "| $key = ($si / $mul) - $off\n";
// Show the alternate unit(s).
$a = $def[3];
$table .= "| " . (is_array($a) ? implode(",", $a) : $a) . "\n";
}
// Finish the table.
$table .= "|}\n";
// If the "source" parameter is set, we return the table source;
// this facilitates pasting a static version of it into a wiki
// page.
if (array_key_exists('source', $argv))
return "<" . "pre>\n" . $table . "</" . "pre>";
// Done. Parse the result to expand the wiki markup.
$ret = $parser->parse($table,
$parser->mTitle,
$parser->mOptions,
false,
false);
return $ret->getText();
}
//////////////////////////////////////////////////////////////////////////
// Decimal Places Handling Code
//////////////////////////////////////////////////////////////////////////
private function decimals($number) {
if (($frac = strstr($number, '.')) !== false)
return strlen($frac) - 1;
return 0;
}
}
?>
