Aller au contenu

Module:Unité

Cette page est protégée.
Une page de Wikipédia, l'encyclopédie libre.

 Documentation[voir] [modifier] [historique] [purger]

Ce module est principalement destiné à implémenter le modèle {{Unité}}.

Utilisation

Fonctions exportables

  • unite( frame ) – implémente le modèle unité. Les paramètres sont pris soit au niveau du modèle appelant le module via #invoke, soit directement dans la table fournie lorsque la fonction est appelée depuis un autre module. Essaye de parser les deux premiers paramètres pour facilité la saisie (par exemple fonction avec p.unite{ '1.23 ±0.05 e5 m/s-2' }) ;
  • _unite( args ) – affiche l'unité à partir des paramètres classiques du modèle Unité (exemple p._unite{ '1.23', 'm', '/s', '-2', ['±'] = '0.05', e='5' }) ;
  • formatNombres( texte ) – formate tous les nombres de la chaine fournie suivant les conventions du français ;
  • formatNombre( nombre ) – transforme un nombre formaté ou non en chaine formatée suivant les conventions du français ; si la chaine n'est pas reconnue comme un nombre, elle n'est pas modifiée ;
  • _formatNum( num ) – transforme un number, ou une chaine correspondant à un number en chaine formatée suivant les conventions du français ; si le paramètre ne représente pas un number lua il est retourné sans modification ;
  • parseNombre( nombre ) – transforme si possible une chaine formatée en un chaine interprétable par tonumber() (retourne une chaine pour éviter les arrondis éventuels de lua) ; les chaines non reconnues sont retournées sans modification.

Autres fonctions

  • sanitizeNum( nombre ) – transforme les signes moins en tiret, les espaces insécables en espace simple (simplifie les pattern ultérieures) ;
  • parseUnit( texte ) – essaye de séparer une chaine en différents paramètres du modèle unité ;
  • nomUnit( unit, exposant ) – retourne si possible le nom de l'unité et son exposant en toute lettre.

Modules externes et autres éléments dont ce module a besoin pour fonctionner

  • Module:Unité/Data – Liste d'unités et de multiples, avec leur abréviation et leur nom en toute lettre.
  • Module:Delink – Utilisé pour supprimer les liens des unités pour essayer de les reconnaitre.

Exemples

Pour des exemples, voir la page de test permettant de tester diverses modifications apportées.

Voir aussi : les tests unitaires et ceux du bac à sable.

-- luacheck: globals mw, no max line length

local p = {}

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

-- Chargement de la base de données des noms 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

local errorCat = '[[Catégorie:Page incorrectement traitée par le Module:Unité]]'
local addErrorCat = false

local supUnicode = { ['0'] = '⁰', ['1'] = '¹', ['2'] = '²', ['3'] = '³', ['4'] = '⁴', ['5'] = '⁵', ['6'] = '⁶', ['7'] = '⁷', ['8'] = '⁸', ['9'] = '⁹',
	['+'] = '⁺', ['-'] = '⁻', ['='] = '⁼', ['('] = '⁽', [')'] = '⁾', ['n'] = 'ⁿ' }
local subUnicode = { ['0'] = '₀', ['1'] = '₁', ['2'] = '₂', ['3'] = '₃', ['4'] = '₄', ['5'] = '₅', ['6'] = '₆', ['7'] = '₇', ['8'] = '₈', ['9'] = '₉',
	['a'] = 'ₐ', ['e'] = 'ₑ', ['o'] = 'ₒ', ['x'] = 'ₓ', ['h'] = 'ₕ', ['k'] = 'ₖ', ['l'] = 'ₗ',
	['m'] = 'ₘ', ['n'] = 'ₙ', ['p'] = 'ₚ', ['s'] = 'ₛ', ['t'] = 'ₜ',
	}
local fractionUnicode = { ['½'] = '1/2', ['⅓'] = '1/3', ['⅕'] = '1/5', ['⅙'] = '1/6', ['⅛'] = '1/8',
	['⅔'] = '2/3', ['⅖'] = '2/5', ['⅚'] = '5/6', ['⅜'] = '3/8', ['¾'] = '3/4', ['⅗'] = '3/5',
	['⅝'] = '5/8', ['⅞'] = '7/8', ['⅘'] = '4/5', ['¼'] = '1/4', ['⅐'] = '1/7', ['⅑'] = '1/9', ['⅒'] = '1/10', ['↉'] = '0/3',
}
local nbsp = '\194\160'        -- espace insécable
local nnbsp = '\226\128\175'   -- espace fine insécable

local mwTrim = mw.text.trim

--- Copie de Outils.trim acceptant les nombres.
local function trim( texte )
	if type( texte ) == 'string' then
		texte = mwTrim( texte )
		if texte ~= '' then
			return texte
		end
	elseif type( texte ) == 'number' then
		return tostring( texte )
	end
end

-- retire les chiffres des strip markers
local function escapeStripMarkers( input )
	return input:gsub( '(UNIQ%-%-%a+%-)(%x%x%x%x%x%x%x%x)(%-QINU)', function ( leading, hexdigits, trailing )
		local escapeddigits = hexdigits:gsub( '%d', {
			['0'] = 'g', ['1'] = 'h', ['2'] = 'i', ['3'] = 'j', ['4'] = 'k',
			['5'] = 'l', ['6'] = 'm', ['7'] = 'n', ['8'] = 'o', ['9'] = 'p',
		} )
		return leading .. escapeddigits .. trailing
	end )
end

-- restaure les strip markers
local function restoreStripMarkers( input )
	return input:gsub( '(UNIQ%-%-%a+%-)(%a%a%a%a%a%a%a%a)(%-QINU)', function ( leading, escapeddigits, trailing )
		local hexdigits = escapeddigits:gsub( '%a', {
			['g'] = '0', ['h'] = '1', ['i'] = '2', ['j'] = '3', ['k'] = '4',
			['l'] = '5', ['m'] = '6', ['n'] = '7', ['o'] = '8', ['p'] = '9',
		} )
		return leading .. hexdigits .. trailing
	end )
end

-- remplacement de certains caractères, pour simplifier les patterns
function p.sanitizeNum( nombre )
	if type( nombre ) == 'number' then
		return tostring( nombre )
	elseif type( nombre ) == 'string' then
		if nombre:match( '^%-?[%d.,]+$' ) then
			return nombre
		end
		local result = nombre
			-- remplacement des signes moins par un tiret
			:gsub( '%−%f[%d]', '-' )  -- U+2212
			:gsub( '−%f[%d]', '-' )  -- html −
			-- remplacement des espaces insécables par des espaces simples
			:gsub( nbsp, ' ' )
			:gsub( ' ', ' ' )
			:gsub( ' ', ' ' )
			:gsub( nnbsp, ' ' )
			:gsub( ' ', ' ' )
			:gsub( ' ', ' ' )
			:gsub( '\226\128[\132-\138]', ' ' ) -- U+2004 à U+200A
			:gsub( ' ', ' ' )
		-- trim en dernier, après avoir normalisé les espaces
		return mwTrim( result )
	else
		return ''
	end
end

---
-- parseNum transforme si possible une chaine formatée en une chaine interprétable par tonumber()
-- retourne une chaine pour éviter les arrondis é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 ''
		end
		-- si nombre est un chiffre en exposant ou indice comme ², retourne ce chiffre
		for i = 0, 9 do
			local is = tostring(i)
			if result == supUnicode[ is ] or result == subUnicode[ is ] then
				return is
			end
		end
		if not result:match( '^%-?[%d., ]*%d$' ) and not result:match( '^%-?[%d., ]*%d ?e[+-]?%d+$' ) then
			return nombre
		end
	end

	-- suppression espaces
	result = result:gsub( ' ', '' )

	-- gestion des points et des virgules
	if result:find( '.', nil, true ) or result:find( ',', nil, true ) 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éparer les décimales avec l'ancien modèle unité ou formatnum)
		then
			result = result:gsub( ',', '' )
		else
			result = result:gsub( ',', '.' )
		end
	end

	return result
end

---
-- _formatNum 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
-- Le paramètre peut être transmis sous forme de table pour ajouter des options :
-- * round : arrondit à n chiffre après la virgule (peut être négatif)
-- * decimals : nombre de décimales affichées (peut être négatif, dans ce cas équivalent à round)
-- * noHtml : n'utilise pas de balise HTML pour afficher les puissances de 10 (pour pouvoir être utilisé en attribut title)
function p.formatNum( num )
	local params = {}
	if type( num ) == 'table' then
		params = num
		num = params[1]
	end
	if type( num ) == 'number' then
		num = tostring( num )
	elseif type( num ) ~= 'string' or num == '' then
		return num
	end

	-- séparation exposant
	local n, exponent = num:match( '^([-%d.]+)[eE]([+-]?%d+)$' )
	if exponent then
		num = n
		if params.noHtml then
			exponent = exponent:gsub('+?%f[%d]0', '' )
				:gsub( '[%d-]', supUnicode )
		else
			exponent = '<sup>' .. exponent:gsub('^%+?(%-?)0?', { ['-'] = '−', [''] = '' } ) .. '</sup>'
		end
		if num == '1' then
			return '10' .. exponent
		end
		exponent = nbsp .. '×' .. nnbsp .. '10' .. exponent
	else
		exponent = ''
	end

	-- arrondi
	local decimals = tonumber( params.decimals )
	local round = tonumber( params.round ) or decimals
	if round and tonumber( num ) then
		local mult = 10 ^ round
		num = tostring( math.floor( num * mult + 0.5 ) / mult )
	end

	local moins, entier, deci = 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)', nbsp .. '%1' )
	end
	if deci ~= '' or ( decimals and decimals > 0 ) then
		if decimals and decimals > #deci then
			deci = deci .. string.rep( '0', decimals - #deci )
		end
		if #deci > 3 then
			deci = ',' .. deci:gsub( '(%d%d%d)', '%1' .. nbsp ):gsub( nbsp .. '$', '' )
		else
			deci = ',' .. deci
		end
	end

	return moins .. entier .. deci .. exponent
end

---
-- formatNombre transforme un nombre formaté ou non en chaine formatée suivant les conventions du français.
-- si la chaine n'est pas reconnue comme un nombre, elle n'est pas modifiée.
function p.formatNombre( num, round, decimals )
	return p.formatNum{ p.parseNombre( num ), round = round, decimals = decimals }
end

--- formatNombres transforme tous les nombres d'une chaine en nombre formaté suivant les conventions du français.
function p.formatNombres( nombres, round, decimals )
	if type( nombres ) == 'number' then
		return p.formatNum{ nombres, round = round, decimals = decimals }
	elseif type( nombres ) == 'string' then
		-- retire les chiffres des strip markers
		nombres = escapeStripMarkers( nombres )

		-- formatage proprement dit
		nombres = p.sanitizeNum( nombres )
		local formatN = function ( n )
			return p.formatNombre( n, round, decimals )
		end
		if nombres:match( '%d%-%d' ) then
			nombres = nombres:gsub( '%f[%d.,][%d., ]*%d', formatN )
		else
			nombres = nombres
				:gsub( '%-?%f[%d.,][%d., ]*%d ?e[+-]?%d+', formatN )
				:gsub( '%-?%f[%d.,][%d., ]*%d', formatN )
		end

		-- restaure les strip markers
		nombres = restoreStripMarkers( nombres )

		return nombres
	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, cadratin et tiret
			['±'] = '±', ['+-'] = '±', ['+/-'] = '±',
			['+'] = '+',
			['−'] = '−', -- signe moins
			['×'] = '×', x = '×', ['*'] = '×',
			['××'] = '××', xx = '××', ['**'] = '××',
		}

		-- valeur numérique
		local cap0, capture = toParse:match( '^(([%d., ]+%f[^%d.,(])%s*)' )
		local prefix
		if not cap0 then
			-- cas d'un nombre entre guillemets, gras, italique...
			cap0, capture = toParse:match( '^((["\']+[%d., ]+["\']+)%s*)' )
		end
		if not cap0 then
			-- cas où le nombre est remplacé par un ou plusieurs points d'interrogation
			cap0, prefix = toParse:match( '^((%?+)%s*)' )
		end
		if not cap0 then
			-- cas où un mot type "vers", "environ" précède le nombre (mot simple, sans accent pour ne pas complexifier pour des cas minoritaires)
			cap0, prefix, capture = toParse:match( '^(([%a ]+[.,]?[: ]* )([+-]? ?%f[%d.,][%d., ]*%d%f[%D])%s*)' )
		end
		if not cap0 then
			-- cas où le nombre est précédé par un signe, un symbole ASCII, ou suivi d'une incertitude entre parenthèses
			cap0, prefix, capture = toParse:match( '^(([(<>=~ ]*)([+-]? ?%f[%d.,][%d., ]*%d%(?[%d%.]*%)?)%s*)' )
		end
		if not cap0 then
			-- cas où le nombre est précédé par un symbole ≤, ≥, ≈, ≃ et quelques autres
			cap0, prefix, capture = toParse:match( '^((\226[\136\137][\131\136\164\165\187\188] ?)([+-]?%f[%d.,][%d., ]*%d%f[%D])%s*)' )
		end
		if not cap0 then
			-- cas où le nombre est précédé par un symbole ± (\194\177)
			cap0, prefix, capture = toParse:match( '^((±) ?(%f[%d.,][%d., ]*%d%f[%D])%s*)' )
		end
		result = { capture or false, prefix = prefix }

		if cap0 then
			toParse = toParse:sub( cap0:len() + 1 )

			-- point de suspensions (ex π = 3.14159...)
			cap0 = toParse:match( '^…%s*' )
			if not cap0 then
				cap0 = toParse:match( '^%.%.%.%s*' )
			end
			if cap0 then
				result[1] = result[1] .. '…'
				toParse = toParse:sub( cap0:len() + 1 )
			end
			if toParse == '' then
				return result
			end
		end

		-- fraction
		capture = mw.ustring.sub( toParse, 1, 1 )
		if fractionUnicode[ capture ] then
			result.fraction = fractionUnicode[ capture ]
			toParse = toParse:sub( capture:len() + 1 ):gsub( '^%s*', '' )
			result[1] = result[1] or ''
		else
			cap0, capture = toParse:match( '^(([%d,]*/%f[%d][%d ]*%d)%s*)' )
			if not cap0 then
				-- caractère de fraction ⁄ = \226\129\132
				cap0, capture = toParse:match( '^((%d*⁄%d+)%s*)' )
				if cap0 then
					capture = capture:gsub( '⁄', '/' )
				end
			end
			if cap0 then
				if result[1] and capture:match( '^/' ) then
					local n = result[1]:match( ' %d+$' ) or result[1]:match( '^%d+$' ) or ''
					result[1] = result[1]:sub( 1, -1 - #n )
					result.fraction = n:gsub( '^ ', '' ) .. capture
				else
					result.fraction = capture
				end
				toParse = toParse:sub( cap0:len() + 1 )
			end
		end

		if toParse ~= '' and ( result[1] or result.fraction ) then
			-- lien avec un deuxième nombre
			local cap0, conj, num = toParse:match( '^(([etou+/;x*-]+) *(%-?%f[%d.,][%d., ]*%d%f[%D]%)?)%s*)' )
			if not cap0 and toParse:byte() > 127 then
				cap0, conj, num = mw.ustring.match( toParse, '^(([à−×±—–]+) *(%-?%f[%d.,][%d., ]*%d%f[%D]%)?)%s*)' )
			end
			if cap0 and specificArgs[ conj ]
				and not (
					specificArgs[ conj ] == '×'
					and (
						mw.ustring.match( toParse, '^[×x] ?10 ?[e%^]' )
						or mw.ustring.match( toParse, '^[×x] ?10<sup>(%-?%d+)</sup>' )
					)
				)
			then
				result[ specificArgs[ conj ] ] = num
				toParse = toParse:sub( cap0:len() + 1 )
			end
			if result['+'] or result['×'] or result['/'] then
				cap0, conj, num = mw.ustring.match( toParse, '^(([/;x*×−-]) *(%-?%f[%d.,][%d., ]*%d%f[%D])%s*)' )
				if cap0 then
					if specificArgs[ conj ] == '×' then
						result['××'] = num
					elseif specificArgs[ conj ] == '/' then
						result['//'] = num
					else
						result['−'] = num
					end
					toParse = toParse:sub( cap0:len() + 1 )
				end
			end
		end

		-- 10 exposant   ( \195\151 = ×, signe multiplié)
		cap0, capture = toParse:match( '^([e%^](%-?%d+)%s*)' )
		if not cap0 then
			cap0, capture = toParse:match( '^([x\195]\151? ?10[e%^](%-?%d+)%s*)' )
		end
		if not cap0 then
			cap0, capture = toParse:match( '^([x\195]\151? ?10<sup>(%-?%d+)</sup>%s*)' )
		end
		if cap0 then
			result.e = capture
			toParse = toParse:sub( cap0:len() + 1 )
		end
		if result[1] == '10' and not result.e and not result.fraction then
			cap0, capture = toParse:match( '^(<sup>(%-?%d+)</sup>%s*)' )
			if cap0 then
				result[1] = false
				result.e = capture
				toParse = toParse:sub( cap0:len() + 1 )
			end
		end

		if toParse == '' then
			return result
		end

		-- unités
		local texteUnit = toParse
		toParse = toParse:gsub( '^([^%[<]-)<sup>(%d)</sup>', '%1%2' )
		if Data.unit[ toParse ]
			or toParse:match( '%b<>' )
			or toParse:match( 'UNIQ%-%-%a+%-%x%x%x%x%x%x%x%x%-QINU' )
			or mw.ustring.match( toParse, '^%a+$' )
		then
			result[ #result + 1 ] = toParse
			toParse = ''
		elseif toParse ~= '' then
			local unit
			local exp
			toParse = toParse:gsub( '²', '2' ):gsub( '³', '3' )
			repeat
				-- unité contenant un lien
				cap0, unit, exp = toParse:match( '^((/? ?[^%s%d/%[%]]*%b[][^%s%d/]*) ?(%-?%d*)%s*)' )
				if not cap0 then
					-- unité ne contenant pas de lien
					cap0, unit, exp = toParse:match( '^((/? ?[^%s%d/]+) ?(%-?%d*)%s*)' )
				end
				if not cap0 then
					-- l/100 km
					cap0, unit, exp = toParse:match( '^((/100 ?[^%s%d/]+) ?(%-?%d*)%s*)' )
				end
				if cap0 then
					if unit:match( '%-$' ) and exp ~= '' then -- rustine pour quand le "-" se retrouve dans la capture "unit" au lieu de la capture "exp"
						unit = unit:gsub( '%-$', '' )
						exp = '-' .. exp
					elseif exp == '-' then -- rustine pour quand un "-" a été capturé dans "exp" mais sans qu'il y ait de chiffres après
						unit = cap0
						exp = ''
					end
					if Data.unit[ unit ] or mw.ustring.match( unit, '[%a€£$¥«»]' ) then
						result[ #result + 1 ] = unit
						result[ #result + 1 ] = exp
						toParse = toParse:sub( cap0:len() + 1 )
					else
						break
					end
				end
			until toParse == '' or not cap0
		end

		if toParse == '' then
			if #result > 3 then
				local estSimpleTexte = true
				for r = 2, #result, 2 do
					if Data.unit[ result[ r ] ]
						or result[ r ]:sub( 1, 1 ) == '/'
						or Data.prefix[ result[ r ]:sub( 1, 1 ) ] and Data.unit[ result[ r ]:sub( 2 ) ]
						or Data.prefix[ result[ r ]:sub( 1, 2 ) ] and Data.unit[ result[ r ]:sub( 3 ) ]
						or result[ r + 1 ] and result[ r + 1 ] ~= ''
					then
						estSimpleTexte = false
						break
					end
				end
				if estSimpleTexte then
					result[ 2 ] = texteUnit
					for r = #result, 3, -1 do
						result[ r ] = nil
					end
				end
			end
			if #result > 1 and result[ #result ] == '' then
				result[ #result ] = nil
			end
			return result
		else
			-- une partie de la chaine n'a pas pu être décodée, on retourne la chaine originale
			addErrorCat = true
			return { texte }
		end
	else
		return { }
	end
end

---
-- nomUnit 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 false.
function p.nomUnit( unit, exposant )
	unit = trim( unit )
	if not dataSuccess or type( unit ) ~= 'string' then
		return false
	end
	-- nettoyage des liens et balises HTML
	if unit:find( '[', nil, true ) then
		local Delink = require( 'Module:Delink' )
		unit = Delink._delink{ unit }
	end
	if unit:find( '<', nil, true ) then
		unit = unit:gsub( '%b<>', '' )
	end

	-- /100
	local divisor = ''
	if unit:sub( 1, 2 ) == '10' then
		divisor, unit = unit:match( '^(1[0 ]*)(.+)$' )
		local divisorName = {
			['10'] = 'dix ',
			['100'] = 'cent ',
			['1000'] = 'mille ',
			['10000'] = 'dix-mille ',
			['100000'] = 'cent-mille ',
			['1000000'] = 'un million de ',
			['1000000000'] = 'un millard de ',
		}
		divisor = divisorName[ divisor:gsub( ' ', '' ) ]
	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és 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 parties
	if type( unitTab ) == 'table' and type( unitTab.nom ) == 'string' then
		return divisor .. unitPrefix.nom .. unitTab.nom .. exposant
	elseif unit:match( '[/%d]' ) then
		-- ce n'est pas du texte simple, on annule l'infobulle
		return false
	else
		return unit .. exposant
	end
end

function p._unite( args )
	-- formatage du nombre
	local nombre = p.formatNombres( args[1], args.arrondi, args['décimales'] )
	if nombre == '' then
		nombre = nil
	end

	local wiki = {}

	-- prefix est un paramètre interne défini par p.parseUnit, utile notamment lorsque {{unité}} est utilisé dans les infobox
	if args.prefix then
		wiki[ #wiki + 1 ] = args.prefix
	end

	if nombre then
		wiki[ #wiki + 1 ] = nombre
	end

	-- fraction
	local fraction = args.fraction
	if fraction then
		fraction = fractionUnicode[ fraction ] or fraction
		local nom, den = fraction:match( '^(.-)/(.+)$' )
		if nom then
			if nom:match( '^[%dn]%d?$' ) and den:match( '^[%daeoxhklmnpst]$' ) then
				nom = nom:gsub( '[%dn()=+-]', supUnicode )
				den = den:gsub( '[%daeoxhklmnpst()=+-]', subUnicode )
			else
				nom = '<sup style="font-size: 70%; vertical-align: 0.4em;">'
					.. p.formatNombres( nom )
					.. '</sup>'
				den = '<sub style="font-size: 70%; vertical-align: 0em;">'
					.. p.formatNombres( den )
					.. '</sub>'
			end
			fraction = nom .. '⁄' .. den
		end

		if nombre then
			wiki[ #wiki + 1 ] = nbsp
		end
		wiki[ #wiki + 1 ] = fraction
	end

	-- à, et, ou, ×, – (tiret cadratin)
	local specificArgs = { '–', 'à', 'et', 'ou', '/', '//', '×', '××', '±' }
	for i = 1, #specificArgs do
		local name = specificArgs[ i ]
		local v = args[ name ] and trim( args[ name ] )
		if v then
			v = p.formatNombres( v )
			if name == '//' then
				name = '/'
			elseif name == '××' then
				name = '×'
			end
			if name == '–' and nombre and nombre:match( '^[^−]' ) and v:match( '^[^−]' ) then
				-- pas d'espace pour le tiret cadratin entre deux nombres positifs
				wiki[ #wiki + 1 ] = '–'
			elseif name == '×' or name == '±' then
				wiki[ #wiki + 1 ] = nbsp .. name .. nbsp
			else
				wiki[ #wiki + 1 ] = nbsp .. name .. ' '
			end
			wiki[ #wiki + 1 ] = v
		end
	end

	-- analyse de l'unité pour la conversion (mais ne sera affiché qu'après l'incertitude + et - séparé)
	local units = ''
	local isAngular = false
	local unitFullName
	do
		local i = 1
		local unit = trim( args[ 2 * i ] )
		local nomUnits = {}
		local par = false
		while unit do
			local exp = p.parseNombre( args[ 2 * i + 1 ] )
			local sep = ''
			-- gestion des exposants
			local expUnit = ''
			if exp == '' then
				local suffix = unit:sub( -2 ) -- yes, it's 2 bytes
				if suffix == '²' then
					exp = '2'
					unit = unit:sub( 1, -3 )
				elseif suffix == '³' then
					exp = '3'
					unit = unit:sub( 1, -3 )
				end
			end
			if #exp > 0 then
				expUnit = '<sup>' .. exp:gsub( '^-', '−' ) .. '</sup>'  -- remplace le tiret par un vrai signe moins
			end
			-- gestion de la séparation des unités et des unités en dénominateur
			if unit:sub( 1, 1 ) == '/' then
				sep = '/'
				unit = trim( unit:sub( 2 ) ) or ''
				if not par then
					par = true
					if unit:sub( 1, 2 ) == '10' then
						nomUnits[ #nomUnits + 1 ] = 'pour'
					else
						nomUnits[ #nomUnits + 1 ] = 'par'
					end
				else
					nomUnits[ #nomUnits + 1 ] = 'et par'
					if nomUnits[ #nomUnits - 2 ] == 'et par' then
						nomUnits[ #nomUnits - 2 ] = 'par'
					end
				end
			elseif units ~= '' then
				sep = nbsp
			end
			if exp:match( '^-' ) and not par then
				par = true
				nomUnits[ #nomUnits + 1 ] = 'par'
			end
			-- remplacement de l'unité par son symbole
			-- désactivé car ne gère pas les multiples tel mL
			--[[
			if Data.unit[ unit ] then
				unit = Data.unit[ unit ].symbole
			end
			--]]
			if units == '' and sep == '' and ( unit == '°' or unit == '′' or unit == '″' ) then
				isAngular = true
			end
			units = units .. sep .. unit .. expUnit
			local nomUnit = p.nomUnit( unit, exp )
			if nomUnit then
				nomUnits[ #nomUnits + 1 ] = nomUnit
			else
				-- si le code de l'unité n'est pas reconnu, insère false en première position de la table.
				table.insert( nomUnits, 1, false )
			end
			i = i + 1
			unit = trim( args[ 2 * i ] )
		end

		unitFullName = nomUnits[1] and table.concat( nomUnits, ' ' ) or false
	end

	-- conversion
	if unitFullName then
		local nameSingular = mw.ustring.gsub( unitFullName, '(%a)s%f[%A]', '%1' )
		local multiple = 1
		local convertTable = Data.convert[ nameSingular ]
		if not convertTable and #nameSingular > 5 then
			-- gestion des multiples (Kilo, méga, mili...)
			local prefix = Data.prefix[ nameSingular:sub( 1, 4 ) ] or Data.prefix[ nameSingular:sub( 1, 5 ) ]
			local _, par = nameSingular:find( ' par ', nil, true )
			local prefix2
			if par then
				prefix2 = Data.prefix[ nameSingular:sub( par + 1, 4 ) ] or Data.prefix[ nameSingular:sub( par + 1, 5 ) ]
			end
			if prefix and Data.convert[ nameSingular:gsub( '^' .. prefix.nom, '' ) ] then
				convertTable = Data.convert[ nameSingular:gsub( '^' .. prefix.nom, '' ) ]
				multiple = 10 ^ prefix.puissance
			elseif prefix2 and Data.convert[ nameSingular:gsub( ' par ' .. prefix2.nom, '' ) ] then
				convertTable = Data.convert[ nameSingular:gsub( ' par ' .. prefix2.nom, '' ) ]
				multiple = 1 / 10 ^ prefix2.puissance
			elseif prefix and prefix2 and Data.convert[ nameSingular:gsub( '^' .. prefix.nom, '' ):gsub( ' par ' .. prefix2.nom, '' ) ] then
				convertTable = Data.convert[ nameSingular:gsub( '^' .. prefix.nom, '' ):gsub( ' par ' .. prefix2.nom, '' ) ]
				multiple = 10 ^ prefix.puissance / 10 ^ prefix2.puissance
			end
		end
		if convertTable then
			if type( convertTable[1] ) ~= 'table' then
				convertTable = { convertTable }
			end
			for i = 1, #wiki do
				local v = wiki[ i ]
				local n = tonumber( p.parseNombre( v ) )
				if n then
					n = n * 10 ^ ( tonumber( p.parseNombre( args.e ) ) or 0 )
					local converted = {}
					for _, c in ipairs( convertTable ) do
						local nConverted = n
						if c.inverse then
							nConverted = 1 / n
						end
						if c.M then
							-- M = masse molaire
							local M = tonumber( args.M )
							if not M then
								break
							end
							if c.M == '*' then
								nConverted = nConverted * M
							elseif c.M == '/' then
								nConverted = nConverted / M
							end
						end
						nConverted = nConverted * multiple * c[2] + ( c[3] or 0 )
						-- format
						nConverted = p.formatNum{ nConverted, round = c.round or 6, noHtml = true }
						local sep = ' '
						if c[1] == '°' then -- as a reminder, this character occupies 2 bytes
							sep = ''
						end
						converted[ #converted + 1 ] = nConverted .. sep.. c[1]
					end
					wiki[ i ] = '<span title="' .. table.concat( converted, ' ou ' ) ..'" style="cursor:help">' .. v ..'</span>'
				end
			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
		wiki[ #wiki + 1 ] = '<span class="nowrap"><span style="display:inline-block; padding-left:0.2em; vertical-align:top; line-height:1em; font-size:80%; text-align:left;">'
		wiki[ #wiki + 1 ] = approximation .. '</span></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, '(' )
				wiki[ #wiki + 1 ] = ')'
			end
			wiki[ #wiki + 1 ] = nbsp .. '×' .. nnbsp .. '10<sup>' .. exposant .. '</sup>'
		else
			wiki[ #wiki + 1 ] = '10<sup>' .. exposant .. '</sup>'
		end
	end

	if units ~= '' then
		local sep = nbsp
		if not ( nombre or args.fraction or exposant ) then
			sep = ''
		elseif isAngular then
			sep = ''
		end
		-- ajoute une abréviation si le nom de l'unité est différent de l'unité (en considérant les espaces qui peuvent être devenues insécables)
		if unitFullName and unitFullName ~= units:gsub( nbsp, ' ' ) then
			units = string.format( '<abbr class="abbr" title="%s">%s</abbr>', unitFullName, units )
		end
		wiki[ #wiki + 1 ] = sep .. units
	end

	if #wiki > 0 then
		return table.concat( wiki )
	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
		addErrorCat = false
		args[1] = trim( args[1] ) or false
		local basique = args.basique and require( 'Module:Yesno' )( args.basique )
		if args[1] and not basique then
			if args[1]:match( '[^%d,. -]' ) then
				local tempArgs = p.parseUnit( args[1] )
				if not ( args[2] and tempArgs[2] ) then
					for k, v in pairs( tempArgs ) do
						args[k] = v
					end
				end
			end
			args[2] = trim( args[2] ) or false
			if args[2] and not args[3] then
				-- cas où le paramètre 2 contient 'm3' ou 'km2'
				local a, d = args[2]:match( '^(%a%a?)(%d)$' )
				if a and Data.unit[a] then
					args[2] = a
					args[3] = d
				end
				-- cas où le paramètre 2 contient 'km/s' ou 'm3/h'
				if args[2]:find( '/', nil, true ) then
					local tempArgs = p.parseUnit( args[2] )
					args[2] = false
					if tempArgs[1] ~= false then
						table.insert( tempArgs, 1, false )
					end
					for k, v in pairs( tempArgs ) do
						if args[k] and v then
							addErrorCat = true
						end
						args[k] = args[k] or v
					end
				end
			end
		end
		-- args alias
		args['×'] = args['×'] or args['x']  -- lettre x → signe multiplié
		args['±'] = args['±'] or args['+-'] or args['+/-']
		if args['+'] then
			args['−'] = args['−'] or args['-'] -- tiret → signe moins
		else
			args['–'] = args['–'] or args['-'] -- tiret → demi-cadratin
		end
		local cat = ''
		if addErrorCat and mw.title.getCurrentTitle():inNamespaces( 0, 4, 8, 10, 12, 14, 100, 828 ) then
			cat = errorCat
			mw.log( errorCat ,' → ', args[1], '|', args[2] )
		end
		return ( p._unite( args ) or '' ) .. cat
	end
end

function p.emulationFormatnum( frame )
	local args = frame:getParent().args
	return p.formatNombres( args[1] )
end

return p