Skip to content

Commit 653a60c

Browse files
committed
Add and test a reader for crypto market quotes
1 parent 9811b74 commit 653a60c

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Globalization;
2+
3+
namespace Taxes.Tests;
4+
5+
[TestClass]
6+
public class CryptoQuotesReaderTest
7+
{
8+
private const string ReportsPath = "../../../../Taxes/Reports";
9+
10+
public static IEnumerable<object[]> AllCryptoQuoteFiles
11+
{
12+
get
13+
{
14+
var files = Directory.GetFiles(ReportsPath, "MarketQuotes_*.csv");
15+
foreach (var file in files)
16+
{
17+
yield return new object[] { file };
18+
}
19+
}
20+
}
21+
22+
[TestMethod]
23+
[DynamicData(nameof(AllCryptoQuoteFiles))]
24+
public void Read_AllFiles_DoesNotThrowAndIsNotEmpty(string filePath)
25+
{
26+
// Act
27+
var quotes = CryptoQuotesReader.Read(filePath).ToList();
28+
29+
// Assert
30+
Assert.IsNotNull(quotes);
31+
Assert.IsTrue(quotes.Count > 0, $"File {filePath} should contain at least one quote.");
32+
33+
// Check that dates are parsed correctly and not default
34+
Assert.IsTrue(quotes[0].Date > new DateTime(2000, 1, 1), $"First quote in {filePath} has an invalid date.");
35+
}
36+
37+
[TestMethod]
38+
public void Read_SpecificFile_ParsesFirstLineCorrectly()
39+
{
40+
// Arrange
41+
var filePath = Path.Combine(ReportsPath, "MarketQuotes_ADAEUR.csv");
42+
// From file: 1747699200000,2025-05-20,ADAEUR,0.6615,0.6703,0.6433,0.6607,2884102.0,1885827.80505,10704
43+
var expectedQuote = new CryptoQuote
44+
{
45+
Unix = 1747699200000,
46+
Date = DateTime.ParseExact("2025-05-20 00:00:00", "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
47+
Symbol = "ADAEUR",
48+
Open = 0.6615m,
49+
High = 0.6703m,
50+
Low = 0.6433m,
51+
Close = 0.6607m,
52+
VolumeCrypto = 2884102.0m,
53+
VolumeBase = 1885827.80505m,
54+
TradeCount = 10704
55+
};
56+
57+
// Act
58+
var firstQuote = CryptoQuotesReader.Read(filePath).FirstOrDefault();
59+
60+
// Assert
61+
Assert.IsNotNull(firstQuote);
62+
Assert.AreEqual(expectedQuote.Unix, firstQuote.Unix);
63+
Assert.AreEqual(expectedQuote.Date, firstQuote.Date);
64+
Assert.AreEqual(expectedQuote.Symbol, firstQuote.Symbol);
65+
Assert.AreEqual(expectedQuote.Open, firstQuote.Open);
66+
Assert.AreEqual(expectedQuote.High, firstQuote.High);
67+
Assert.AreEqual(expectedQuote.Low, firstQuote.Low);
68+
Assert.AreEqual(expectedQuote.Close, firstQuote.Close);
69+
Assert.AreEqual(expectedQuote.VolumeCrypto, firstQuote.VolumeCrypto);
70+
Assert.AreEqual(expectedQuote.VolumeBase, firstQuote.VolumeBase);
71+
Assert.AreEqual(expectedQuote.TradeCount, firstQuote.TradeCount);
72+
}
73+
}

Taxes/CryptoQuote.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Taxes;
2+
3+
internal record CryptoQuote
4+
{
5+
public long Unix { get; init; }
6+
public DateTime Date { get; init; }
7+
public string Symbol { get; init; } = string.Empty;
8+
public decimal Open { get; init; }
9+
public decimal High { get; init; }
10+
public decimal Low { get; init; }
11+
public decimal Close { get; init; }
12+
public decimal VolumeCrypto { get; init; }
13+
public decimal VolumeBase { get; init; }
14+
public long TradeCount { get; init; }
15+
}

Taxes/CryptoQuotesReader.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Globalization;
2+
3+
namespace Taxes;
4+
5+
internal static class CryptoQuotesReader
6+
{
7+
public static IEnumerable<CryptoQuote> Read(string filePath)
8+
{
9+
var lines = File.ReadLines(filePath).Skip(1); // Skip header
10+
11+
foreach (var line in lines)
12+
{
13+
if (string.IsNullOrWhiteSpace(line))
14+
{
15+
continue;
16+
}
17+
18+
var columns = line.Split(',');
19+
if (columns.Length < 10)
20+
{
21+
throw new FormatException($"Line has {columns.Length} columns, expected at least 10. File: '{filePath}', Line: '{line}'");
22+
}
23+
24+
if (!long.TryParse(columns[0], out var unix) ||
25+
!DateTime.TryParse(columns[1], CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ||
26+
!decimal.TryParse(columns[3], NumberStyles.Any, CultureInfo.InvariantCulture, out var open) ||
27+
!decimal.TryParse(columns[4], NumberStyles.Any, CultureInfo.InvariantCulture, out var high) ||
28+
!decimal.TryParse(columns[5], NumberStyles.Any, CultureInfo.InvariantCulture, out var low) ||
29+
!decimal.TryParse(columns[6], NumberStyles.Any, CultureInfo.InvariantCulture, out var close) ||
30+
!decimal.TryParse(columns[7], NumberStyles.Any, CultureInfo.InvariantCulture, out var volumeCrypto) ||
31+
!decimal.TryParse(columns[8], NumberStyles.Any, CultureInfo.InvariantCulture, out var volumeBase) ||
32+
!long.TryParse(columns[9], out var tradeCount))
33+
{
34+
throw new FormatException($"Failed to parse one or more values. File: '{filePath}', Line: '{line}'");
35+
}
36+
37+
yield return new CryptoQuote
38+
{
39+
Unix = unix,
40+
Date = date,
41+
Symbol = columns[2],
42+
Open = open,
43+
High = high,
44+
Low = low,
45+
Close = close,
46+
VolumeCrypto = volumeCrypto,
47+
VolumeBase = volumeBase,
48+
TradeCount = tradeCount
49+
};
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)