Jump to content

Module:Exchangerate

From Wikivoyage

local errormsg = ('[[Category:Articles that use unexpected currency]]'
	.. '<span class="exchangeinfo" style="display:none;" '
	.. 'title="Exchange rate not found">Unexpected currency</span>'
	.. 'rate not found')

local function countSigificantDigits(number)
	number = string.gsub(number, '%.', '', 1)
	number = mw.text.trim(number, '0')
	return #number
end

local function round(num, numSigificantDigits)
	local numDecimalPlaces = numSigificantDigits - math.floor(math.log10(num)) - 1
	local mult = 10^(numDecimalPlaces or 0)
	return math.floor(num * mult + 0.5) / mult
end

local function getTabularDataFieldNames(tabularData)
	local fields = {}
	for _,field in pairs(tabularData.schema.fields) do
		table.insert(fields, field.name)
	end
	return fields
end

local function getColumnIndices(fields)
	local rowCurrencyIndex, dateIndex
	local targetCurrencyIndices = {}
	local sourceCurrencyIndices = {}
	for i,v in pairs(fields) do
		if v == 'currency' then
			rowCurrencyIndex = i
		elseif v == 'date' then
			dateIndex = i
		elseif string.match(v, '^%u%u%u$') then
			sourceCurrencyIndices[v] = i
		elseif string.match(v, '^_%u%u%u$') then
			targetCurrencyIndices[string.sub(v,2)] = i
		end
	end
	return rowCurrencyIndex, dateIndex, sourceCurrencyIndices, targetCurrencyIndices
end

local function getConversionTable(dataPageName)
	local tabularData = mw.ext.data.get(dataPageName)
	if not tabularData then return nil end
	local fields = getTabularDataFieldNames(tabularData)
	local rowCurrencyIndex, dateIndex, sourceCurrencyIndices, targetCurrencyIndices = getColumnIndices(fields)
	
	local conversionTable = {}
	if rowCurrencyIndex then
		for _,row in pairs(tabularData.data) do
			for sourceCurrency,index in pairs(sourceCurrencyIndices) do
				if not conversionTable[sourceCurrency] then
					conversionTable[sourceCurrency] = {}
				end
				conversionTable[sourceCurrency][row[rowCurrencyIndex]] = {rate = row[index], revisionTime = row[dateIndex]}
			end
			for targetCurrency,index in pairs(targetCurrencyIndices) do
				if not conversionTable[row[rowCurrencyIndex]] then
					conversionTable[row[rowCurrencyIndex]] = {}
				end
				conversionTable[row[rowCurrencyIndex]][targetCurrency] = {rate = row[index], revisionTime = row[dateIndex]}
			end
		end
	end
	return conversionTable
end

local function getDataFromRateDataPage(dataPageName, source, target)
	local conversionTable = getConversionTable(dataPageName)
	if not conversionTable then return nil end
	local rate, revisionTime
	if conversionTable[source] and conversionTable[source][target] then
		rate = conversionTable[source][target]['rate']
		rateSignificantDigits = countSigificantDigits(rate)
		revisionTime = conversionTable[source][target]['revisionTime']
	elseif conversionTable[target] and conversionTable[target][source] then
		local targetToSourceRate = conversionTable[target][source]['rate']
		rate = targetToSourceRate^-1
		rateSignificantDigits = countSigificantDigits(targetToSourceRate)
		revisionTime = conversionTable[target][source]['revisionTime']
	end
	return rate, rateSignificantDigits, revisionTime
end

local p = {}

function p._rate(source, target, rounded)
	local dataPageNames = {
		'ECB euro foreign exchange reference rates.tab', 
		'Xe.com exchange rates.tab'}
	local rate, revisionTime, rateSignificantDigits
	for _,name in pairs(dataPageNames) do
		rate, rateSignificantDigits, revisionTime = getDataFromRateDataPage(name, source, target)
		if not rate or not revisionTime then
			for _,name in pairs(dataPageNames) do
				local USDtoTargetRate, UtoTSigDig, UtoTRevTime = getDataFromRateDataPage(name, 'USD', target)
				local USDtoSourceRate, UtoSSigDig, UtoSRevTime = getDataFromRateDataPage(name, 'USD', source)
				if USDtoTargetRate and USDtoSourceRate then
					rate = USDtoTargetRate/USDtoSourceRate
					revisionTime = UtoTRevTime < UtoSRevTime and UtoTRevTime or UtoSRevTime
					rateSignificantDigits = UtoTSigDig < UtoSSigDig and UtoTSigDig or UtoSSigDig
				end
			end
		end
		if rate and revisionTime then
			break
		end
	end
	if rate and revisionTime then
		if rounded then
			rate = round(rate, rateSignificantDigits)
		end
		return rate, revisionTime
	end
end

function p._convert(source, target, amount)
	local rate = p._rate(source, target)
	if rate then
		local amountSigificantDigitsCount = countSigificantDigits(amount)
		return round(amount * rate, amountSigificantDigitsCount + 1)
	end
end

function p._convertSingelOrRange(source, target, amounts)
	local amounts = string.gsub(amounts, ',', '')
	local splitOffset = mw.ustring.find(amounts, '-')
	local converted
	if splitOffset then
		local firstAmount = mw.ustring.sub(amounts, 0, splitOffset -1)
		local secondAmount = mw.ustring.sub(amounts, splitOffset + 1)
		local first = p._convert(source, target, firstAmount)
		local second = p._convert(source, target, secondAmount)
		converted = first and second and first .. '&ndash;' .. second
	else
		converted = p._convert(source, target, amounts)
	end
	return converted
end


function p.rate(frame)
	local args = frame.args
	local rate = p._rate(args.source, args.target, true)
	local result = rate or args.verbose and errormsg
	return result
end

function p.revisionTime(frame)
	local args = frame.args
	local _,revisionTime = p._rate(args.source, args.target)
	local result = revisionTime or args.verbose and errormsg
	return result
end

function p.convert(frame)
	local args = frame.args
	local amount = 	string.gsub(args.amount, ',', '')
	local convertedAmount = p._convert(args.source, args.target, amount)
	local result = convertedAmount or args.verbose and errormsg
	return result
end

function p.convertSingelOrRange(frame)
	local args = frame.args
	local convertedAmounts = p._convertSingelOrRange(
		args.source, args.target, args.amounts)
	local result = convertedAmounts or args.verbose and errormsg
	return result
end

local function currencyWithSymbol(currency, symbolFormat, amount)
	local currencyWithSymbol = (
		symbolFormat and string.format(symbolFormat, amount)
		or currency .. amount)
	return currencyWithSymbol
end

function p.currencyWithConversions(frame)
	local args = frame.args
	local amount = (args.amount and args.amount ~= '') and args.amount or 1
	local i18n = mw.loadData('Module:Exchangerate/i18n')
	local currencySymbols = i18n.symbols[args.currency]
	local shortSymbol = currencySymbols and currencySymbols.shortSymbol
	local currencyWithShortSymbol = currencyWithSymbol(
		args.currency, shortSymbol, amount)
	local uniqueSymbol = currencySymbols and currencySymbols.uniqueSymbol
	local currencyWithUniqueSymbol = currencyWithSymbol(
		args.currency, uniqueSymbol, amount)
	local conversionCurrencies = i18n.defaultConversions or {'USD', 'EUR'}
	local convertedStrings = {}
	for _,convCurrency in ipairs(conversionCurrencies) do
		if args.currency ~= convCurrency then
			local convertedAmount = p._convertSingelOrRange(
				args.currency, convCurrency, amount)
			local convCurrencyUniqueSymbol = (i18n.symbols[convCurrency]
				and i18n.symbols[convCurrency].uniqueSymbol)
			local convCurrencyWithSymbol = convertedAmount and currencyWithSymbol(
				convCurrency, convCurrencyUniqueSymbol, convertedAmount)
			table.insert(convertedStrings, convCurrencyWithSymbol)
		end
	end
	local comma = mw.message.new('comma-separator'):plain()
	local allConvertedStrings = table.concat(convertedStrings, comma)
	local conversions = (allConvertedStrings ~= '') and ' ≈ ' .. allConvertedStrings or ''
	local resultFormat = '<abbr title="%s%s">%s</abbr>'
	local result = string.format(resultFormat, currencyWithUniqueSymbol,
		conversions, currencyWithShortSymbol)
	return result
end

return p