Módulo:Exchange rate

Icono de documentación de módulo Documentación del módulo[ver] [editar] [historial] [purgar]

Version check

Designación de la versión en Wikidata: 2022-05-30 Contra Este módulo está desactualizado/obsoleto

Uso

This modules provides functions for use of currency exchange rates.
Esta documentación está transcluida desde Módulo:Exchange rate/doc.
Los editores pueden experimentar en la zona de pruebas (crear) y en los casos de prueba (crear) del módulo.
Por favor, añade las categorías en la subpágina de documentación. Subpáginas de este módulo.

--[[
	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 = '2022-05-21',
		item   = 112066294
	}
}

-- require( 'Module:No globals' )

local errorMsg      = '[[Category:Páginas con código de moneda desconocido]] <span class="error">Páginas con código de moneda desconocido</span>'
local tableNames    =  {
	'ECB euro foreign exchange reference rates.tab', 
	'Xe.com exchange rates.tab'
}
local rateTables       = {} -- to prevent multiple fetching
local defaultUnits     = { 'EUR', 'USD' }
local decimalSep       = ',' -- decimal separator
local thousandsSep     = '.'
local commaSep         = mw.message.new( 'comma-separator' ):plain()
local dateFormat       = 'j. M Y'
local defaultFormatter = '%s&#x202F;unit'
local wrapperClass     = 'voy-currency'
local cu -- for currencies-table module

-- 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 )
	if not isSet( isoCode ) then
		isoCode = 'XXX'
	end
	isoCode = isoCode:upper()

	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 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', tab.iso )
		end
	end
	return default:gsub( 'unit', isoCode )
end

-- rounds the number n to count of significant digits idp
local function round( n, idp )
	local m = 10^( idp or 0 )
	if n >= 0 then
		return math.floor( n * m + 0.5 ) / m
	else
		return math.ceil( n * m - 0.5 ) / m
	end
end

-- concats frame and parent arguments
local function getArguments( frame )
	local fargs = frame.args
	local args = frame.getParent and frame:getParent().args or {}

	local argList = { 'source', 'target', 'which', 'amount', 'with-unit', 'format' }
	local arg
	for i = 1, #argList do
		arg = argList[ i ]
		args[ arg ] = args[ arg ] or fargs[ arg ] or ''
	end
	return args
end

-- returns count of significant digits
-- zeros after decimal separator are significant
local function getDigitCount( num )
	num = num:gsub( '%.', '' ):gsub( '^0+', '' )
	return #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 rateTable = {}
	local data, fields, tData
	if rateTables[ tableName ] then
		rateTable = rateTables[ tableName ].rateTable
		fields = rateTables[ tableName ].fields
	else
		local tabularData = mw.ext.data.get( tableName )
		if not tabularData then
			return nil
		end
		fields = getFields( tabularData )
		tData = tabularData.data
		for i = 1, #tData do
			data = tData[ i ]
			rateTable[ data[ fields[ 'currency' ] ] ] =
				{ EUR = data[ fields[ 'EUR' ] ], USD = data[ fields[ 'USD' ] ],
				  asOf = data[ fields[ 'date' ] ] or '1970-01-01' } 
		end
		rateTables[ tableName ] = {
			rateTable = rateTable,
			fields = fields
		}
	end
	return rateTable, fields
end

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

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

	local rateTable, fields, rate, digitCount, asOf

	for _, tableName in ipairs( tableNames ) do
		rateTable, fields = getRateTable( tableName )
		if rateTable then
			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 )
			else
				rate, digitCount, asOf = getCurrencyData( rateTable, source, 'EUR' )
				local rate2, digitCount2, asOf2 = getCurrencyData( rateTable, target, 'EUR' )
				if rate and rate2 then
					rate = rate2/rate
					digitCount = digitCount < digitCount2 and digitCount or digitCount2
					asOf = asOf < asOf2 and asOf 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

-- adds the currency unit of isoCode to a value
-- converts range separator
local function addUnit( value, isoCode, externalFormatter )
	local formatStr = getFormatter( isoCode, externalFormatter )

	value = mw.ustring.gsub( value, '-', '–' )
	return mw.ustring.format( mw.text.decode( formatStr ), value )
end

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

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

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

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

-- changes between different rate outputs due to which
local function formatRate( rate, asOf, which )
	rate = formatNumber( tostring( rate ) )
	if which == 'all' then
		return rate .. ' (' .. getDate( asOf, dateFormat ) .. ')'
	elseif which == 'date' then
		return getDate( asOf, dateFormat )
	else
		return rate
	end
end

-- converts a single currency amount without adding the currency unit
local function convertSingle( source, target, amount )
	local rate, asOf, digitCount = er.getRate( source, target )
	if rate then
		amount = amount:gsub( '[ %a%' .. thousandsSep .. ']+', '' )
		if decimalSep ~= '.' then
			amount = amount:gsub( decimalSep, '.' )
		end
		amount = tonumber( amount )
		if amount then
			return string.format( '%.2f', round( amount * rate, digitCount ) )
				:gsub( '%.00', '' )
		end
	end
	return nil
end

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

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

	for _, target in ipairs( targets ) do
		if target ~= source then
			pos = amount:find( '-' )
			if pos then
				amount1 = convertSingle( source, target, amount:sub( 1, pos - 1 ) )
				amount2 = convertSingle( source, target, amount:sub( pos + 1 ) )
				result = amount1 and amount2 and ( amount1 .. '–' .. amount2 )
			else
				result = convertSingle( source, target, amount )
			end
			if result then
				result = formatNumber( result )
				if withUnit then
					result = addUnit( result, target, externalFormatter )
				end
				table.insert( results, result )
			end
		end
	end
	return table.concat( results, commaSep )
end

-- returns a wrapper format string with tooltip title
function er.getWrapper( amount, source, target, externalFormatter )
	local formatStr = getFormatter( source, externalFormatter )
	local title = er._convert( source, target, amount, true )
	if isSet( title ) then
		return tostring( mw.html.create( 'abbr' )
			:attr( 'title', title )
			:addClass( wrapperClass )
			:wikitext( formatStr )
		)
	end
	return formatStr
end

-- #invoke function returning the exchange rate
function er.rate( frame )
	local args = getArguments( frame )
	local rate, asOf, _ = er.getRate( args.source, args.target, true )
	
	return rate and formatRate( rate, asOf, args.which ) or errorMsg
end

-- #invoke function returning the converted amount or amount range
function er.convert( frame )
	local args = getArguments( frame )
	local result = er._convert( args.source, args.target,
		isSet( args.amount ) and args.amount or '1', args[ 'with-unit' ] == '1' )

	return isSet( result ) and result or errorMsg
end

-- #invoke function returning exchange-rate information
-- amount is set: returns the formatted amount or amount range with a tooltip
--                containing converted values
-- amount is not set: returns the formatted exchange rate
function er.currencyWithConversions( frame )
	local args = frame.args
	local result, asOf, title

	if not isSet( args.amount ) then
		-- returning exchange rate
		if not isSet( args.target ) then
			args.target = 'EUR'
		end
		result, asOf, _ = er.getRate( args.target, args.source, true )
		if result then
			result = string.format( isSet( args.format ) and args.format or '%.2f',
				tonumber( result ) )
			result = addUnit( result, args.source )
			result = result and formatRate( result, asOf,
				isSet( args.which ) and args.which or 'all' )
		end
	else
		-- returning value with conversions
		result = mw.ustring.format( er.getWrapper( args.amount, args.source, args.target ),
			args.amount )
	end

	return isSet( result ) and result or errorMsg
end

return er