Modul:Sandbox/Jhertel/Formattal

Fra Wikipedia, den frie encyklopædi
Documentation icon Moduldokumentation[opret]
local p = {}
p.angivFejlSynligt = false
	-- Angiver om fejl skal angives synligt med en rød stjerne efter det 
	-- fejlende tal, med en fejlbeskrivelse som viser sig, når man holder musen
	-- over stjernen. Nogle mener, at det ikke vil være godt altid at angive
	-- synlige fejl, da det måske vil kunne forvirre almindelige brugere. 
	-- Derfor kan funktionaliteten slås til og fra her, fx midlertidigt mens
	-- man fejlsøger sider, der er havnet i kategorien "Sider med tal hvis
	-- format ikke kendes af formattal".
	-- angivFejlSynligt placeres i p for at unittests kan tjekke værdien.
local fejlkategorikode = "[[Kategori:Sider med tal hvis format ikke kendes af formattal]]"
	-- Wikikode som altid sættes til sidst i output, hvis der er fejl i tallet.
	-- Denne kode medtages altid ved fejl, uanset værdien af p.angivFejlSynligt.

--[[
	formattal(frame)

	Formaterer et tal med højst to decimaler på dansk. Det oprindelige tal kan 
	enten bruge punktum som decimaladskiller og komma som tusindadskiller 
	("123,456,789.12"), eller komma som decimaladskiller og punktum som
	tusindadskiller ("123.456.789,12").

	Uanset hvordan tallet var formateret i input, formateres det i output på dansk
	vis med punktum som tusindadskiller og komma som decimaladskiller.

	Fortegn ("-" eller "+") håndteres også, og der kan endda være et vilkårligt
	antal mellemrum mellem fortegnet og resten af tallet, selv om dette er en 
	ukorrekt skrivemåde; dette rettes i output ved at disse mellemrum fjernes.

	Eksempler (se Modul:Formattal/testcases for langt flere eksempler):
		Opfattes som 6-cifret heltal, da tallet ikke må have mere end to decimaler:
			formattal("123456") = "123.456"
			formattal("123,456") = "123.456" 
			formattal("123.456") = "123.456"
		Opfattes som 5-cifret tal med 2 decimaler:
			formattal("123,45) = "123,45"
			formattal("123.45") = "123,45"
		Giver fejl for ukendt format (tusindadskiller og decimaltegn er ens):
			formattal("123.456.7") = "123.456.7" 
		Giver fejl for ukendt format (bruger forskellige tusindadskillere eller
		har for mange decimaler):
			formattal("123.456,789") = "123.456,789"

	Alle HTML-kommentarer og visse blanktegn i tallet ignores; andre uvedkommende
	tegn vil give fejl.

	Ved fejl returneres tallet uændret samtidig med at artiklen placeres i 
	fejlkategorien "Sider med tal hvis format ikke kendes af formattal".

]]
function p.formattal(frame)
    local originaltInputEllerTom = (frame.args[1] or "")
	
	return p.formaterIndledendeTalPaaDansk(
		originaltInputEllerTom, 
		p.angivFejlSynligt)
end


--[[
	p.formaterIndledendeTalPaaDansk()
	
	Funktion som indeholder hele kernefunktionaliteten på "ren" form (uden 
	frame.args[1] or ""), for at gøre unittests nemmere.
]]
function p.formaterIndledendeTalPaaDansk(originaltInputEllerTom, 
		angivFejlSynligt)
	
	-- Fjern HTML-kommentarer og ydre blanktegn.
    local inputUdenEventuelleHtmlKommentarerOgBlanktegn 
		= p.fjernHtmlkommentarerOgYdreBlanktegn(originaltInputEllerTom)

	-- Opdel i inputtal (fx "-1.234.553,43" eller "") 
	-- og inputrest (fx "<ref>…</ref>" eller "").
	local inputtalEllerTom, inputrest 
		= p.opdelITalOgRest(inputUdenEventuelleHtmlKommentarerOgBlanktegn)

	-- Fortolk tallet, hvis der er et, og dan det færdige output, inklusive
	-- eventuelle fejlangivelser. Bemærk at der altid er et gyldigt output,
	-- også hvis der er fejl.
	local faerdigtOutput = p.fortolkTalOgDanFaerdigtOutput(inputtalEllerTom, 
		inputrest, angivFejlSynligt)
	
	return faerdigtOutput
	
end


function p.fjernHtmlkommentarerOgYdreBlanktegn(s)
	
	-- Fjern HTML-kommentarer.
    local sUdenEventuelleHtmlKommentarer 
    	= string.gsub(s, "<!%-%-.-%-%->", "") 
    	-- Mon dette mønster virkelig er helt gennemtænkt?

    -- Fjern blanktegn i enderne
    local sUdenEventuelleHtmlKommentarerOgYdreBlanktegn
    	= mw.text.trim(sUdenEventuelleHtmlKommentarer)
    	
    return sUdenEventuelleHtmlKommentarerOgYdreBlanktegn
end


--[[
	Opdel strengen s i hvad der kan minde om et tal (fx "-1.234.553,43") og 
	resten (fx "<ref>…</ref>" eller "").
	
	Bemærk at det "der kan minde om et tal" ikke behøver faktisk være et 
	korrekt tal. Det skal blot matche et simplistisk mønster.
	
	Eksempler:
		"-1.234.553,43<ref>…</ref>" → "-1.234.553,43", "<ref>…</ref>"
		"-<ref>…</ref>" → "-", "<ref>…</ref>"
		".2blabla" → ".2", "blabla"
		"..2..,,. blabla" → "..2..,,.", " blabla"
		"+..,,..,,. blabla" → "+..,,..,,.", " blabla"
		"blabla" → "", "blabla"
	
]]
function p.opdelITalOgRest(s)

	-- Opdel i tal (fx "-1.234.553,43") og rest (fx "<ref>…</ref>" eller "").
	-- Mønsteret er designet, så det ikke kan fejle; hverken talEllerTom 
	-- eller rest kan derfor blive nil.
	local talEllerTom, rest = s:match("^([+-]? *[%d,.]*)(.*)$")
	
	return talEllerTom, rest 
		-- Ingen af disse kan være nil, men begge kan være tomme.
	
end


--[[
	p.fortolkTalOgDanFaerdigtOutput()
	
	Fortolk tallet, hvis der er et, og dan det færdige output, inkl. evt. 
	fejlhåndtering. Hvis der ikke er et tal, så returner inputrest uændret.
	
	inputtalEllerTom (string, ikke nil)
	
		Det tal, der skal fortolkes, eller den tomme streng. Den tomme streng
		er en gyldig situation, som ikke giver fejl.
		
		Kan være et ikke-fortolkbart "tal" a la "23.43.3,34", i hvilket 
		tilfælde der returneres en fejlkategori (det bør der i hvert fald).
		Hvis angivFejlSynligt er true angives der også en synlig fejlangivelse
		med fejlmeddelelse.
	
	inputrest (string, ikke nil)
	
		Den del af inputtet, der følger efter tallet. Hvis "tallet" er tomt,
		er inputrest hele inputtet.

	angivFejlSynligt (boolean, ikke nil)
	
		Om fejl og fejlbeskrivelse skal angives synligt i output til højre
		for det fejlende tal. 
]]
function p.fortolkTalOgDanFaerdigtOutput(
		inputtalEllerTom, 
		inputrest, 
		angivFejlSynligt)
	
	local faerdigtOutput 

	if inputtalEllerTom == "" then
		-- Taldelen er tom.
		-- Dette er en gyldig situation, hvor output så skal være
		-- identisk med input. Det antages dog, at det er okay at 
		-- HTML-kommentarer og ydre blanktegn stadig fjernes.
		-- Jf. https://da.wikipedia.org/wiki/Brugerdiskussion:Jhertel#Modul_formattal_og_tomt_input
		faerdigtOutput = inputrest  
			-- inputrest udgør det hele, da inputtalEllerTom er tom.

	else
		local inputtal = inputtalEllerTom  -- Den kan ikke længere være tom.
		
		-- Opdel tallet (inputtal) i enkeltdele.
		-- Dette kan fejle, og i så fald vil fejltekstEllerNil være ikke-nil og 
		-- indeholde en fejlbeskrivelse.
	    local eventueltFortegnEllerTom, heltalsdelUdenTusindadskillere, 
	    	decimalerEllerNil, fejltekstEllerNil = p.opdelTal(inputtal)
	
		-- Returner output (enten et formateret tal eller fejl).
		if not fejltekstEllerNil then
	    	-- Succes. Tallet er fuldt fortolket og opdelt i enkeltdele.
	    	
	    	-- Dan et færdigt dansk formateret tal.
	    	local outputtal
	    		= p.danDanskOutputtal(
	    			eventueltFortegnEllerTom, 
					heltalsdelUdenTusindadskillere, 
					decimalerEllerNil) 
	
			faerdigtOutput = outputtal .. inputrest
	
		else
	    	-- Fejlsituation.
	    	
    		-- FejltekstEllerNil kan ikke længere være nil. Angiv dette klart 
    		-- som et værn mod bugs.
	    	local fejltekst = fejltekstEllerNil 

	        faerdigtOutput = p.danSamletFejlreturstreng(
	        	inputtal, 
	        	inputrest, 
	        	fejltekst, 
	        	angivFejlSynligt)

	    end
	end

	return faerdigtOutput
	
end


--[[
	p.opdelTal()

	Returnerer fire værdier:
		eventueltFortegnEllerTom, 
		heltalsdelUdenTusindadskillere, 
		decimalerEllerNil, 
		fejltekstEllerNil.

	Hvis fejltekstEllerNil ikke er nil, gik opdelingen godt, og tallet er 
	accepteret som korrekt. Ellers indeholder fejltekstEllerNil en streng, som 
	beskriver fejlen.

	Returværdien decimalerEllerNil er nil, hvis der ingen decimaler er.
]]
function p.opdelTal(inputtal)

	local fejltekstEllerNil = nil
	local heltalsdelUdenTusindadskillere = nil

    -- Fjern et evt. fortegn på første position og husk det.
    local eventueltFortegnEllerTom, inputtaldelUdenEventueltFortegn
    	= p.opsplitIFortegnOgRest(inputtal)

	-- Prøv at matche et kommatal med 1 eller 2 decimaler.
    local heltalsdelMedEventuelleTusindadskillereEllerNil, 
    	decimaladskillerEllerNil, decimalerEllerNil
    	= inputtaldelUdenEventueltFortegn:match("^([%d,.]-)([,.])(%d%d?)$")

	-- Afgør om der var et match.
	-- Hvis ikke der er et match, vil både heltalsdelEllerNil, 
	-- decimaladskillerEllerNil og decimalerEllerNil være nil.
	local derVarEtMatch = (heltalsdelMedEventuelleTusindadskillereEllerNil ~= nil)

    if derVarEtMatch then
    	-- Der var et match: Tallet er et kommatal med 1 eller 2 decimaler.
    	
    	-- Nye variable for at indikere, at de ikke længere kan være nil:
    	-- Værn mod bugs.
    	local heltalsdelMedEventuelleTusindadskillere
    		= heltalsdelMedEventuelleTusindadskillereEllerNil
    	local decimaladskiller = decimaladskillerEllerNil

    	-- Bestemt tusindadskillertegnet.
    	-- Bemærk at decimaladskiller logisk kun kan være "." eller ","
    	-- pga. mønsteret ovenfor.
    	local tusindadskiller = p.modsatAdskillertegn(decimaladskiller)

    	-- Fjern alle tusindadskillertegn fra heltalsdel.
        heltalsdelUdenTusindadskillere, fejltekstEllerNil
        	= p.fjernTusindadskillere(heltalsdelMedEventuelleTusindadskillere, tusindadskiller)

    else
    	-- Der var ikke et match. Hvis tallet er på korrekt form, er det et heltal.

	    local heltalsdelMedEventuellePunktumtusindadskillere
    		= inputtaldelUdenEventueltFortegn:match("^([%d.]+)$")
    	
    	if heltalsdelMedEventuellePunktumtusindadskillere then
	    	-- Fjern alle tusindadskillertegn fra heltalsdel.
	        heltalsdelUdenTusindadskillere, fejltekstEllerNil
	        	= p.fjernTusindadskillere(heltalsdelMedEventuellePunktumtusindadskillere, ".")
    	else
		    local heltalsdelMedEventuelleKommatusindadskillere
	    		= inputtaldelUdenEventueltFortegn:match("^([%d,]+)$")

			if heltalsdelMedEventuelleKommatusindadskillere then
		    	-- Fjern alle tusindadskillertegn fra heltalsdel.
		        heltalsdelUdenTusindadskillere, fejltekstEllerNil
		        	= p.fjernTusindadskillere(heltalsdelMedEventuelleKommatusindadskillere, ",")
			else
				-- Angiv fejl.
				heltalsdelUdenTusindadskillere = nil  
				
				fejltekstEllerNil = "Kunne ikke fortolke tallet '" .. inputtal .. "'."
			end
		end
    end

	if not fejltekstEllerNil then
		-- Håndter situationer som ".12" (= 0,12).
		if heltalsdelUdenTusindadskillere == "" then
			heltalsdelUdenTusindadskillere = "0"
		end
	
		-- Tjek for ugyldige tusindadskillere.
	    if heltalsdelUdenTusindadskillere:match("%D") then
	    	-- Fejlsituation: Heltalsdelen uden tusindadskillere indeholder
	    	-- mindst ét ikke-ciffer (%D), som kun kan være en forkert
	    	-- tusindadskiller.
	    	fejltekstEllerNil = "Heltalsdelen '" .. heltalsdelUdenTusindadskillere .. "' indeholder ugyldige tusindadskillere; hele tallet er '" .. inputtal .. "'."
	    	eventueltFortegnEllerTom = ""
	    	heltalsdelUdenTusindadskillere = nil
	    	decimalerEllerNil = nil
		end
	end
	
	return eventueltFortegnEllerTom, heltalsdelUdenTusindadskillere, decimalerEllerNil, fejltekstEllerNil
end

--[[
	Returner heltalsdelMedEventuelleTusindadskillere med alle tusindadskillere 
	af den angivne type ("." eller ",") fjernet.

	heltalsdelMedEventuelleTusindadskillere:
		En streng med nul eller flere cifre og tusindadskillere.

	Returværdier:
		heltalsdelUdenTusindadskillere (string), 
		fejltekstEllerNil (string eller nil)
	
	Eksempler på korrekt opførsel, som dog endnu ikke er opnået:
		"", "," → "", nil
		"0", "," → "0", nil
		"12", "," → "12", nil
		"123", "," → "123", nil
		"1234", "," → "1234", nil
		"1,234", "," → "1,234", nil
		"1,234", "." → nil, "Ugyldige tusindadskillere i heltalsdel"
		"1,234.", "," → nil, "Ugyldige tusindadskillere i heltalsdel"
		"1,234,567", "," → "1234567", nil
		"1,234,56", "," → nil, "Ugyldig placering af tusindadskillere i heltalsdel"
]]
function p.fjernTusindadskillere(heltalsdelMedEventuelleTusindadskillere, tusindadskiller)

	local heltalsdelUdenTusindadskillere
	local fejltekstEllerNil

	local talletErGyldigt = erGyldigtHeltalMedMuligeTusindadskillere(
			heltalsdelMedEventuelleTusindadskillere, tusindadskiller) 

	if talletErGyldigt then
		heltalsdelUdenTusindadskillere
			= p.removeAll(
				heltalsdelMedEventuelleTusindadskillere, 
				p.tusindadskillerpattern(tusindadskiller))
		fejltekstEllerNil = nil
	else
		heltalsdelUdenTusindadskillere = nil
		fejltekstEllerNil = "Ugyldigt placerede eller ugyldige tusindadskillere i heltalsdelen '" .. heltalsdelMedEventuelleTusindadskillere .. "'."
	end

	return heltalsdelUdenTusindadskillere, fejltekstEllerNil

end

function erGyldigtHeltalMedMuligeTusindadskillere(
		heltalsdelMedEventuelleTusindadskillere, tusindadskiller)

	local s = heltalsdelMedEventuelleTusindadskillere

	-- Fjern grupper af "tddd" i slutningen indtil intet match, hvor t er 
	-- en tusindadskiller og d er et ciffer.
	local pattern = p.tusindadskillerpattern(tusindadskiller) .. "%d%d%d$"
	local numberOfReplacements
	local totalNumberOfReplacements = 0
	repeat
		s, numberOfReplacements = s:gsub(pattern, "")
		totalNumberOfReplacements = totalNumberOfReplacements + numberOfReplacements
	until numberOfReplacements == 0
	
	local theNumberIsValid
	
	if totalNumberOfReplacements == 0 then
		-- Der var ingen tusindadskillere, i hvert fald ikke korrekt placeret.
		theNumberIsValid = p.isSequenceOfAtLeastOneDigit(s)
	else
		-- Der var tusindadskillere. Hvis resten er mellem 1 og 3 cifre, var
		-- tallet korrekt.
		theNumberIsValid = p.isSequenceOf1To3Digits(s)
	end

	return theNumberIsValid
	
end

function p.tusindadskillerpattern(tusindadskiller)
	return "[" .. tusindadskiller .. "]"
end

function p.isSequenceOfAtLeastOneDigit(s)
	return not not s:match("^%d+$")
end

function p.isSequenceOf1To3Digits(s)
	return not not s:match("^%d%d?%d?$")
end


function p.danDanskOutputtal(eventueltFortegnEllerTom, 
		heltalsdelUdenTusindadskillere, decimalerEllerNil)

	local heltalsdelMedDanskeTusindadskillere 
		= p.indsaetTusindadskillere(heltalsdelUdenTusindadskillere)
			-- Giver fx "123.456.789" eller "123".

	local danskDecimaldel
		= p.danDanskDecimaldel(decimalerEllerNil) -- Giver fx ",12", ",6" eller "".

	local samletTal
		= eventueltFortegnEllerTom 
			.. heltalsdelMedDanskeTusindadskillere 
			.. danskDecimaldel

    return samletTal
end


function p.danDanskDecimaldel(decimalerEllerNil)

    if decimalerEllerNil then
		return "," .. decimalerEllerNil
	else
		return ""
	end
	
end


function p.fejlangivelseskode(fejltekst, angivFejlSynligt)
	
	if angivFejlSynligt then
		return '<sup><span style="color:red" title="Skabelonen Formattal kan ikke konvertere dette tal: ' .. fejltekst .. '">*</span></sup>'
	else
		return ""
	end
	
end


function p.danSamletFejlreturstreng(inputtal, inputrest, fejltekst, 
	angivFejlSynligt)
     
    return 
    	inputtal 
    	.. p.fejlangivelseskode(fejltekst, angivFejlSynligt)
    	.. inputrest 
    	.. fejlkategorikode

end


function p.removeAll(s, pattern)
	return s:gsub(pattern, "")
end


--[[
	p.modsatAdskillertegn(adskillertegn)
	
	Returner det modsatte adskillertegn til det givne.
	
	adskillertegn:
		Skal være enten "." eller ",".

	Returværdi:
		"," hvis adskillertegn er ".".
		"." hvis adskillertegn er "," (eller hvad som helst andet).
]]
function p.modsatAdskillertegn(adskillertegn)
    if adskillertegn == "." then
    	return ","
    else
        return "."
    end
end

function p.opsplitIFortegnOgRest(s)

    local eventueltFortegnEllerTom, reststrengUdenFortegn = s:match("^([+-]?) *(.*)$")

	return eventueltFortegnEllerTom, reststrengUdenFortegn

end

--[[
	p.indsaetTusindadskillere(heltalsStreng)
	
	Hjælpefunktion. 
	
	Indsæt tusindadskillere i en given streng af cifre.

	Eksempler:	
		p.indsaetTusindadskillere("") == ""
		p.indsaetTusindadskillere("1") == "1"
		p.indsaetTusindadskillere("12") == "12"
		p.indsaetTusindadskillere("123") == "123"
		p.indsaetTusindadskillere("1234") == "1.234"
		p.indsaetTusindadskillere("123456789") == "123.456.789"
]]
function p.indsaetTusindadskillere(heltalsstreng)
	local akkumulator = heltalsstreng -- Heltal uden tusindadskillere, fx "123456789".
	local antalTusindAdskillereIndsat

    repeat  
        akkumulator, antalTusindAdskillereIndsat 
        	= string.gsub(akkumulator, "^(%d+)(%d%d%d)", '%1.%2')
    until antalTusindAdskillereIndsat == 0

    return akkumulator  -- Heltal med tusindadskillere, fx "123.456.789".
end

return p