モジュール:Val/sandbox

-- For Template:Val, output a number and optional unit.
-- Format options include scientific and uncertainty notations.
local data_module = 'Module:Val/units'
local convert_module = 'Module:Convert'
local function valerror(msg, nocat, iswarning)
	-- Return formatted message text for an error or warning.
	-- Can append "#FormattingError" to URL of a page with a problem to find it.
	local anchor = 
	local body, category
	if nocat or mw.title.getCurrentTitle():inNamespaces(1, 2, 3, 5) then
		-- No category in Talk, User, User_talk, or Wikipedia_talk.
		category = 
	else
		category = 
	end
	iswarning = false  -- problems are infrequent so try showing large error so editor will notice
	if iswarning then
		body =  ..
			'warning'
	else
		body =  ..
			'Error in : ' ..
			msg ..
			
	end
	return anchor .. body .. category
end
local range_types = {
	-- No need for ' ' because nowrap applies to all output.
	","   = ", ",
	"by"  = " by ",
	"-"   = "–",
	"–"   = "–",
	"and" = " and ",
	"or"  = " or " ,
	"to"  = " to " ,
	"x"   = " × ",
	"×"   = " × ",
	"/"   = "/",
	"~"   = "〜",
	"〜"   = "〜",
}
local range_repeat_unit = {
	-- WP:UNIT wants unit repeated when a "multiply" range is used.
	"x"   = true,
	"×"   = true,
}
local function extract_item(index, numbers, arg)
	-- Extract an item from arg and store the result in numbersindex.
	-- If no argument or if argument is valid, return nil (no error);
	-- otherwise, return an error message.
	-- The stored result is:
	-- * a table for a number (empty if there was no specified number); or
	-- * a string for range text
	-- Input like 1e3 is regarded as invalid for all except argument 1
	-- which accepts e notation as an alternative to the 'e' argument.
	-- Input commas are removed so 1,234 is the same as 1234.
	local which = index
	local function fail(msg)
		local description
		if which == 'e' then
			description = 'exponent parameter (e)'
		else
			description = 'parameter ' .. which
		end
		return description .. ' ' .. (msg or 'is not a valid number') .. '.'
	end
	local result = {}
	local range = range_typesarg
	if range then
		if type(index) == 'number' and (index % 2 == 0) then
			if index == 2 then
				if numbers1 and numbers1.exp then
					return fail('cannot use a range if the first parameter includes "e"')
				end
				numbers.has_ranges = true
			else
				if not numbers.has_ranges then
					return fail('needs a range in parameter 2')
				end
			end
			numbersindex = range
			if range_repeat_unitarg then
				-- Any "repeat" range forces unit (if any) to be repeated for all items.
				numbers.isrepeat = true
			end
			return nil
		end
		return fail('does not accept a range')
	end
	if numbers.has_ranges and type(index) == 'number' and (index % 2 == 0) then
		return fail('should be a range')
	end
	if index == 'e' then
		local e = numbers1 and numbers1.exp
		if e then
			if arg then
				return fail('cannot be used if the first parameter includes "e"')
			end
			arg = e
			which = 1
		end
	end
	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 a, b = arg:match('^(.+)Ee(.+)$')
		if a then
			if index == 1 then
				arg = a
				result.exp = b
			else
				return fail('cannot use e notation')
			end
		end
		local isnegative, propersign, prefix
		local minus = '−'
		prefix, arg = arg:match('^(.-)(%d.+)$')
		local value = tonumber(arg)
		if not value then
			return fail()
		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 fail()
		end
		result.clean = arg
		result.sign = propersign or 
		result.value = isnegative and -value or value
	end
	numbersindex = result
	return nil  -- no error
end
local function get_args(numbers, args)
	-- Extract arguments and store the results in numbers.
	-- Return nothing (no error) if ok; otherwise, return an error message.
	for index = 1, 99 do
		local which = index
		local arg = argswhich  -- has been trimmed
		if not arg then
			which = 'e'
			arg = argswhich
		end
		local msg = extract_item(which, numbers, arg)
		if msg then
			return msg
		end
		if which == 'e' then
			break
		end
		if index > 19 then
			return 'too many parameters'
		end
	end
	if numbers.has_ranges and (#numbers % 2 == 0) then
		return 'need a number after the last parameter because it is a range.'
	end
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('%s*\n', pos)
		if endline then
			local result = {}
			local n = 0
			local text = definitions:sub(pos + 1, 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
						local link, symbol = item:match('^%%(.+)%%$')
						if link then
							result.symbol = symbol
							result.link = link
							n = 2
						else
							result.symbol = item
						end
					elseif n == 2 then
						result.link = item
					elseif n == 3 then
						result.scale_text = item
						result.scale = get_scale(item, ucode)
					else
						result.more_ignored = item
						break
					end
				end
			end
			if result.si then
				local s = result.symbol
				if ucode == 'mc' .. s or ucode == 'mu' .. s then
					result.ucode = 'µ' .. s  -- unit code for convert should be this
				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(convert_module)._unit
	return lookup(ucode, {
			value = value,
			scaled_top = scaled_top,
			link = want_link,
			si = si,
			sort = options.sortable,
		})
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_convert
	if result then
		if result.alias then
			ucode = result.symbol
			use_convert = true
		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
			ucode = result.ucode or ucode
			si = { result.symbol, result.link }
			use_convert = true
		end
	else
		result = {}
		use_convert = true
	end
	local convert_unit = convert_lookup(ucode, value, scaled_top, want_link, si, options)
	result.sortkey = convert_unit.sortspan
	if use_convert then
		result.text = convert_unit.text
		result.scaled_top = convert_unit.scaled_value
	else
		if want_link then
			result.text =  .. result.symbol .. 
		else
			result.text = result.symbol
		end
		result.scaled_top = 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 or options.nospace) then
		text = ' ' .. text
	end
	return { text = text, isangle = unit.isangle, sortkey = sortkey }
end
local function list_units(mode)
	-- Return wikitext to list the built-in units.
	-- A unit code should not contain wikimarkup so don't bother escaping.
	local data = mw.loadData(data_module)
	local definitions = data.builtin_units .. data.builtin_units_long_scale
	local last_was_blank = true
	local n = 0
	local result = {}
	local function add(line)
		if line ==  then
			last_was_blank = true
		else
			if last_was_blank and n > 0 then
				n = n + 1
				resultn = 
			end
			last_was_blank = false
			n = n + 1
			resultn = line
		end
	end
	local si_prefixes = {
		-- These are the prefixes recognized by convert; u is accepted for micro.
		y = 'y',
		z = 'z',
		a = 'a',
		f = 'f',
		p = 'p',
		n = 'n',
		u = 'µ',
		'µ' = 'µ',
		m = 'm',
		c = 'c',
		d = 'd',
		da = 'da',
		h = 'h',
		k = 'k',
		M = 'M',
		G = 'G',
		T = 'T',
		P = 'P',
		E = 'E',
		Z = 'Z',
		Y = 'Y',
	}
	local function is_valid(ucode, unit)
		if unit and not unit.more_ignored then
			assert(type(unit.symbol) == 'string' and unit.symbol ~= )
			if unit.alias then
				if unit.link or unit.scale_text or unit.si then
					return false
				end
			end
			if unit.si then
				if unit.scale_text then
					return false
				end
				ucode = unit.ucode or ucode
				local base = unit.symbol
				if ucode == base then
					unit.display = base
					return true
				end
				local plen = #ucode - #base
				if plen > 0 then
					local prefix = si_prefixesucode:sub(1, plen)
					if prefix and ucode:sub(plen + 1) == base then
						unit.display = prefix .. base
						return true
					end
				end
			else
				unit.display = unit.symbol
				return true
			end
		end
		return false
	end
	local lookup = require(convert_module)._unit
	local function show_convert(ucode, unit)
		-- If a built-in unit defines a scale or sets the SI flag, any unit defined in
		-- convert is not used (the scale or SI prefix's scale is used for a sort key).
		-- If there is no scale or SI flag, and the unit is not defined in convert,
		-- the sort key may not be correct; this allows such units to be identified.
		if not (unit.si or unit.scale_text) then
			if mode == 'convert' then
				unit.show = not lookup(unit.alias and unit.symbol or ucode).unknown
				unit.show_text = 'CONVERT'
			elseif mode == 'unknown' then
				unit.show = lookup(unit.alias and unit.symbol or ucode).unknown
				unit.show_text = 'UNKNOWN'
			elseif not unit.alias then
				-- Show convert's scale in square brackets ('1' for an unknown unit).
				-- Don't show scale for an alias because it's misleading for temperature
				-- and an alias is probably not useful for anything else.
				local scale = lookup(ucode, {value=1, sort='on'}).scaled_value
				if type(scale) == 'number' then
					scale = string.format('%.5g', scale):gsub('e%+?(%-?)0*(%d+)', 'e%1%2')
				else
					scale = '?'
				end
				unit.show = true
				unit.show_text =  .. scale .. 
			end
		end
	end
	for line in definitions:gmatch('(^\n*)\n') do
		local pos, _ = line:find('  ', 1, true)
		if pos then
			local ucode = line:sub(1, pos - 1)
			local unit = get_builtin_unit(ucode, '\n' .. line .. '\n')
			if is_valid(ucode, unit) then
				show_convert(ucode, unit)
				local flags, text
				if unit.alias then
					text = unit.symbol
				else
					text =  .. unit.display .. 
				end
				if unit.isangle then
					unit.nospace = nil  -- don't show redundant flag
				end
				for _, f in ipairs({
						{ 'alias', 'ALIAS' },
						{ 'isangle', 'ANGLE' },
						{ 'nospace', 'NOSPACE' },
						{ 'si', 'SI' },
						{ 'scale_text', unit.scale_text },
						{ 'show', unit.show_text },
					}) do
					if unitf1 then
						local t = f2
						if t:match('^%u+$') then
							t =  .. t .. 
						end
						if flags then
							flags = flags .. ' ' .. t
						else
							flags = t
						end
					end
				end
				if flags then
					text = text .. ' • ' .. flags
				end
				add(ucode .. ' = ' .. text .. )
			else
				add(line .. ' ◆ invalid definition')
			end
		else
			add(line)
		end
	end
	return table.concat(result, '\n')
end
local delimit_groups = require('Module:Gapnum').groups
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 = {}
		for _, v in ipairs(ipart) do
			groups#groups + 1 = string.gsub(v, '(%d*)(%d)', '%1%2')
		end
		if dpart then
			dpart1 = string.gsub(dpart1, '(%d*)(%d)', '%1%2')
			groups#groups = string.gsub(groups#groups, , )
			groups#groups + 1 = '.' .. (table.remove(dpart, 1) or )
			for _, v in ipairs(dpart) do
				groups#groups + 1 = string.gsub(v, '(%d*)(%d)', '%1%2')
			end
		end
		result = sign .. table.concat(groups)
	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 range_text(items, unit_table, options)
	local fmt = options.fmt
	local nend = items.nend or 
	if items.isrepeat or unit_table.isangle then
		nend = nend .. unit_table.text
	end
	local text = 
	for i = 1, #items do
		if i % 2 == 0 then
			text = text .. itemsi
		else
			text = text .. delimit(itemsi.sign, itemsi.clean, fmt) .. nend
		end
	end
	return text
end
local function uncertainty_text(uncertainty, unit_table, options)
	local angle, text, need_parens
	if unit_table.isangle then
		angle = unit_table.text
	end
	local upper = uncertainty.upper or {}
	local lower = uncertainty.lower or {}
	local uncU = upper.clean
	if uncU then
		local fmt = options.fmt
		local uncL = lower.clean
		if uncL then
			uncU = delimit('+', uncU, fmt) .. (upper.errend or )
			uncL = delimit('−', uncL, fmt) .. (lower.errend or )
			if angle then
				uncU = uncU .. angle
				uncL = uncL .. angle
			end
			text = (angle or ) .. sup_sub(uncU, uncL, options.align) .. 
		else
			if upper.parens then
				text = '(' .. uncU .. ')'  -- old template did not delimit
			else
				text = (angle or ) .. '±' .. delimit(, uncU, fmt)
				need_parens = true
			end
			if uncertainty.errend then
				text = text .. uncertainty.errend
			end
			if angle then
				text = text .. angle
			end
		end
	else
		if angle then
			text = angle
		end
	end
	return text, need_parens
end
local function _main(values, unit_spec, options)
	if options.sandbox then
		data_module = data_module .. '/sandbox'
		convert_module = convert_module .. '/sandbox'
	end
	local action = options.action
	if action then
		if action == 'list' then
			-- Kludge: am using the align parameter (a=xxx) for type of list.
			return list_units(options.align)
		end
		return valerror('invalid action "' .. action .. '".', options.nocat)
	end
	local number = values.number or (values.numbers and values.numbers1) or {}
	local e_10 = options.e or {}
	local novalue = (number.value == nil and e_10.clean == nil)
	local fmt = options.fmt
	local want_sort = true
	local sortable = options.sortable
	if sortable == 'off' or (sortable == nil and novalue) then
		want_sort = false
	elseif sortable == 'debug' then
		-- Same as sortable = 'on' but the sort key is displayed.
	else
		sortable = 'on'
	end
	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 e=1234 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,
						nospace = novalue,
						want_longscale = unit_spec.want_longscale,
						sortable = sortable,
					})
	local sortkey
	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, { sortable = sortable }).sortspan
		end
	end
	local final_unit = unit_table.isangle and  or unit_table.text
	local e_text, n_text, need_parens
	local uncertainty = values.uncertainty
	if uncertainty then
		if number.clean then
			n_text = delimit(number.sign, number.clean, fmt) .. (number.nend or )
			local text
			text, need_parens = uncertainty_text(uncertainty, unit_table, options)
			if text then
				text = string.gsub(text, '(.*)(%d)$', '%1%2')
				if string.match(text, '^%b()$') then
					n_text = string.gsub(n_text, '(.*)(.-)$', '%1%2')
					text = string.gsub(text, '(%p)$', '%1')
				end
				n_text = n_text .. text
			end
		else
			n_text = 
		end
	else
		if values.numbers.isrepeat then
			final_unit = 
		end
		n_text = range_text(values.numbers, unit_table, options)
		need_parens = true
	end
	if e_10.clean then
		if need_parens then
			n_text = '(' .. string.gsub(n_text, '(.*)(.-)$', '%1%2') .. ')'
		end
		e_text = '10' .. delimit(e_10.sign, e_10.clean, fmt) .. 
		if number.clean then
			e_text = '×' .. e_text
		end
	else
		e_text = 
	end
	local result =
		(sortkey or ) ..
		(options.prefix or ) ..
		n_text ..
		e_text ..
		final_unit ..
		(options.suffix or )
	if result ~=  then
		result =  .. string.gsub(result, '(.*)(.-)$', '%1%2') .. 
	end
	return result .. (options.warning or )
end
local function check_parameters(args, has_ranges, nocat)
	-- Return warning text for the first problem parameter found, or nothing if ok.
	local whitelist = {
		a = true,
		action = true,
		debug = true,
		e = true,
		'end' = true,
		errend = true,
		'+errend' = true,
		'-errend' = true,
		fmt = true,
		'long scale' = true,
		long_scale = true,
		longscale = true,
		nocategory = true,
		p = true,
		s = true,
		sortable = true,
		u = true,
		ul = true,
		up = true,
		upl = true,
	}
	for k, v in pairs(args) do
		if type(k) == 'string' and not whitelistk then
			local warning = 'Val parameter "' .. k .. '=' .. v .. '" is not supported'
			return valerror(warning, nocat, true)
		end
	end
	if not has_ranges and args4 then
		return valerror('Val parameter 4 ignored', nocat, true)
	end
end
local function main(frame)
	local getArgs = require('Module:Arguments').getArgs
	local args = getArgs(frame, {wrappers = { 'Template:Val' }})
	local nocat = args.nocategory
	local numbers = {}  -- table of number tables, perhaps with range text
	local msg = get_args(numbers, args)
	if msg then
		return valerror(msg, nocat)
	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 values
	if numbers.has_ranges then
		-- Multiple values with range separators but no uncertainty.
		numbers.nend = args'end'
		values = {
			numbers = numbers,
		}
	else
		-- A single value with optional uncertainty.
		local function setfield(i, dst, src)
			local v = argssrc
			if v then
				if numbersi then
					numbersidst = v
				else
					numbersi = { dst = v }
				end
			end
		end
		setfield(1, 'nend', 'end')
		setfield(2, 'errend', '+errend')
		setfield(3, 'errend', '-errend')
		values = {
			number = numbers1,
			uncertainty = {
				upper = numbers2,
				lower = numbers3,
				errend = args.errend,
			}
		}
	end
	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 options = {
			action = args.action,
			align = args.a,
			e = numbers.e,
			fmt = args.fmt,
			nocat = nocat,
			prefix = args.p,
			sandbox = string.find(frame:getTitle(), 'sandbox', 1, true) ~= nil,
			sortable = args.sortable or (args.debug == 'yes' and 'debug' or nil),
			suffix = args.s,
			warning = check_parameters(args, numbers.has_ranges, nocat),
		}
	return _main(values, unit_spec, options)
end
return { main = main, _main = _main }