Saltu al enhavo

Modulo:Exchange rate

El Vikivojaĝo

Dokumentado por ĉi tiu modulo povas esti kreata ĉe Modulo:Exchange rate/dokumentado

--[[
	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-10-22',
		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 )
	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 .. messages.wrongParams
	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 ),
		args.amount:gsub( '-', '–' )
	)
end

return er