Modul:Exchange rate

Aus Wikivoyage
Dokumentation für das Modul Exchange rate[Ansicht] [Bearbeiten] [Versionsgeschichte] [Aktualisieren]

Hinweis: Seit dem 24. Februar 2023 werden die Wechselkurse auf Wikimedia Commons nicht mehr aktualisiert.

Versionsbezeichnung auf Wikidata: 2024-02-24 Ok!

Benötigte weitere Module

Dieses Modul benötigt folgende weitere Module: CountryData/Currencies

Verwendung in anderen Modulen

Dieses Modul ist notwendig für die Ausführung folgender Module. Bei Anpassungen sollte die Funktionstüchtigkeit der folgenden Module geprüft werden. Benutze dazu auch diese Tracking-Kategorie um Fehler zu finden, die sich dann auf Artikel auswirken:

Häufig genutzte Variablen

  • amount: string Wert oder Wertebereich eines Geldbetrages,
  • source: string dreistelliger ISO-4217-Code der Ursprungswährung,
  • target: string dreistelliger ISO-4217-Code der Zielwährung,
  • frame: table Parametertabelle, die durch einen #invoke-Aufruf übergeben wird.

Extern nutzbare Funktionen

function er.getRate( source, target, toRound )

Die Funktion liefert drei Werte zurück: rate, asOf, digitCount.

  • Vorgabe:
    • toRound: boolean. Falls true wird rate nur mit maximal signifikanten Stellen ausgegeben.
  • Ergebnis:
    • rate: number. Wechselkurs für die Umrechnung aus Ursprungs- in die Zielwährung.
    • asOf: string. Datumsangabe für den Wechselkurs in der Form YYYY-MM-DD.
    • digitCount: number: Anzahl signifikanter Stellen für den Wechselkurs.
function er.getWrapper( amount, source, target, digits, externalFormatter )

Die Funktion liefert eine Formatierungszeichenkette für ein span-Tag, wobei öffnendes und schließendes Tag den %s-Platzhalter umschließen. Das title-Attribut des Tags enthält den umgerechneten Betrag in mehreren Währungen (üblicherweise EUR, CHF und USD), das class-Attribut zwei Werte, nämlich voy-currency und voy-currency-xxx, wobei xxx den ISO-4217-Code der Ursprungswährung in Kleinbuchstaben darstellen.

  • Vorgabe:
    • digits: number. Anzahl der Nachkommastellen der umgerechneten Beträge. Standard ist 2.
    • externalFormatter: function. Externe Funktion, die eine Formatierungszeichenkette für das Einfügen eines Betrages zurückliefert. Sie stellt einen Ersatz für die lokale Funktion getFormatter dar, um den Zugriff auf das externe Modul mit den Währungscodes zu vermeiden.
  • Ergebnis:
    • string: Formatierungszeichenkette.
function er.rate( frame )

Die Funktion liefert den Wechselkurs für die Umrechnung aus Ursprungs- in die Zielwährung in wählbaren Formaten zur Verfügung.

  • Vorgabe:
    • args.source, args.target, args.show und args.digits.
    • args.show: date: nur Datum des Wechselkurses, all: Wechselkurs mit Datum in Klammern.
    • args.digits: maximale Anzahl der Nachkommastellen.
  • Ergebnis:
    • string: Formatierungszeichenkette.
function er.convert( frame )
function er.currencyWithConversions( frame )

Lokale Funktionen

local function getFormatter( isoCode, externalFormatter )
local function getDigitCount( num )
local function round( num, digitCount )
local function getFields( tabularData )
local function getRateTable( tableName )
local function getCurrencyData( rateTable, source, target )
local function getDate( aDate, formatStr )
local function insertThousandsSep( amount )
local function formatNumber( num )
local function addUnit( amount, isoCode, externalFormatter )
local function formatRate( rate, asOf, show, digits, target )
local function convertSingle( source, target, amount, digits )
function er._convert( source, targets, amount, withUnit, digits, externalFormatter )
Hinweise
--[[
	Thanks to GiftBot who is uploading/updating currency exchange rates to Wikimedia
	Commons. This service is available since March of 2022.
]]--

-- module variable and administration
local er = {
	moduleInterface = {
		suite  = 'Exchange rate',
		serial = '2024-02-24',
		item   = 112066294
	}
}

-- require( 'strict' )

-- Exchange-rate tables stored on Wikimedia Commons
local tableNames =  {
	'ECB euro foreign exchange reference rates.tab', 
	'Xe.com exchange rates.tab'
}

-- language-dependent error messages
local messages = {
	unknownIsoCode = '[[Category:Währung: Seiten mit unbekanntem Währungscode]] <span class="error">Unbekannter Währungscode</span>',
	wrongParams    = '[[Category:Währung: Fehlerhafte Parameter]] <span class="error">Fehlerhafte(r) Parameter</span>'
}

-- language-dependent constants
local language = {
	defaultUnits     = { 'EUR', 'CHF', 'USD' },
	decimalSep       = ',', -- decimal separator
	thousandsSep     = '.',
	commaSep         = mw.message.new( 'comma-separator' ):plain(),
	dateFormat       = 'j. M Y',
	convertFormatter = '≈ %s',
	defaultFormatter = '%s&#x202F;unit',
	wrapperClass     = 'voy-currency',
	conversionVia    = 'EUR', -- EUR or USD
	all              = 'alle', -- lowercase letters
	date             = 'datum'
}

-- variables for internal use
local cu -- for currencies-table module
local rateTables = {} -- to prevent multiple fetching

-- check if arg is set
local function isSet( arg )
	return arg and arg ~= ''
end

-- returns a currency formatter string for isoCode
-- the following function must be localized
local function getFormatter( isoCode, externalFormatter )
	isoCode = isSet( isoCode ) and isoCode:upper() or 'XXX'

	if externalFormatter then
		return externalFormatter( isoCode )
	elseif not cu then
		cu = mw.loadData( 'Module:CountryData/Currencies' )
	end

	local tab = cu.isoToQid[ isoCode ] and cu.currencies[ cu.isoToQid[ isoCode ] ]
	local default = cu.currencies.default or language.defaultFormatter
	if tab then
		if tab.f then
			return tab.f
		else
			local unit = tab.add and tab.add:gsub( ',.*', '' ) or tab.iso
			return default:gsub( 'unit', unit )
		end
	end
	return default:gsub( 'unit', isoCode )
end

-- returns count of significant digits
-- zeros after decimal separator are significant
local function getDigitCount( num )
	num = num:gsub( '%.', '' ):gsub( '^0+', '' )
	return #num
end

-- rounds mantissa/significand of number num to digit count digitCount
local function round( num, digitCount )
	return tonumber( string.format( '%.' .. digitCount .. 'g', num ) )
end

-- returns tabularData fields schema as associative table
local function getFields( tabularData )
	local fields = {}
	local tFields = tabularData.schema.fields
	for i = 1, #tFields do
		fields[ tFields[ i ].name ] = i
	end
	return fields
end

-- returns currency-rates table as associative table
-- this is an expensive function: the rateTables should be established only once
local function getRateTable( tableName )
	local rows = {}
	local colNo, fields, row, tData
	if not rateTables[ tableName ] then
		local tabularData = mw.ext.data.get( tableName )
		if not tabularData then
			return nil
		end
		fields = getFields( tabularData )
		colNo = fields[ 'currency' ]
		tData = tabularData.data
		for i = 1, #tData do
			row = tData[ i ]
			rows[ row[ colNo ] ] = row
		end
		rateTables[ tableName ] = {
			fields = fields,
			rows = rows
		}
	end
	return rateTables[ tableName ]
end

-- returns exchange-rate properties for source -> target iso codes
local function getCurrencyData( rateTable, source, target )
	local rate, digitCount, asOf
	local fields = rateTable.fields
	local row = rateTable.rows[ source ]
	if row then
		rate = row[ fields[ target ] ]:gsub( ',', '' )
			-- remove English thousands separator
		digitCount = getDigitCount( rate )
		rate = tonumber( rate )
		asOf = row[ fields[ 'date' ] ]
	end
	return rate, digitCount, asOf
end

-- returns exchange rate for source -> target iso codes
-- toRound: Boolean
function er.getRate( source, target, toRound )
	-- source, target are three-letter ISO 4217 codes
	if not source:match( '^%a%a%a$' ) or not target:match( '^%a%a%a$' ) then
		return nil
	end

	local rateTable, fields, rate, rows, digitCount, asOf
	source = source:upper()
	target = target:upper()

	for i = 1, #tableNames do
		rateTable = getRateTable( tableNames[ i ] )
		if rateTable then
			fields = rateTable.fields
			if fields[ target ] then
				rate, digitCount, asOf = getCurrencyData( rateTable, source, target )
				if rate then
					rate = 1/rate
				end
			elseif fields[ source ] then 
				rate, digitCount, asOf = getCurrencyData( rateTable, target, source )
			elseif fields[ language.conversionVia ] then
				local rate1, digitCount1, asOf1 = getCurrencyData( rateTable, source, language.conversionVia )
				local rate2, digitCount2, asOf2 = getCurrencyData( rateTable, target, language.conversionVia )
				if rate1 and rate2 then
					rate = rate2/rate1
					digitCount = digitCount1 < digitCount2 and digitCount1 or digitCount2
					asOf = asOf1 < asOf2 and asOf1 or asOf2
				end
			end
		end
		if rate then
			break
		end
	end
	if rate and toRound then
		rate = round( rate, digitCount )
	end
	return rate, asOf, digitCount
end

-- returns a converted date for aDate due to formatStr
local function getDate( aDate, formatStr )
	local function formatDate( aDate, formatStr )
		return mw.getContentLanguage():formatDate( formatStr, aDate, true )
	end

	if isSet( aDate ) then
		local success, t = pcall( formatDate, aDate, formatStr )
		return success and t or ''
	else
		return ''
	end
end

-- inserts thousands separators in amount string
local function insertThousandsSep( amount )
	local k
	local sep = '%1' .. language.thousandsSep .. '%2'
	while true do  
		amount, k = amount:gsub( '^(-?%d+)(%d%d%d)', sep )
		if k == 0 then
			break
		end
	end
	return amount
end

-- localizes a number string
local function formatNumber( num )
	if language.decimalSep ~= '.' then
		num = num:gsub( '%.', language.decimalSep )
	end
	return insertThousandsSep( num )
end

-- adds the currency unit of isoCode to amount string
local function addUnit( amount, isoCode, externalFormatter )
	local formatStr = getFormatter( isoCode, externalFormatter )
	return mw.ustring.format( mw.text.decode( formatStr ), amount )
end

local function outputFormat( digits )
	digits = math.floor( tonumber( digits ) or 2 )
	if digits < 0 or digits > 6 then
		digits = 2
	end
	return '%.'.. digits .. 'f'
end

-- selects different rate outputs due to show
local function formatRate( rate, asOf, show, digits, target )
	show = ( show or '' ):lower()
	rate = formatNumber( isSet( digits ) and outputFormat( digits ):format( rate )
		or tostring( rate ) )
	if isSet( digits ) or show == 'all' or show == language.all then
		rate = addUnit( rate, target )
	end
		
	if show == 'all' or show == language.all then
		return rate .. ' (' .. getDate( asOf, language.dateFormat ) .. ')'
	elseif show == 'date' or show == language.date then
		return getDate( asOf, language.dateFormat )
	else
		return rate
	end
end

-- converts a single currency amount without adding the currency unit
local function convertSingle( source, target, amount, digits )
	local rate, asOf, digitCount = er.getRate( source, target )
	if rate then
		return formatNumber( outputFormat( digits ):format(
			round( amount * rate, digitCount ) ):gsub( '%.0*$', '' ) )
	else
		return nil
	end
end

-- converts a single currency amount or an amount range and adding the currency unit
function er._convert( source, targets, amount, withUnit, digits, externalFormatter )
	local amount1, amount2, pos, result
	local results = {}

	if not isSet( targets ) then
		targets = language.defaultUnits
		withUnit = true
	elseif type( targets ) == 'string' then
		targets = { targets }
	end

	amount = amount:gsub( '[ %a%' .. language.thousandsSep .. ']+', '' ):gsub( '-', '–' )
	if language.decimalSep ~= '.' then
		amount = amount:gsub( language.decimalSep, '.' )
	end

	for i, target in ipairs( targets ) do
		if target ~= source then
			pos = mw.ustring.find( amount, '[^,%.%d]' )
			if pos then
				amount1 = mw.ustring.sub( amount, 1, pos - 1 )
				amount2 = tonumber( mw.ustring.sub( amount, pos + 1 ) )
			else
				amount1 = amount
			end
			amount1 = tonumber( amount1 ) or 1
			result = convertSingle( source, target, amount1, digits )
			if pos and result and amount2 then
				amount2 = convertSingle( source, target, amount2, digits )
				result = amount2 and
					( result .. mw.ustring.sub( amount, pos, pos ) .. amount2 )
			end
			if result then
				if withUnit then
					result = addUnit( result, target, externalFormatter )
				end
				table.insert( results, result )
			end
		end
	end
	result = table.concat( results, language.commaSep )
	return result ~= '' and result
end

-- returns a wrapper format string with tooltip title
function er.getWrapper( amount, source, target, digits, externalFormatter,
	withMaintenance)
	local formatStr = getFormatter( source, externalFormatter )
	local title = er._convert( source, target, amount, true, digits )
	if title then
		return tostring( mw.html.create( 'abbr' )
			:attr( 'title', mw.ustring.format( language.convertFormatter, title ) )
			:addClass( language.wrapperClass )
			:addClass( language.wrapperClass .. '-' .. source:lower() )
			:wikitext( formatStr )
		)
	else
		return formatStr .. ( withMaintenance and messages.wrongParams or '' )
	end
end

-- #invoke function returning the exchange rate
function er.rate( frame )
	local args = frame.args
	local rate, asOf, digitCount = er.getRate( args.source, args.target, true )
	return rate and formatRate( rate, asOf, args.show, args.digits, args.target )
		or messages.unknownIsoCode
end

-- #invoke function returning the converted amount or amount range
function er.convert( frame )
	local args = frame.args
	if isSet( args.show ) then
		return er.rate( frame )
	else
		return er._convert( args.source, args.target,
			isSet( args.amount ) and args.amount or '1', ( args.plain or '' ) ~= '1',
			args.digits ) or messages.wrongParams
	end
end

-- #invoke function returning exchange-rate information
-- returns the formatted amount or amount range with a tooltip containing
-- converted values
function er.currencyWithConversions( frame )
	local args = frame.args
	if not isSet( args.amount ) then
		args.amount = '1'
	end
	return mw.ustring.format(
		er.getWrapper( args.amount, args.source, args.target, args.digits, nil, true ),
		args.amount:gsub( '-', '–' )
	)
end

return er