The purpose of this module is to prepare the data used byModule:Convert to allow conversion between units of measurement.
Usage: Put one of the following lines (with nothing else) in a sandbox:
{{#invoke:convert/makeunits|makeunits}}{{subst:#invoke:convert/makeunits|makeunits}}Previewing the sandbox should display the wikitext that needs to be copied and pasted intoModule:Convert/data. If a problem occurs, warning messages will be displayed to indicate that the unit definitions need to be fixed.
By default, the module reads the unit definitions fromModule:Convert/documentation/conversion data. For testing purposes, it is possible to specify that the definitions are read from another page, for example,User:Johnuniq/sandbox2, by specifying the wanted title:
{{#invoke:convert/makeunits|makeunits|User:Johnuniq/sandbox2}}{{subst:#invoke:convert/makeunits|makeunits|User:Johnuniq/sandbox2}}The module contains tablespecials which is used to insert a small amount of "built-in" data that is not currently defined in the input wikitext.
The module reads data fromModule:Convert/text to allowlocalization of the table of units for use on another wiki.
-- This module generates the wikitext required at Module:Convert/data-- by reading and processing the wikitext of the master list of units-- (see conversion_data for the page title).---- Script method:-- * Read lines, ignoring everything before "== Conversions ==".-- * Process the following lines:-- * Find next level-3 heading like "=== Length ===".-- * Parse each following line starting with "|"-- (but ignore lines starting with "|-" or "|}".-- * Split such lines into fields (delimiter "||") and trim-- leading/trailing whitespace from each field.-- Remove any "colspan" at front of second field (symbol).-- * Remove thousand separators (commas) from the scale field.-- If the scale is a number, do not change it.-- Otherwise, it should be an expression like "5/9", in-- which case it is replaced by the value of the expression.-- * Remove wiki formatting '[[...]]' from the link field.-- * Remove redundant fields from the unit to reduce size of data table.-- * Create alternative forms of a unit such as an alias or a combination.-- * Stop processing when encounter end of text or a line starting-- with a level-2 heading ("==" but not "===").-- * Repeat above for each heading listed at prepare_data().-- * Output Lua source for the units table.---- -- Output has the following form.-- local all_units = {-- ["unitcode"] = { -- standard format-- name1 = "singular name", -- omitted if redundant-- name1_us = "singular name sp=us", -- omitted if redundant-- name2 = "plural name", -- omitted if redundant-- name2_us = "plural name sp=us", -- omitted if redundant-- symbol = "symbol",-- sym_us = "symbol sp=us", -- omitted if redundant-- usename = 1, -- omitted if empty-- utype = "unit type", -- from level-3 heading-- scale = 1, -- a value, if necessary from evaluating an expression-- subdivs = { ["ft"] = { 5280, default = "km" }, ["yd"] = { 1760 } } -- composite input; omitted if empty-- link = "title of article for wikilink", -- omitted if empty or redundant-- ... -- other values-- },-- ["unitcode"] = { -- alternative format to generate an alias-- target = "unit code",-- ... -- optional values to override those of target-- },-- ["unitcode"] = { -- alternative format to generate a "per" unit like $/acre or BTU/h-- per = {u1, u2}, -- numbered table of unitcodes (u1 may be a currency symbol)-- ... -- optional values-- },-- ["unitcode"] = { -- alternative format to generate an error message-- shouldbe = "message that some other unit code should be used",-- },-- ["unitcode"] = { -- alternative format for combination outputs (like 'm ft')-- combination = {u1, u2, ...}, -- numbered table of unitcodes-- utype = "unit type", -- as for standard format-- },-- ["unitcode"] = { -- alternative format for output multiples (like 'ftin')-- combination = {u1, u2, ...}, -- numbered table of unitcodes-- multiple = {f1, f2, ...}, -- numbered table of integer factors-- utype = "unit type", -- as for standard format-- },-- ...-- }localulower=mw.ustring.lowerlocalusub=mw.ustring.sublocaltext_codelocalspecials={-- This table is used to add extra fields when defining some units which-- require exceptions to normal processing.-- Each key is in the local language, while each value is fixed text.-- However, this script should NOT be edited.-- Instead, the translation_table in Module:Convert/text can be edited,-- and this script will replace sections of the following with localized-- definitions from Module:Convert/text, if given.-- Ask for assistance at [[:en:Module talk:Convert]].-- LATER: It would be better if this was defined in the conversion data.utype={-- ["unit type in local language"] = "name_used_in_this_script"["fuel efficiency"]="type_fuel_efficiency",["length"]="type_length",["temperature"]="type_temperature",["volume"]="type_volume",},ucode={exception={-- ["unit code in local language"] = "name_used_in_module_convert"["ft"]="integer_more_precision",["in"]="subunit_more_precision",["lb"]="integer_more_precision",},istemperature={-- Common temperature scales (not keVT or MK).-- ["unit code in local language"] = true["C"]=true,["F"]=true,["K"]=true,["R"]=true,},usesymbol={-- Use unit symbol not name if abbr not specified.-- ["unit code in local language"] = 1["C"]=1,["F"]=1,["K"]=1,["R"]=1,["C-change"]=1,["F-change"]=1,["K-change"]=1,},alttype={-- Unit has an alternate type that is a valid conversion.-- ["unit code in local language"] = "alternate type in local language"["Nm"]="energy",["ftlb"]="torque",["ftlb-f"]="torque",["ftlbf"]="torque",["inlb"]="torque",["inlb-f"]="torque",["inlbf"]="torque",["inoz-f"]="torque",["inozf"]="torque",},},}-- Module text for the local language (localization).-- A default table of text for enwiki is provided here.-- If needed for another wiki, wanted sections from the table can be-- copied into translation_table in Module:Convert/text.-- For example, copying and modifying only the titles section may give:---- local translation_table = {-- ... -- other items-- mtext = {-- titles = {-- -- name_used_in_this_script = 'Title of page'-- conversion_data = 'Modul:Convert/documentation/conversion data/dok',-- },-- },-- }localmtext={section_names={-- name_used_in_this_script = 'Section title used in conversion data'overrides='Overrides',conversions='Conversions',outmultiples='Output multiples',combinations='Combinations',inmultiples='Input multiples',defaults='Defaults',links='Links',perunits='Automatic per units',varnames='Variable names',pernames='Names for second unit in a per',},titles={-- name_used_in_this_script = 'Title of page'conversion_data='Module:Convert/documentation/conversion data',},messages={-- name_used_in_this_script = 'Error message ($1 = first parameter, $2 = second)'m_als_bad='Alias has invalid text in field "$1".',m_als_dup='Alias "$1" already defined.',m_als_link='Alias "$1" must include a wikilink ("[[...]]") in the symlink text.',m_als_mul='Alias "$1" has multiplier "$2" which is not a number.',m_als_same='Should omit "$1" for alias "$2" because it is the same as its target.',m_als_type='Target of alias "$1" has wrong type.',m_als_undef='Primary unit must be defined before alias "=$1"',m_cmb_miss='Missing unit code for a combination.',m_cmb_none='No units specified for combination "$1"',m_cmb_one='Only one unit specified for combination "$1"',m_cmb_type='Unit "$1" in combination "$2" has wrong type.',m_cmb_undef='Unit "$1" in combination "$2" not defined.',m_cmp_def='Composite "$1" must specify a default unit code.',m_cmp_int='Composite "$1" has components where scale ratios are not integers.',m_cmp_inval='Composite "$1" has a component with an invalid scale, "$2".',m_cmp_many='Composite "$1" has too many fields.',m_cmp_miss='Missing unit code for a composite.',m_cmp_order='Composite "$1" has components in wrong order or with invalid scales.',m_cmp_scale='Alternate unit "$1" in composite "$2" has wrong scale.',m_cmp_two='Composite "$1" must specify exactly two unit codes.',m_cmp_type='Unit "$1" in composite "$2" has wrong type.',m_cmp_undef='Unit "$1" in composite "$2" not defined.',m_def_cond='Invalid condition in default "$1" for unit "$2".',m_def_fmt='Default output "$1" for unit "$2" should have 2 or 3 "!".',m_def_rpt='Default output "$1" for unit "$2" is repeated.',m_def_same='Default output for unit "$1" is the same unit.',m_def_type='Default output "$1" for unit "$2" has wrong type.',m_def_undef='Default output "$1" for unit "$2" is not defined.',m_dfs_code='Defaults section: no unit code specified.',m_dfs_dup='Defaults section: unit "$1" has already been specified.',m_dfs_none='Defaults section: unit "$1" has no default specified.',m_dfs_sym='Defaults section: unit "$1" must have a symbol.',m_dfs_two='Defaults section: unit "$1" should have two fields only.',m_dfs_undef='Defaults section: unit "$1" is not defined.',m_dup_code='Unit code "$1" has already been defined.',m_error='Error:',m_ftl_read='Could not read wikitext from "[[$1]]".',m_ftl_table='[[$1]] should export table "$2".',m_ftl_type='Fatal error: unknown data type for "$1"',m_hdg_lev2='Level 2 heading "$1" not found.',m_hdg_lev3='No level 3 heading before: $1',m_line_num=' (line $1).',m_lnk_brack='Link "$1" has wrong number of brackets.',m_lnk_dup='Link exception "$1" is already defined.',m_lnk_miss='Missing unit code for a link.',m_lnk_none='No link defined for unit "$1".',m_lnk_sym='Unit code "$1" for a link must have a symbol.',m_lnk_two='Row for unit "$1" link should have two fields only.',m_lnk_type='Link exception "$1" has wrong type.',m_lnk_undef='Unit code "$1" for a link is not defined.',m_miss_code='Missing unit code.',m_miss_sym='Missing symbol.',m_miss_type='Missing unit type.',m_mul_int='Multiple "$1" has components where scale ratios are not integers.',m_mul_miss='Missing unit code for a multiple.',m_mul_none='No units specified for multiple "$1"',m_mul_one='Only one unit specified for multiple "$1"',m_mul_order='Multiple "$1" has components in wrong order or with invalid scales.',m_mul_scale='Multiple "$1" has a component with an invalid scale, "$2".',m_mul_std='Unit "$1" in multiple "$2" must be a standard unit.',m_mul_type='Unit "$1" in multiple "$2" has wrong type.',m_mul_undef='Unit "$1" in multiple "$2" not defined.',m_no_title='Need title of page with unit definitions.',m_ovr_dup='Override "$1" is already defined.',m_ovr_miss='Missing unit code for an override.',m_per_dup='Per unit "$1" already defined.',m_per_empty='Unit "$1" has an empty field in the "per".',m_per_fuel='Unit "$1" has invalid unit types for fuel efficiency.',m_per_inv='Invalid field for a "per".',m_per_two='Unit "$1" does not have exactly 2 fields in the "per".',m_per_undef='Unit "$1" has undefined unit code "$2" in the "per".',m_percent_s='Field "$1" must not contain "%s".',m_pnm_cnt='Names for second unit in a per section: each row must have two columns.',m_pnm_dup='Unit "$1" already has a per name.',m_pnm_miss='Missing field for a per name.',m_pnm_undef='Unit "$1" in per names is not defined.',m_pfx_bad='Unknown prefix: "$1".',m_pfx_name='Unit with Prefix set must include Name.',m_scl_bad='Scale expression is invalid: "$1".',m_scl_miss='Missing scale.',m_scl_oflow='Scale expression gives an invalid value: "$1".',m_var_cnt='Variable names section: each row must have the configured number of columns.',m_var_dup='Unit "$1" already has a variable name.',m_var_miss='Missing field for a variable name.',m_var_undef='Unit "$1" in variable names is not defined.',m_warning='Warning:',m_wrn_more=' (and more not shown)',m_wrn_nbsp='Line $1 contains a nonbreaking space.',m_wrn_nodef='Units with the following unit codes have no default output.',m_wrn_ucode=' $1',},}localfunctionmessage(key,...)-- Return a message from the message table, which can be localized.-- '$1', '$2', ... are replaced with the first, second, ... parameters,-- each of which must be a string or a number.-- The global variable is_test_run can be set by a testing program to-- check the messages generated by this program.localrep={}fori,vinipairs({...})dorep['$'..i]=vendkey=keyor'???'localextraifis_test_runandkey~='m_line_num'thenextra=key..': 'elseextra=''endreturnextra..string.gsub(mtext.messages[key]orkey,'$%d+',rep)endlocalfunctionquit(key,...)-- Use error() to pass an error message to the surrounding pcall().error(message(key,...),0)endlocalfunctionquit_no_message()-- Throw an error.-- This is used in some functions which can throw an error with a message,-- but where the message is in fact never displayed because the calling-- function uses pcall to catch errors, and any message is ignored.-- Using this function documents that the message (which may be useful in-- some other application) does not need translation as it never appears.error('this message is not displayed',0)endlocalfunctioncollection()-- Return a table to hold items.return{n=0,add=function(self,item)self.n=self.n+1self[self.n]=itemend,pop=function(self,item)ifself.n>0thenlocaltop=self[self.n]self.n=self.n-1returntopendend,join=function(self,sep)returntable.concat(self,sepor'\n')end,}endlocalwarnings=collection()localfunctionadd_warning(key,...)-- Add a warning that will be inserted before the final result.warnings:add(message(key,...))end---Begin code to evaluate expressions------------------------------------- This is needed because Lua's loadstring() is not available in Scribunto,-- and each scale value can be specifed as an expression such as "5/9".-- More complex expressions are supported, including use of parentheses-- and the binary operators: + - * / ^localoperators={['+']={precedence=1,associativity=1,func=function(a,b)returna+bend},['-']={precedence=1,associativity=1,func=function(a,b)returna-bend},['*']={precedence=2,associativity=1,func=function(a,b)returna*bend},['/']={precedence=2,associativity=1,func=function(a,b)returna/bend},['^']={precedence=3,associativity=2,func=function(a,b)returna^bend},['(']='(',[')']=')',}localfunctiontokenizer(text)-- Function 'next' returns the next token which is one of:-- number-- table (operator)-- string ('(' or ')')-- nil (end of text)-- If invalid, an error is thrown.-- The number is unsigned (unary operators are not supported).return{pos=1,maxpos=#text,text=text,next=function(self)ifself.pos<=self.maxposthenlocalp1,p2,hit=self.text:find('^%s*([+%-*/^()])',self.pos)ifhitthenself.pos=p2+1returnoperators[hit]endp1,p2,hit=self.text:find('^%s*(%d*%.?%d*[eE][+-]?%d*)',self.pos)ifnothitthenp1,p2,hit=self.text:find('^%s*(%d*%.?%d*)',self.pos)endlocalvalue=tonumber(hit)ifvaluethenself.pos=p2+1returnvalueendquit_no_message('invalid number "'..self.text:sub(self.pos)..'"')endend}endlocalfunctionevaluate_tokens(tokens,inparens)-- Return the value from evaluating tokenized expression, or throw an error.localnumstack,opstack=collection(),collection()localfunctionperform_ops(precedence,associativity)whileopstack.n>0and(opstack[opstack.n].precedence>precedenceor(opstack[opstack.n].precedence==precedenceandassociativity==1))dolocalrhs=numstack:pop()locallhs=numstack:pop()ifnot(rhsandlhs)thenquit_no_message('missing number')endlocalop=opstack:pop()numstack:add(op.func(lhs,rhs))endendlocaltoken_lastlocalfunctionset_state(token_type)iftoken_last==token_typethenlocalmissing=(token_type=='number')and'operator'or'number'quit_no_message('missing '..missing)endtoken_last=token_typeendwhiletruedolocaltoken=tokens:next()iftype(token)=='number'thenset_state('number')numstack:add(token)elseiftype(token)=='table'thenset_state('operator')perform_ops(token.precedence,token.associativity)opstack:add(token)elseiftoken=='('thenset_state('number')numstack:add(evaluate_tokens(tokens,true))elseiftoken==')'thenifinparensthenbreakendquit_no_message('unbalanced parentheses')elsebreakendendperform_ops(0)ifnumstack.n>1thenquit_no_message('missing operator')endifnumstack.n<1thenquit_no_message('missing number')endreturnnumstack:pop()endlocalfunctionevaluate(expression)-- Return value (a number) from evaluating expression (a string),-- or throw an error if invalid.-- This is not bullet proof, but it should support the expressions used.returnevaluate_tokens(tokenizer(expression))end---End code to evaluate expressions----------------------------------------Begin code adapted from Module:Convert-------------------------------localplural_suffix='s'-- may be changed from translation.plural_suffix belowlocalfunctionshallow_copy(t)-- Return a shallow copy of t.-- Do not need the features and overhead of mw.clone() provided by Scribunto.localresult={}fork,vinpairs(t)doresult[k]=vendreturnresultendlocalfunctionsplit(text,delimiter)-- Return a numbered table with fields from splitting text.-- The delimiter is used in a regex without escaping (for example, '.' would fail).-- Each field has any leading/trailing whitespace removed.localt={}text=text..delimiter-- to get last itemforitemintext:gmatch('%s*(.-)%s*'..delimiter)dotable.insert(t,item)endreturntendlocalunit_mt={-- Metatable to get missing values for a unit that does not accept SI prefixes.-- Warning: The boolean value 'false' is returned for any missing field-- so __index is not called twice for the same field in a given unit.__index=function(self,key)localvalueifkey=='name1'orkey=='sym_us'thenvalue=self.symbolelseifkey=='name2'thenvalue=self.name1..plural_suffixelseifkey=='name1_us'thenvalue=self.name1ifnotrawget(self,'name2_us')then-- If name1_us is 'foot', do not make name2_us by appending plural_suffix.self.name2_us=self.name2endelseifkey=='name2_us'thenlocalraw1_us=rawget(self,'name1_us')ifraw1_usthenvalue=raw1_us..plural_suffixelsevalue=self.name2endelseifkey=='link'thenvalue=self.name1elsevalue=falseendrawset(self,key,value)returnvalueend}localfunctionprefixed_name(unit,name,index)-- Return unit name with SI prefix inserted at correct position.-- index = 1 (name1), 2 (name2), 3 (name1_us), 4 (name2_us).-- The position is a byte (not character) index, so use Lua's sub().localpos=rawget(unit,'prefix_position')iftype(pos)=='string'thenpos=tonumber(split(pos,',')[index])endifposthenreturnname:sub(1,pos-1)..unit.si_name..name:sub(pos)endreturnunit.si_name..nameendlocalunit_prefixed_mt={-- Metatable to get missing values for a unit that accepts SI prefixes.-- Before use, fields si_name, si_prefix must be defined.-- The unit must define _symbol, _name1 and-- may define _sym_us, _name1_us, _name2_us-- (_sym_us, _name2_us may be defined for a language using sp=us-- to refer to a variant unrelated to U.S. units).__index=function(self,key)localvalueifkey=='symbol'thenvalue=self.si_prefix..self._symbolelseifkey=='sym_us'thenvalue=rawget(self,'_sym_us')ifvaluethenvalue=self.si_prefix..valueelsevalue=self.symbolendelseifkey=='name1'thenvalue=prefixed_name(self,self._name1,1)elseifkey=='name2'thenvalue=rawget(self,'_name2')ifvaluethenvalue=prefixed_name(self,value,2)elsevalue=self.name1..plural_suffixendelseifkey=='name1_us'thenvalue=rawget(self,'_name1_us')ifvaluethenvalue=prefixed_name(self,value,3)elsevalue=self.name1endelseifkey=='name2_us'thenvalue=rawget(self,'_name2_us')ifvaluethenvalue=prefixed_name(self,value,4)elseifrawget(self,'_name1_us')thenvalue=self.name1_us..plural_suffixelsevalue=self.name2endelseifkey=='link'thenvalue=self.name1elsevalue=falseendrawset(self,key,value)returnvalueend}localfunctionlookup(units,unitcode,sp,what)-- Return a copy of the unit if found, or return nil.-- In this cut-down code, sp is always nil, and what is ignored.localt=units[unitcode]iftthenift.shouldbethenreturnnilendlocalresult=shallow_copy(t)ifresult.prefixesthenresult.si_name=''result.si_prefix=''returnsetmetatable(result,unit_prefixed_mt)endreturnsetmetatable(result,unit_mt)endlocalSIprefixes=text_code.SIprefixesforplen=SIprefixes[1]or2,1,-1do-- Look for an SI prefix; should never occur with an alias.-- Check for longer prefix first ('dam' is decametre).-- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub).localprefix=usub(unitcode,1,plen)localsi=SIprefixes[prefix]ifsithenlocalt=units[usub(unitcode,plen+1)]iftandt.prefixesthenlocalresult=shallow_copy(t)if(sp=='us'ort.sp_us)andsi.name_usthenresult.si_name=si.name_uselseresult.si_name=si.nameendresult.si_prefix=si.prefixorprefix-- In this script, each scale is a string.result.scale=tostring(tonumber(t.scale)*10^(si.exponent*t.prefixes))result.prefixes=nil-- a prefixed unit does not take more prefixes (in this script, the returned unit may be added to the list of units)returnsetmetatable(result,unit_prefixed_mt)endendendlocalexponent,baseunit=unitcode:match('^e(%d+)(.*)')ifexponentthenlocalengscale=text_code.eng_scales[exponent]ifengscalethenlocalresult=lookup(units,baseunit,sp,'no_combination')ifnotresultthenreturnnilendifnot(result.offsetorresult.builtinorresult.engscale)thenresult.defkey=unitcode-- key to lookup default exceptionresult.engscale=engscale-- Do not set result.scale as this code is called for units where that is not set.returnresultendendendreturnnilendlocalfunctionevaluate_condition(value,condition)-- Return true or false from applying a conditional expression to value,-- or throw an error if invalid.-- A very limited set of expressions is supported:-- v < 9-- v * 9 < 9-- where-- 'v' is replaced with value-- 9 is any number (as defined by Lua tonumber)-- '<' can also be '<=' or '>' or '>='-- In addition, the following form is supported:-- LHS and RHS-- where-- LHS, RHS = any of above expressions.localfunctioncompare(value,text)localarithop,factor,compop,limit=text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$')ifarithop==nilthenquit_no_message('Invalid default expression.')elseifarithop=='*'thenfactor=tonumber(factor)iffactor==nilthenquit_no_message('Invalid default expression.')endvalue=value*factorendlimit=tonumber(limit)iflimit==nilthenquit_no_message('Invalid default expression.')endifcompop=='<'thenreturnvalue<limitelseifcompop=='<='thenreturnvalue<=limitelseifcompop=='>'thenreturnvalue>limitelseifcompop=='>='thenreturnvalue>=limitendquit_no_message('Invalid default expression.')-- should not occurendlocallhs,rhs=condition:match('^(.-%W)and(%W.*)')iflhs==nilthenreturncompare(value,condition)endreturncompare(value,lhs)andcompare(value,rhs)end---End adapted code-----------------------------------------------------localfunctionstrip(text)-- Return text with no leading/trailing whitespace.returntext:match("^%s*(.-)%s*$")endlocalfunctionempty(text)-- Return true if text is nil or empty (assuming a string).returntext==nilortext==''end-- Tables of units: k = unit code, v = unit table.localunits_index={}-- all units: normal, alias, per, combination, or multiplelocalalias_index={}-- all aliases (to detect attempts to define more than once)localper_index={}-- all "per" units (to detect attempts to define more than once)localfunctionget_unit(ucode,utype)-- Look up unit code in our cache of units.-- If utype == nil, the unit should already have been defined.-- Otherwise, ucode may represent an automatically generated combination-- where each component must have the given utype; a dummy unit is returned.ifempty(ucode)thenreturnnilendlocalunit=lookup(units_index,ucode)ifunitornotutypethenreturnunitendlocalcombo=collection()ifucode:find('+',1,true)thenforitemin(ucode..'+'):gmatch('%s*(.-)%s*%+')doifitem~=''thencombo:add(item)endendelseifucode:find('%s')thenforiteminucode:gmatch('%S+')docombo:add(item)endendifcombo.n>1thenlocalresult=setmetatable({utype=utype},{__index=function(self,key)error('Bug: invalid use of automatically generated unit')end})for_,vinipairs(combo)dolocalcomponent=lookup(units_index,v)ifnotcomponentorcomponent.shouldbeorcomponent.combinationthenreturnnilendifutype~=component.utypethenresult.utype=component.utype-- set wrong type which caller will detectbreakendendreturnresultendendlocaloverrides={}-- read from input for unit codes that should not be checked for a duplicatelocalfunctioninsert_unique_unit(data,unit,index)-- After inserting any required built-in data, insert the unit into the-- data table and (if index not nil) add to index,-- but not if the unit code is already defined.localucode=unit.unitcodelocalknown=get_unit(ucode)ifknownandnotoverrides[ucode]thenquit('m_dup_code',ucode)endforitem,tinpairs(specials.ucode)dounit[item]=t[ucode]endifindexthenindex[ucode]=unitendtable.insert(data,unit)endlocalfunctioncheck_condition(condition)-- Return true if condition appears to be valid; otherwise return false.for_,valueinipairs({0,0.1,1,1.1,10,100,1000,1e4,1e5})dolocalsuccess,result=pcall(evaluate_condition,value,condition)ifnotsuccessthenreturnfalseendendreturntrueendlocalfunctioncheck_default_expression(default,ucode)-- Return a numbered table of names present in param default-- (two names if an expression, or one name (param default) otherwise).-- Throw an error if a problem occurs.-- An expression uses pipe-delimited fields with 'v' representing-- the input value for the conversion.-- Example (suffix is optional): 'v < 120 ! small ! big ! suffix'-- returns { 'smallsuffix', 'bigsuffix' }.ifnotdefault:find('!',1,true)thenreturn{default}endlocalt={}foritemin(default..'!'):gmatch('%s*(.-)%s*!')dot[#t+1]=item-- split on '!', removing leading/trailing whitespaceendifnot(#t==3or#t==4)thenquit('m_def_fmt',default,ucode)endlocalcondition,default1,default2=t[1],t[2],t[3]if#t==4thendefault1=default1..t[4]default2=default2..t[4]endifnotcheck_condition(condition)thenquit('m_def_cond',default,ucode)endreturn{default1,default2}endlocalfunctioncheck_default(default,ucode,utype,unit_table)-- Check the given name (or expression) of a default output.-- Normally a unit must not define itself as its default. However,-- some units are defined merely for use in per units, and they have-- the same ucode, utype and default.-- Example: unit cent which cannot be converted to anything other than-- a cent, but which can work, for example, in cent/km and cent/mi.-- Throw an error if a problem occurs.localdone={}for_,defaultinipairs(check_default_expression(default,ucode))doifdone[default]thenquit('m_def_rpt',default,ucode)endifdefault==ucodeanducode~=utypethenquit('m_def_same',ucode)endlocaldefault_table=get_unit(default,utype)ifnotdefault_tablethenquit('m_def_undef',default,ucode)endifnot(utype==unit_table.utypeandutype==default_table.utype)thenquit('m_def_type',default,ucode)enddone[default]=trueendendlocalfunctioncheck_all_defaults(cfg,units)-- Check each default in units and warn if needed.-- This is done after all input data has been processed.-- Throw an error if a problem occurs.localerrors=collection()localmissing=collection()-- unitcodes with missing defaultsfor_,unitinipairs(units)doifnotunit.shouldbeandnotunit.combinationthen-- This is a standard unit or an alias/per (not shouldbe, combo).-- An alias may have a default defined, but it is optional.localdefault=unit.defaultlocalucode=unit.unitcodeifempty(default)thenifnotunit.targetthen-- unit should have a defaultmissing:add(ucode)endelselocalok,msg=pcall(check_default,default,ucode,unit.utype,unit)ifnotokthenerrors:add(msg)iferrors.n>=cfg.maxerrorsthenbreakendendendendendiferrors.n>0thenerror(errors:join(),0)endifmissing.n>0thenadd_warning('m_wrn_nodef')locallimit=cfg.maxerrorsfor_,vinipairs(missing)dolimit=limit-1iflimit<0thenadd_warning('m_wrn_more')breakendadd_warning('m_wrn_ucode',v)endendendlocalfunctioncheck_all_pers(cfg,units)-- Check each component of each "per" unit and warn if needed.-- In addition, add any required extra fields for some types of units.-- This is done after all input data has been processed.-- Throw an error if a problem occurs.localerrors=collection()localfunctionerrmsg(key,...)errors:add(message(key,...))endfor_,unitinipairs(units)dolocalper=unit.perifperthenlocalucode=unit.unitcodeif#per~=2thenerrmsg('m_per_two',ucode)elselocaltypes={}fori,vinipairs(per)doifempty(v)thenerrmsg('m_per_empty',ucode)endifnottext_code.currency[v]thenlocalt=get_unit(v)iftthentypes[i]=t.utypeelseerrmsg('m_per_undef',ucode,v)endendendifspecials.utype[unit.utype]=='type_fuel_efficiency'thenlocalexpected={type_volume=1,type_length=2}localtop_type=expected[specials.utype[types[1]]]localbot_type=expected[specials.utype[types[2]]]iftop_typeandbot_typeandtop_type~=bot_typethenunit.iscomplex=trueiftop_type==1thenunit.invert=1elseunit.invert=-1endelseerrmsg('m_per_fuel',ucode)endendendendiferrors.n>=cfg.maxerrorsthenbreakendendiferrors.n>0thenerror(errors:join(),0)endendlocalfunctionupdate_units(units,composites,varnames,pernames)-- Update some unit definitions with extra data defined in other sections.-- This is done after all input data has been processed.for_,unitinipairs(units)dolocalcomp=composites[unit.unitcode]ifcompthenunit.subdivs='{ '..table.concat(comp.subdivs,', ')..' }'endifvarnames[unit.unitcode]thenunit.varname=varnames[unit.unitcode]endifpernames[unit.unitcode]thenunit.pername=pernames[unit.unitcode]endendendlocalfunctionmake_override(cfg,data)-- Return a function which, when called, stores a unit code that is not to be-- checked for a duplicate. The table is stored in data (also a table).returnfunction(utype,fields)localucode=fields[1]ifempty(ucode)thenquit('m_ovr_miss')endifdata[ucode]thenquit('m_ovr_dup',ucode)enddata[ucode]=trueendendlocalfunctionmake_default(cfg,data)-- Return a function which, when called, stores a table that defines a-- default output unit. The table is stored in data (also a table).localdefaults_index={}-- to detect attempts to define a default twicereturnfunction(utype,fields)-- Store a table defining a unit.-- This is for a unit such as 'kg' that has a default output unit-- different from what is defined for the base unit ('g').-- Throw an error if a problem occurs.localucode=fields[1]localdefault=fields[2]ifempty(ucode)thenquit('m_dfs_code')endifempty(default)thenquit('m_dfs_none',ucode)endif#fields~=2thenquit('m_dfs_two',ucode)endlocalunit_table=get_unit(ucode)ifnotunit_tablethenquit('m_dfs_undef',ucode)endlocalsymbol=unit_table.defkeyorunit_table.symbolifempty(symbol)thenquit('m_dfs_sym',ucode)endcheck_default(default,ucode,utype,unit_table)ifdefaults_index[ucode]thenquit('m_dfs_dup',ucode)enddefaults_index[ucode]=defaulttable.insert(data,{symbol=symbol,default=default})endendlocalfunctionclean_link(link,name)-- Return link, customary where:-- link = given link after removing any '[[...]]' wiki formatting-- and removing any leading '+' or '*' or '@';-- customary = 1 if leading '+', or 2 if '*' or 3 if '@', or nil-- (for extra "US" or "U.S." or "Imperial" customary units link).-- Result has leading/trailing whitespace removed, and is nil if empty-- or if link matches the name, if a name is specified.-- Exception: If the link is empty and the name starts with '[[',-- the link is stored as '' (for a unit name which is always linked).-- If the resulting link is nil, no link field is stored, and-- if a link is required, it will be set from the unit's name.localoriginal=linkifempty(link)thenreturn(nameandname:sub(1,2)=='[[')and''ornilendlocalprefixes={['+']=1,['*']=2,['@']=3}localcustomary=prefixes[link:sub(1,1)]ifcustomarythenlink=strip(link:sub(2))endiflink:sub(1,2)=='[['thenlink=link:sub(3)endiflink:sub(-2)==']]'thenlink=link:sub(1,-3)endlink=strip(link)iflink:sub(1,1)=='['orlink:sub(-1)==']'thenquit('m_lnk_brack',original)endiflink==''thenlink=nilelseifnamethenlocall=ulower(usub(link,1,1))..usub(link,2)localn=ulower(usub(name,1,1))..usub(name,2)ifl==nthenlink=nil-- link == name, ignoring case of first letterendendreturnlink,customaryendlocalfunctionmake_link(cfg,data)-- Return a function which, when called, stores a table that defines a-- link exception. The table is stored in data (also a table).locallinks_index={}-- to detect attempts to define a link twicereturnfunction(utype,fields)-- Store a table defining a unit.-- This is for a unit such as 'kg' that has a linked article-- different from what is defined for the base unit ('g').-- Throw an error if a problem occurs.localucode=fields[1]locallink=clean_link(fields[2])ifempty(ucode)thenquit('m_lnk_miss')endifempty(link)thenquit('m_lnk_none',ucode)endif#fields~=2thenquit('m_lnk_two',ucode)endlocalunit_table=get_unit(ucode)ifnotunit_tablethenquit('m_lnk_undef',ucode)endifutype~=unit_table.utypethenquit('m_lnk_type',ucode)endlocalsymbol=unit_table.symbolifempty(symbol)thenquit('m_lnk_sym',ucode)endiflinks_index[ucode]thenquit('m_lnk_dup',ucode)endlinks_index[ucode]=linktable.insert(data,{symbol=symbol,link=link})endendlocalfunctionclean_scale(scale)-- Return cleaned scale as a string, after evaluating any expression.-- It would be better to retain scale expressions like "5/9" so that-- the expression is evaluated on the server and maintains the full-- resolution of the server. However, there are many such expressions-- in the table of all units, and it seems pointless to require the-- server to evaluate all of them just to do one convert.ifempty(scale)thenquit('m_scl_miss')endassert(type(scale)=='string','Bug: scale has an unexpected type')scale=string.gsub(scale,',','')-- remove comma separatorsiftonumber(scale)then-- not an expressionreturnscaleendlocalstatus,value=pcall(evaluate,scale)ifnot(statusandtype(value)=='number')thenquit('m_scl_bad',scale)endlocalresult=string.format('%.17g',value)ifresult:find('[#n]')then-- Lua can give results like "#INF" while Scribunto gives "inf". Either is an error.quit('m_scl_oflow',scale)end-- Omit redundant zeros from results like '1.2e-005'.-- Do not bother looking for results like '1.2e+005' as none occur in practice.locallhs,zeros,rhs=result:match('^(.-e%-)(0+)(.*)')ifzerosthenresult=lhs..rhsendreturnresultendlocalfunctionadd_alias_optional_fields(unit,start,fields,target)-- Inspect fields[i] for i = start, start+1 ..., and extract any-- definitions appropriate for an alias or "per", and add them to unit.-- For an alias, target is a valid unit; for a "per", target is nil.-- Throw error if encounter an invalid entry.fori=start,#fieldsdolocalfield=fields[i]ifnotempty(field)thenlocallhs,rhs=field:match('^%s*(.-)%s*=%s*(.-)%s*$')localgoodifnotempty(rhs)thenfor_,iteminipairs({'sp','default','link','multiplier','symbol','symlink','abbr'})doiflhs==itemthenifitem=='sp'thenifrhs=='us'thenunit.sp_us=truegood=trueendelseifitem=='link'thenlocaltlinkiftargetthentlink=target[item]endlocallink,customary=clean_link(rhs,tlink)iflinkthenunit[item]=linkendifcustomarythenunit.customary=customaryendgood=trueelseifitem=='symlink'thenlocalpos1=rhs:find('[[',1,true)localpos2=rhs:find(']]',1,true)ifnot(pos1andpos2and(pos1<pos2))thenquit('m_als_link',unit.unitcode)endunit.symlink=rhsgood=trueelseifitem=='multiplier'thenifnottonumber(rhs)thenquit('m_als_mul',unit.unitcode,rhs)endunit[item]=rhsgood=trueelseifitem=='abbr'theniftargetandrhs=='off'thenunit.usename=1good=trueendelseiftargetandrhs==target[item]thenquit('m_als_same',item,unit.unitcode)endunit[item]=rhsgood=trueendbreakendendendifnotgoodthenquit('m_als_bad',field)endendendendlocalfunctionmake_alias(fields,ucode,utype,symbol)-- Return a new alias unit, or return nil if symbol is not already-- defined as the unit code of the target unit.-- Throw an error if invalid.localtarget=get_unit(symbol)ifnottargetthenreturnnilendlocalunit={unitcode=ucode,utype=utype,target=symbol}add_alias_optional_fields(unit,3,fields,target)ifalias_index[ucode]thenquit('m_als_dup',ucode)elsealias_index[ucode]=unitendiftarget.utype~=utypethenquit('m_als_type',ucode)endreturnunitendlocalfunctionmake_per(fields,ucode,utype,symbol)-- Return a new "per" unit, or return nil if symbol is not of form "x/y".-- Throw an error if invalid.-- The top, bottom unit codes are checked later, after all units are defined.localtop,bottom=symbol:match('^(.-)/(.*)$')ifnottopthenreturnnilendlocalunit={unitcode=ucode,utype=utype,per={strip(top),strip(bottom)}}add_alias_optional_fields(unit,3,fields)ifper_index[ucode]thenquit('m_per_dup',ucode)elseper_index[ucode]=unitendreturnunitendlocalfunctionmake_unit(cfg,data)-- Return a function which, when called, stores a table that defines a-- single unit. The table is stored in data (also a table).localfieldnames={-- Fields in the Conversions section are assumed to be in the following order.'unitcode','symbol','sym_us','scale','extra','name1','name2','name1_us','name2_us','prefixes','default','link',}returnfunction(utype,fields)-- Store a table defining a unit.-- Throw an error if a problem occurs.localucode,symbol=fields[1],fields[2]ifempty(utype)thenquit('m_miss_type')endifempty(ucode)thenquit('m_miss_code')endifempty(symbol)thenquit('m_miss_sym')endlocalprefix=symbol:sub(1,1)ifprefix=='~'orprefix=='='orprefix=='!'orprefix=='*'thenifsymbol:sub(1,2)=='=='thenprefix=symbol:sub(1,2)endsymbol=strip(symbol:sub(#prefix+1))-- omit prefix and any following whitespacefields[2]=symbolelseprefix=nil-- not a valid prefixendifprefix=='='orprefix=='=='then-- ucode is an alias (a fake unit code used in a convert template), or-- defines a "per" unit like "$/acre" or "BTU/h".-- For an alias, symbol is the unit code of the actual unit.-- For a "per", symbol is of form "x/y" where x and y are unit codes,-- or x is a recognized currency symbol and y is a unit code.-- Checking that x and y are valid is deferred until all units have-- been defined so, for example, "BTU/h" can be defined before "h".localunitifprefix=='='thenunit=make_alias(fields,ucode,utype,symbol)elseunit=make_per(fields,ucode,utype,symbol)endifnotunitthen-- Do not define an alias in terms of another alias.quit('m_als_undef',symbol)endinsert_unique_unit(data,unit,units_index)returnelseifprefix=='!'then-- ucode may be incorrectly entered as a unit code.-- symbol is a message saying what unit code should be used.localunit={unitcode=ucode,shouldbe=symbol}insert_unique_unit(data,unit,nil)returnend-- Make the unit.localunit={utype=utype}fori,nameinipairs(fieldnames)doifnotempty(fields[i])thenunit[name]=fields[i]endend-- Remove redundancy from unit.ifunit.sym_us==symbolthenunit.sym_us=nilendlocalprefixes=unit.prefixeslocalname1,name2=unit.name1,unit.name2ifname1thenifname1==symbolandnotprefixesthen-- A unit which takes an SI prefix must not have a nil name because,-- for example, the name for "kW" = "kilo" .. "watt" (name for "W").-- The "not prefixes" test is needed for bnwiki where the-- watt unit has the same name and symbol.unit.name1=nilendelsename1=symbolendifname2thenifname2==name1..plural_suffixthenunit.name2=nilendelsename2=name1..plural_suffixendlocalname1_us,name2_us=unit.name1_us,unit.name2_usifname1_usthenifname1_us==name1thenunit.name1_us=nilendendifname2_usthenifunit.name1_usthenifname2_us==unit.name1_us..plural_suffixthenunit.name2_us=nilendelseifname2_us==name2thenunit.name2_us=nilendend-- Other changes to unit.unit.scale=clean_scale(unit.scale)localextra=unit.extraifnotempty(extra)then-- Set appropriate fields for a unit that needs more than a simple-- multiplication by a ratio of unit scales to convert values.unit.iscomplex=trueifextra=='volume/length'thenunit.invert=1elseifextra=='length/volume'thenunit.invert=-1elseifspecials.utype[utype]=='type_temperature'thenunit.offset=extraelseifextra=='invert'thenunit.invert=-1elseunit.builtin=extraendendifprefix=='~'then-- Magic code for units like "acre" where the symbol is not really a-- symbol, and output should use the singular or plural name instead.unit.usename=1elseifprefix=='*'then-- Magic code for units like "pitch" which have a symbol that is the same as-- another unit with entries defined in the default or link exceptions tables.unit.defkey=ucode-- key for default exceptionsunit.linkey=ucode-- key for link exceptionsendlocalname_for_linkifprefixesthenifprefixes=='SI'thenunit.prefixes=1elseifprefixes=='SI2'thenunit.prefixes=2elseifprefixes=='SI3'thenunit.prefixes=3elsequit('m_pfx_bad',prefixes)endelse-- Only units which do not accept SI prefixes have name_for_link set.-- That is because, for example, if set name_for_link = name1 for unit g,-- then the link is "kilogram" for kg, and "yottagram" for Yg, and so on-- for all prefixes. That might be desirable for some units, but not all.name_for_link=name1endunit.link,unit.customary=clean_link(unit.link,name_for_link)ifprefixesthen-- The SI prefix is always at the start (position = 1) for symbol and sym_us.-- However, each name (name1, name2, name1_us, name2_us) can have the SI prefix-- at any position, and that position can be different for each name.-- For enwiki, the only units with names where the prefix is not at the start-- are "square metre" and "cubic metre" ("square meter" and "cubic meter" for sp=us).-- Some other wikis want the flexibility that the prefix position can be different-- so the position is stored as nil (if always 1), or N (an integer, if always N),-- or a string of four comma-separated numbers such as "5,7,9,11" which means the-- prefix position for (name1, name2, name1_us, name2_us) is (5, 7, 9, 11)-- respectively.localname1,name1_us=unit.name1,unit.name1_us-- after redundancy removedifnotname1thenquit('m_pfx_name')endlocalpositions=collection()fori,kinipairs({'name1','name2','name1_us','name2_us'})dolocalname=unit[k]localposifnamethenpos=name:find('%s',1,true)ifposthenunit[k]=name:sub(1,pos-1)..name:sub(pos+2)endelseifi==2ori==3thenpos=positions[1]elseifi==4thenpos=positions[unit.name1_usand3or2]endpositions:add(posor1)endlocalpos=positions[1]fori=2,positions.ndoifpos~=positions[i]thenpos='"'..positions:join(',')..'"'breakendendifpos~=1thenunit.prefix_position=posendfor_,nameinipairs({'symbol','sym_us','name1','name1_us','name2','name2_us'})dounit['_'..name]=unit[name]unit[name]=nil-- force call to __index metamethod so any SI prefix can be handledendendforname,vinpairs(unit)do-- Reject if a string field includes "%s" (should not occur after above).iftype(v)=='string'andv:find('%s',1,true)thenquit('m_percent_s',name)endendinsert_unique_unit(data,unit,units_index)endendlocalfunctionmake_combination(cfg,data)-- Return a function which, when called, stores a table that defines a-- single combination unit. The table is stored in data (also a table).returnfunction(utype,fields)-- Store a table defining a unit.-- This is for a combination unit that specifies more than one output.-- The target units must be defined first.-- Throw an error if a problem occurs.localunit={utype=utype,combination={}}fori,vinipairs(fields)doifi==1then-- unitcodeifv==''thenquit('m_cmb_miss')endunit.unitcode=velseifv==''then-- Ignore empty fields.elselocaltarget=get_unit(v)ifnottargetthenquit('m_cmb_undef',v,unit.unitcode)endiftarget.utype~=utypethenquit('m_cmb_type',v,unit.unitcode)endtable.insert(unit.combination,v)endendif#unit.combination<2thenquit(#unit.combination==0and'm_cmb_none'or'm_cmb_one',unit.unitcode)endinsert_unique_unit(data,unit,units_index)endendlocalfunctionmake_perunit(cfg,data)-- Return a function which, when called, stores a table that defines a-- fixup for an automatic per unit. The table is stored in data (also a table).localpertype_index={}-- to detect attempts to define a fixup twicereturnfunction(utype,fields)-- Store a table to define a fixup.-- Typos or other errors in the input are not detected!-- Parameter utype is ignored (it is nil).-- Throw an error if a problem occurs.locallhs,rhs,link,multiplierfori,vinipairs(fields)doifv==''then-- Ignore empty fields.elseifi==1thenlhs=v-- like "length/time"elseifi==2thenrhs=v-- like "speed"elseifi==3thenlink=velseifi==4thenifnottonumber(v)thenquit('m_per_inv')endmultiplier=velsequit('m_per_inv')endendiflhsand(rhsorlinkormultiplier)theniflinkormultiplierthenlocalparts=collection()ifrhsthenparts:add('utype = "'..rhs..'"')endiflinkthenparts:add('link = "'..link..'"')endifmultiplierthenparts:add('multiplier = '..multiplier)endrhs='{ '..parts:join(', ')..' }'elserhs='"'..rhs..'"'endifpertype_index[lhs]thenquit('m_per_dup',lhs)endpertype_index[lhs]=rhstable.insert(data,{lhs=lhs,rhs=rhs})elsequit('m_per_inv')endendendlocalfunctionmake_varname(cfg,data)-- Return a function which, when called, stores a table that defines a-- variable name for a unit. The table is stored in data (also a table).returnfunction(utype,fields)-- Set or update an entry in the data table to record that a unit has a variable name.-- This is for slwiki where a unit name depends on the value.-- The target units must be defined first.-- Parameter utype is ignored (it is nil).-- Throw an error if a problem occurs.localcount=#fieldsifcount~=cfg.varcolumnsthenquit('m_var_cnt')endlocalucodelocalnames={}fori=1,countdolocalv=fields[i]ifempty(v)thenquit('m_var_miss')endifi==1then-- unitcodeucode=vifnotget_unit(v)thenquit('m_var_undef',v)endelsetable.insert(names,v)endendifdata[ucode]thenquit('m_var_dup',ucode)enddata[ucode]=table.concat(names,'!')endendlocalfunctionmake_pername(cfg,data)-- Return a function which, when called, stores a table that defines a-- per name for a unit. The table is stored in data (also a table).returnfunction(utype,fields)-- Set or update an entry in the data table to record that a unit has a-- non-standard per name if used as the second unit in a per unit (x per y).-- The target units must be defined first.-- Parameter utype is ignored (it is nil).-- Throw an error if a problem occurs.localcount=#fieldsifcount~=2thenquit('m_pnm_cnt')endlocalucode,pernamefori=1,countdolocalv=fields[i]ifempty(v)thenquit('m_pnm_miss')endifi==1then-- unitcodeucode=vifnotget_unit(v)thenquit('m_pnm_undef',v)endelsepername=vendendifdata[ucode]thenquit('m_pnm_dup',ucode)enddata[ucode]=pernameendendlocalfunctionreversed(t)-- Return a numbered table in reverse order.localreversed,count={},#tfori=1,countdoreversed[i]=t[count+1-i]endreturnreversedendlocalfunctionmake_inputmultiple(cfg,data)-- Return a function which, when called, stores a table that defines a-- single composite (multiple input) unit. The table is stored in data (also a table).returnfunction(utype,fields)-- Set or update an entry in the data table to record that a unit-- accepts subdivisions to make a composite input unit like '|2|ft|6|in'.-- The target units must be defined first.-- Throw an error if a problem occurs.localunitcode-- dummy code required for simplicity, but which is not used in outputlocalalternate_code-- an alternative unit code can be specified to replace convert inputlocalfixed_name-- a fixed name can be specified to replace the unit's normal symbol/namelocaldefault_codelocalucodes,scales={},{}fori,vinipairs(fields)do-- 1=composite, 2=ucode1, 3=ucode2, 4=default, 5=alternate, 6=nameifi==1thenifv==''thenquit('m_cmp_miss')endunitcode=velseif2<=iandi<=5thenifnot(i==5andv=='')thenlocaltarget=get_unit(v,(i==4)andutypeornil)-- the default may be an auto combinationifnottargetthenquit('m_cmp_undef',v,unitcode)endiftarget.utype~=utypethenquit('m_cmp_type',v,unitcode)endifi<4thenifnottarget.scalethenquit('m_mul_std',v,unitcode)endtable.insert(ucodes,v)table.insert(scales,target.scale)elseifi==4thendefault_code=velseifscales[#scales]~=target.scalethenquit('m_cmp_scale',v,unitcode)endalternate_code=vendendelseifi==6thenifv~=''thenfixed_name=vendelsequit('m_cmp_many',unitcode)endendif#ucodes~=2thenquit('m_cmp_two',unitcode)endifnotdefault_codethenquit('m_cmp_def',unitcode)end-- Component units must be specified from most-significant to least-significant,-- and each ratio of a pair of scales must be very close to an integer.-- Currently, there will be exactly two scales and one ratio.localratios,count={},#scalesfori=1,countdolocalscale=tonumber(scales[i])ifscale==nilorscale<=0thenquit('m_cmp_inval',unitcode,scales[i])endscales[i]=scaleendfori=1,count-1dolocalratio=scales[i]/scales[i+1]localrounded=math.floor(ratio+0.5)ifrounded<2thenquit('m_cmp_order',unitcode)endifmath.abs(ratio-rounded)/ratio>1e-6thenquit('m_cmp_int',unitcode)endratios[i]=roundedendlocaltext={tostring(ratios[1])}localfunctionadd_text(key,value)table.insert(text,string.format('%s = %q',key,value))endifdefault_codethenadd_text('default',default_code)endifalternate_codethenadd_text('unit',alternate_code)endiffixed_namethenadd_text('name',fixed_name)endlocalsubdiv=string.format('["%s"] = { %s }',ucodes[2],table.concat(text,', '))localmain_code=ucodes[1]localitem=data[main_code]ifitemthentable.insert(item.subdivs,subdiv)elsedata[main_code]={subdivs={subdiv}}endendendlocalfunctionmake_outputmultiple(cfg,data)-- Return a function which, when called, stores a table that defines a-- single multiple output unit. The table is stored in data (also a table).returnfunction(utype,fields)-- Store a table defining a unit.-- This is for a multiple unit like 'ydftin' (result in yards, feet, inches).-- The target units must be defined first.-- Throw an error if a problem occurs.localunit={utype=utype}localucodes,scales={},{}fori,vinipairs(fields)doifi==1then-- unitcodeifv==''thenquit('m_mul_miss')endunit.unitcode=velseifv==''then-- Ignore empty fields.elselocaltarget=get_unit(v)ifnottargetthenquit('m_mul_undef',v,unit.unitcode)endiftarget.utype~=utypethenquit('m_mul_type',v,unit.unitcode)endifnottarget.scalethenquit('m_mul_std',v,unit.unitcode)endtable.insert(ucodes,v)table.insert(scales,target.scale)endendif#ucodes<2thenquit(#ucodes==0and'm_mul_none'or'm_mul_one',unit.unitcode)end-- Component units must be specified from most-significant to least-significant-- (so scale values will be in descending order),-- and each ratio of a pair of scales must be very close to an integer.-- The componenets and ratios are stored in reverse order (least significant first).-- This script stores a unit scale as a string (might be an expression like "5/9"),-- but scales in a multiple are handled as numbers (should never be expressions).localratios,count={},#scalesfori=1,countdolocalscale=tonumber(scales[i])ifscale==nilorscale<=0thenquit('m_mul_scale',unit.unitcode,scales[i])endscales[i]=scaleendfori=1,count-1dolocalratio=scales[i]/scales[i+1]localrounded=math.floor(ratio+0.5)ifrounded<2thenquit('m_mul_order',unit.unitcode)endifmath.abs(ratio-rounded)/ratio>1e-6thenquit('m_mul_int',unit.unitcode)endratios[i]=roundedendunit.combination=reversed(ucodes)unit.multiple=reversed(ratios)insert_unique_unit(data,unit,units_index)endend-- To make updating the data module easier, this script inserts a preamble-- and a postamble so the result can be used to replace the whole page.localdata_preamble=[=[-- Conversion data used by [[Module:Convert]] which uses mw.loadData() for-- read-only access to this module so that it is loaded only once per page.-- See [[:en:Template:Convert/Transwiki guide]] if copying to another wiki.---- These data tables follow:-- all_units all properties for a unit, including default output-- default_exceptions exceptions for default output ('kg' and 'g' have different defaults)-- link_exceptions exceptions for links ('kg' and 'g' have different links)---- These tables are generated by a script which reads the wikitext of a page that-- documents the required properties of each unit; see [[:en:Module:Convert/doc]].]=]localdata_postamble=[=[return {all_units = all_units,default_exceptions = default_exceptions,link_exceptions = link_exceptions,per_unit_fixups = per_unit_fixups,}]=]localout_unit_prefix=[[----------------------------------------------------------------------------- Do not change the data in this table because it is created by running ---- a script that reads the wikitext from a wiki page (see note above). -----------------------------------------------------------------------------local all_units = {]]localout_unit_suffix=[[}]]localout_default_prefix=[[----------------------------------------------------------------------------- Do not change the data in this table because it is created by running ---- a script that reads the wikitext from a wiki page (see note above). -----------------------------------------------------------------------------local default_exceptions = {-- Prefixed units with a default different from that of the base unit.-- Each key item is a prefixed symbol (unitcode for engineering notation).]]localout_default_suffix=[[}]]localout_default_item=[[["{symbol}"] = "{default}",]]localout_link_prefix=[[----------------------------------------------------------------------------- Do not change the data in this table because it is created by running ---- a script that reads the wikitext from a wiki page (see note above). -----------------------------------------------------------------------------local link_exceptions = {-- Prefixed units with a linked article different from that of the base unit.-- Each key item is a prefixed symbol (not unitcode).]]localout_link_suffix=[[}]]localout_link_item=[[["{symbol}"] = "{link}",]]localout_perunit_prefix=[[----------------------------------------------------------------------------- Do not change the data in this table because it is created by running ---- a script that reads the wikitext from a wiki page (see note above). -----------------------------------------------------------------------------local per_unit_fixups = {-- Automatically created per units of form "x/y" may have their unit type-- changed, for example, "length/time" is changed to "speed".-- Other adjustments can also be specified.]]localout_perunit_suffix=[[}]]localout_perunit_item=[[["{lhs}"] = {rhs},]]localcombination_specification={-- pure combination like 'm ft', or a multiple like 'ftin''combination','multiple','utype',}localalias_specification={'target','symbol','sp_us','usename','default','link','symlink','customary','multiplier',}localper_specification={'per','symbol','sp_us','utype','invert','iscomplex','default','link','symlink','customary','multiplier',}localshouldbe_specification={'shouldbe',}localunit_specification={'_name1','_name1_us','_name2','_name2_us','_symbol','_sym_us','prefix_position','name1','name1_us','name2','name2_us','pername','varname','symbol','sym_us','usename','usesymbol','utype','alttype','builtin','scale','offset','invert','iscomplex','istemperature','exception','prefixes','default','subdivs','defkey','linkey','link','customary','sp_us',}localno_quotes={combination=true,customary=true,multiple=true,multiplier=true,offset=true,per=true,prefix_position=true,scale=true,subdivs=true,}localfunctionadd_unit_lines(results,unit,spec)-- Add lines of Lua source to define a unit to the results collection.localfunctionadd_line(line)-- Had planned to replace sequences of spaces with 4-column tabs here-- (because the CodeEditor now assumes the use of such tabs).-- However, 4-column tabs are only visible when editing a module-- with browser scripting and the CodeEditor enabled, and that is rare.-- A module is usually viewed (with 8-column tabs), and some indents-- would be messed up unless 8-column tabs are used. Therefore,-- have decided to simply replace 8 spaces at start of line with a single-- tab which reduces the size of the module, and is correct for viewing.ifline:sub(1,8)==string.rep(' ',8)thenline='\t'..line:sub(9)endresults:add(line)endlocalfirst_item=' ["'..unit.unitcode..'"] = {'locallast_item=' },'add_line(first_item)for_,kinipairs(spec)dolocalv=unit[k]ifvthenlocalwant_quotes=(type(v)=='string'andnotno_quotes[k])iftype(v)=='boolean'thenv=tostring(v)elseiftype(v)=='number'ork=='scale'then-- Replace results like '1e-006' with '1e-6'.v=string.gsub(tostring(v),'(e[+-])0+([1-9].*)','%1%2',1)elseiftype(v)~='string'thenquit('m_ftl_type',unit.unitcode)endlocalfmt=string.format('%8s%%-9s= %%%s,','',want_quotesand'q'or's')add_line(fmt:format(k,v))endendadd_line(last_item)endlocalfunctionnumbered_table_as_string(data,unit)localt={}for_,vinipairs(data)doiftype(v)=='string'thentable.insert(t,'"'..v..'"')elseiftype(v)=='number'thentable.insert(t,tostring(v))elsequit('m_ftl_type',unit.unitcode)endendreturn'{ '..table.concat(t,', ')..' }'endlocalfunctionextract_heading(line)-- Return n, s where n = heading level number (nil if none), and-- s = heading text (with leading/trailing whitespace removed).localpattern='^(==+)%s*(.-)%s*(==+)%s*$'localbefore,heading,after=line:match(pattern)ifheadingand#heading>0then-- Don't bother checking if before == after.return#before,headingendendlocalfunctionfields(line)-- Return a numbered table of fields split from line.-- Items are delimited by "||".-- Each item has leading/trailing whitespace removed, and any encoded pipe-- characters are decoded.-- The second field (for symbol when processing units) is adjusted to-- remove any "colspan" at the front of lines like:-- "| unitcode || colspan="11" | !Text to display for an error message".localt={}line=line.."||"-- to get last fieldforiteminline:gmatch("%s*(.-)%s*||")dotable.insert(t,(item:gsub('|','|')))endift[2]thenlocalcleaned=t[2]:match('^%s*colspan%s*=.-|%s*(.*)$')ifcleanedthent[2]=cleanedendendreturntendlocalfunctionprepare_section(cfg,maker,lines,section,need_section,need_utype)-- Process the first level-two section with the given section name-- in the given table of lines of wikitext.-- If successful, maker inserts each item into a table.-- Otherwise, an error is thrown.localskip=truelocalerrors=collection()localutype-- unit type (from level-three heading)localnbsp='\194\160'-- nonbreaking space is utf-8 encoded as hex c2 a0forlinenumber,lineinipairs(lines)doifskipthen-- Skip down to and including the starting heading.locallevel,heading=extract_heading(line)iflevel==2andheading==sectionthenskip=falseendelse-- Accummulate unit definitions.localc1=line:sub(1,1)localc2=line:sub(2,2)ifc1=='|'andnot(c2=='-'orc2=='}')thenifneed_utypeandempty(utype)thenquit('m_hdg_lev3',line)endifline:find(nbsp,1,true)then-- For example, "acre ft" does not work if it contains nbsp.add_warning('m_wrn_nbsp',linenumber)endlocalok,msg=pcall(maker,utype,fields(line:sub(2)))ifnotokthenifmsg:sub(-1)=='.'thenmsg=msg:sub(1,-2)enderrors:add(msg..message('m_line_num',linenumber))iferrors.n>=cfg.maxerrorsthenbreakendendelselocallevel,heading=extract_heading(line)iflevel==3thenutype=ulower(heading)elseiflevel==2thenbreakendendendendifskipandneed_sectionthenquit('m_hdg_lev2',section)endiferrors.n>0thenerror(errors:join(),0)endendlocalfunctionget_page_lines(page_title)-- Read the wikitext of the page at the given title; split the text into-- lines with leading and trailing space removed from each line.-- Return a numbered table of the lines, or throw an error.ifempty(page_title)thenquit('m_no_title')endlocalt=mw.title.new(page_title)iftthenlocalcontent=t:getContent()ifcontentthenifcontent:sub(-1)~='\n'thencontent=content..'\n'endlocallines=collection()forlineinstring.gmatch(content,'[\t ]*(.-)[\t\r ]*\n')dolines:add(line)endreturnlinesendendquit('m_ftl_read',page_title)endlocalfunctionprepare_data(cfg,is_sandbox)-- Read the page of conversion data, and process the wikitext-- in the sections with wanted level-two headings.-- Return units, defaults, links (three tables).-- Throw an error if a problem occurs.localcomposites,defaults,links,units,perunits,varnames,pernames={},{},{},{},{},{},{}localsections={{'overrides',make_override,overrides,0},{'conversions',make_unit,units,0},{'outmultiples',make_outputmultiple,units,0},{'combinations',make_combination,units,0},{'inmultiples',make_inputmultiple,composites,0},-- after all units defined so default will be defined{'defaults',make_default,defaults,0},{'links',make_link,links,0},{'perunits',make_perunit,perunits,1},{'varnames',make_varname,varnames,1},{'pernames',make_pername,pernames,1},}locallines=get_page_lines(cfg.data_title)for_,sectioninipairs(sections)dolocalheading=mtext.section_names[section[1]]localmaker=section[2](cfg,section[3])localcode=section[4]localneed_section,need_utypeifcode==0andnotis_sandboxthenneed_section=trueendifcode==0thenneed_utype=trueendprepare_section(cfg,maker,lines,heading,need_section,need_utype)endcheck_all_defaults(cfg,units)check_all_pers(cfg,units)update_units(units,composites,varnames,pernames)returnunits,defaults,links,perunitsendlocalfunction_makeunits(cfg,results)-- Read the wikitext for the conversion data.-- Append output to given results collection, or throw error if a problem.text_code=require(cfg.text_title)for_,nameinipairs({'SIprefixes','eng_scales','currency'})doiftype(text_code[name])~='table'thenquit('m_ftl_table',cfg.text_title,name)endendlocaltranslation=text_code.translation_tableiftranslationtheniftranslation.plural_suffixthenplural_suffix=translation.plural_suffixendlocalts=translation.specialsiftsthenifts.utypethenspecials.utype=ts.utypeendifts.ucodethenspecials.ucode=ts.ucodeendendlocaltm=translation.mtextiftmtheniftm.section_namesthenmtext.section_names=tm.section_namesendiftm.titlesthenmtext.titles=tm.titlesendiftm.messagesthenmtext.messages=tm.messagesendendendlocalis_sandboxlocalconversion_data_title=mtext.titles.conversion_dataifcfg.data_titleandcfg.data_title~=conversion_data_titlethenifis_test_runthenis_sandbox=truedata_preamble=nildata_postamble=nilout_unit_prefix='local all_units = {'out_unit_suffix='}'out_default_prefix='\nlocal default_exceptions = {'out_default_suffix='}'out_default_item='\t["{symbol}"] = "{default}",'out_link_prefix='\nlocal link_exceptions = {'out_link_suffix='}'out_link_item='\t["{symbol}"] = "{link}",'out_perunit_prefix='\nlocal per_unit_fixups = {'out_perunit_suffix='}'out_perunit_item='\t["{lhs}"] = {rhs},'endelsecfg.data_title=conversion_data_titleendlocalunits,defaults,links,perunits=prepare_data(cfg,is_sandbox)ifdata_preamblethenresults:add(data_preamble)endresults:add(out_unit_prefix)for_,unitinipairs(units)dolocalspecifunit.targetthenspec=alias_specificationelseifunit.perthenspec=per_specificationunit.per=numbered_table_as_string(unit.per,unit)elseifunit.shouldbethenspec=shouldbe_specificationelseifunit.combinationthenspec=combination_specificationunit.combination=numbered_table_as_string(unit.combination,unit)ifunit.multiplethenunit.multiple=numbered_table_as_string(unit.multiple,unit)endelsespec=unit_specificationendadd_unit_lines(results,unit,spec)endresults:add(out_unit_suffix)for_,tinipairs({{defaults,out_default_prefix,out_default_item,out_default_suffix},{links,out_link_prefix,out_link_item,out_link_suffix},{perunits,out_perunit_prefix,out_perunit_item,out_perunit_suffix}})dolocaldata,prefix,item,suffix=t[1],t[2],t[3],t[4]if#data>0ornotis_sandboxthenresults:add(prefix)for_,unitinipairs(data)doresults:add((item:gsub('{([%w_]+)}',unit)))endresults:add(suffix)endendifdata_postamblethenresults:add(data_postamble)endendlocalfunctionmakeunits(frame)localargs=frame.argslocalconfig={data_title=args[1],text_title=args[2]or'Module:Convert/text',varcolumns=tonumber(args.varcolumns)or5,-- #columns in "Variable names" section; slwiki uses 5maxerrors=20,}localresults=collection()localok,msg=pcall(_makeunits,config,results)ifnotokthenresults:add(message('m_error'))results:add('')results:add(msg)endlocalwarn=''ifwarnings.n>0thenwarn=message('m_warning')..'\n\n'..warnings:join()..'\n\n'end-- Pre tags returned by a module are html tags, not like wikitext <pre>...</pre>.-- The following renders the text as is, and preserves tab characters.return'<pre>\n'..mw.text.nowiki(warn..results:join())..'\n</pre>\n'endreturn{makeunits=makeunits}