Module:Unité

De Lagny-sur-Marne Wiki
Révision datée du 7 mai 2017 à 09:46 par Zebulon84 (discussion) (correction bugs avec params + et -)
Aller à : navigation, rechercher

La documentation pour ce module peut être créée à Module:Unité/doc

local p = {}

-- local Delink = require( 'Module:Delink' ) -- chargé uniquement si nécessaire

-- Chargement de la base de données des nom d'unités avec gestion d'erreur.
local moduleData = 'Module:Unité/Data'
local dataSuccess, Data = pcall ( mw.loadData, moduleData )
if dataSuccess and type( Data ) == 'table' then
	dataSuccess = type( Data.unit ) == 'table' 
		and type( Data.prefix ) == 'table'
		and type( Data.exposant ) == 'table'
end

--- Copie de Outils.trim acceptant les nombres.
local function trim( texte )
	if type( texte ) == 'string' then
		texte = texte:gsub( '^%s*(.*)%f[%s]%s*$', '%1' )
		if texte ~= '' then
			return texte
		end
	elseif type( texte ) == 'number' then
		return tostring( texte )
	end
end

function p.sanitizeNum( nombre )
	if type( nombre ) == 'number' then
		return tostring( nombre )
	elseif trim( nombre ) then
		local result = nombre
			-- trim
			:gsub( '^%s*(.*)%f[%s]%s*$', '%1' )
			-- remplacement des signes moins ou demi-cadratin par un tiret
			:gsub( '%−%f[%d]', '-')  -- U+2212
			:gsub( '−%f[%d]', '-')  -- html −
			:gsub( '\226\128[\146\147]%f[%d]', '-') -- U+2212, U+2213 (tiret numérique et demi-cadratin)
			-- remplacement des espaces insécable par des espace simple
			:gsub( '\194\160', ' ' )
		return result
	else 
		return ''
	end
end

---
-- parseNum transforme si possible une chaine formatée en un chaine interprétable par tonumber()
-- retourne une chaine pour éviter les arrondi éventuels de lua.
-- si "nombre" est une chaine non reconnue comme un nombre par la fonction, retourne "nombre".
-- si "nombre" n'est pas un number ou une chaine retourne une chaine vide.
function p.parseNombre( nombre )
	local result
	if type( nombre ) == 'number' then
		return tostring( nombre )
	else
		-- remplacement des signes moins ou demi-cadratin par un tiret
		result = p.sanitizeNum( nombre )
		if result == '' then
			return ''
		elseif not result:match( '^%-?[%d., ]*%d$' ) then
			return nombre
		end		
	end
	
	-- suppression espaces
	result = result:gsub( ' ', '' )
	
	-- gestion des points et des virgules
	if result:match( '[.,]' ) then
		if result:match( '%d%.%d%d%d%.%d%' ) then
			-- type 12.345.678
			result = result:gsub( '%.', '' ):gsub( ',', '.' )
		elseif result:match( '%d,%d%d%d,%d' ) -- type 1,234,567 ou 1.234,567,8
			or result:match( '%d,%d%d%d%.%d' )  -- format anglo-saxon type 1,234.5  
			or result:match( '%d%.%d%d%d,%d' ) -- type 1.123,56 (utilisé en exemple pour sépararer les décimales avec l'ancien modèle unité ou formatnum)
		then
			result = result:gsub( ',', '' )
		else
			result = result:gsub( ',', '.' )
		end
	end
	
	return result
end

---
-- _formantNum transforme un nombre ou une chaine représentant un nombre en chaine formatée suivant les conventions du français
-- si le paramètre ne représente pas un nombre lua il est retourné sans modification
function p._formatNum( num )
	if type( num ) == 'number' then
		num = tostring( num )
	elseif type( num ) ~= 'string' or num == '' then
		return num
	end
	
	local moins, entier, fraction = num:match( '^(%-?)(%d*)%.?(%d*)$' )
	if not entier then
		return num
	end
	
	if moins == '-' then
		moins = '−' -- signe moins (U+2212)
	end
	
	if entier == '' then
		entier = '0'
	elseif entier:len() > 3 then
		local ini = math.fmod( entier:len() - 1, 3 ) + 1
		entier = ( entier:sub( 1, ini ) or '') .. entier:sub( ini + 1 ):gsub( '(%d%d%d)', '\194\160%1' )
	end
	if fraction ~= '' then
		fraction = ',' .. fraction:gsub( '(%d%d%d)', '%1\194\160' ):gsub( '\194\160$', '' )
	end
	
	return moins .. entier .. fraction
end

---
-- formatNum transforme les nombres d'une chaine en chaine formatée suivant les conventions du français.
-- Le nombre fourni doit un de type number ou chaine équivalente :
-- pas de séparateur de millier, point comme séparamèteur décimal, tiret comme signe moins.
-- Équivalent de FormatNum, mais avec vrai signe moins et séparateur de millier en partie décimale.
function p.formatNum( num )
	if type( num ) == 'number' then
		return p._formatNum( num )
	elseif type( num ) == 'string' then
		return num:gsub( '%-?%d*%.?%d+', p.formatNombre )
	else
		return ''
	end
end

---
-- formatNombre transforme un nombre formaté ou non en chaine formatée suivant les convention du français.
-- si la chaine n'est pas reconnu comme un nombre, elle n'est pas modifiée.
function p.formatNombre( nombre )
	return p._formatNum( p.parseNombre( nombre ) )
end

--- formatNombres transforme tous les nombres d'une chaine en nombre formaté suivant les conventions du français.
function p.formatNombres( texte )
	if type( texte ) == 'number' then
		return p.formatNum( texte )
	elseif type( texte ) == 'string' then
		return texte:gsub( '%-?%f[%d.,][%d., ]+%f[%D]', p.formatNombre )
	else 
		return ''
	end
end

function p.parseUnit( texte )
	local toParse = p.sanitizeNum( texte )
	if toParse ~= '' then
		local result
		local specificArgs = {
			['à'] = 'à',
			et = 'et',
			ou = 'ou',
			['–'] = '–', -- demi cadratin
			['±'] = '±', ['+-'] = '±', ['+/-'] = '±',
			['+'] = '+',
			['−'] = '−', ['-'] = '−', -- signe moins et tiret
			['x'] = 'x', x = 'x', ['*'] = 'x',
		}
		
		-- valeur numérique
		local match, capture = toParse:match( '^((%-?%f[%d.,][%d., ]*%d%f[%D])%s*)' )
		if match then
			toParse = toParse:sub( match:len() + 1 )
		end
		result = { capture or false }
		
		-- lien avec un deuxième nombre
		local match, conj, num = mw.ustring.match( toParse, '^(([àetouM+/−x*×±–-]+) ?(%-?%f[%d.,][%d., ]*%d%f[%D])%s*)' )
		if match and specificArgs[ conj ] then
			result[ specificArgs[ conj ] ] = num
			toParse = toParse:sub( match:len() + 1 )
		end
		if result['+'] or result['x'] then
			match, conj, num = mw.ustring.match( toParse, '^(([x*×−-]) ?(%-?%f[%d.,][%d., ]*%d%f[%D])%s*)' )
			if match then
				if specificArgs[ conj ] == '×' then
					result['xx'] = num
				else
					result['−'] = num
				end
				toParse = toParse:sub( match:len() + 1 )
			end
		end
		
		-- 10 exposant   ( \195\151 = ×, signe multiplié)
		match, capture = toParse:match( '^(%s*e(%-?%d+)%s*)' )
		if not match then
			match, capture = toParse:match( '^(%s*[x\195]\151?10e(%-?%d+)%s*)' )
		end
		if match then
			result.e = capture
			toParse = toParse:sub( match:len() + 1 )
		end
		
		-- unités
		toParse = toParse:gsub( '⋅', '.' ):gsub( '²', '2' ):gsub( '³', '3' )
		local unit, exp
		local unitRegex = '^(%.?(/?[%aà°±]+) ?(%-?%d*)%s*)'
		match, unit, exp = mw.ustring.match( toParse, unitRegex )
		while match do -- and unit:len() < 5
			table.insert( result, unit )
			table.insert( result, exp )
			toParse = toParse:sub( match:len() + 1 )
			match, unit, exp = mw.ustring.match( toParse, unitRegex )
		end
		if toParse == '' then
			return result
		else
			-- une partie de la chaine n'a pas pu être décodée, on retourne la chaine originale
			return { texte }
		end
	else
		return { }
	end
end

---
-- nomUtnit retourne le nom français du code d'une unité et de son exposant.
-- si le code de l'unité n'est pas reconnu retourne 1 et false, de façon à ajouter false en première position d'une table. 
function p.nomUnit( unit, exposant )
	if not dataSuccess or type( unit ) ~= 'string' then
		return 1, false
	end
	
	-- nettoyage des liens et balise HTML
	unit = unit:gsub( '^/' , '' )
	if unit:match( '%[' ) then
		local Delink = require( 'Module:Delink' )
		unit = Delink._delink{ unit }
	end
	if unit:match( '<' ) then
		unit = unit:gsub( '%b<>', '' )
	end
	
	-- récupère le nom de l'unité
	local unitTab = Data.unit[ unit ]
	local unitPrefix = { nom = '' }
	if not unitTab then
		unitTab = Data.unit[ unit:sub( 2 ) ]
		unitPrefix = Data.prefix[ unit:sub( 1, 1 ) ]
		if not ( unitTab and unitPrefix ) then
			-- pour µ, Ki, Mi, Gi... qui sont codé sur deux octets
			unitTab = Data.unit[ unit:sub( 3 ) ]
			unitPrefix = Data.prefix[ unit:sub( 1, 2 ) ]
			if not ( unitTab and unitPrefix ) then
				unitTab = false
			end
		end
end

	-- récupère le nom de l'exposant
	if trim( exposant ) then 
		local exp = tonumber( exposant )
		exp = exp and Data.exposant[ math.abs( exp ) ]
		exposant = exp or ' puissance ' .. exposant
	else 
		exposant = ''
	end
	
	-- assemble les deux partie
	if type( unitTab ) == 'table' and type( unitTab.nom ) == 'string' then
		return unitPrefix.nom .. unitTab.nom .. exposant 
	elseif unit:match( '[/%d]' ) then
		-- ce n'est pas du texte simple, on anule l'infobule
		return 1, false
	else
		return unit .. exposant
	end
end

function p._unite( args )
	-- remplacement de certains caractères, pour simplifier les pattern
	local nombre = p.sanitizeNum( args[1] )
	if nombre == '' then
		nombre = nil
	else	
		-- formatage du nombre
		nombre, nbNombre = nombre:gsub( '%-?%f[%d.,][%d., ]+%f[%D]', p.formatNombre )
	end
	
	local wiki = { nombre }
	
	-- à, et, ou, ×, – (tiret cadratin)
	local specificArgs = { '–', 'à', 'et', 'ou', 'x', 'xx', '±'	}
	for _, name in ipairs( specificArgs ) do
		local v = trim( args[ name ] )
		if v then
			v = p.sanitizeNum( v ):gsub( '%-?%f[%d.,][%d., ]+%f[%D]', p.formatNombre )
			if name == '–' and nombre and nombre:match( '^[^−]' ) and v:match( '^[^−]' ) then
				-- pas d'espace pour le tiret cadratin entre deux nombres positifs
				table.insert( wiki, '–' .. v )
			else
				table.insert( wiki, ' ' .. name .. ' ' .. v )
			end
		end
	end
	
	-- incertitude avec + et − séparés
	if trim( args['+'] ) then
		local approximation = '+' .. p.formatNombre( args['+'] ) .. ''
		if trim( args['−'] ) then
			approximation = approximation .. '<br> −' .. p.formatNombre( args['−'] )
		end
		table.insert( wiki, '<span style="display:inline-block; vertical-align:top; line-height:1em; font-size:80%; text-align:left;">' )
		table.insert( wiki, approximation .. '</span>' )
	end
	
	-- puissance de 10
	local exposant = trim( args.e )
	if exposant then
		exposant = p.formatNombre( exposant )
		if nombre then
			if trim( args['±'] ) and not nombre:match( '^%(' ) then
				table.insert( wiki, 1, '(' )
				table.insert( wiki, ')' )
			end
			table.insert( wiki, ' × 10<sup>' .. exposant .. '</sup>' )
		else
			table.insert( wiki, '10<sup>' .. exposant .. '</sup>' )
		end
	end
	
	-- unités
	local i = 1
	local unit = trim( args[ 2 * i ] )
	local units = ''
	local nomUnits, par = {}, false
	while unit do
		local exp = p.parseNombre( args[ 2 * i + 1 ] )
		local sep = ''
		if exp == '' then
			if unit:sub( -2 ) == '²' then
				exp = '2'
				unit = unit:sub( 1, -3 )
			elseif unit:sub( -2 ) == '³' then
				exp = '3'
				unit = unit:sub( 1, -3 )
			end
		end
		if units ~= '' then
			if unit:sub( 1, 1 ) == '/' then
				if not par then
					par = true
					table.insert( nomUnits, 'par' )
				end
			else
				sep = '\194\160'  -- point médian désactivé : '⋅\194\160'  
				if exp:match( '^-' ) and not par then
					par = true
					table.insert( nomUnits, 'par' )
				end
			end
		end
		local expUnit = ''
		if #exp > 0 then
			expUnit = '<sup>' .. exp:gsub( '^-', '−') .. '</sup>'  -- remplace le tiret par un vrai signe moins
		end
		units = units .. sep .. unit .. expUnit
		table.insert( nomUnits, p.nomUnit( unit, exp ) )
		i = i + 1
		unit = trim( args[ 2 * i ] )
	end
	
	-- ajoute une abbréviation si le nom de l'unité est différent de l'unité (en retirant les espace qui peuvent être devenus insécables)
	if units and nomUnits[1] and mw.ustring.gsub( table.concat( nomUnits ), '[ \194\160]', '' ) ~= mw.ustring.gsub( units, '[ \194\160]', '' ) then
		units = string.format( ' <abbr class=abbr title="%s">%s</abbr>', table.concat( nomUnits, ' ' ), units )
	elseif units and units ~= '°' then
		units = ' ' .. units
	end
	table.insert( wiki, units )
	
	if #wiki > 0 then
		local result = table.concat( wiki )
		if result:match( ' ' ) then
			return '<span class="nowrap">' .. result .. '</span>'
		else
			return result
		end
	end
end

function p.unite( frame )
	local args
	if type( frame ) == 'table' then
		if type( frame.getParent ) == 'function' then
			args = frame:getParent().args;
		else
			args = frame
		end
	end
	if args then
		if not ( args.e or args.et or args['à'] or args['±'] ) and trim( args[1] ) then
			if args[1]:match('[^%d,. -]') then
				if args[2] then
					local tempArgs = p.parseUnit( trim( args[1] ) )
					if not tempArgs[2] then
						for k, v in pairs( tempArgs ) do
							args[k] = v
						end
					end
				else
					args = p.parseUnit( trim( args[1] ) )
				end
			end
			if args[2] and not args[3] and args[2]:match('[%d./ -]') then
				local tempArgs = p.parseUnit( trim( args[2] ) )
				if tempArgs[1] == false then
					tempArgs[1] = args[1]
				else
					table.insert( tempArgs, 1, args[1] )
				end
				args = tempArgs
			end
		end
		-- args alias
		args['x'] = args['×'] -- lettre x → signe multiplié
		if args['+'] then
			args['−'] = args['−'] or args['-'] -- tiret → signe moins
		else
			args['–'] = args['–'] or args['-'] -- tiret → demi-cadratin
		end
		return p._unite( args )
	end
end

return p