Modulo:Exchange rate
Aspekto
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 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