Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempt at solving the C# home assignment #629

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Task\ExchangeRateUpdater.csproj" />
</ItemGroup>

</Project>
32 changes: 32 additions & 0 deletions jobs/Backend/ExchangeRateUpdater.Tests/Ext/CnbFxRowTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using ExchangeRateUpdater.Model;
using JetBrains.Annotations;
using Xunit;

namespace ExchangeRateUpdater.Tests.Ext;

[TestSubject(typeof(CnbFxRow))]
public class CnbFxRowTest
{

[Fact]
public void ThrowsIfNot5Parts()
{
var testStr = "Brazílie|real|1|BRL";
Assert.Throws<ArgumentException>(() => CnbFxRow.FromSeparatedString(testStr, '|'));
}


[Fact]
public void ParsesDelimitedString()
{
var testStr = "Brazílie|real|1|BRL|4,283";
var result = CnbFxRow.FromSeparatedString(testStr, '|');

Assert.Equal("Brazílie", result.Country);
Assert.Equal("real", result.CurrencyStr);
Assert.Equal(1, result.Amount);
Assert.Equal(new Currency("BRL"), result.Currency);
Assert.Equal(4.283, (double) result.Rate);
}
}
23 changes: 23 additions & 0 deletions jobs/Backend/ExchangeRateUpdater.Tests/Ext/CnbTxtParserTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using ExchangeRateUpdater.Model;
using JetBrains.Annotations;
using Xunit;

namespace ExchangeRateUpdater.Tests.Ext;

[TestSubject(typeof(CnbTxtParser))]
public class CnbTxtParserTest
{

[Fact]
public void CanParseResponse()
{
var response = @"15.07.2024 #135
země|měna|množství|kód|kurz
Austrálie|dolar|1|AUD|15,800
Brazílie|real|1|BRL|4,283
";

var result = CnbTxtParser.ParseResponse(response);
Assert.Equal(2, result.Count);
}
}
10 changes: 10 additions & 0 deletions jobs/Backend/ExchangeRateUpdater.Tests/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace ExchangeRateUpdater.Tests;

// I don't know why I need this class, but I cannot run tests without it
public class Program
{
public static void Main()
{

}
}
18 changes: 18 additions & 0 deletions jobs/Backend/Task/CurrencyRateClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ExchangeRateUpdater.Model;

namespace ExchangeRateUpdater;

public interface ICurrencyRateClient
{

Task<List<ExchangeRate>> GetLatestExchangeRates()
{
return GetExchangeRates(DateOnly.FromDateTime(DateTime.Now));
}

Task<List<ExchangeRate>> GetExchangeRates(DateOnly forDate);

}
30 changes: 30 additions & 0 deletions jobs/Backend/Task/DbMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#nullable enable

using System;
using System.Collections.Generic;
using ExchangeRateUpdater.Model;

namespace ExchangeRateUpdater;

/**
* Simulates a real data store of some kind, perhaps an SQL instance
*/

public class DbMock
{
private static readonly Dictionary<DateOnly, List<ExchangeRate>> Cache = new();

/**
* Would be a database call to see if we have already persisted rates for the given date
*/
public static List<ExchangeRate>? FindExchangeRates(DateOnly forDate)
{
return Cache.GetValueOrDefault(forDate);
}

public static void Insert(DateOnly forDate, List<ExchangeRate> newRates)
{
Cache[forDate] = newRates;
}
}

26 changes: 24 additions & 2 deletions jobs/Backend/Task/ExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using ExchangeRateUpdater.Model;

namespace ExchangeRateUpdater
{

public class ExchangeRateProvider
{
private readonly ICurrencyRateClient _currencyRateClient;
public ExchangeRateProvider(ICurrencyRateClient currencyRateClient)
{
_currencyRateClient = currencyRateClient;
}

/// <summary>
/// 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",
Expand All @@ -13,7 +22,20 @@ public class ExchangeRateProvider
/// </summary>
public IEnumerable<ExchangeRate> GetExchangeRates(IEnumerable<Currency> currencies)
{
return Enumerable.Empty<ExchangeRate>();
var fromDateTime = DateOnly.FromDateTime(DateTime.Now);
var existingRates = DbMock.FindExchangeRates(fromDateTime);
var allRates = existingRates ?? FetchAndStoreNewRates(fromDateTime);
// Filter for the queried ones only
return allRates.FindAll(rate => currencies.Contains(rate.SourceCurrency));
}

private List<ExchangeRate> FetchAndStoreNewRates(DateOnly forDate)
{
Console.WriteLine($"Fetching new exchange rates for {forDate}");
var newRates = _currencyRateClient.GetExchangeRates(forDate).Result;
Console.WriteLine($"Received {newRates.Count} new rates from {_currencyRateClient.GetType().Name}");
DbMock.Insert(forDate, newRates);
return newRates;
}
}
}
50 changes: 28 additions & 22 deletions jobs/Backend/Task/ExchangeRateUpdater.sln
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25123.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{FD216A8A-1723-4627-BB9C-D0FFB198D515}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Tests", "..\ExchangeRateUpdater.Tests\ExchangeRateUpdater.Tests.csproj", "{0BFF5B5E-DACE-4BED-8B72-7E7D19A8643D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FD216A8A-1723-4627-BB9C-D0FFB198D515}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD216A8A-1723-4627-BB9C-D0FFB198D515}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD216A8A-1723-4627-BB9C-D0FFB198D515}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD216A8A-1723-4627-BB9C-D0FFB198D515}.Release|Any CPU.Build.0 = Release|Any CPU
{0BFF5B5E-DACE-4BED-8B72-7E7D19A8643D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0BFF5B5E-DACE-4BED-8B72-7E7D19A8643D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BFF5B5E-DACE-4BED-8B72-7E7D19A8643D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BFF5B5E-DACE-4BED-8B72-7E7D19A8643D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
32 changes: 32 additions & 0 deletions jobs/Backend/Task/Ext/CnbClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using ExchangeRateUpdater.Model;

namespace ExchangeRateUpdater.Ext;

public class CnbClient : ICurrencyRateClient
{
private const string TxtUrl = "https://www.cnb.cz/cs/financni-trhy/devizovy-trh/kurzy-devizoveho-trhu/kurzy-devizoveho-trhu/denni_kurz.txt";
private const string DateFmt = "dd.MM.YYYY";
private static readonly Currency TargetRate = new("CZK");

public async Task<List<ExchangeRate>> GetExchangeRates(DateOnly forDate)
{
using var client = new HttpClient();
var fullUrl = TxtUrl + "?date=" + forDate.ToString(DateFmt);
var response = await client.GetStringAsync(fullUrl);
var parsedResponse = CnbTxtParser.ParseResponse(response);

return parsedResponse.Select(ToExchangeRate).ToList();
}

private static ExchangeRate ToExchangeRate(CnbFxRow row)
{
// Some exchange rates are reported in 100s, e.g. JPY, so adjust rate accordingly.
var adjustedRate = row.Rate / row.Amount;
return new ExchangeRate(row.Currency, TargetRate, adjustedRate);
}
}
28 changes: 28 additions & 0 deletions jobs/Backend/Task/Ext/CnbFxRow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Globalization;

namespace ExchangeRateUpdater.Model;

public record CnbFxRow(string Country, string CurrencyStr, long Amount, Currency Currency, decimal Rate)
{
public static CnbFxRow FromSeparatedString(string theString, char delimiter)
{
var split = theString.Split(delimiter);
if (split.Length != 5)
{
throw new ArgumentException("Expected 5 parameters, got: " + split.Length);
}

Console.WriteLine(split[4]);
// Decimal notation is a `,` comma, so parse it with a comma-supported culture/fmt, using FR here
var parsedDecimal = decimal.Parse(split[4], NumberStyles.Number, new CultureInfo("fr-FR"));

return new CnbFxRow(
Country: split[0],
CurrencyStr: split[1],
Amount: long.Parse(split[2]),
Currency: new Currency(split[3]),
Rate: parsedDecimal
);
}
}
20 changes: 20 additions & 0 deletions jobs/Backend/Task/Ext/CnbTxtParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace ExchangeRateUpdater.Model;

public class CnbTxtParser
{
private static readonly char DELIMITER = '|';

public static List<CnbFxRow> ParseResponse(string rawResponse)
{
return rawResponse
.Split("\n")
.Skip(2) // Header rows skipped
.SkipLast(1) // Skip empty row at the end
.Select(line => CnbFxRow.FromSeparatedString(line, DELIMITER))
.ToList();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
namespace ExchangeRateUpdater
using System;
using System.Text.RegularExpressions;

namespace ExchangeRateUpdater.Model
{
public class Currency
public record Currency
{

private static readonly Regex ISO_4217 = new("^[A-Z]{3}$");

public Currency(string code)
{
if (!ISO_4217.IsMatch(code)) throw new ArgumentException("Invalid currency code: " + code);
Code = code;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ExchangeRateUpdater
namespace ExchangeRateUpdater.Model
{
public class ExchangeRate
{
Expand Down
12 changes: 10 additions & 2 deletions jobs/Backend/Task/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ExchangeRateUpdater.Ext;
using ExchangeRateUpdater.Model;

namespace ExchangeRateUpdater
{
Expand All @@ -23,13 +25,19 @@ public static void Main(string[] args)
{
try
{
var provider = new ExchangeRateProvider();
var provider = new ExchangeRateProvider(new CnbClient());
var rates = provider.GetExchangeRates(currencies);

Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
foreach (var rate in rates)
{
Console.WriteLine(rate.ToString());
Console.WriteLine(rate);
}

foreach (var rate in rates)
{
var exampleAmount = Math.Round(1000 * rate.Value, 2, MidpointRounding.AwayFromZero);
Console.WriteLine($"Converting 1000 {rate.SourceCurrency} to {rate.TargetCurrency} using ({rate}): {exampleAmount} {rate.TargetCurrency}");
}
}
catch (Exception e)
Expand Down