From edf1cc2200a05ea0164f4516a9473e9af9e36945 Mon Sep 17 00:00:00 2001 From: Eva Raimundez Date: Mon, 27 Apr 2026 13:48:54 +0100 Subject: [PATCH 1/2] first version --- jobs/Backend/Task/ExchangeRateProvider.cs | 80 ++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 6f82a97fbe..02bfff2c83 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,19 +1,95 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; +using System.Net.Http; namespace ExchangeRateUpdater { public class ExchangeRateProvider { + private const string SourceUrl = "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt"; + private readonly Currency _targetCurrency = new("CZK"); + private readonly HttpClient _client = new(); + /// /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", /// 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. /// + /// public IEnumerable GetExchangeRates(IEnumerable currencies) { - return Enumerable.Empty(); + var requestedCurrencies = currencies.ToDictionary(c => c.Code, c => c, StringComparer.OrdinalIgnoreCase); + + if (requestedCurrencies.Count == 0) + { + return []; + } + + string rawData; + + try + { + rawData = ReadRawData(); + } + catch (HttpRequestException) + { + return []; + } + catch (IOException) + { + return []; + } + + var lines = rawData.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries); + + var rates = new List(); + + // Skip header rows 0 and 1 + foreach (var line in lines.Skip(2)) + { + try + { + var cols = line.Split('|'); + if (cols.Length < 5) continue; + + var code = cols[3]; + if (requestedCurrencies.ContainsKey(code)) + { + if (int.TryParse(cols[2], out int amount) && amount > 0 && + decimal.TryParse(cols[4], CultureInfo.InvariantCulture, out decimal rateValue)) + { + decimal unitValue = rateValue / amount; + + rates.Add(new ExchangeRate( + requestedCurrencies[code], + _targetCurrency, + unitValue + )); + } + } + } + catch (Exception) // Catch any parsing exceptions and skip the line + { + continue; + } + } + + return rates; + } + + private string ReadRawData() + { + using var request = new HttpRequestMessage(HttpMethod.Get, SourceUrl); + using var response = _client.Send(request); + + response.EnsureSuccessStatusCode(); + + using var reader = new StreamReader(response.Content.ReadAsStream()); + return reader.ReadToEnd(); } } } From 9389a2771426980f5dc673e214dc4842e03d16ab Mon Sep 17 00:00:00 2001 From: Eva Raimundez Date: Tue, 28 Apr 2026 15:08:38 +0100 Subject: [PATCH 2/2] final solution --- jobs/Backend/Task/CnbClient.cs | 56 ++++++++++++++ jobs/Backend/Task/CnbSettings.cs | 5 ++ jobs/Backend/Task/ConfigurationReader.cs | 26 +++++++ jobs/Backend/Task/ExchangeRateProvider.cs | 78 +++++++------------- jobs/Backend/Task/ExchangeRateUpdater.csproj | 14 +++- jobs/Backend/Task/ICnbClient.cs | 7 ++ jobs/Backend/Task/IConfigurationReader.cs | 8 ++ jobs/Backend/Task/Program.cs | 16 +++- jobs/Backend/Task/appSettings.json | 7 ++ 9 files changed, 163 insertions(+), 54 deletions(-) create mode 100644 jobs/Backend/Task/CnbClient.cs create mode 100644 jobs/Backend/Task/CnbSettings.cs create mode 100644 jobs/Backend/Task/ConfigurationReader.cs create mode 100644 jobs/Backend/Task/ICnbClient.cs create mode 100644 jobs/Backend/Task/IConfigurationReader.cs create mode 100644 jobs/Backend/Task/appSettings.json diff --git a/jobs/Backend/Task/CnbClient.cs b/jobs/Backend/Task/CnbClient.cs new file mode 100644 index 0000000000..09eb97111f --- /dev/null +++ b/jobs/Backend/Task/CnbClient.cs @@ -0,0 +1,56 @@ +using Polly; +using Polly.Retry; +using System; +using System.IO; +using System.Net.Http; + +namespace ExchangeRateUpdater +{ + + internal class CnbClient : ICnbClient + { + private readonly CnbSettings _settings; + private readonly HttpClient _client = new(); + private readonly RetryPolicy _retryPolicy; + + public CnbClient(CnbSettings settings) + { + _settings = settings; + _retryPolicy = Policy + .Handle() + .Or() + .WaitAndRetry(_settings.RetryAttempts, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + } + + public (bool Success, string RawData) GetRawDailyExchanges() + { + string rawData; + + try + { + rawData = _retryPolicy.Execute(ReadRawData); + } + catch (HttpRequestException) + { + return (false, string.Empty); + } + catch (IOException) + { + return (false, string.Empty); + } + + return (true, rawData); + } + + private string ReadRawData() + { + using var request = new HttpRequestMessage(HttpMethod.Get, _settings.SourceUrl); + using var response = _client.Send(request); + + response.EnsureSuccessStatusCode(); + + using var reader = new StreamReader(response.Content.ReadAsStream()); + return reader.ReadToEnd(); + } + } +} diff --git a/jobs/Backend/Task/CnbSettings.cs b/jobs/Backend/Task/CnbSettings.cs new file mode 100644 index 0000000000..5af458da4b --- /dev/null +++ b/jobs/Backend/Task/CnbSettings.cs @@ -0,0 +1,5 @@ +namespace ExchangeRateUpdater +{ + public record CnbSettings(string SourceUrl, string TargetCurrencyCode, int RetryAttempts); +} + diff --git a/jobs/Backend/Task/ConfigurationReader.cs b/jobs/Backend/Task/ConfigurationReader.cs new file mode 100644 index 0000000000..e2b6e14184 --- /dev/null +++ b/jobs/Backend/Task/ConfigurationReader.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Configuration; +using System; + +namespace ExchangeRateUpdater +{ + public class ConfigurationReader(IConfiguration config) : IConfigurationReader + { + private readonly IConfiguration _config = config; + private const string SectionName = "CnbSettings"; + + public CnbSettings ReadSettings() + { + var section = _config.GetSection("CnbSettings"); + string url = section["SourceUrl"] ?? throw new InvalidOperationException($"{SectionName}:SourceUrl is missing in appsettings.json."); + string target = section["TargetCurrencyCode"] ?? "CZK"; + + if (!int.TryParse(section["RetryAttempts"], out int retryAttempts)) + { + retryAttempts = 3; // Default fallback + } + + return new CnbSettings(url, target, retryAttempts); + } + } +} + diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 02bfff2c83..70d3b4d05b 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,17 +1,22 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; -using System.Net.Http; namespace ExchangeRateUpdater { public class ExchangeRateProvider { - private const string SourceUrl = "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt"; - private readonly Currency _targetCurrency = new("CZK"); - private readonly HttpClient _client = new(); + private readonly CnbSettings _settings; + private readonly Currency _targetCurrency; + private readonly ICnbClient _cnbClient; + + public ExchangeRateProvider(CnbSettings settings, ICnbClient cnbClient) + { + _settings = settings; + _targetCurrency = new Currency(_settings.TargetCurrencyCode); + _cnbClient = cnbClient; + } /// /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined @@ -27,69 +32,40 @@ public IEnumerable GetExchangeRates(IEnumerable currenci if (requestedCurrencies.Count == 0) { return []; - } - string rawData; - - try - { - rawData = ReadRawData(); } - catch (HttpRequestException) - { - return []; - } - catch (IOException) + + var (success, rawData) = _cnbClient.GetRawDailyExchanges(); + if (!success) { return []; } - var lines = rawData.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries); + var rates = ParseData(rawData, requestedCurrencies); + return rates; + } + private IEnumerable ParseData(string rawData, Dictionary requestedCurrencies) + { var rates = new List(); + var lines = rawData.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries); - // Skip header rows 0 and 1 foreach (var line in lines.Skip(2)) { - try - { - var cols = line.Split('|'); - if (cols.Length < 5) continue; + var cols = line.Split('|'); + if (cols.Length < 5) continue; - var code = cols[3]; - if (requestedCurrencies.ContainsKey(code)) + var code = cols[3].Trim(); + if (requestedCurrencies.TryGetValue(code, out var sourceCurrency)) + { + if (int.TryParse(cols[2].Trim(), out int amount) && amount > 0 && + decimal.TryParse(cols[4].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out decimal rateValue)) { - if (int.TryParse(cols[2], out int amount) && amount > 0 && - decimal.TryParse(cols[4], CultureInfo.InvariantCulture, out decimal rateValue)) - { - decimal unitValue = rateValue / amount; - - rates.Add(new ExchangeRate( - requestedCurrencies[code], - _targetCurrency, - unitValue - )); - } + rates.Add(new ExchangeRate(sourceCurrency, _targetCurrency, rateValue / amount)); } } - catch (Exception) // Catch any parsing exceptions and skip the line - { - continue; - } } - return rates; } - - private string ReadRawData() - { - using var request = new HttpRequestMessage(HttpMethod.Get, SourceUrl); - using var response = _client.Send(request); - - response.EnsureSuccessStatusCode(); - - using var reader = new StreamReader(response.Content.ReadAsStream()); - return reader.ReadToEnd(); - } } } diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 2fc654a12b..b469ca77ff 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -2,7 +2,19 @@ Exe - net6.0 + net10.0 + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/jobs/Backend/Task/ICnbClient.cs b/jobs/Backend/Task/ICnbClient.cs new file mode 100644 index 0000000000..e5d109b3b9 --- /dev/null +++ b/jobs/Backend/Task/ICnbClient.cs @@ -0,0 +1,7 @@ +namespace ExchangeRateUpdater +{ + public interface ICnbClient + { + (bool Success, string RawData) GetRawDailyExchanges(); + } +} diff --git a/jobs/Backend/Task/IConfigurationReader.cs b/jobs/Backend/Task/IConfigurationReader.cs new file mode 100644 index 0000000000..a04b81d630 --- /dev/null +++ b/jobs/Backend/Task/IConfigurationReader.cs @@ -0,0 +1,8 @@ +namespace ExchangeRateUpdater +{ + public interface IConfigurationReader + { + CnbSettings ReadSettings(); + } +} + diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 379a69b1f8..5cd47752b0 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,5 +1,7 @@ -using System; +using Microsoft.Extensions.Configuration; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace ExchangeRateUpdater @@ -23,7 +25,17 @@ public static void Main(string[] args) { try { - var provider = new ExchangeRateProvider(); + var config = new ConfigurationBuilder() + .AddJsonFile(Path.Combine(AppContext.BaseDirectory, "appSettings.json")) + .Build(); + + var configurationReader = new ConfigurationReader(config); + + var settings = configurationReader.ReadSettings(); + + var client = new CnbClient(settings); + + var provider = new ExchangeRateProvider(settings, client); var rates = provider.GetExchangeRates(currencies); Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); diff --git a/jobs/Backend/Task/appSettings.json b/jobs/Backend/Task/appSettings.json new file mode 100644 index 0000000000..50949eca47 --- /dev/null +++ b/jobs/Backend/Task/appSettings.json @@ -0,0 +1,7 @@ +{ + "CnbSettings": { + "SourceUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt", + "TargetCurrencyCode": "CZK", + "RetryAttempts": 3 + } +} \ No newline at end of file