Мой совет - преобразовать Rates в десятичные числа перед тем, как вы начнете их использовать, или фактически: преобразовать все типы в тип, который они действительно представляют.
var result = exchangeRatesTable.Rows.Cast<DataRow>()
.Select(row => new
{
OriginalCurrency = row.Field<string>("OriginalCurrency"),
TargetCurrency = row.Field<string>("TargetCurrency"),
Rate = row.Field<decimal?>("Rate")),
})
// Do the GroupBy and Average:
.GroupBy(row => new {OriginalCurrency, TargetCurrency}, // keySelector
row => row.Rate // elementSelector
(key, ratesWithThisKey) => // resultSelector
{
OriginalCurrency = key.OriginalCurrency,
TargetCurrency = key.TargetCurrency,
// AverageRate: use only Rates that have a value
AverageRate = ratesWithThisKey.Where(rate => rate.HasValue())
.Average();
});
Нулевое значение в DataRow - это DbNull, Field<decimal?>
автоматически преобразует это значение в ноль. Если у вас также есть пустые строки, которые должны быть преобразованы в нуль, рассмотрите:
Rate = String.IsNullOrEmpty(row.Field<string>("Rate") ?
(decimal?)null, // null if null or empty string
row.Field<decimal?>("Rate") // otherwise a decimal?
возможный путь улучшения
Довольно часто люди отделяют данные от способа сериализации (хранения) этих данных. Преимущество в том, что ваш код не зависит от того, как и где хранятся данные. Если впоследствии вы решите сохранить свои данные в формате CSV, JSon, в SQLite или в сложной системе управления базами данных, ваш код не должен будет изменяться.
Поскольку вы делаете данные независимыми от того, как они хранятся, проще создавать тестовые данные для модульных тестов.
Аналогичным образом, если ваши таблицы изменяются, есть только одно место, в котором вы необходимо изменить преобразование из таблицы в данные, которые оно представляет; только в одном месте, где вы должны проверить это преобразование.
Это довольно часто делается в классе Repository. Репозиторий является своего рода фасадом или адаптером между таблицей данных и последовательностью элементов, которые представляют строки в таблице данных.
Довольно часто это преобразование выполняется с использованием метода расширения. Это сделает его похожим на метод LINQ. См. демистифицированные методы расширения
class ExchangeRate
{
public string OriginalCurrency {get; set;}
public string TargetCurrency {get; set;}
public decimal? Rate {get; set;}
}
public static IEnumerable<ExchangeRate> ToExchangeRates(this DataTable dataTable)
{
return dataTable.Rows.Cast<DataRow>().ToExchangeRates();
}
public static IEnumerable<ExchangeRateRate> ToExchangeRates(this IEnumerable<DataRow> source)
{
// TODO: exception if source is null
return source.Select(row => new
{
OriginalCurrency = row.Field<string>("OriginalCurrency"),
TargetCurrency = row.Field<string>("TargetCurrency"),
Rate = row.Field<decimal?>("Rate")),
}
}
(Или используйте альтернативу для Rate).
Использование:
DataTable exchangeRatesTable = ...
var exchangeRates = exchangeRatesTable.ToExchangeRates();
Вы можете использовать это для все функции, где вы планируете использовать exchangeRatesTable. Теперь вам не нужно набирать деталь dataTable.Rows.Cast<DataRow>().Select(row => new ...)
снова и снова. Также есть только одно место, где вы должны его протестировать.
Теперь, когда мы освоили методы расширения, давайте также создадим метод расширения для вычисления средних значений:
class AverageExchangeRate
{
public string OriginalCurrency {get; set;}
public string TargetCurrency {get; set;}
public decimal AverageExchangeRate {get; set;}
}
public static IEnumerable<AverageExchangeRate> ToAverageExchangeRates(this IEnumerable<ExchangeRate> exchangeRates)
{
// TODO: exception if exchangeRates is null
return exchangeRates.GroupBy(row => new {OriginalCurrency, TargetCurrency}, // keySelector
row => row.Rate // elementSelector
(key, ratesWithThisKey) => // resultSelector
{
OriginalCurrency = key.OriginalCurrency,
TargetCurrency = key.TargetCurrency,
// AverageRate: use only Rates that have a value
AverageRate = ratesWithThisKey.Where(rate => rate.HasValue())
.Average();
});
}
Использование:
DataTable exchangeRatesTable = ...
var exchangeRates = exchangeRatesTable.ToExchangeRates()
.ToAverageExchangeRates();
Приятно, что вы можете переплетать это с другими операторами LINQ:
var DollarRates = exchangeRatesTable
.ToExchangeRates()
.Where(exchangeRate => exchangeRate.OriginalCurrency == "USD"
|| exchangeRate.TargetCurrency == "USD")
.ToAverageExchangeRates()
// if desired: continue with other LINQ statements
.Where(...)
.ToList();