Module:Val

-- For Template:Val, output a number and optional unit. -- Format options include scientific and uncertainty notations.

local data_module -- name of module defining units local delimit_groups = require('Module:Gapnum').groups

local function valerror(msg, nocat) -- Return formatted message for errors. if is_test_run then -- LATER remove return 'Error: "' .. msg .. '"' end local ret = mw.html.create('strong') :addClass('error') :wikitext('Error in &#123;&#123;Val&#125;&#125;: ' .. msg) -- Not in talk, user, user_talk, or wikipedia_talk if not nocat and not mw.title.getCurrentTitle:inNamespaces(1,2,3,5) then ret:wikitext('') end return tostring(ret) end

local function extract_number(index, numbers, args) -- Extract number from args[index] and store result in numbers[index] -- and return true if no argument or if argument is valid. -- The result is a table which is empty if there was no specified number. -- Input like 1e3 is regarded as invalid; should use e=3 parameter. -- Input commas are removed so 1,234 is the same as 1234. local result = {} local arg = args[index] -- has been trimmed if arg and arg ~= '' then arg = arg:gsub(',', '') if arg:sub(1, 1) == '(' and arg:sub(-1) == ')' then result.parens = true arg = arg:sub(2, -2) end local minus = '−' local isnegative, propersign, prefix prefix, arg = arg:match('^(.-)([%d.]+)$') local value = tonumber(arg) if not value then return false end if arg:sub(1, 1) == '.' then arg = '0' .. arg end if prefix == '' then -- Ignore. elseif prefix == '±' then -- Display for first number, ignore for others. if index == 1 then propersign = '±' end elseif prefix == '+' then propersign = '+' elseif prefix == '-' or prefix == minus then propersign = minus isnegative = true else return false end result.clean = arg result.sign = propersign or '' result.value = isnegative and -value or value end numbers[index] = result return true end

local function get_scale(text, ucode) -- Return the value of text as a number, or throw an error. -- This supports extremely basic expressions of the form: --  a / b	--   a ^ b	-- where a and b are numbers or 'pi'. local n = tonumber(text) if n then return n	end n = text:gsub('pi', math.pi) for _, op in ipairs({ '/', '^' }) do local a, b = n:match('^(.-)' .. op .. '(.*)$') if a then a = tonumber(a) b = tonumber(b) if a and b then if op == '/' then return a / b				elseif op == '^' then return a ^ b				end end break end end error('Unit "' .. ucode .. '" has invalid scale "' .. text .. '"') end

local function get_builtin_unit(ucode, definitions) -- Return table of information for the specified built-in unit, or nil if not known. -- Each defined unit code must be followed by two spaces (not tab characters). local _, pos = definitions:find('\n' .. ucode .. ' ', 1, true) if pos then local endline = definitions:find('\n', pos, true) if endline then local result = {} local n = 0 local text = definitions:sub(pos, endline - 1):gsub('%s%s+', '\t') for item in (text .. '\t'):gmatch('(%S.-)\t') do				if item == 'ALIAS' then result.alias = true elseif item == 'ANGLE' then result.isangle = true result.nospace = true elseif item == 'NOSPACE' then result.nospace = true elseif item == 'SI' then result.si = true else n = n + 1 if n == 1 then result.symbol = item elseif n == 2 then result.link = item elseif n == 3 then result.scale = get_scale(item, ucode) else break end end end if n >= 2 or (n >= 1 and result.alias) then return result end -- Ignore invalid definition, treating it as a comment. end end end

local function convert_lookup(ucode, value, scaled_top, want_link, si, options) local lookup = require('Module:Convert/sandbox')._unit -- TODO remove "/sandbox" when convert updated return lookup(ucode, {			value = value,			scaled_top = scaled_top,			link = want_link,			si = si,			sort = options.want_sort,		}) end

local function get_unit(ucode, value, scaled_top, options) local want_link = options.want_link if scaled_top then want_link = options.want_per_link end local data = mw.loadData(data_module) local result = options.want_longscale and get_builtin_unit(ucode, data.builtin_units_long_scale) or		get_builtin_unit(ucode, data.builtin_units) local si, use_result if result then use_result = true if result.alias then ucode = result.symbol use_result = false end if result.scale then -- Setting si means convert will use the unit as given, and the sort key -- will be calculated from the value without any extra scaling that may -- occur if convert found the unit code. For example, if val defines the -- unit 'year' with a scale and if si were not set, convert would also apply -- its own scale because convert knows that a year is 31,557,600 seconds. si = { result.symbol, result.link } value = value * result.scale end if result.si then si = { result.symbol, result.link } use_result = false end end local convert_unit = convert_lookup(ucode, value, scaled_top, want_link, si, options) if use_result then if want_link then result.text =  .. result.symbol ..  else result.text = result.symbol end result.sortkey = convert_unit.sortspan result.scaled_top = value else result = { text = convert_unit.text, sortkey = convert_unit.sortspan, scaled_top = convert_unit.scaled_value, }	end return result end

local function makeunit(value, options) -- Return table of information for the requested unit and options, or -- return nil if no unit. options = options or {} local unit local ucode = options.u	local percode = options.per if ucode then unit = get_unit(ucode, value, nil, options) elseif percode then unit = { nospace = true, scaled_top = value } else return nil end local text = unit.text or '' local sortkey = unit.sortkey if percode then local function bracketed(code, text) return code:find('[*./]') and '(' .. text .. ')' or text end local perunit = get_unit(percode, 1, unit.scaled_top, options) text = (ucode and bracketed(ucode, text) or '') .. '/' .. bracketed(percode, perunit.text) sortkey = perunit.sortkey end if not unit.nospace then text = ' ' .. text end return { text = text, isangle = unit.isangle, sortkey = sortkey } end

local function delimit(sign, numstr, fmt) -- Return sign and numstr (unsigned digits or '.' only) after formatting. -- Four-digit integers are not formatted with gaps. fmt = (fmt or ''):lower if fmt == 'none' or (fmt == '' and #numstr == 4 and numstr:match('^%d+$')) then return sign .. numstr end -- Group number by integer and decimal parts. -- If there is no decimal part, delimit_groups returns only one table. local ipart, dpart = delimit_groups(numstr) local result if fmt == 'commas' then result = sign .. table.concat(ipart, ',') if dpart then result = result .. '.' .. table.concat(dpart) end else -- Delimit with a small gap by default. local groups = {} groups[1] = table.remove(ipart, 1) for _, v in ipairs(ipart) do table.insert(groups, ' ' .. v .. ' ') end if dpart then table.insert(groups, '.' .. (table.remove(dpart, 1) or '')) for _, v in ipairs(dpart) do table.insert(groups, ' ' .. v .. ' ') end end result = table.concat(groups) -- LATER Is the following needed? --      It is for compatibility with  which uses. result = ' ' .. sign .. result .. ' '	end return result end

local function sup_sub(sup, sub, align) -- Return the same result as Module:Su except val defaults to align=right. if align == 'l' or align == 'left' then align = 'left' elseif align == 'c' or align == 'center' then align = 'center' else align = 'right' end return '' .. sup .. ' ' .. sub .. ' ' end

local function _main(number, uncertainty, unit_spec, misc_tbl) local e_10 = misc_tbl.e	local fmt = misc_tbl.fmt -- Unit. local sortkey local want_sort = not (misc_tbl.sortable == 'off') local sort_value = 1 if want_sort then sort_value = number.value or 1 if e_10.value and sort_value ~= 0 then -- The 'if' avoids $0$ giving an invalid sort_value due to overflow. sort_value = sort_value * 10^e_10.value end end local unit_table = makeunit(sort_value, {						u = unit_spec.u,						want_link = unit_spec.want_link,						per = unit_spec.per,						want_per_link = unit_spec.want_per_link,						want_longscale = unit_spec.want_longscale,						want_sort = want_sort,					}) if unit_table then if want_sort then sortkey = unit_table.sortkey end else unit_table = { text = '' } if want_sort then sortkey = convert_lookup('dummy', sort_value, nil, nil, nil, { want_sort = true }).sortspan end end -- Uncertainty. local unc_text local paren_left, paren_right = ,  local uncU = uncertainty.upper.clean if uncU and number.clean then -- Cannot enter an uncertainty without a preceding number, however, if it were -- possible, the uncertainty should be ignored to avoid displaying junk. local uncL = uncertainty.lower.clean if uncL then uncU = delimit('+', uncU, fmt) .. (uncertainty.upper.errend or '') uncL = delimit('−', uncL, fmt) .. (uncertainty.lower.errend or '') if unit_table.isangle then uncU = uncU .. unit_table.text uncL = uncL .. unit_table.text end unc_text = ' ' .. sup_sub(uncU, uncL, misc_tbl.align) .. ' '		else if uncertainty.upper.parens then unc_text = '(' .. uncU .. ')' -- template does not delimit else unc_text = ' ± ' .. delimit('', uncU, fmt) if e_10.clean then paren_left = '('					paren_right = ')' end end if uncertainty.errend then unc_text = unc_text .. uncertainty.errend end if unit_table.isangle then unc_text = unc_text .. unit_table.text end end end local e_text, n_text if number.clean then n_text = delimit(number.sign, number.clean, fmt) .. (number.nend or '') if not uncertainty.upper.parens and unit_table.isangle then n_text = n_text .. unit_table.text end else n_text = '' if not e_10.clean then e_10.clean = '0' e_10.sign = '' end end if e_10.clean then e_text = '10' .. delimit(e_10.sign, e_10.clean, fmt) .. ''		if number.clean then e_text = ' × ' .. e_text end else e_text = '' end return table.concat({			' ',			sortkey or ,			misc_tbl.prefix or ,			paren_left,			n_text,			unc_text or ,			paren_right,			e_text,			unit_table.isangle and  or unit_table.text,			misc_tbl.suffix or '',			' '		}) end

local function main(frame) data_module = 'Module:Val/units' if string.find(frame:getTitle, 'sandbox', 1, true) then data_module = data_module .. '/sandbox' end local getArgs = require('Module:Arguments').getArgs local args = getArgs(frame, {wrappers = { 'Template:Val', 'Template:Val/sandboxlua' }}) local nocat = args.nocategory local numbers = {} local checks = { -- index, description { 1, 'first parameter' }, { 2, 'second parameter' }, { 3, 'third parameter' }, { 'e', 'exponent parameter (e)' }, }	for _, item in ipairs(checks) do		if not extract_number(item[1], numbers, args) then return valerror(item[2] .. ' is not a valid number.', nocat) end end if args.u and args.ul then return valerror('unit (u) and unit with link (ul) are both specified, only one is allowed.', nocat) end if args.up and args.upl then return valerror('unit per (up) and unit per with link (upl) are both specified, only one is allowed.', nocat) end local number = numbers[1] local uncertainty = { upper = numbers[2], lower = numbers[3], errend = args.errend, }	local unit_spec = { u = args.ul or args.u,			want_link = args.ul ~= nil, per = args.upl or args.up, want_per_link = args.upl ~= nil, want_longscale = (args.longscale or args.long_scale or args['long scale']) == 'on', }	local misc_tbl = { e = numbers.e,			prefix = args.p,			suffix = args.s,			fmt = args.fmt or '', align = args.a,			nocat = args.nocategory, sortable = args.sortable, }	number.nend = args['end'] uncertainty.upper.errend = args['+errend'] uncertainty.lower.errend = args['-errend'] return _main(number, uncertainty, unit_spec, misc_tbl) end

return { main = main, _main = _main }