Extension:UnitsFormatter/0.1

From MediaWiki.org
Jump to: navigation, search
<?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;
        }

}

?>
Personal tools
Namespaces
Variants
Actions
Site
Support
Download
Development
Communication
Print/export
Toolbox