Extension:UnitsFormatter/0.1

<?php

/* * Unit formatting extension. * * This extension installs a new tag, ... . 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 * ===== * *    4.00 usgal * * 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: * *    1 m       =>  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: * *    12 km/h            =>  12 kilometres per hour (7 mph) *    12 km/h =>  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: * *    -40.0 C    =>  -40.0 °F (-40.0 °C, 233.2 K) * * Units are specified using identifiers based on their standard symbols. * The tag displays a table of all known units: * *     */

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 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&sup2;', 'square metre'), 'cm2'  => array('square centimetre', 'square centimetres', 'cm&sup2;', 'square metre'), 'a'    => array('are', 'ares', 'a', 'are'), 'ha'   => array('hectare', 'hectares', 'ha', 'hectare'), 'km2'  => array('square kilometre', 'square kilometres', 'km&sup2;', 'square kilometre'), 'in2'  => array('square inch', 'square inches', 'in&sup2;', 'square inch'), 'ft2'  => array('square foot', 'square feet', 'ft&sup2;', 'square foot'), 'yd2'  => array('square yard', 'square yards', 'yd&sup2;', 'square yard'), 'acre' => array('acre', 'acres', 'acre', 'acre'), 'mi2'  => array('square mile', 'square miles', 'mi&sup2;', '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&sup3;', 'cubic metre'), 'in3' => array('cubic inch', 'cubic inches', 'in&sup3;', 'cubic inch'), 'ft3' => array('cubic foot', 'cubic feet', 'ft&sup3;', 'cubic foot'), 'yd3' => array('cubic yard', 'cubic yards', 'yd&sup3;', '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', 'calth', 'calorie'), 'cal-m' =>array('mean calorie', 'mean calories', 'calmean', 'calorie'), 'cal-15'=>array('15 &deg;C calorie', '15 &deg;C calories', 'cal15', 'calorie'), 'cal-20'=>array('20 &deg;C calorie', '20 &deg;C calories', 'cal20', 'calorie'), 'cal-ns'=>array('IUNS calorie', 'IUNS calories', 'calIUNS', 'calorie'), 'cal-it'=>array('international calorie', 'international calories', 'calINT', 'calorie'), 'kcal-ns'=>array('IUNS kilocalorie', 'IUNS kilocalories', 'kcalIUNS', 'calorie'), 'btu-th'=>array('thermochemical BTU', 'thermochemical BTUs', 'BTUth', 'British thermal unit'), 'btu-m' =>array('mean BTU', 'mean BTUs', 'BTUmean', 'British thermal unit'), 'btu-15'=>array('15 &deg;C BTU', '15 &deg;C BTUs', 'BTU15', 'British thermal unit'), 'btu-it'=>array('international BTU', 'international BTUs', 'BTUINT', 'British thermal unit'), 'btu-is'=>array('ISO BTU', 'ISO BTUs', 'BTUISO', '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('&deg;C', '&deg;C', '&deg;C', 'celsius'), 'f'    => array('&deg;F', '&deg;F', '&deg;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 " " 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 " ... " 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", $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 " " 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", $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. "";

// 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; }

}

?>