Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions jobs/Backend/Task/ExchangeRateModel.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<kurzy banka="CNB" datum="23.10.2025" poradi="207">
<tabulka typ="XML_TYP_CNB_KURZY_DEVIZOVEHO_TRHU">
<radek kod="AUD" mena="dolar" mnozstvi="1" kurz="13,634" zeme="Austrálie"/>
<radek kod="BRL" mena="real" mnozstvi="1" kurz="3,893" zeme="Brazílie"/>
<radek kod="BGN" mena="lev" mnozstvi="1" kurz="12,428" zeme="Bulharsko"/>
<radek kod="CNY" mena="žen-min-pi" mnozstvi="1" kurz="2,943" zeme="Čína"/>
<radek kod="DKK" mena="koruna" mnozstvi="1" kurz="3,254" zeme="Dánsko"/>
<radek kod="EUR" mena="euro" mnozstvi="1" kurz="24,305" zeme="EMU"/>
<radek kod="PHP" mena="peso" mnozstvi="100" kurz="35,766" zeme="Filipíny"/>
<radek kod="HKD" mena="dolar" mnozstvi="1" kurz="2,698" zeme="Hongkong"/>
<radek kod="INR" mena="rupie" mnozstvi="100" kurz="23,863" zeme="Indie"/>
<radek kod="IDR" mena="rupie" mnozstvi="1000" kurz="1,262" zeme="Indonesie"/>
<radek kod="ISK" mena="koruna" mnozstvi="100" kurz="17,092" zeme="Island"/>
<radek kod="ILS" mena="nový šekel" mnozstvi="1" kurz="6,342" zeme="Izrael"/>
<radek kod="JPY" mena="jen" mnozstvi="100" kurz="13,734" zeme="Japonsko"/>
<radek kod="ZAR" mena="rand" mnozstvi="1" kurz="1,208" zeme="Jižní Afrika"/>
<radek kod="CAD" mena="dolar" mnozstvi="1" kurz="14,982" zeme="Kanada"/>
<radek kod="KRW" mena="won" mnozstvi="100" kurz="1,455" zeme="Korejská republika"/>
<radek kod="HUF" mena="forint" mnozstvi="100" kurz="6,232" zeme="Maďarsko"/>
<radek kod="MYR" mena="ringgit" mnozstvi="1" kurz="4,958" zeme="Malajsie"/>
<radek kod="MXN" mena="peso" mnozstvi="1" kurz="1,139" zeme="Mexiko"/>
<radek kod="XDR" mena="ZPČ" mnozstvi="1" kurz="28,551" zeme="MMF"/>
<radek kod="NOK" mena="koruna" mnozstvi="1" kurz="2,098" zeme="Norsko"/>
<radek kod="NZD" mena="dolar" mnozstvi="1" kurz="12,041" zeme="Nový Zéland"/>
<radek kod="PLN" mena="zlotý" mnozstvi="1" kurz="5,743" zeme="Polsko"/>
<radek kod="RON" mena="leu" mnozstvi="1" kurz="4,782" zeme="Rumunsko"/>
<radek kod="SGD" mena="dolar" mnozstvi="1" kurz="16,134" zeme="Singapur"/>
<radek kod="SEK" mena="koruna" mnozstvi="1" kurz="2,226" zeme="Švédsko"/>
<radek kod="CHF" mena="frank" mnozstvi="1" kurz="26,279" zeme="Švýcarsko"/>
<radek kod="THB" mena="baht" mnozstvi="100" kurz="63,859" zeme="Thajsko"/>
<radek kod="TRY" mena="lira" mnozstvi="100" kurz="49,941" zeme="Turecko"/>
<radek kod="USD" mena="dolar" mnozstvi="1" kurz="20,968" zeme="USA"/>
<radek kod="GBP" mena="libra" mnozstvi="1" kurz="27,965" zeme="Velká Británie"/>
</tabulka>
</kurzy>
133 changes: 131 additions & 2 deletions jobs/Backend/Task/ExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Xml.Serialization;
using System.Threading.Tasks;
using Microsoft.VisualBasic;

namespace ExchangeRateUpdater
{
// Class assosiated with kurzy element in API response
[XmlRoot("kurzy")]
public class XMLCourses
{
[XmlAttribute("banka")]
public string Banka { get; set; }

[XmlAttribute("datum")]
public string Datum { get; set; }

[XmlAttribute("poradi")]
public int Poradi { get; set; }

[XmlElement("tabulka")]
public XMLTable Tabulka { get; set; }
}

// Class assosiated with tabulka element in API response
public class XMLTable
{
[XmlAttribute("typ")]
public string Typ { get; set; }

[XmlElement("radek")]
public List<XMLLine> Radek { get; set; }
}

// Class assosiated with radek element in API response
public class XMLLine
{
[XmlAttribute("kod")]
public string Kod { get; set; }

[XmlAttribute("mena")]
public string Mena { get; set; }

[XmlAttribute("mnozstvi")]
public int Mnozstvi { get; set; }

[XmlAttribute("kurz")]
public string Kurz { get; set; }

[XmlAttribute("zeme")]
public string Zeme { get; set; }
}


public class ExchangeRateProvider
{
/// <summary>
Expand All @@ -11,9 +65,84 @@ public class ExchangeRateProvider
/// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide
/// some of the currencies, ignore them.
/// </summary>
public IEnumerable<ExchangeRate> GetExchangeRates(IEnumerable<Currency> currencies)


private async Task<XMLCourses> XMLResponseData()
{
return Enumerable.Empty<ExchangeRate>();
using HttpClient client = new HttpClient();

try
{
// GET request for CNB API
HttpResponseMessage response = await client.GetAsync("https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml");

// Wait for succesful response from API
response.EnsureSuccessStatusCode();

// Read the XML response
using Stream stream = await response.Content.ReadAsStreamAsync();

// Serialize
var serializer = new XmlSerializer(typeof(XMLCourses));

// Instead of reading from file locally, read directly from the response stream
var responseData = (XMLCourses)serializer.Deserialize(stream);

// return the deserialised response
return responseData;

}
catch (HttpRequestException e)
{
Console.WriteLine($"API request error: {e.Message}");
// Need return as will exit with error
return null;
}
}




public async Task<IEnumerable<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies)
{
var data = await XMLResponseData();
var resultData = new List<ExchangeRate>();

foreach(var item in data.Tabulka.Radek)
{
// Extract the currency code from the API response
var ExternalCurrency = new Currency(item.Kod);
bool isContained = false;

// Filter to only show specified currencies
foreach (var cur in currencies)
{
// Check if the currency code from API is in defined currency List
if (cur.Code == ExternalCurrency.Code)
{
isContained = true;
break;
}
}

// If not part of defined list continue
if (!isContained)
continue;


var replaceValue = item.Kurz;

// Replace "," with "." - to be converted to decimal
replaceValue = replaceValue.Replace(",", ".");

// Convert to decimal with ToDecimal() - input of replaceValue
decimal currency = System.Convert.ToDecimal(replaceValue) / item.Mnozstvi;

// Add the exhange to the list in new format
resultData.Add(new ExchangeRate(new Currency("CZK"), ExternalCurrency, currency));
}

return resultData;
}
}
}
6 changes: 4 additions & 2 deletions jobs/Backend/Task/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ public static class Program
new Currency("RUB"),
new Currency("THB"),
new Currency("TRY"),
new Currency("XYZ")
new Currency("XYZ"),
new Currency(""),
};

public static void Main(string[] args)
{
try
{
var provider = new ExchangeRateProvider();
var rates = provider.GetExchangeRates(currencies);
// Use of .GetAwaiter().GetResult() as async not supported for Main() in .NET 6
var rates = provider.GetExchangeRates(currencies).GetAwaiter().GetResult();

Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
foreach (var rate in rates)
Expand Down
19 changes: 19 additions & 0 deletions jobs/Backend/Task/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Technical Assesment
Jon Kehoe

# Features
* Class implemnation of strucurre of CNB API XML response.
* Http call, serialisation and deserialisations.
* Conversion / replacing / filtering

# Getting Started
## Prerequisites
* .NET 6

## Usage
* Navigate to "developers/jobs/Backend/Task"
* run "dotnet run Program.cs"

# Additional Details

Navigate to "developers/jobs/Backend/Task/docs" for more details on approach
Binary file added jobs/Backend/Task/docs/Approach1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added jobs/Backend/Task/docs/Approach2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions jobs/Backend/Task/docs/ExchangeRateApproach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Approach 1

## System Design
![alt text](Approach1.png?raw=true)


The idea of this approach is to use the API provided by Czech National Bank to retrieve the exchange data in the form of XML.

By creating a XML template file that stores an example output of the XML response it can be used in different ways.
Implementing a class associated with this XML format we can determine the expected response and map the elements and attributes. This also offers a mock response that can be used in the case of testing, by using the model of the response we can see that the expected functioning of the program.

The logic in relation to the getting the associated xml attributes is done in the ExchangeRateprovider file. The goal was to read the model based on the XML response and then deserialise into a string. An issue became present in which instead of reading the xml file we could just read it directly from the API stream. Reading directly from the XML file is only useful for files that are already store in the machine or in use in testing. This quickly changed the approach in what would be more efficient.


# Approach 2
## System Design
![alt text](Approach2.png?raw=true)

Based off of Approach 1, instead of using an XML model response file for receiving the data, we could directly extract the information from the stream itself. This lead to using stream async to parse the content in the response. Once the http call has been made, and the response is successful it is then serliasided based off the XML class format allowing to receive different attributes and elements of the response. Once the response is then deserliased, it then can be returned for use in the GetExchangeRates() method.

The GetExchangeRates() method is where the logic is to perform the extraction, filtering and conversion of the currency types.

The main flow is calls the http method which using async Task allows for the asynchronous call instead of waiting for a response it can move onto the next step, this is an advantage if the API takes a while to call and receive a response, especially with a large response. The flow proceeds to create a list to store the new format of the exchange rate. After this it iterates through the XML response based on the “Tabulka.Radek” item which corresponds to the line for each currency based off the elements in the XML response. To filter the data a lock is implemented as a boolean to determine what currencies to return based on the defined currencies and its associated three char code. For the replace of “,” for “.” It uses the Replace() method which replaces the commas for a full stop to represent a decimal place. From here the conversion happens where we can see how much CZK is to the defined currencies. Based off this the data is retuned in a new structure in the format of the CZK / “Currency” and value defined in the public override ToString() method.

# Testing
The test that could be used is to read the mocked XML response instead of calling the http method. It would test to ensure that the data is being properly read and processed according to main implementation.

# Output
Successfully retrieved 5 exchange rates:

CZK/EUR=24.325

CZK/JPY=0.13671

CZK/THB=0.6394

CZK/TRY=0.49848

CZK/USD=20.897

# Challenges

* Instead of using a reader to read based off the model of the XML file, stream was used instead as it provided direct extraction from the response itself.

* Task was used for the GetExchangeRates() method as Task<IEnumerable<ExhangeRate>> does not contain a public instance or extension definition for GetEnumerator.

* To keep the program in line with asynchronous processing, aysnc was used in the Main() method but .NET 6 doesn’t support the use of async for the Main() method so GetAwaiter() and GetResult() were used. By using these methods the call could be made to produce the output but the disadvantage is that it waits for a response which is slower for real time applications and use in production.

* Format of the output was an issue with the use of commas as base on the API response it used commas. To display the value accurately decimal point had to be used as the decimal point would be in a different location leading to an inaccurate output. By using the built in Replace() method, the commas were replaced with decimal points which allowed the ToDecimal() method to convert the string representation to decimal format accurately.

* For the designated test I wasnt able to get working in a test folder due to it not being read by project.