विभाग:Convert
![]() | See how to copy the convert template for information on copying this module and modifying it for use on another wiki. |
![]() | This template is used on ८,१९,०००+ pages. To avoid large-scale disruption and unnecessary server load, any changes to it should first be tested in its /sandbox or /testcases subpages or in your own user subpage. The tested changes can then be added to this page in a single edit. Please consider discussing any changes on the talk page before implementing them. |
![]() | हा विभाग (मॉड्यूल) पान सुरक्षेच्या अधीन असलेला आहे. तो खूप पानांवर वापरल्या जाणारा उच्च-दृश्यतेचा विभाग आहे किंवा, त्याचे substitution वारंवार होते. त्यामधील उत्पात किंवा चुका या अनेक पानांवर परिणाम करु शकतात. किरकोळ किंवा क्षुद्र संपादनही विदागारावर प्रचंड ताण उत्पन्न करु शकते. म्हणून, त्यास संपादनांपासून सुरक्षित केले आहे. |
This module converts a value from one unit of measurement to another. For example:
{{convert|123|lb|kg}}
→ 123 pounds (56 kg)
The module is called using a template—parameters passed to the template are used by this module to control how a conversion is performed. For example, units can be abbreviated (like kg
), or displayed as names (like kilogram
), and the output value can be rounded to a specified precision. For usage information, see Help:Convert.
The template that invokes this module is:
The following modules are required:
- Module:Convert – (this module) code to convert units
- Module:Convert/data – unit definitions
- Module:Convert/text – text messages, and parameter names and values
The following modules are optional and are used only if required and if the module exists:
- Module:Convert/extra – extra (temporary) unit definitions; used if a unit is not found in Module:Convert/data
- Module:ConvertNumeric – code to spell an input value in words (only English is supported; however, see vi:Module:ConvertNumeric)
The following help pages are available:
- Help:Convert – overview
- Help:Convert messages – describes error and warning messages; messages link to this page so it is required when the module is copied to another wiki
- Help:Convert units – overview of units
A page containing a convert error is added to the following hidden category, providing the page is in a specified namespace (articles, by default):
Units are defined in the wikitext of the master list of units.
- Module:Convert/documentation/conversion data/doc – master list of unit definitions
- Module:Convert/makeunits – translates wikitext from the master list to Lua
- Module talk:Convert/makeunits – makeunits results; copy the text to Module:Convert/data
Module:Convert/data is transcluded into every page using the convert module, so experimenting with a new unit in that module would involve a significant overhead. The Module:Convert/extra module is an alternative which is only transcluded on pages with a unit that is not defined in the main data module.
Sandbox
When making a change, copy the current modules to the sandbox pages, then edit the sandbox copies:
- Module:Convert/sandbox
- Module:Convert/data/sandbox
- Module:Convert/text/sandbox
- Module:Convert/extra/sandbox
Use the following template to test the results (example {{convert/sandbox|123|lb|kg}}
):
Template:Convert/sandbox invokes Module:Convert/sandbox with parameter |sandbox=sandbox
which causes convert to use the sandbox modules rather than the normal modules.
The following should be used to test the results of editing the convert modules.
- Template:Convert/testcases#Sandbox testcases – links to testcases
- Module:Convert/tester – module to run tests by comparing template output with fixed text
It is not necessary to save a testcases page before viewing test results. For example, Template:Convert/testcases/sandbox4 could be edited to change the tests. While still editing that page, paste "Template talk:Convert/testcases/sandbox4
" (without quotes) into the page title box under "Preview page with this template", then click "Show preview".
Configuration
The template that invokes this module can define options to configure the module. For example:
{{#invoke:convert|convert|numdot=,|numsep=.}}
- Sets the decimal mark to be a comma, and the thousands separator to be a dot.
Other options, with default values, are:
|maxsigfig=14
– maximum number of significant figures|nscat=0
– namespaces (comma separated) in which an error or warning adds a category to the page|warnings=0
– 0 (zero) disables warnings; 1 shows important warnings; 2 shows all warnings
An option in the template can specify that the sandbox versions of the modules be used. If specified, the text on the right-hand side of the equals sign must be the name of the subpage for each sandbox module.
|sandbox=sandbox
– omit for normal operation
All text used for input parameters and for output messages and categories can be customized. For example, at enwiki the option |lk=on
can be used to link each displayed unit to its article. The "lk
" and "on
" can be replaced with any desired text. In addition, input and output numbers can be formatted and can use digits in the local language. See the translation guide for more information.
To do
Document the modules to access Wikidata! Error
Could not read wikitext from "Module:Convert/data/sandbox".
-- Convert a value from one unit of measurement to another. -- Example: {{convert|123|lb|kg}} --> 123 pounds (56 kg) local MINUS = '−' -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92) local abs = math.abs local floor = math.floor local format = string.format local log10 = math.log10 local ustring = mw.ustring local ulen = ustring.len -- mw.ustring has two bugs: following works around one of them; see "2013-07-05" for second. -- Bug has been reported, and the fix will be deployed, possibly before August 2013. -- local usub = ustring.sub -- When fix is deployed, remove following usub function, and uncomment above usub variable. local function usub(s, i, j) return (j and i > j) and '' or ustring.sub(s, i, j) end -- Configuration options to keep magic values in one location. -- The conversion data and message text are defined in separate modules. local numdot, numsep -- each must be a single byte for simple regex search/replace local maxsigfig, warnings local default_exceptions, link_exceptions, all_units local SIprefixes, all_categories, all_messages, customary_units, disp_joins local en_option_name, en_option_value, eng_scales, range_aliases, range_types local group_method = 3 -- code for how many digits are in a group local plural_suffix = 's' -- only other useful value is probably '' to disable plural unit names local from_en_table -- to translate an output string of en digits to local language local to_en_table -- to translate an input string of digits in local language to en -- All units should be defined in the data module. However, to cater for quick changes -- and experiments, any unknown unit is looked up in an extra data module, if it exists. -- That module would be transcluded in only a small number of pages, so there should be -- little server overhead from making changes, and changes should propagate quickly. local extra_module -- name of module with extra units local extra_units -- nil or table of extra units from extra_module local function boolean(text) -- Return true if text represents a "true" option value. if text then text = text:lower() if text == '1' or text == 'y' or text == 'yes' or text == 'on' or text == 'true' then return true end end end local function from_en(text) -- Input is a string representing a number in en digits with '.' decimal mark, -- without digit grouping (which is done just after calling this). -- Return the translation of the string with numdot and digits in local language. if numdot ~= '.' then text = text:gsub('%.', numdot) end if from_en_table then text = text:gsub('%d', from_en_table) end return text end local function to_en(text) -- Input is a string representing a number in the local language with -- an optional numdot decimal mark and numsep digit grouping. -- Return the translation of the string with '.' mark and en digits, -- and no separators (they have to be removed here to handle cases like -- numsep = '.' and numdot = ',' with input "1.234.567,8"). if numsep ~= '' then text = text:gsub('[' .. numsep .. ']', '') -- use '[x]' in case x is '.' end if numdot ~= '.' then text = text:gsub('[' .. numdot .. ']', '.') end if to_en_table then text = ustring.gsub(text, '%d', to_en_table) end return text end local spell_module -- name of module that can spell numbers local speller -- function from that module to handle spelling (set if spelling is wanted) local function set_config(frame) -- Set configuration options from template #invoke or defaults. local args = frame.args numdot = args.numdot or '.' -- decimal mark before fractional digits numsep = args.numsep or ',' -- group separator for numbers (',', '.', '') maxsigfig = args.maxsigfig or 14 -- maximum number of significant figures warnings = boolean(args.warnings) -- true if want warnings for invalid options -- Scribunto sets the global variable 'mw'. -- A testing program can set the global variable 'is_test_run'. local data_module, text_module, data_code, text_code if is_test_run then local langcode = mw.language.getContentLanguage().code data_module = "convertdata-" .. langcode text_module = "converttext-" .. langcode extra_module = "convertextra" spell_module = "ConvertNumeric" else data_module = "Module:Convert/data" text_module = "Module:Convert/text" extra_module = "Module:Convert/extra" spell_module = "Module:ConvertNumeric" end data_code = mw.loadData(data_module) text_code = mw.loadData(text_module) default_exceptions = data_code.default_exceptions link_exceptions = data_code.link_exceptions all_units = data_code.all_units SIprefixes = text_code.SIprefixes all_categories = text_code.all_categories all_messages = text_code.all_messages customary_units = text_code.customary_units disp_joins = text_code.disp_joins en_option_name = text_code.en_option_name en_option_value = text_code.en_option_value eng_scales = text_code.eng_scales range_aliases = text_code.range_aliases range_types = text_code.range_types local translation = text_code.translation_table if translation then if translation.group then group_method = translation.group end if translation.plural_suffix then plural_suffix = translation.plural_suffix end from_en_table = translation.from_en local use_workaround = true if use_workaround then -- 2013-07-05 workaround bug by making a copy of the required table. -- mw.ustring.gsub fails with a table (to_en_table) as the replacement, -- if the table is accessed via mw.loadData. local source = translation.to_en if source then to_en_table = {} for k, v in pairs(source) do to_en_table[k] = v end end else to_en_table = translation.to_en end end end local function collection() -- Return a table to hold items. return { n = 0, add = function (self, item) self.n = self.n + 1 self[self.n] = item end, } end local function split(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. local t = {} text = text .. delimiter -- to get last item for item in text:gmatch('%s*(.-)%s*' .. delimiter) do table.insert(t, item) end return t end local function strip(text) -- If text is a string, return its content with no leading/trailing -- whitespace. Otherwise return nil (a nil argument gives a nil result). if type(text) == 'string' then return text:match("^%s*(.-)%s*$") end end local function message(mcode) -- Return wikitext for an error message, including category if specified -- for the message type. -- mcode = numbered table specifying the message: -- mcode[1] = 'cvt_xxx' (string used as a key to get message info) -- mcode[2] = 'parm1' (string to replace first %s if any in message) -- mcode[3] = 'parm2' (string to replace second %s if any in message) -- mcode[4] = 'parm3' (string to replace third %s if any in message) local msg = all_messages[mcode[1]] if msg then local text = format(msg[1] or 'Missing message', mcode[2] or '?', mcode[3] or '?', mcode[4] or '?') local cat = all_categories[msg[2]] or '' local prefix = all_messages[msg.warning and 'cvt_prefix_warning' or 'cvt_prefix_error'] or '' local suffix = (prefix == '') and '' or '</span>' local regex, replace = msg.regex, msg.replace if regex and replace then text = text:gsub(regex, replace) end return prefix .. ' ' .. text .. cat .. suffix end return 'Convert internal error: unknown message' end local function add_warning(parms, mcode, text) -- If enabled, add a warning that will be displayed after the convert result. -- To reduce output noise, only the first warning is displayed. if warnings then if parms.warnings == nil then parms.warnings = message({ mcode, text }) end end end local function spell_number(parms, number, numerator, denominator) -- Return result of spelling (number, numerator, denominator), or -- return nil if spelling is not available or not supported for given text. -- Input should use one of these forms: -- number numerator denominator example output -- ------ --------- ----------- -------------- -- string nil nil one point two three -- string string string one and two thirds -- nil string string two thirds if not speller then local function get_speller(module) return require(module).spell_number end local success success, speller = pcall(get_speller, spell_module) if not success or type(speller) ~= 'function' then add_warning(parms, 'cvt_spell_unavailable') return nil end end local case = parms.opt_spell_upper parms.opt_spell_upper = nil -- only uppercase first number in a multiple unit local sp = not parms.opt_sp_us local adj = parms.opt_adjectival return speller(number, numerator, denominator, case, sp, adj) end ------------------------------------------------------------------------ -- BEGIN: Code required only for built-in units. -- LATER: If need much more code, move to another module to simplify this module. local function speed_of_sound(altitude) -- This is for the Mach built-in unit of speed. -- Return speed of sound in metres per second at given altitude in feet. -- If no altitude given, use default (zero altitude = sea level). -- Table gives speed of sound in miles per hour at various altitudes: -- altitude = -17,499 to 302,499 feet -- mach_table[a + 4] = s where -- a = (altitude / 5000) rounded to nearest integer (-3 to 60) -- s = speed of sound (mph) at that altitude -- LATER: Should calculate result from an interpolation between the next -- lower and higher altitudes in table, rather than rounding to nearest. -- From: http://www.aerospaceweb.org/question/atmosphere/q0112.shtml local mach_table = { -- a = 799.5, 787.0, 774.2, 761.207051, -- -3 to 0 748.0, 734.6, 721.0, 707.0, 692.8, 678.3, 663.5, 660.1, 660.1, 660.1, -- 1 to 10 660.1, 660.1, 660.1, 662.0, 664.3, 666.5, 668.9, 671.1, 673.4, 675.6, -- 11 to 20 677.9, 683.7, 689.9, 696.0, 702.1, 708.1, 714.0, 719.9, 725.8, 731.6, -- 21 to 30 737.3, 737.7, 737.7, 736.2, 730.5, 724.6, 718.8, 712.9, 707.0, 701.1, -- 31 to 40 695.0, 688.9, 682.8, 676.6, 670.4, 664.1, 657.8, 652.9, 648.3, 643.7, -- 41 to 50 639.1, 634.4, 629.6, 624.8, 620.0, 615.2, 613.2, 613.2, 613.2, 613.5, -- 51 to 60 } altitude = altitude or 0 local a = (altitude < 0) and -altitude or altitude a = floor(a / 5000 + 0.5) if altitude < 0 then a = -a end if a < -3 then a = -3 elseif a > 60 then a = 60 end return mach_table[a + 4] * 0.44704 -- mph converted to m/s end -- END: Code required only for built-in units. ------------------------------------------------------------------------ local function override_from(out_table, in_table, fields) -- Copy the specified fields from in_table to out_table, but do not -- copy nil fields (keep any corresponding field in out_table). for _, field in ipairs(fields) do if in_table[field] then out_table[field] = in_table[field] end end end local function shallow_copy(t) -- Return a shallow copy of table t. -- Do not need the features and overhead of the Scribunto mw.clone(). local result = {} for k, v in pairs(t) do result[k] = v end return result end local unit_mt = { -- Metatable to get missing values for a unit that does not accept SI prefixes, -- or for a unit that accepts prefixes but where no prefix was used. -- In the latter case, and before use, fields symbol, name1, name1_us -- must be set from _symbol, _name1, _name1_us respectively. __index = function (self, key) local value if key == 'name1' or key == 'sym_us' then value = self.symbol elseif key == 'name2' then value = self.name1 .. plural_suffix elseif key == 'name1_us' then value = self.name1 if not rawget(self, 'name2_us') then -- If name1_us is 'foot', do not make name2_us by appending plural_suffix. self.name2_us = self.name2 end elseif key == 'name2_us' then local raw1_us = rawget(self, 'name1_us') if raw1_us then value = raw1_us .. plural_suffix else value = self.name2 end elseif key == 'link' then value = self.name1 elseif key == 'builtin' then value = false else return nil end rawset(self, key, value) return value end } local unit_prefixed_mt = { -- Metatable to get missing values for a unit that accepts SI prefixes, -- and where a prefix has been used. -- Before use, fields si_name, si_prefix must be defined. __index = function (self, key) local value if key == 'symbol' then value = self.si_prefix .. self._symbol elseif key == 'sym_us' then value = self.symbol -- always the same as sym_us for prefixed units elseif key == 'name1' then -- prefix_position is a byte (not character) position, so use Lua's sub(). local pos = rawget(self, 'prefix_position') or 1 value = self._name1 value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos) elseif key == 'name2' then value = self.name1 .. plural_suffix elseif key == 'name1_us' then value = rawget(self, '_name1_us') if value then local pos = rawget(self, 'prefix_position') or 1 value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos) else value = self.name1 end elseif key == 'name2_us' then if rawget(self, '_name1_us') then value = self.name1_us .. plural_suffix else value = self.name2 end elseif key == 'link' then value = self.name1 elseif key == 'builtin' then value = false else return nil end rawset(self, key, value) return value end } local unit_per_mt = { -- Metatable to get values for a "per" unit of form "x/y". -- This is never called to determine a unit name or link because "per" units -- are handled as a special case. __index = function (self, key) local value if key == 'symbol' then local per = self.per local unit1, unit2 = per[1], per[2] if unit1 then value = unit1[key] .. '/' .. unit2[key] else value = '/' .. unit2[key] end elseif key == 'sym_us' then value = self.symbol elseif key == 'scale' then local per = self.per local unit1, unit2 = per[1], per[2] value = (unit1 and unit1.scale or 1) / unit2.scale elseif key == 'builtin' then value = false else return nil end rawset(self, key, value) return value end } local function lookup(unitcode, opt_sp_us, what, utable, fails, depth) -- Return true, t where t is a copy of the unit's converter table, -- or return false, t where t is an error message table. -- Parameter opt_sp_us is true for US spelling of SI prefixes and -- the symbol and name of the unit. If true, the result includes field -- sp_us = true (that field may also have been in the unit definition). -- Parameter 'what' determines whether combination units are accepted: -- 'no_combination' : single unit only -- 'any_combination' : single unit or combination or output multiple -- 'only_multiple' : single unit or output multiple only -- Parameter unitcode is a symbol (like 'g'), with an optional SI prefix (like 'kg'). -- If, for example, 'kg' is in this table, that entry is used; -- otherwise the prefix ('k') is applied to the base unit ('g'). -- If unitcode is a known combination code (and if allowed by what), -- a table of output multiple unit tables is included in the result. -- For compatibility with the old template, underscores in unitcode are replaced -- with spaces so {{convert|350|board_feet}} --> 350 board feet (0.83 m³). utable = utable or all_units fails = fails or {} depth = depth and depth + 1 or 1 if depth > 9 then -- There are ways to mistakenly define units which result in infinite -- recursion when lookup() is called. That gives a long delay and very -- confusing error messages, so the depth parameter is used as a guard. return false, { 'cvt_lookup', unitcode } end if unitcode == nil or unitcode == '' then return false, { 'cvt_no_unit' } end unitcode = unitcode:gsub('_', ' ') local t = utable[unitcode] if t then if t.shouldbe then return false, { 'cvt_should_be', t.shouldbe } end local force_sp_us = opt_sp_us if t.sp_us then force_sp_us = true opt_sp_us = true end local target = t.target -- nil, or unitcode is an alias for this target if target then local success, result = lookup(target, opt_sp_us, what, utable, fails, depth) if not success then return false, result end override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink' }) local multiplier = t.multiplier if multiplier then result.multiplier = tostring(multiplier) result.scale = result.scale * multiplier end return true, result end local per = t.per -- nil/false, or a numbered table for "x/y" units if per then local result = { utype = t.utype, per = {} } override_from(result, t, { 'default', 'invert', 'iscomplex', 'symbol', 'symlink' }) result.symbol_raw = (result.symbol or false) -- to distinguish between a defined exception and a metatable calculation local cvt = result.per local prefix for i, v in ipairs(per) do if i == 1 and (v == '$' or v == '£') then prefix = v else local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth) if not success then return false, t end cvt[i] = t if t.sp_us then -- if the top or bottom unit forces sp=us, set the per unit to use the correct name/symbol force_sp_us = true end end end if prefix then result.vprefix = prefix else result.vprefix = false -- to avoid calling __index end result.sp_us = force_sp_us return true, setmetatable(result, unit_per_mt) end local combo = t.combination -- nil or a table of unitcodes if combo then local multiple = t.multiple if what == 'no_combination' or (what == 'only_multiple' and multiple == nil) then return false, { 'cvt_bad_unit', unitcode } end -- Recursively create a combination table containing the -- converter table of each unitcode. local result = { utype = t.utype, multiple = multiple, combination = {} } local cvt = result.combination for i, v in ipairs(combo) do local success, t = lookup(v, opt_sp_us, multiple and 'no_combination' or 'only_multiple', utable, fails, depth) if not success then return false, t end cvt[i] = t end return true, result end local result = shallow_copy(t) result.sp_us = force_sp_us if result.prefixes then result.symbol = result._symbol result.name1 = result._name1 result.name1_us = result._name1_us end return true, setmetatable(result, unit_mt) end for plen = SIprefixes[1] or 2, 1, -1 do -- 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). local prefix = usub(unitcode, 1, plen) local si = SIprefixes[prefix] if si then local t = utable[usub(unitcode, plen+1)] if t and t.prefixes then local result = shallow_copy(t) if opt_sp_us then result.sp_us = true end if result.sp_us and si.name_us then result.si_name = si.name_us else result.si_name = si.name end result.si_prefix = si.prefix or prefix result.scale = t.scale * 10 ^ (si.exponent * t.prefixes) return true, setmetatable(result, unit_prefixed_mt) end end end -- Accept any unit with an engineering notation prefix like "e6cuft" -- (million cubic feet), but not chained prefixes like "e3e6cuft", -- and not if the unit is a combination or multiple, -- and not if the unit has an offset or is a built-in. -- Only en digits are accepted. local exponent, baseunit = unitcode:match('^e(%d+)(.*)') if exponent then local engscale = eng_scales[exponent] if engscale then local success, result = lookup(baseunit, opt_sp_us, 'no_combination', utable, fails, depth) if not success then return false, result end if not (result.offset or result.builtin or result.engscale) then result.defkey = unitcode -- key to lookup default exception result.engscale = engscale result.scale = result.scale * 10 ^ tonumber(exponent) return true, result end end end if not extra_units then local success, extra = pcall(function () return require(extra_module).extra_units end) if success and type(extra) == 'table' then extra_units = extra end end if extra_units then -- A unit in one data table might refer to a unit in the other table, so -- switch between them, relying on fails or depth to terminate loops. local failed = fails[unitcode] if not failed then fails[unitcode] = true local other = (utable == all_units) and extra_units or all_units local success, result = lookup(unitcode, opt_sp_us, what, other, fails, depth) if success then return true, result end end end return false, { 'cvt_unknown', unitcode } end local function valid_number(num) -- Return true if num is a valid number. -- Expressed as a string, overflow or other problems are indicated with -- text like "1.#INF" or ".#IND" which are regarded as invalid here. if type(num) == 'number' and tostring(num):find('#', 1, true) == nil then return true end end local function ntsh(num, debug) -- Return html text to be used for a hidden sort key so that -- the given number will be sorted in numeric order. -- If debug == true, output is in a box (not hidden). -- This implements Template:Ntsh (number table sorting, hidden). local result, style if not valid_number(num) then if num < 0 then result = '1000000000000000000' else result = '9000000000000000000' end elseif num == 0 then result = '5000000000000000000' else local mag = floor(log10(abs(num)) + 1e-14) local prefix if num > 0 then prefix = 7000 + mag else prefix = 2999 - mag num = num + 10^(mag+1) end result = format('%d', prefix) .. format('%015.0f', floor(num * 10^(14-mag))) end if debug then style = 'border:1px solid' else style = 'display:none' end return '<span style="' .. style .. '">' .. result .. '</span>' end local function hyphenated(name) -- Return a hyphenated form of given name (for adjectival usage). -- This uses a simple and efficient procedure that works for most cases. -- Some units (if used) would require more, and can later think about -- adding a method to handle exceptions. -- The procedure is to replace each space with a hyphen, but -- not a space after ')' [for "(pre-1954 US) nautical mile"], and -- not spaces immediately before '(' or in '(...)' [for cases like -- "British thermal unit (ISO)" and "Calorie (International Steam Table)"]. local pos if name:sub(1, 1) == '(' then pos = name:find(')', 1, true) if pos then return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-') end elseif name:sub(-1, -1) == ')' then pos = name:find('(', 1, true) if pos then return name:sub(1, pos-2):gsub(' ', '-') .. name:sub(pos-1) end end return name:gsub(' ', '-') end local function hyphenated_maybe(parms, want_name, sep, id, inout) -- Return s, f where -- s = id, possibly modified -- f = true if hyphenated -- Possible modifications: hyphenate; prepend '-'; append mid text. if id == nil or id == '' then return '' end local mid if parms.opt_adjectival then if inout == (parms.opt_flip and 'out' or 'in') then mid = parms.mid end if want_name then return '-' .. hyphenated(id) .. (mid or ''), true end end return sep .. id .. (mid or '') end local function change_sign(text) -- Change sign of text for correct appearance because it is negated. if text:sub(1, 1) == '-' then return text:sub(2) end return '-' .. text end local function use_minus(text) -- Return text with Unicode minus instead of '-', if present. if text:sub(1, 1) == '-' then return MINUS .. text:sub(2) end return text end local function digit_grouper(method, gaps) -- Return a table to hold groups of digits which can be joined with -- suitable separators (such as commas). -- Each group is separately translated to the local language because the -- separators may include '.' characters which should not be translated. -- Parameter method is a number or nil: -- 3 for 3-digit grouping, or -- 2 for 3-then-2 grouping. -- Parameter gaps is true to use <span> gaps (numsep ignored). return { n = 0, add = function (self, digits) self.n = self.n + 1 self[self.n] = from_en(digits) end, join = function (self, rhs) -- Concatenate in reverse order. if gaps then local result = '' for i = 1, self.n - 1 do result = '<span style="margin-left: 0.25em">' .. self[i] .. '</span>' .. result end return '<span style="white-space: nowrap">' .. self[self.n] .. result .. from_en(rhs) .. '</span>' else local result = self[1] for i = 2, self.n do result = self[i] .. numsep .. result end return result .. from_en(rhs) end end, step = 3, next_position = function (self, previous) -- Return position of digit just before next group. -- Digits are grouped from right-to-left (least significant first). local result = previous - self.step if method == 2 then self.step = 2 -- may need more (3, 2, 2, 3, 2, 2, ...) in some languages end return (result < 0) and 0 or result end, } end local function with_separator(parms, text) -- Input text is a number in en digits and with '.' decimal mark. -- Return an equivalent of text, formatted for display: -- with a custom decimal mark instead of '.', if wanted -- with thousand separators inserted, if wanted -- digits in local language -- The given text is like '123' or '12345.6789' or '1.23e45' -- (e notation can only occur when processing an input value). -- The text has no sign (caller inserts that later, if necessary). -- Separator is inserted only in the integer part of the significand -- (not after the decimal mark, and not after 'e' or 'E'). if parms.opt_nocomma or numsep == '' then return from_en(text) end local last = text:match('()[.eE]') -- () returns position if last == nil then last = #text else last = last - 1 -- index of last character before dot/e/E end if last < 4 or (last == 4 and parms.opt_comma5) then return from_en(text) end local groups = digit_grouper(group_method, parms.opt_gaps) local i = last while i > 0 do local position = groups:next_position(i) groups:add(text:sub(position+1, i)) i = position end return groups:join(text:sub(last+1)) end -- Input values can use values like 1.23e12, but are never displayed -- using scientific notation like 1.23×10¹². -- Very small or very large output values use scientific notation. -- Use format(fmtpower, significand, '10', exponent) where each arg is a string. local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>%s<sup>%s</sup>' local function with_exponent(show, exponent) -- Return wikitext to display the implied value in scientific notation. -- Input uses en digits; output uses digits in local language. if #show > 1 then show = show:sub(1, 1) .. '.' .. show:sub(2) end return format(fmtpower, from_en(show), from_en('10'), use_minus(from_en(tostring(exponent)))) end local function make_sigfig(value, sigfig) -- Return show, exponent that are equivalent to the result of -- converting the number 'value' (where value >= 0) to a string, -- rounded to 'sigfig' significant figures. -- The returned items are: -- show: a string of digits; no sign and no dot; -- there is an implied dot before show. -- exponent: a number (an integer) to shift the implied dot. -- Resulting value = tonumber('.' .. show) * 10^exponent. -- Examples: -- make_sigfig(23.456, 3) returns '235', 2 (.235 * 10^2). -- make_sigfig(0.0023456, 3) returns '235', -2 (.235 * 10^-2). -- make_sigfig(0, 3) returns '000', 1 (.000 * 10^1). if sigfig <= 0 then sigfig = 1 elseif sigfig > maxsigfig then sigfig = maxsigfig end if value == 0 then return string.rep('0', sigfig), 1 end local exp, frac = math.modf(log10(value)) if frac >= 0 then frac = frac - 1 exp = exp + 1 end local digits = format('%.0f', 10^(frac + sigfig)) if #digits > sigfig then -- Overflow (for sigfig=3: like 0.9999 rounding to "1000"; need "100"). digits = digits:sub(1, sigfig) exp = exp + 1 end assert(#digits == sigfig, 'Bug: rounded number has wrong length') return digits, exp end local function format_number(parms, show, exponent, isnegative) -- Parameter show is a number in en digits and with '.' decimal mark. -- Return t where t is a table with fields: -- show = wikitext formatted to display implied value -- (digits in local language) -- is_scientific = true if show uses scientific notation -- clean = unformatted show (possibly adjusted and with inserted '.') -- (en digits) -- sign = '' or MINUS -- exponent = exponent (possibly adjusted) -- The clean and exponent fields can be used to calculate the -- rounded absolute value, if needed. -- -- The value implied by the arguments is found from: -- exponent is nil; and -- show is a string of digits (no sign), with an optional dot; -- show = '123.4' is value 123.4, '1234' is value 1234.0; -- or: -- exponent is an integer indicating where dot should be; -- show is a string of digits (no sign and no dot); -- there is an implied dot before show; -- show does not start with '0'; -- show = '1234', exponent = 3 is value 0.1234*10^3 = 123.4. -- -- The formatted result: -- * Is for an output value and is spelled if wanted and possible. -- * Includes a Unicode minus if isnegative. -- * Uses a custom decimal mark, if wanted. -- * Has digits grouped where necessary, if wanted. -- * Uses scientific notation for very small or large values -- (which forces output to not be spelled). -- * Has no more than maxsigfig significant digits -- (same as old template and {{#expr}}). local sign = isnegative and MINUS or '' local maxlen = maxsigfig if exponent == nil then local integer, dot, fraction = show:match('^(%d*)(%.?)(.*)') if #integer >= 10 then show = integer .. fraction exponent = #integer elseif integer == '0' or integer == '' then local zeros, figs = fraction:match('^(0*)([^0]?.*)') if #figs == 0 then if #zeros > maxlen then show = '0.' .. zeros:sub(1, maxlen) end elseif #zeros >= 4 then show = figs exponent = -#zeros elseif #figs > maxlen then show = '0.' .. zeros .. figs:sub(1, maxlen) end else maxlen = maxlen + #dot if #show > maxlen then show = show:sub(1, maxlen) end end end if exponent then if #show > maxlen then show = show:sub(1, maxlen) end if exponent > 10 or exponent <= -4 or (exponent == 10 and show ~= '1000000000') then -- Rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10). return { clean = '.' .. show, exponent = exponent, sign = sign, show = sign .. with_exponent(show, exponent-1), is_scientific = true, } end if exponent >= #show then show = show .. string.rep('0', exponent - #show) -- result has no dot elseif exponent <= 0 then show = '0.' .. string.rep('0', -exponent) .. show else show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1) end end if isnegative and show:match('^0.?0*$') then sign = '' -- don't show minus if result is negative but rounds to zero end local formatted_show = sign .. with_separator(parms, show) if parms.opt_spell_out then formatted_show = spell_number(parms, sign .. show) or formatted_show end return { clean = show, sign = sign, show = formatted_show, } end -- Fraction output format. -- 2013-07-20 Trying new styles proposed at [[Template talk:Convert]]. local fracfmt = { { -- Like {{frac}} (fraction slash). -- 1/2 : sign, numerator, denominator -- 1+2/3 : signed_wholenumber, numerator, denominator '<span class="frac nowrap">%s<sup>%s</sup>⁄<sub>%s</sub></span>', '<span class="frac nowrap">%s<sup> %s</sup>⁄<sub>%s</sub></span>', }, { -- Like {{sfrac}} (fraction horizontal bar). -- 1//2 : sign, numerator, denominator (sign should probably be before the fraction, but then it can wrap, and html is already too long) -- 1+2//3 : signed_wholenumber, numerator, denominator '<span class="sfrac nowrap" style="display:inline-block; vertical-align:-0.5em; font-size:85%%; text-align:center;"><span style="display:block; line-height:1em; padding:0 0.1em;">%s%s</span><span style="display:none;">/</span><span style="display:block; line-height:1em; padding:0 0.1em; border-top:1px solid;">%s</span></span>', '<span class="sfrac nowrap">%s<span style="display:none;"> </span><span style="display:inline-block; vertical-align:-0.5em; font-size:85%%; text-align:center;"><span style="display:block; line-height:1em; padding:0 0.1em;">%s</span><span style="display:none;">/</span><span style="display:block; line-height:1em; padding:0 0.1em; border-top:1px solid;">%s</span></span></span>', }, { -- Like old {{convert}} template. -- 1///2 : sign, numerator, denominator -- 1+2///3: signed_wholenumber, sign, numerator, denominator '<span style="white-space:nowrap">%s<sup>%s</sup>⁄<sub>%s</sub></span>', '<span class="frac nowrap">%s<s style="display:none">%s</s><sup>%s</sup>⁄<sub>%s</sub></span>', }, } local function extract_fraction(parms, text, negative) -- If text represents a fraction, return value, show, spelled where -- value is a number (value of the fraction in argument text) -- show is a string (formatted text for display of an input value, -- and is spelled if wanted and possible) -- spelled is true if show was spelled -- Otherwise, return nil. -- Input uses en digits and '.' decimal mark (input has been translated). -- Output uses digits in local language and custom decimal mark, if any. -- -- In the following, '(3/8)' represents the wikitext required to -- display a fraction with numerator 3 and denominator 8. -- In the wikitext, Unicode minus is used for a negative value. -- text value, show value, show -- if not negative if negative -- 3 / 8 0.375, '(3/8)' -0.375, '−(3/8)' -- 2 + 3 / 8 2.375, '2(3/8)' -1.625, '−2(−3/8)' -- 2 - 3 / 8 1.625, '2(−3/8)' -2.375, '−2(3/8)' -- 1 + 20/8 3.5 , '1/(20/8)' 1.5 , '−1/(−20/8)' -- 1 - 20/8 -1.5., '1(−20/8)' -3.5 , '−1(20/8)' -- Wherever an integer appears above, numbers like 1.25 or 12.5e-3 -- (which may be negative) are also accepted (like old template). -- Template interprets '1.23e+2+12/24' as '123(12/24)' = 123.5! local numstr, whole, value local lhs, slash, denstr = text:match('^%s*([^/]-)%s*(/+)%s*(.-)%s*$') local denominator = tonumber(denstr) if denominator == nil then return nil end local wholestr, negfrac, rhs = lhs:match('^%s*(.-[^eE])%s*([+-])%s*(.-)%s*$') if wholestr == nil or wholestr == '' then wholestr = nil whole = 0 numstr = lhs else whole = tonumber(wholestr) if whole == nil then return nil end numstr = rhs end negfrac = (negfrac == '-') local numerator = tonumber(numstr) if numerator == nil then return nil end -- Spelling of silly inputs like "-2+3/8" or "2+3/+8" (mixed or excess signs) is not supported. local do_spell if negative == negfrac or wholestr == nil then value = whole + numerator / denominator do_spell = parms.opt_spell_in if do_spell then if not (numstr:match('^%d') and denstr:match('^%d')) then -- if either has a sign do_spell = false end end else value = whole - numerator / denominator numstr = change_sign(numstr) do_spell = false end if not valid_number(value) then return nil -- overflow or similar end numstr = use_minus(numstr) denstr = use_minus(denstr) local style = #slash -- kludge: 1, 2, or 3 slashes can be used to select style if style > 3 then style = 3 end local wikitext if wholestr then if negative then wholestr = change_sign(wholestr) end local fmt = fracfmt[style][2] if style < 3 then wikitext = format(fmt, use_minus(from_en(wholestr)), from_en(numstr), from_en(denstr)) else local sign = negative and MINUS or '+' wikitext = format(fmt, use_minus(from_en(wholestr)), sign, from_en(numstr), from_en(denstr)) end else local sign = negative and MINUS or '' wikitext = format(fracfmt[style][1], sign, from_en(numstr), from_en(denstr)) end if do_spell then local numsign = (wholestr or not negative) and '' or '-' wikitext = spell_number(parms, wholestr, numsign .. numstr, denstr) or wikitext end return value, wikitext, do_spell end local function extract_number(parms, text, another, no_fraction) -- Return true, info if can extract a number from text, -- where info is a table with the result, -- or return false, t where t is an error message table. -- Input can use en digits or digits in local language. -- Parameter another = true if the expected value is not the first. -- Before processing, the input text is cleaned: -- * Any thousand separators (valid or not) are removed. -- * Any sign (and optional following whitespace) is replaced with -- '-' (if negative) or '' (otherwise). -- That replaces Unicode minus with '-'. -- If successful, the returned info table contains named fields: -- value = a valid number -- singular = true if value is 1 (to use singular form of units) -- = false if value is -1 (like old template) -- clean = cleaned text with any separators and sign removed -- (en digits and '.' decimal mark) -- show = text formatted for output -- (digits in local language and custom decimal mark) -- The resulting show: -- * Is for an input value and is spelled if wanted and possible. -- * Has a rounded value, if wanted. -- * Has digits grouped where necessary, if wanted. -- * If negative, a Unicode minus is used; otherwise the sign is -- '+' (if the input text used '+'), or is '' (if no sign in input). text = strip(text or '') local clean = to_en(text) if clean == '' then return false, { another and 'cvt_no_num2' or 'cvt_no_num' } end local isnegative, propersign = false, '' -- most common case local singular, show local value = tonumber(clean) if value then local sign = clean:sub(1, 1) if sign == '+' or sign == '-' then propersign = (sign == '+') and '+' or MINUS clean = clean:sub(2) end if value < 0 then isnegative = true value = -value end else local valstr for _, prefix in ipairs({ '-', MINUS, '−' }) do -- Including '-' means inputs like '- 2' (with space) are accepted as -2. -- It also sets isnegative in case input is a fraction like '-2-3/4'. local plen = #prefix if clean:sub(1, plen) == prefix then valstr = clean:sub(plen + 1) break end end if valstr then isnegative = true propersign = MINUS clean = valstr value = tonumber(clean) end if value == nil then local spelled if not no_fraction then value, show, spelled = extract_fraction(parms, clean, isnegative) end if value == nil then return false, { 'cvt_bad_num', text } end if spelled and value <= 1 then singular = true -- for example, "one half mile" (singular unit) else singular = false -- any numeric fraction (even with value 1) is regarded as plural end end end if not valid_number(value) then -- for example, "1e310" overflows return false, { 'cvt_invalid_num' } end if show == nil then singular = (value == 1 and not isnegative) local precision = parms.input_precision if precision and 0 <= precision and precision <= 8 then value = value + 2e-14 -- fudge for some common cases of bad rounding local fmt = '%.' .. format('%d', precision) .. 'f' show = fmt:format(value) else show = clean end show = propersign .. with_separator(parms, show) if parms.opt_spell_in then show = spell_number(parms, propersign .. clean) or show end end if isnegative and (value ~= 0) then value = -value end return true, { value = value, singular = singular, clean = clean, show = show, } end local function get_number(text) -- Return v, f where: -- v = nil (text is not a number) -- or -- v = value of text (text is a number) -- f = true if value is an integer -- Input can use en digits or digits in local language, -- but no separators, no Unicode minus, and no fraction. if text then local number = tonumber(to_en(text)) if number then local integer, fraction = math.modf(number) return number, (fraction == 0) end end end local function preunits(count, preunit1, preunit2) -- If count is 1: -- ignore preunit2 -- return p1 -- else: -- preunit1 is used for preunit2 if the latter is empty -- return p1, p2 -- where: -- p1 is text to insert before the input unit -- p2 is text to insert before the output unit -- p1 or p2 may be nil to mean "no preunit" -- Using '+ ' gives output like "5+ feet" (no preceding space). local function withspace(text, i) -- Insert space at beginning if i == 1, or at end if i == -1. -- However, no space is inserted if there is a space or ' ' -- or '-' at that position ('-' is for adjectival text). local current = text:sub(i, i) if current == ' ' or current == '-' then return text end if i == 1 then current = text:sub(1, 6) else current = text:sub(-6, -1) end if current == ' ' then return text end if i == 1 then return ' ' .. text end return text .. ' ' end preunit1 = preunit1 or '' local trim1 = strip(preunit1) if count == 1 then if trim1 == '' then return nil end return withspace(withspace(preunit1, 1), -1) end preunit2 = preunit2 or '' local trim2 = strip(preunit2) if trim1 == '' and trim2 == '' then return nil, nil end if trim1 ~= '+' then preunit1 = withspace(preunit1, 1) end if trim2 == ' ' then -- trick to make preunit2 empty preunit2 = nil elseif trim2 == '' then preunit2 = preunit1 elseif trim2 ~= '+' then preunit2 = withspace(preunit2, 1) end return preunit1, preunit2 end local function range_text(range, want_name, parms, before, after) -- Return before .. rtext .. after -- where rtext is the text that separates two values in a range. local rtext, adj_text, exception if type(range) == 'table' then -- Table must specify range text for abbr=off and for abbr=on, -- and may specify range text for 'adj=on', -- and may specify exception = true. rtext = range[want_name and 'off' or 'on'] adj_text = range['adj'] exception = range['exception'] else rtext = range end if parms.opt_adjectival then if want_name or (exception and parms.abbr_org == 'on') then rtext = adj_text or rtext:gsub(' ', '-'):gsub(' ', '-') end end if rtext == '–' and after:sub(1, #MINUS) == MINUS then rtext = ' – ' end return before .. rtext .. after end local function get_composite(parms, iparm, total, in_unit_table) -- Look for a composite input unit. For example, "{{convert|1|yd|2|ft|3|in}}" -- would result in a call to this function with -- iparm = 3 (parms[iparm] = "2", just after the first unit) -- total = 1 (number of yards) -- in_unit_table = (unit table for "yd") -- Return true, iparm, unit where -- iparm = index just after the composite units (7 in above example) -- unit = composite unit table holding all input units, -- or return true if no composite unit is present in parms, -- or return false, t where t is an error message table. local default, subinfo local composite_units, count = { in_unit_table }, 1 local fixups = {} local subunit = in_unit_table while subunit.subdivs do -- subdivs is nil or a table of allowed subdivisions local subcode = strip(parms[iparm+1]) local subdiv = subunit.subdivs[subcode] if not subdiv then break end local success success, subunit = lookup(subcode, parms.opt_sp_us, 'no_combination') if not success then return false, subunit end -- should never occur success, subinfo = extract_number(parms, parms[iparm]) if not success then return false, subinfo end iparm = iparm + 2 subunit.inout = 'in' subunit.valinfo = { subinfo } -- Recalculate total as a number of subdivisions. -- subdiv[1] = number of subdivisions per previous unit (integer > 1). total = total * subdiv[1] + subinfo.value if not default then -- set by the first subdiv with a default defined default = subdiv.default end count = count + 1 composite_units[count] = subunit if subdiv.unit or subdiv.name then fixups[count] = { unit = subdiv.unit, name = subdiv.name, valinfo = subunit.valinfo } end end if count == 1 then return true -- no error and no composite unit end for i, fixup in pairs(fixups) do local unit = fixup.unit local name = fixup.name if not unit or (count > 2 and name) then composite_units[i].fixed_name = name else local success, alternate = lookup(unit, parms.opt_sp_us, 'no_combination') if not success then return false, alternate end -- should never occur alternate.inout = 'in' alternate.valinfo = fixup.valinfo composite_units[i] = alternate end end return true, iparm, { utype = in_unit_table.utype, scale = subunit.scale, -- scale of last (least significant) unit valinfo = { { value = total, clean = subinfo.clean } }, composite = composite_units, default = default or in_unit_table.default } end local function translate_parms(parms, named_keys) -- Update fields in parms by translating parameters to those used at enwiki. -- Also, checks are performed which may display warnings, if enabled. -- Return true if successful or return false, t where t is an error message table. for _, loc_name in ipairs(named_keys) do local loc_value = parms[loc_name] local en_name = en_option_name[loc_name] if en_name then local en_value if en_name == 'sigfig' then if loc_value == '' then add_warning(parms, 'cvt_empty_option', loc_name) else local number, is_integer = get_number(loc_value) if not number or not is_integer or number <= 0 then return false, { 'cvt_bad_sigfig', loc_value } end en_value = number end else en_value = en_option_value[en_name][loc_value] end if en_value == nil then if loc_value == '' then add_warning(parms, 'cvt_empty_option', loc_name) else -- Using, for example, aliases like |sing=off|adj=on can give -- loc_value == nil when adj is processed after sing. In that case, -- the following gives a slightly misleading but reasonable warning. local text = loc_value and (loc_name .. '=' .. loc_value) or loc_name add_warning(parms, 'cvt_unknown_option', text) end elseif en_value == '' then en_value = nil -- an ignored option like adj=off elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then for _, v in ipairs(split(en_value, ',')) do parms[v] = true end en_value = nil end parms[en_name] = en_value else add_warning(parms, 'cvt_unknown_option', loc_name .. '=' .. loc_value) end end if parms.adj then if parms.adj:sub(1, 2) == 'ri' then -- It is known that adj is 'ri1' or 'ri2' or 'ri3', so precision is valid. -- Only en digits are accepted. parms.input_precision = tonumber(parms.adj:sub(-1)) parms.adj = nil end end if parms.abbr then parms.abbr_org = parms.abbr -- original abbr that was set, before any flip else parms.abbr = 'out' -- default is to abbreviate output only (use symbol, not name) end if parms.opt_flip then local function swap_in_out(option) local value = parms[option] if value == 'in' then parms[option] = 'out' elseif value == 'out' then parms[option] = 'in' end end swap_in_out('abbr') swap_in_out('lk') if parms.opt_spell_in then -- For simplicity, and because it does not appear to be needed, -- user cannot set an option to spell the output. parms.opt_spell_in = nil parms.opt_spell_out = true end end if parms.opt_table or parms.opt_tablecen then if parms.abbr_org == nil and parms.lk == nil then parms.opt_values = true end local align = format('align="%s"', parms.opt_table and 'right' or 'center') parms.table_joins = { align .. '|', '\n|' .. align .. '|' } end if parms.opt_lang_en then from_en_table = nil end return true end local function get_values(parms) -- If successful, update parms and return true, v, i where -- v = table of input values -- i = index to next entry in parms after those processed here -- or return false, t where t is an error message table. local valinfo = collection() -- numbered table of input values local range = collection() -- numbered table of range items (having, for example, 2 range items requires 3 input values) local had_nocomma -- true if removed "nocomma" kludge from second parameter (like "tonocomma") local parm2 = strip(parms[2]) if parm2 and parm2:sub(-7, -1) == 'nocomma' then parms[2] = strip(parm2:sub(1, -8)) parms.opt_nocomma = true had_nocomma = true end local i = 1 while true do local success, info = extract_number(parms, parms[i], i > 1) -- need to set parms.opt_nocomma before calling this if not success then return false, info end i = i + 1 valinfo:add(info) local next = strip(parms[i]) local range_item = range_types[next] or range_types[range_aliases[next]] if not range_item then break end i = i + 1 range:add(range_item) parms.is_range_x = (type(range_item) == 'table') and range_item.is_range_x or nil end if range.n > 0 then if range.n > 30 then -- limit abuse, although 4 is a more likely upper limit return false, { 'cvt_invalid_num' } -- misleading message but it will do end parms.range = range elseif had_nocomma then return false, { 'cvt_unknown', parm2 } end return true, valinfo, i end local function get_parms(pframe) -- If successful, return true, parms, unit where -- parms is a table of all arguments passed to the template -- converted to named arguments, and -- unit is the input unit table; -- or return false, t where t is an error message table. -- MediaWiki removes leading and trailing whitespace from the values of -- named arguments. However, the values of numbered arguments include any -- whitespace entered in the template, and whitespace is used by some -- parameters (example: the numbered parameters associated with "disp=x"). local parms = {} -- arguments passed to template local named_keys = collection() -- numbered table of named keys in parms: needed because cannot iterate parms and add new fields to it for k, v in pairs(pframe.args) do parms[k] = v if type(k) == 'string' then named_keys:add(k) end end local success, msg = translate_parms(parms, named_keys) if not success then return false, msg end local success, valinfo, i = get_values(parms) if not success then return false, valinfo end local in_unit = strip(parms[i]) i = i + 1 local success, in_unit_table = lookup(in_unit, parms.opt_sp_us, 'no_combination') if not success then return false, in_unit_table end if parms.test == 'msg' then -- Am testing the messages produced when no output unit is specified, and -- the input unit has a missing or invalid default. -- Set two units for testing that. -- LATER: Remove this code. if in_unit == 'chain' then in_unit_table.default = nil -- no default elseif in_unit == 'rd' then in_unit_table.default = "ft!X!m" -- an invalid expression end end in_unit_table.valinfo = valinfo in_unit_table.inout = 'in' -- this is an input unit if not parms.range then local success, inext, composite_unit = get_composite(parms, i, valinfo[1].value, in_unit_table) if not success then return false, inext end if composite_unit then in_unit_table = composite_unit i = inext end end if in_unit_table.builtin == 'mach' then -- As with old template, a number following Mach as the input unit is the altitude, -- and there is no way to specify an altitude for the output unit. -- Could put more code in this function to get any output unit and check for -- an altitude following that unit. local success, info = extract_number(parms, parms[i], false, true) if success then i = i + 1 in_unit_table.altitude = info.value end end local next = strip(parms[i]) i = i + 1 local precision, is_bad_precision local function set_precision(text) local number, is_integer = get_number(text) if number then if is_integer then precision = number else precision = text is_bad_precision = true end return true -- text was used for precision, good or bad end end if not set_precision(next) then parms.out_unit = next if set_precision(strip(parms[i])) then i = i + 1 end end if parms.opt_adj_mid then parms.opt_adjectival = true next = parms[i] i = i + 1 if next then -- mid-text words if next:sub(1, 1) == '-' then parms.mid = next else parms.mid = ' ' .. next end end end if parms.opt_one_preunit then parms[parms.opt_flip and 'preunit2' or 'preunit1'] = preunits(1, parms[i]) i = i + 1 end if parms.disp == 'x' then -- Following is reasonably compatible with the old template. local first = parms[i] or '' local second = parms[i+1] or '' i = i + 2 if strip(first) == '' then -- user can enter ' ' rather than ' ' to avoid the default first = ' [ ' .. first second = ' ]' .. second end parms.joins = { first, second } elseif parms.opt_two_preunits then local p1, p2 = preunits(2, parms[i], parms[i+1]) i = i + 2 if parms.preunit1 then -- To simplify documentation, allow unlikely use of adj=pre with disp=preunit -- (however, an output unit must be specified with adj=pre and with disp=preunit). parms.preunit1 = parms.preunit1 .. p1 parms.preunit2 = p2 else parms.preunit1, parms.preunit2 = p1, p2 end end if precision == nil then if set_precision(strip(parms[i])) then i = i + 1 end end if is_bad_precision then return false, { 'cvt_bad_prec', precision } end parms.precision = precision return true, parms, in_unit_table end local function default_precision(invalue, inclean, outvalue, in_current, out_current, extra) -- Return a default value for precision (an integer like 2, 0, -2). -- Code follows procedures used in old template. local fudge = 1e-14 -- {{Order of magnitude}} adds this, so we do too local prec, minprec, adjust local utype = out_current.utype local subunit_ignore_trailing_zero local subunit_more_precision -- kludge for "in" used in input like "|2|ft|6|in" local composite = in_current.composite if composite then subunit_ignore_trailing_zero = true -- input "|2|st|10|lb" has precision 0, not -1 if composite[#composite].exception == 'subunit_more_precision' then subunit_more_precision = true -- do not use standard precision with input like "|2|ft|6|in" end end -- Count digits after decimal mark, handling cases like '12.345e6'. local exponent local integer, dot, fraction, expstr = inclean:match('^(%d*)(%.?)(%d*)(.*)') local e = expstr:sub(1, 1) local boost = 0 -- can increase default precision if e == 'e' or e == 'E' then exponent = tonumber(expstr:sub(2)) elseif expstr:find('/', 1, true) then boost = 1 -- any input fraction is regarded as one extra digit of precision end if dot == '' then prec = subunit_ignore_trailing_zero and 0 or -integer:match('0*$'):len() else prec = #fraction end if exponent then -- So '1230' and '1.23e3' both give prec = -1, and '0.00123' and '1.23e-3' give 5. prec = prec - exponent end local exception = (utype == 'temperature' and not (in_current.exception == 'temperature' or out_current.exception == 'temperature')) if exception then -- Kelvin value can be almost zero, or small but negative due to precision problems. -- Also, an input value like -300 C (below absolute zero) gives negative kelvins. -- Calculate minimum precision from absolute value. adjust = 0 local kelvin = abs((invalue - in_current.offset) * in_current.scale) if kelvin < 1e-8 then -- assume nonzero due to input or calculation precision problem minprec = 2 else minprec = 2 - floor(log10(kelvin) + fudge) -- 3 sigfigs in kelvin end else if invalue == 0 or outvalue <= 0 then -- We are never called with a negative outvalue, but it might be zero. -- This is special-cased to avoid calculation exceptions. return 0 end if out_current.exception == 'integer_more_precision' and floor(invalue) == invalue then -- With certain output units that sometimes give poor results -- with default rounding, use more precision when the input -- value is equal to an integer. An example of a poor result -- is when input 50 gives a smaller output than input 49.5. -- Experiment shows this helps, but it does not eliminate all -- surprises because it is not clear whether "50" should be -- interpreted as "from 45 to 55" or "from 49.5 to 50.5". adjust = -log10(in_current.scale) elseif subunit_more_precision then -- Conversion like "{{convert|6|ft|1|in|cm}}" (where subunit is "in") -- has a non-standard adjust value, to give more output precision. adjust = log10(out_current.scale) + 2 else adjust = log10(abs(invalue / outvalue)) end adjust = adjust + log10(2) -- Ensure that the output has at least two significant figures. minprec = 1 - floor(log10(outvalue) + fudge) end if extra then adjust = extra.adjust or adjust minprec = extra.minprec or minprec end return math.max(floor(prec + adjust + boost), minprec) end local function convert(invalue, inclean, in_current, out_current) -- Convert given input value from one unit to another. -- Return output_value (a number) if a simple convert, or -- return f, t where -- f = true, t = table of information with results, or -- f = false, t = error message table. local inscale = in_current.scale local outscale = out_current.scale if not in_current.iscomplex and not out_current.iscomplex then return invalue * (inscale / outscale) -- minimize overhead for most common case end if in_current.invert then -- Fuel efficiency (there are no built-ins for this type of unit). if in_current.invert * out_current.invert < 0 then return 1 / (invalue * inscale * outscale) end return invalue * (inscale / outscale) elseif in_current.offset then -- Temperature (there are no built-ins for this type of unit). return (invalue - in_current.offset) * (inscale / outscale) + out_current.offset else -- Built-in unit. local in_builtin = in_current.builtin local out_builtin = out_current.builtin if in_builtin and out_builtin then if in_builtin == out_builtin then return invalue end -- There are no cases (yet) where need to convert from one -- built-in unit to another, so this should never occur. return false, { 'cvt_bug_convert' } end if in_builtin == 'mach' or out_builtin == 'mach' then local adjust if in_builtin == 'mach' then inscale = speed_of_sound(in_current.altitude) adjust = outscale / 0.1 else outscale = speed_of_sound(out_current.altitude) adjust = 0.1 / inscale end return true, { outvalue = invalue * (inscale / outscale), adjust = log10(adjust) + log10(2), } elseif in_builtin == 'hand' then -- 1 hand = 4 inches; 1.2 hands = 6 inches. -- Fractions of a hand are only defined for the first digit, and -- the first fractional digit should be a number of inches (1, 2 or 3). -- However, this code interprets the entire fraction as the number -- of inches / 10 (so 1.75 inches would be 0.175 hands). -- A value like 12.3 hands is exactly 12*4 + 3 inches; base default precision on that. local integer, fraction = math.modf(invalue) local outvalue = (integer + 2.5 * fraction) * (inscale / outscale) local inch_value = 4 * integer + 10 * fraction -- equivalent number of inches local fracstr = inclean:match('%.(.*)') or '' local fmt if fracstr == '' then fmt = '%.0f' else fmt = '%.' .. format('%d', #fracstr - 1) .. 'f' end return true, { invalue = inch_value, inclean = format(fmt, inch_value), outvalue = outvalue, minprec = 0, } end end return false, { 'cvt_bug_convert' } -- should never occur end local function cvtround(parms, info, in_current, out_current) -- Return true, t where t is a table with the conversion results; fields: -- show = rounded, formatted string with the result of converting value in info, -- using the rounding specified in parms. -- singular = true if result is positive, and (after rounding) -- is "1", or like "1.00"; -- (and more fields shown below, and a calculated 'absvalue' field). -- or return true, nil if no value specified; -- or return false, t where t is an error message table. -- Input info.clean uses en digits (it has been translated, if necessary). -- Output show uses en or non-en digits as appropriate, or can be spelled. local invalue, inclean, show, exponent, singular if info then invalue, inclean = info.value, info.clean end if invalue == nil or invalue == '' then return true, nil end if out_current.builtin == 'hand' then -- Convert to hands, then convert the fractional part to inches. -- Code is not correct when output is spelled, and it ignores any requested -- precision if the output uses scientific notation (very large, or very -- small). Not worth more complexity as these cases should be very rare. local dummy_unit_table = { scale = out_current.scale } local success, outinfo = cvtround(parms, info, in_current, dummy_unit_table) if not success then return false, outinfo end local fmt if outinfo.is_scientific then fmt = '%.1f' else local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or '' -- outinfo.show is in local language if fraction == '' then if not outinfo.use_default_precision then return true, outinfo end fmt = '%.0f' else fmt = '%.' .. format('%d', ulen(fraction) - 1) .. 'f' end end local hands, inches = math.modf(outinfo.raw_absvalue) inches = format(fmt, inches * 4) if inches:sub(1, 1) == '4' then hands = hands + 1 inches = '0' .. inches:sub(2) if tonumber(inches) == 0 then inches = '0' end end if inches:sub(2, 2) == '.' then inches = inches:sub(1, 1) .. inches:sub(3) end return true, { sign = outinfo.sign, singular = outinfo.singular, show = outinfo.sign .. with_separator(parms, format('%d', hands)) .. numdot .. from_en(inches) } end local outvalue, extra = convert(invalue, inclean, in_current, out_current) if extra then if not outvalue then return false, extra end invalue = extra.invalue or invalue inclean = extra.inclean or inclean outvalue = extra.outvalue end if not valid_number(outvalue) then return false, { 'cvt_invalid_num' } end local isnegative if outvalue < 0 then isnegative = true outvalue = -outvalue end local success, use_default_precision local precision = parms.precision if not precision then local sigfig = parms.sigfig if sigfig then show, exponent = make_sigfig(outvalue, sigfig) elseif parms.opt_round5 then show = format('%.0f', floor((outvalue / 5) + 0.5) * 5) else use_default_precision = true precision = default_precision(invalue, inclean, outvalue, in_current, out_current, extra) end end if precision then if precision >= 0 then if precision <= 8 then -- Add a fudge to handle common cases of bad rounding due to inability -- to precisely represent some values. This makes the following work: -- {{convert|-100.1|C|K}} and {{convert|5555000|um|m|2}}. -- Old template uses #expr round, which invokes PHP round(). -- LATER: Investigate how PHP round() works. outvalue = outvalue + 2e-14 end local fmt = '%.' .. format('%d', precision) .. 'f' local success success, show = pcall(format, fmt, outvalue) if not success then return false, { 'cvt_big_prec', tostring(precision) } end else precision = -precision -- #digits to zero (in addition to any digits after dot) local shift = 10 ^ precision show = format('%.0f', outvalue/shift) if show ~= '0' then exponent = #show + precision end end end if (show == '1' or show:match('^1%.0*$') ~= nil) and not isnegative then -- Use match because on some systems 0.99999999999999999 is 1.0. singular = true end local t = format_number(parms, show, exponent, isnegative) t.singular = singular t.raw_absvalue = outvalue -- absolute value before rounding t.use_default_precision = use_default_precision return true, setmetatable(t, { __index = function (self, key) if key == 'absvalue' then -- Calculate absolute value after rounding, if needed. local clean, exponent = rawget(self, 'clean'), rawget(self, 'exponent') local value = tonumber(clean) -- absolute value (any negative sign has been ignored) if exponent then value = value * 10^exponent end rawset(self, key, value) return value end end }) end local function evaluate_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) -- only en digits are accepted -- '<' can also be '<=' or '>' or '>=' -- In addition, the following form is supported: -- LHS and RHS -- where -- LHS, RHS = any of above expressions. local function compare(value, text) local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$') if arithop == nil then error('Invalid default expression', 0) elseif arithop == '*' then factor = tonumber(factor) if factor == nil then error('Invalid default expression', 0) end value = value * factor end limit = tonumber(limit) if limit == nil then error('Invalid default expression', 0) end if compop == '<' then return value < limit elseif compop == '<=' then return value <= limit elseif compop == '>' then return value > limit elseif compop == '>=' then return value >= limit end error('Invalid default expression', 0) -- should not occur end local lhs, rhs = condition:match('^(.-%W)and(%W.*)') if lhs == nil then return compare(value, condition) end return compare(value, lhs) and compare(value, rhs) end local function get_default(value, unit_table) -- Return true, s where s = name of unit's default output unit, -- or return false, t where t is an error message table. -- Some units have a default that depends on the input value -- (the first value if a range of values is used). -- If '!' is in the default, the first bang-delimited field is an -- expression that uses 'v' to represent the input value. -- Example: 'v < 120 ! small ! big ! suffix' (suffix is optional) -- evaluates 'v < 120' as a boolean with result -- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise. -- Input must use en digits and '.' decimal mark. local default = default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default if default == nil then return false, { 'cvt_no_default', unit_table.symbol } end if default:find('!', 1, true) == nil then return true, default end local t = split(default, '!') if #t == 3 or #t == 4 then local success, result = pcall(evaluate_condition, value, t[1]) if success then default = result and t[2] or t[3] if #t == 4 then default = default .. t[4] end return true, default end end return false, { 'cvt_bad_default', unit_table.symbol } end local linked_pages -- to record linked pages so will not link to the same page more than once local function make_link(link, id, link_key) -- Return wikilink "[[link|id]]", possibly abbreviated as in examples: -- [[Mile|mile]] --> [[mile]] -- [[Mile|miles]] --> [[mile]]s -- However, just id is returned if: -- * no link given (so caller does not need to check if a link was defined); or -- * link has previously been used during the current convert (to avoid overlinking). -- Linking with a unit uses the unit table as the link key, which fails to detect -- overlinking for conversions like (each links "mile" twice): -- {{convert|1|impgal/mi|USgal/mi|lk=on}} -- {{convert|1|l/km|impgal/mi USgal/mi|lk=on}} link_key = link_key or link -- use key if given (the key, but not the link, may be known when need to cancel a link record) if link == nil or link == '' or linked_pages[link_key] then return id end linked_pages[link_key] = true -- Following only works for language en, but it should be safe on other wikis, -- and overhead of doing it generally does not seem worthwhile. local l = link:sub(1, 1):lower() .. link:sub(2) if link == id or l == id then return '[[' .. id .. ']]' elseif link .. 's' == id or l .. 's' == id then return '[[' .. id:sub(1, -2) .. ']]s' else return '[[' .. link .. '|' .. id .. ']]' end end local function linked_id(unit_table, key_id, want_link) -- Return final unit id (symbol or name), optionally with a wikilink, -- and update unit_table.sep if required. -- key_id is one of: 'symbol', 'sym_us', 'name1', 'name1_us', 'name2', 'name2_us'. local abbr_on = (key_id == 'symbol' or key_id == 'sym_us') if abbr_on and want_link then local symlink = rawget(unit_table, 'symlink') if symlink then return symlink -- for exceptions that have the linked symbol built-in end end local multiplier = rawget(unit_table, 'multiplier') local per = unit_table.per if per then local unit1 = per[1] -- top unit_table, or nil local unit2 = per[2] -- bottom unit_table if abbr_on then if not unit1 then unit_table.sep = '' -- no separator in "$2/acre" end if not want_link then local symbol = unit_table.symbol_raw if symbol then return symbol -- for exceptions that have the symbol built-in end end end local key_id2 -- unit2 is always singular if key_id == 'name2' then key_id2 = 'name1' elseif key_id == 'name2_us' then key_id2 = 'name1_us' else key_id2 = key_id end local result if abbr_on then result = '/' elseif unit1 then result = ' per ' else result = 'per ' end if unit1 then result = linked_id(unit1, key_id, want_link) .. result end return result .. linked_id(unit2, key_id2, want_link) end if multiplier then -- A multiplier (like "100" in "100km") forces the unit to be plural. if abbr_on then multiplier = multiplier .. ' ' else multiplier = multiplier .. ' ' if key_id == 'name1' then key_id = 'name2' elseif key_id == 'name1_us' then key_id = 'name2_us' end end else multiplier = '' end local id = unit_table.fixed_name or unit_table[key_id] if want_link then local link = link_exceptions[unit_table.symbol] or unit_table.link if link then local before = '' local i = unit_table.customary if i == 1 and unit_table.sp_us then i = 2 -- show "U.S." not "US" end if i == 3 and abbr_on then i = 4 -- abbreviate "imperial" to "imp" end local customary = customary_units[i] if customary then -- LATER: This works for language en only, but it's esoteric so ignore for now. local pertext if id:sub(1, 1) == '/' then -- Want unit "/USgal" to display as "/U.S. gal", not "U.S. /gal". pertext = '/' id = id:sub(2) elseif id:sub(1, 4) == 'per ' then -- Similarly want "per U.S. gallon", not "U.S. per gallon" (but in practice this is unlikely to be used). pertext = 'per ' id = id:sub(5) else pertext = '' end -- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted. local removes = (i < 3) and { 'US ', 'US ', 'U.S. ', 'U.S. ' } or { 'imp ', 'imp ', 'imperial ' } for _, prefix in ipairs(removes) do local plen = #prefix if id:sub(1, plen) == prefix then id = id:sub(plen + 1) break end end before = pertext .. make_link(customary.link, customary[1]) .. ' ' end id = before .. make_link(link, id, unit_table) end end return multiplier .. id end local function make_id(parms, which, unit_table) -- Return id, f where -- id = unit name or symbol, possibly modified -- f = true if id is a name, or false if id is a symbol -- using 1st or 2nd values (which), and for 'in' or 'out' (unit_table.inout). -- Result is '' if no symbol/name is to be used. -- In addition, set unit_table.sep = ' ' or ' ' or '' -- (the separator that caller will normally insert before the id). if parms.opt_values then unit_table.sep = '' return '' end local inout = unit_table.inout local valinfo = unit_table.valinfo local abbr_org = parms.abbr_org local adjectival = parms.opt_adjectival local disp = parms.disp local lk = parms.lk local usename = unit_table.usename local singular = valinfo[which].singular if usename then -- Old template does something like this. if lk == 'on' or lk == inout then -- A linked unit uses the standard singular. else -- Set non-standard singular. local flipped = parms.opt_flip if inout == 'in' then if not adjectival and (abbr_org == 'out' or flipped) then local value = valinfo[which].value singular = (0 < value and value < 1.0001) end else if (abbr_org == 'on') or (not flipped and (abbr_org == nil or abbr_org == 'out')) or (flipped and abbr_org == 'in') then singular = (valinfo[which].absvalue < 1.0001 and not valinfo[which].is_scientific) end end end end local want_name if usename then want_name = true else if abbr_org == nil then if disp == 'br' or disp == 'or' or disp == 'slash' then want_name = true end if unit_table.utype == 'temperature' or unit_table.utype == 'temperature change' then if not (unit_table.exception == 'temperature') then want_name = false end end end if want_name == nil then local abbr = parms.abbr if abbr == 'on' or abbr == inout or (abbr == 'mos' and inout == 'out') then want_name = false else want_name = true end end end local key if want_name then if parms.opt_use_nbsp then unit_table.sep = ' ' else unit_table.sep = ' ' end if parms.opt_singular then local value if inout == 'in' then value = valinfo[which].value else value = valinfo[which].absvalue end if value then -- some unusual units do not always set value field value = abs(value) singular = (0 < value and value < 1.0001) end end if unit_table.engscale or parms.is_range_x then -- engscale: so "|1|e3kg" gives "1 thousand kilograms" (plural) -- is_range_x: so "|0.5|x|0.9|mi" gives "0.5 by 0.9 miles" (plural) singular = false end key = (adjectival or singular) and 'name1' or 'name2' if unit_table.sp_us then key = key .. '_us' end else unit_table.sep = ' ' key = unit_table.sp_us and 'sym_us' or 'symbol' end return linked_id(unit_table, key, lk == 'on' or lk == inout), want_name end local function decorate_value(parms, unit_table, which) -- If needed, update unit_table so values will be shown with extra information. -- For consistency with the old template (but different from fmtpower), -- the style to display powers of 10 includes "display:none" to allow some -- browsers to copy, for example, "10³" as "10^3", rather than as "103". local engscale = unit_table.engscale if engscale then local inout = unit_table.inout local info = unit_table.valinfo[which] local abbr = parms.abbr if abbr == 'on' or abbr == inout then info.show = info.show .. '<span style="margin-left:0.2em">×<span style="margin-left:0.1em">' .. from_en('10') .. '</span></span><s style="display:none">^</s><sup>' .. from_en(tostring(engscale.exponent)) .. '</sup>' else local number_id local lk = parms.lk if lk == 'on' or lk == inout then number_id = make_link(engscale.link, engscale[1]) else number_id = engscale[1] end -- WP:NUMERAL recommends " " in values like "12 million". info.show = info.show .. (parms.opt_adjectival and '-' or ' ') .. number_id end end local prefix = unit_table.vprefix if prefix then local info = unit_table.valinfo[which] info.show = prefix .. info.show end end local function process_input(parms, in_current) -- Processing required once per conversion. -- Return block of text to represent input (value/unit). if parms.opt_output_only or parms.opt_output_number_only or parms.opt_output_unit_only then parms.joins = { '', '' } return '' end local first_unit local composite = in_current.composite -- nil or table of units if composite then first_unit = composite[1] else first_unit = in_current end local id1, want_name = make_id(parms, 1, first_unit) local sep = first_unit.sep -- separator between value and unit, set by make_id local preunit = parms.preunit1 if preunit then sep = '' -- any separator is included in preunit else preunit = '' end if parms.opt_input_unit_only then parms.joins = { '', '' } if composite then local parts = { id1 } for i, unit in ipairs(composite) do if i > 1 then table.insert(parts, (make_id(parms, 1, unit))) end end id1 = table.concat(parts, ' ') end if want_name and parms.opt_adjectival then return preunit .. hyphenated(id1) end return preunit .. id1 end local abbr = parms.abbr local disp = parms.disp if disp == nil then -- special case for the most common setting parms.joins = disp_joins['b'] elseif disp ~= 'x' then -- Old template does this. if disp == 'slash' then if parms.abbr_org == nil then disp = 'slash-nbsp' elseif abbr == 'in' or abbr == 'out' then disp = 'slash-sp' else disp = 'slash-nosp' end elseif disp == 'sqbr' then if abbr == 'on' then disp = 'sqbr-nbsp' else disp = 'sqbr-sp' end end parms.joins = disp_joins[disp] or disp_joins['b'] end if parms.opt_also_symbol and not composite then local join1 = parms.joins[1] if join1 == ' (' or join1 == ' [' then parms.joins = { join1 .. first_unit[first_unit.sp_us and 'sym_us' or 'symbol'] .. ', ', parms.joins[2] } end end if in_current.builtin == 'mach' then local prefix = id1 .. ' ' local range = parms.range local valinfo = first_unit.valinfo local result = prefix .. valinfo[1].show if range then -- For simplicity and because more not needed, handle one range item only. local prefix2 = make_id(parms, 2, first_unit) .. ' ' result = range_text(range[1], want_name, parms, result, prefix2 .. valinfo[2].show) end return preunit .. result end if composite then -- Simplify: assume there is no range, and no decoration. local mid = '' local sep1 = ' ' local sep2 = ' ' if parms.opt_adjectival then if not parms.opt_flip then mid = parms.mid or '' end if want_name then sep1 = '-' sep2 = '-' end end local parts = { first_unit.valinfo[1].show .. sep1 .. id1 } for i, unit in ipairs(composite) do if i > 1 then table.insert(parts, unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, unit))) end end return table.concat(parts, sep2) .. mid end local result, mos local range = parms.range if range then mos = (abbr == 'mos') if not (mos or (parms.is_range_x and not want_name)) then linked_pages[first_unit] = nil -- so the second and only id will be linked, if wanted end end local id = (range == nil) and id1 or make_id(parms, 2, first_unit) local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'in') if mos and was_hyphenated then mos = false -- suppress repeat of unit in a range if linked_pages[first_unit] then linked_pages[first_unit] = nil id = make_id(parms, 2, first_unit) extra = hyphenated_maybe(parms, want_name, sep, id, 'in') end end local valinfo = first_unit.valinfo if range then if range.n == 1 then -- Like {{convert|1|x|2|ft}} (one range item; two values). -- Do what old template did. local sep1 = first_unit.sep if mos then decorate_value(parms, in_current, 1) decorate_value(parms, in_current, 2) result = valinfo[1].show .. sep1 .. id1 elseif parms.is_range_x and not want_name then if abbr == 'in' or abbr == 'on' then decorate_value(parms, in_current, 1) end decorate_value(parms, in_current, 2) result = valinfo[1].show .. sep1 .. id1 else if abbr == 'in' or abbr == 'on' then decorate_value(parms, in_current, 1) end decorate_value(parms, in_current, 2) result = valinfo[1].show end result = range_text(range[1], want_name, parms, result, valinfo[2].show) else -- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify. decorate_value(parms, in_current, 1) result = valinfo[1].show for i = 1, range.n do decorate_value(parms, in_current, i+1) result = range_text(range[i], want_name, parms, result, valinfo[i+1].show) end end else decorate_value(parms, first_unit, 1) result = valinfo[1].show end return result .. preunit .. extra end local function process_one_output(parms, out_current) -- Processing required for each output unit. -- Return block of text to represent output (value/unit). local id1, want_name = make_id(parms, 1, out_current) local sep = out_current.sep -- set by make_id local preunit = parms.preunit2 if preunit then sep = '' -- any separator is included in preunit else preunit = '' end if parms.opt_output_unit_only then if want_name and parms.opt_adjectival then return preunit .. hyphenated(id1) end return preunit .. id1 end if out_current.builtin == 'mach' then local prefix = id1 .. ' ' local range = parms.range local valinfo = out_current.valinfo local result = prefix .. valinfo[1].show if range then -- For simplicity and because more not needed, handle one range item only. result = range_text(range[1], want_name, parms, result, prefix .. valinfo[2].show) end return preunit .. result end local result local range = parms.range if range then if not (parms.is_range_x and not want_name) then linked_pages[out_current] = nil -- so the second and only id will be linked, if wanted end end local id = (range == nil) and id1 or make_id(parms, 2, out_current) local extra = hyphenated_maybe(parms, want_name, sep, id, 'out') local valinfo = out_current.valinfo if range then if range.n == 1 then local sep1 = out_current.sep local abbr = parms.abbr if parms.is_range_x and not want_name then if abbr == 'out' or abbr == 'on' then decorate_value(parms, out_current, 1) end decorate_value(parms, out_current, 2) result = valinfo[1].show .. sep1 .. id1 else if abbr == 'out' or abbr == 'on' then decorate_value(parms, out_current, 1) end decorate_value(parms, out_current, 2) result = valinfo[1].show end result = range_text(range[1], want_name, parms, result, valinfo[2].show) else -- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify. decorate_value(parms, out_current, 1) result = valinfo[1].show for i = 1, range.n do decorate_value(parms, out_current, i+1) result = range_text(range[i], want_name, parms, result, valinfo[i+1].show) end end else decorate_value(parms, out_current, 1) result = valinfo[1].show end if parms.opt_output_number_only then return result end return result .. preunit .. extra end local function make_output_single(parms, in_unit_table, out_unit_table) -- Return true, item where item = wikitext of the conversion result -- for a single output (which is not a combination or a multiple); -- or return false, t where t is an error message table. out_unit_table.valinfo = collection() local range = parms.range for i = 1, (range and (range.n + 1) or 1) do local success, info = cvtround(parms, in_unit_table.valinfo[i], in_unit_table, out_unit_table) if not success then return false, info end out_unit_table.valinfo:add(info) end return true, process_one_output(parms, out_unit_table) end local function make_output_multiple(parms, in_unit_table, out_unit_table) -- Return true, item where item = wikitext of the conversion result -- for an output which is a multiple (like 'ftin'); -- or return false, t where t is an error message table. local multiple = out_unit_table.multiple -- table of scaling factors (will not be nil) local combos = out_unit_table.combination -- table of unit tables (will not be nil) local abbr = parms.abbr local abbr_org = parms.abbr_org local disp = parms.disp local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or not (abbr == 'on' or abbr == 'out' or abbr == 'mos') local want_link = (parms.lk == 'on' or parms.lk == 'out') local mid = '' local sep1 = ' ' local sep2 = ' ' if parms.opt_adjectival then if parms.opt_flip then mid = parms.mid or '' end if want_name then sep1 = '-' sep2 = '-' end end local function make_result(info) local fmt, outvalue, sign local results = {} for i = 1, #combos do local thisvalue, strforce local out_current = combos[i] out_current.inout = 'out' local scale = multiple[i] if i == 1 then -- least significant unit ('in' from 'ftin') local fraction local success, outinfo = cvtround(parms, info, in_unit_table, out_current) if not success then return false, outinfo end sign = outinfo.sign if outinfo.is_scientific then strforce = outinfo.show fraction = '' else fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or '' -- outinfo.show is in local language end fmt = '%.' .. ulen(fraction) .. 'f' -- to reproduce precision if fraction == '' then outvalue = floor(outinfo.raw_absvalue + 0.5) -- keep all integer digits of least significant unit else outvalue = outinfo.absvalue end end if scale then outvalue, thisvalue = floor(outvalue / scale), outvalue % scale else thisvalue = outvalue end local id if want_name then id = out_current[(thisvalue == 1) and 'name1' or 'name2'] else id = out_current['symbol'] end if want_link then local link = out_current.link if link then id = make_link(link, id, out_current) end end local strval if strforce and outvalue == 0 then sign = '' -- any sign is in strforce strval = strforce -- show small values in scientific notation; will only use least significant unit else strval = (thisvalue == 0) and from_en('0') or with_separator(parms, format(fmt, thisvalue)) end table.insert(results, strval .. sep1 .. id) if outvalue == 0 then break end fmt = '%.0f' -- only least significant unit can have a fraction end local reversed, count = {}, #results for i = 1, count do reversed[i] = results[count + 1 - i] end return true, sign .. table.concat(reversed, sep2) end local valinfo = in_unit_table.valinfo local success, result = make_result(valinfo[1]) if not success then return false, result end local range = parms.range if range then for i = 1, range.n do local success, result2 = make_result(valinfo[i+1]) if not success then return false, result2 end result = range_text(range[i], want_name, parms, result, result2) end end return true, result .. mid end local function process(parms, in_unit_table) -- Return true, s where s = final wikitext result, -- or return false, t where t is an error message table. linked_pages = {} local success, out_unit_table local invalue1 = in_unit_table.valinfo[1].value local out_unit = parms.out_unit if out_unit == nil or out_unit == '' then success, out_unit = get_default(invalue1, in_unit_table) if not success then return false, out_unit end end success, out_unit_table = lookup(out_unit, parms.opt_sp_us, 'any_combination') if not success then return false, out_unit_table end if in_unit_table.utype ~= out_unit_table.utype then return false, { 'cvt_mismatch', in_unit_table.utype, out_unit_table.utype } end local flipped = parms.opt_flip local parts = {} for part = 1, 2 do -- The LHS (parts[1]) is normally the input, but is the output if flipped. -- Process LHS first so it will be linked, if wanted. -- Linking to the same item is suppressed in the RHS to avoid overlinking. if (part == 1 and not flipped) or (part == 2 and flipped) then parts[part] = process_input(parms, in_unit_table) else local outputs = {} local combos -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft') if out_unit_table.multiple == nil then -- nil ('ft' or 'm ft'), or table of factors ('ftin') combos = out_unit_table.combination end local imax = combos and #combos or 1 -- 1 (single unit) or number of unit tables for i = 1, imax do local success, item local out_current = combos and combos[i] or out_unit_table out_current.inout = 'out' if out_current.multiple == nil then success, item = make_output_single(parms, in_unit_table, out_current) else success, item = make_output_multiple(parms, in_unit_table, out_current) end if not success then return false, item end table.insert(outputs, item) end parts[part] = parms.opt_input_unit_only and '' or table.concat(outputs, '; ') end end if parms.opt_sortable then parts[1] = ntsh(invalue1, parms.opt_sortable_debug) .. parts[1] end local wikitext if parms.table_joins then wikitext = parms.table_joins[1] .. parts[1] .. parms.table_joins[2] .. parts[2] else wikitext = parts[1] .. parms.joins[1] .. parts[2] .. parms.joins[2] end if parms.warnings then wikitext = wikitext .. parms.warnings end return true, wikitext end local function main_convert(frame) set_config(frame) local result local success, parms, in_unit_table = get_parms(frame:getParent()) if success then success, result = process(parms, in_unit_table) else result = parms end if success then return result end return message(result) end return { convert = main_convert }