Module:Val: Difference between revisions

From Zoophilia Wiki
Jump to navigationJump to search
meta>Johnuniq
major refactor and fixes to match template's output
meta>Johnuniq
refactor number handling and use a loop for validation; handle sortkey (more to do); use long_scale parameter
Line 1: Line 1:
-- TODO put '0' in front of number like '.123'; remove signs from input uncertainties
local getArgs
local delimit_groups = require('Module:Gapnum').groups
local delimit_groups = require('Module:Gapnum').groups


Line 18: Line 16:
end
end


-- true/false whether or not the string is a valid number
local function extract_number(index, numbers, args)
-- ignores parentheses and parity symbolts
-- Extract number from args[index] and store result in numbers[index]
local function validnumber(n)
-- and return true if no argument or if argument is valid.
-- Look for a number that may be surrounded by parentheses or may have +/-
-- The result is a table which is empty if there was no specified number.
n = mw.ustring.match(tostring(n), '^%(?[±%-%+]?([%d\.]+)%)?$')
-- Input like 1e3 is regarded as invalid; should use e=3 parameter.
return tonumber(n) ~= nil
-- 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.]+)$')
if arg:sub(1, 1) == '.' then
arg = '0' .. arg
end
local value = tonumber(arg)
if not value then
return false
end
if prefix == '' or prefix == '±' then
-- Ignore.
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
end


Line 109: Line 141:
end
end


-- TODO: Add other format options
local function delimit(numstr, fmt)
local function delimit(n, fmt)
-- Return numstr (unsigned digits or '.' only) after formatting.
local prefix, num
local result
if not fmt then fmt = '' end
fmt = (fmt or ''):lower()
fmt = fmt:lower()
-- Group number by integer and decimal parts.
-- look for + or - preceding the number
-- If there is no decimal part, delimit_groups returns only one table.
if n:find('[-+]') then
local ipart, dpart = delimit_groups(numstr)
prefix, num = string.match(n, '([-+])([%d.]+)')
else
num = n
end
 
-- integer and decimal parts of number
-- if there is no decimal part, delimit_groups only returns 1 table
local ipart, dpart = delimit_groups(num)
if fmt == 'commas' then
if fmt == 'commas' then
num = table.concat(ipart, ',')
result = table.concat(ipart, ',')
if dpart then
if dpart then
dpart = table.concat(dpart)
result = result .. '.' .. table.concat(dpart)
num = num .. '.' .. dpart
end
end
elseif fmt == 'none' then
elseif fmt == 'none' then
-- TODO Why not use original num?
result = numstr
num = table.concat(ipart)
if dpart then
dpart = table.concat(dpart)
num = num .. '.' .. dpart
end
else
else
-- Delimit with a small gap by default.
-- Delimit with a small gap by default.
num = {}
local groups = {}
num[1] = table.remove(ipart, 1)
groups[1] = table.remove(ipart, 1)
for _, v in ipairs(ipart) do
for _, v in ipairs(ipart) do
table.insert(num, '<span style="margin-left:.25em">' .. v .. '</span>')
table.insert(groups, '<span style="margin-left:.25em">' .. v .. '</span>')
end
end
if dpart then
if dpart then
table.insert(num, '.' .. table.remove(dpart, 1))
table.insert(groups, '.' .. table.remove(dpart, 1))
for _, v in ipairs(dpart) do
for _, v in ipairs(dpart) do
table.insert(num, '<span style="margin-left:.25em">' .. v .. '</span>')
table.insert(groups, '<span style="margin-left:.25em">' .. v .. '</span>')
end
end
end
end
num = table.concat(num)
result = table.concat(groups)
-- LATER Is the following needed?
--      It is for compatibility with {{val}} which uses {{val/delimitnum}}.
result = '<span style="white-space:nowrap">' .. result .. '</span>'
end
end
 
return result
-- add prefix back if it had one
if prefix then
-- change hyphen to proper minus sign
if prefix == '-' then
prefix = '−'
end
num = prefix .. num
end
 
return tostring(num)
end
end


Line 170: Line 181:


-- Unit
-- Unit
local want_sort = not (misc_tbl.sortable == 'off')
local unit_table, sortkey
local unit_table, sortkey
local sort_value = want_sort and ((number.value or 1) * (e_10.value and 10^e_10.value or 1)) or 1
if unit_spec.u then
if unit_spec.u then
unit_table = makeunit(unit_spec.u, {
unit_table = makeunit(unit_spec.u, {
Line 176: Line 189:
per = unit_spec.per,
per = unit_spec.per,
per_link = unit_spec.per_link,
per_link = unit_spec.per_link,
value = (tonumber(number.n) or 1) * (e_10 and 10^e_10 or 1),
longscale = unit_spec.longscale,
longscale = nil, -- TODO set from 'long scale' parameter
value = sort_value,
})
})
sortkey = unit_table.sortkey
if want_sort then
else
sortkey = unit_table.sortkey
sortkey = convert_lookup('dummy', { value = number.n }).sortkey
end
elseif want_sort then
sortkey = convert_lookup('dummy', { value = sort_value }).sortkey
end
if sortkey then
-- TODO convert should return sortkey in span so following is not needed.
sortkey = '<span style="display:none" class="sortkey">' .. sortkey .. '</span>'
end
end


-- Uncertainty
-- Uncertainty
local unc_text
local unc_text
local uncU, uncL = uncertainty.upper, uncertainty.lower -- TODO caller should ensure uncU and uncL have no sign
local uncU = uncertainty.upper.clean
-- The entire number needs to be wrapped in parentheses if:
local uncL = uncertainty.lower.clean
--  the exponent parameter (e) is defined AND
--  no lower uncertainty is defined AND
--  upper uncertainty is defined and contains no parentheses
local paren_wrap = e_10 and (not uncL and (uncU and not uncU:find('%(')))
local paren_uncertainty
if uncU then
if uncU then
if uncL then
if uncL then
local mSu = require('Module:Su')._main  -- sup/sub format
local mSu = require('Module:Su')._main  -- sup/sub format
uncU = '+' .. delimit(uncU, fmt) .. (uncertainty.upperend or '')  
uncU = '+' .. delimit(uncU, fmt) .. (uncertainty.upper.errend or '')
uncL = '−' .. delimit(uncL, fmt) .. (uncertainty.lowerend or '')  
uncL = '−' .. delimit(uncL, fmt) .. (uncertainty.lower.errend or '')
if unit_table and unit_table.isangle then
if unit_table and unit_table.isangle then
uncU = uncU .. unit_table.text
uncU = uncU .. unit_table.text
Line 204: Line 218:
unc_text = '<span style="margin-left:0.3em;">' .. mSu(uncU, uncL) .. '</span>'
unc_text = '<span style="margin-left:0.3em;">' .. mSu(uncU, uncL) .. '</span>'
else
else
-- Look for parentheses surrounding upper uncertainty
if uncertainty.upper.parens then
paren_uncertainty = (uncU:sub(1, 1) == '(' and uncU:sub(-1) == ')')
unc_text = '(' .. uncU .. ')' -- template does not delimit
if paren_uncertainty then
unc_text = '(' .. delimit(uncU:sub(2, -2), fmt) .. ')'
else
else
unc_text = '<span style="margin-left:0.3em;margin-right:0.15em">±</span>' .. delimit(uncU, fmt) .. '</span>'
unc_text = '<span style="margin-left:0.3em;margin-right:0.15em">±</span>' .. delimit(uncU, fmt)
end
end
if uncertainty.errend then
if uncertainty.errend then
Line 220: Line 232:
end
end
local e_text, n_text
local e_text, n_text
if number.n then
if number.clean then
n_text = delimit(number.n, fmt) .. (number.nend  or '')
n_text = number.sign .. delimit(number.clean, fmt) .. (number.nend  or '')
if not paren_uncertainty and unit_table and unit_table.isangle then
if not uncertainty.upper.parens and unit_table and unit_table.isangle then
n_text = n_text .. unit_table.text
n_text = n_text .. unit_table.text
end
end
else
else
n_text = ''
n_text = ''
e_10 = e_10 or '0'
if not e_10.clean then
e_10.clean = '0'
e_10.sign = ''
end
end
end
if e_10 then
if e_10.clean then
e_text = '10<sup>' .. delimit(e_10) .. '</sup>'
e_text = '10<sup>' .. e_10.sign .. delimit(e_10.clean, fmt) .. '</sup>'
if number.n then
if number.clean then
e_text = '<span style="margin-left:0.25em;margin-right:0.15em">×</span>' .. e_text
e_text = '<span style="margin-left:0.25em;margin-right:0.15em">×</span>' .. e_text
end
end
Line 237: Line 252:
e_text = ''
e_text = ''
end
end
local paren_wrap = e_10.clean and uncU and not uncertainty.upper.parens and not uncL  -- TODO should this be before e_10.clean = '0' above?
return table.concat({
return table.concat({
'<span class="nowrap">',
'<span class="nowrap">',
sortkey or '',
misc_tbl.prefix or '',
misc_tbl.prefix or '',
paren_wrap and '(' or '',
paren_wrap and '(' or '',
Line 252: Line 269:


local function main(frame)
local function main(frame)
if not getArgs then
local getArgs = require('Module:Arguments').getArgs
getArgs = require('Module:Arguments').getArgs
end
local args = getArgs(frame, {wrappers = { 'Template:Val', 'Template:Val/sandboxlua' }})
local args = getArgs(frame, {wrappers = { 'Template:Val', 'Template:Val/sandboxlua' }})
local number = {n=args[1], nend=args['end']}
local nocat = args.nocategory
local nocat = args.nocategory
 
local numbers = {}
-- Error checking
local checks = {
if args[1] and not validnumber(args[1]) then
-- index, description
return valerror('first argument is not a valid number.',nocat)
{ 1, 'first parameter' },
end
{ 2, 'second parameter' },
if args[2] and not validnumber(args[2]) then
{ 3, 'third parameter' },
return valerror('second argument is not a valid number.',nocat)
{ 'e', 'exponent parameter (<b>e</b>)' },
end
}
if args[3] and not validnumber(args[3]) then
for _, item in ipairs(checks) do
return valerror('third argument is not a valid number.',nocat)
if not extract_number(item[1], numbers, args) then
end
return valerror(item[2] .. ' is not a valid number.', nocat)
if args.e and not validnumber(args.e) then
end
return valerror('exponent argument (<b>e</b>) is not a valid number.',nocat)
end
end
if args.u and args.ul then
if args.u and args.ul then
return valerror('unit (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.',nocat)
return valerror('unit (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.', nocat)
end
end
if args.up and args.upl then
if args.up and args.upl then
return valerror('unit per (<b>up</b>) and unit per with link (<b>upl</b>) are both specified, only one is allowed.',nocat)
return valerror('unit per (<b>up</b>) and unit per with link (<b>upl</b>) are both specified, only one is allowed.', nocat)
end
end
 
local number = numbers[1]
-- Group arguments into related categories and unpack when needed
local uncertainty = {
local uncertainty = {
upper = args[2],
upper = numbers[2],
lower = args[3],
lower = numbers[3],
errend = args.errend,
errend = args.errend,
upperend = args['+errend'],
lowerend = args['-errend'],
}
}
local unit_spec = {
local unit_spec = {
Line 292: Line 302:
per = args.upl or args.up,
per = args.upl or args.up,
per_link = args.upl ~= nil,
per_link = args.upl ~= nil,
longscale = (args.longscale or args.long_scale or args['long scale']) == 'on',
}
}
local misc_tbl = {
local misc_tbl = {
e = args.e,
e = numbers.e,
prefix = args.p,
prefix = args.p,
suffix = args.s,
suffix = args.s,
fmt = args.fmt or '',
fmt = args.fmt or '',
nocat = args.nocategory,
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)
return _main(number, uncertainty, unit_spec, misc_tbl)
end
end


return { main = main, _main = _main }
return { main = main, _main = _main }

Revision as of 11:01, 15 July 2015

Documentation for this module may be created at Module:Val/doc

local delimit_groups = require('Module:Gapnum').groups

-- Specific message for {{Val}} errors
local function valerror(msg, nocat)
	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('[[Category:Pages with incorrect formatting templates use]]')
	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.]+)$')
		if arg:sub(1, 1) == '.' then
			arg = '0' .. arg
		end
		local value = tonumber(arg)
		if not value then
			return false
		end
		if prefix == '' or prefix == '±' then
			-- Ignore.
		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_builtin_unit(unitcode, 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' .. unitcode .. '  ', 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)
			for item in (text .. '  '):gmatch('(%S[^\n]-)%s%s') do
				if item == 'NOSPACE' then
					result.nospace = true
				elseif item == 'ANGLE' then
					result.isangle = true
					result.nospace = true
				else
					n = n + 1
					if n == 1 then
						result.symbol = item
					elseif n == 2 then
						result.link = item
					else
						break
					end
				end
			end
			if n == 2 then
				return result
			end
			-- Ignore invalid definition, treating it as a comment.
		end
	end
end

local function convert_lookup(ucode, options)
	local lookup = require('Module:Convert/sandbox')._unit
	return lookup(ucode, { value = options.value, link = options.want_link })
end

local function get_unit(ucode, value, want_link, want_longscale)
	local data = mw.loadData('Module:Val/units')
	local result = want_longscale and
		get_builtin_unit(ucode, data.builtin_units_long_scale) or
		get_builtin_unit(ucode, data.builtin_units)
	local convert_unit = convert_lookup(ucode, { value = value, link = want_link })
	if result then
		-- Have: result.symbol + result.link + result.isangle + result.nospace
		if want_link then
			result.text = '[[' .. result.link .. '|' .. result.symbol .. ']]'
		else
			result.text = result.symbol
		end
		result.sortkey = convert_unit.sortkey
	else
		result = {
			text = convert_unit.text,
			sortkey = convert_unit.sortkey,
		}
	end
	return result
end

local function makeunit(ucode, options)
	-- Return wikitext, sortkey for the requested unit and options.
	-- TODO The sortkey does not account for any per unit.
	local function bracketed(ucode, text)
		return ucode:find('[*./]') and '(' .. text .. ')' or text
	end
	options = options or {}
	local unit = get_unit(ucode, options.value, options.link, options.longscale)
	local text = unit.text
	local percode = options.per
	if percode then
		local perunit = get_unit(percode, 0, options.per_link, options.longscale)
		text = bracketed(ucode, text) .. '/' .. bracketed(percode, perunit.text)
	end
	if not unit.nospace then
		text = '&nbsp;' .. text
	end
	return { text = text, isangle = unit.isangle, sortkey = unit.sortkey }
end

local function delimit(numstr, fmt)
	-- Return numstr (unsigned digits or '.' only) after formatting.
	local result
	fmt = (fmt or ''):lower()
	-- 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)
	if fmt == 'commas' then
		result = table.concat(ipart, ',')
		if dpart then
			result = result .. '.' .. table.concat(dpart)
		end
	elseif fmt == 'none' then
		result = numstr
	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, '<span style="margin-left:.25em">' .. v .. '</span>')
		end
		if dpart then
			table.insert(groups, '.' .. table.remove(dpart, 1))
			for _, v in ipairs(dpart) do
				table.insert(groups, '<span style="margin-left:.25em">' .. v .. '</span>')
			end
		end
		result = table.concat(groups)
		-- LATER Is the following needed?
		--       It is for compatibility with {{val}} which uses {{val/delimitnum}}.
		result = '<span style="white-space:nowrap">' .. result .. '</span>'
	end
	return result
end

local function _main(number, uncertainty, unit_spec, misc_tbl)
	local e_10 = misc_tbl.e
	local fmt = misc_tbl.fmt

	-- Unit
	local want_sort = not (misc_tbl.sortable == 'off')
	local unit_table, sortkey
	local sort_value = want_sort and ((number.value or 1) * (e_10.value and 10^e_10.value or 1)) or 1
	if unit_spec.u then
		unit_table = makeunit(unit_spec.u, {
						link = unit_spec.link,
						per = unit_spec.per,
						per_link = unit_spec.per_link,
						longscale = unit_spec.longscale,
						value = sort_value,
					})
		if want_sort then
			sortkey = unit_table.sortkey
		end
	elseif want_sort then
		sortkey = convert_lookup('dummy', { value = sort_value }).sortkey
	end
	if sortkey then
		-- TODO convert should return sortkey in span so following is not needed.
		sortkey = '<span style="display:none" class="sortkey">' .. sortkey .. '</span>'
	end

	-- Uncertainty
	local unc_text
	local uncU = uncertainty.upper.clean
	local uncL = uncertainty.lower.clean
	if uncU then
		if uncL then
			local mSu = require('Module:Su')._main  -- sup/sub format
			uncU = '+' .. delimit(uncU, fmt) .. (uncertainty.upper.errend or '')
			uncL = '−' .. delimit(uncL, fmt) .. (uncertainty.lower.errend or '')
			if unit_table and unit_table.isangle then
				uncU = uncU .. unit_table.text
				uncL = uncL .. unit_table.text
			end
			unc_text = '<span style="margin-left:0.3em;">' .. mSu(uncU, uncL) .. '</span>'
		else
			if uncertainty.upper.parens then
				unc_text = '(' .. uncU .. ')'  -- template does not delimit
			else
				unc_text = '<span style="margin-left:0.3em;margin-right:0.15em">±</span>' .. delimit(uncU, fmt)
			end
			if uncertainty.errend then
				unc_text = unc_text .. uncertainty.errend
			end
			if unit_table and 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 = number.sign .. delimit(number.clean, fmt) .. (number.nend  or '')
		if not uncertainty.upper.parens and unit_table 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<sup>' .. e_10.sign .. delimit(e_10.clean, fmt) .. '</sup>'
		if number.clean then
			e_text = '<span style="margin-left:0.25em;margin-right:0.15em">×</span>' .. e_text
		end
	else
		e_text = ''
	end
	local paren_wrap = e_10.clean and uncU and not uncertainty.upper.parens and not uncL  -- TODO should this be before e_10.clean = '0' above?
	return table.concat({
			'<span class="nowrap">',
			sortkey or '',
			misc_tbl.prefix or '',
			paren_wrap and '(' or '',
			n_text,
			unc_text or '',
			paren_wrap and ')' or '',
			e_text,
			(unit_table and not unit_table.isangle) and unit_table.text or '',
			misc_tbl.suffix or '',
			'</span>'
		})
end

local function main(frame)
	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 (<b>e</b>)' },
	}
	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 (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.', nocat)
	end
	if args.up and args.upl then
		return valerror('unit per (<b>up</b>) and unit per with link (<b>upl</b>) 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,
			link = args.ul ~= nil,
			per = args.upl or args.up,
			per_link = args.upl ~= nil,
			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 '',
			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 }