Преобразование строки в десятичное в функции LINQ Average при обработке возможных нулевых значений - PullRequest
1 голос
/ 31 марта 2020

У меня есть коллекция курсов валют, сохраненная в DataTable, которую я хотел бы сгруппировать, получая среднее значение курсов в группе. Моя проблема состоит из двух частей.

  1. Скорость сохраняется в виде строки и должна быть преобразована в десятичную, прежде чем ее можно будет усреднить
  2. Скорость может отсутствовать или быть нулевой, и если это я не хочу включать его в среднее значение

Так вот некоторые примеры данных ...

+------------------+----------------+---------+
| OriginalCurrency | TargetCurrency |   Rate  |
+------------------+----------------+---------+
|        CAD       |       AUD      | 114.495 |
+------------------+----------------+---------+
|        GBP       |       EUR      | 116.111 |
+------------------+----------------+---------+
|        USD       |       GBP      |  77.993 |
+------------------+----------------+---------+
|        GBP       |       EUR      | 115.516 |
+------------------+----------------+---------+
|        USD       |       GBP      |  88.452 |
+------------------+----------------+---------+
|        CAD       |       AUD      | 112.774 |
+------------------+----------------+---------+

Вот оператор LINQ, над которым я работал ...

var groupedRates = exchangeRatesTable.Rows.Cast<DataRow>()
.GroupBy(x => new
{
    OriginalCurrency = x.Field<string>("OriginalCurrency ").ToString(),
    TargetCurrency = x.Field<string>("TargetCurrency ").ToString(),
}).Select(y => new
{
    OriginalCurrency = y.Key.OriginalCurrency ,
    TargetCurrency = y.Key.TargetCurrency ,
    AverageRate = y.Average(r => r.Field<decimal>("Rate"))
});

У меня работает группировка, но Average нет, потому что я не могу понять, как преобразовать string представление "Rate" в decimal в пределах LINQ заявление. Я также не уверен, как обрабатывать строки, в которых «Скорость» пуста или отсутствует.

Ответы [ 3 ]

4 голосов
/ 31 марта 2020

Давайте сделаем шаг назад и переформулируем проблему. У нас есть это предложение:

y.Average(r => r.Field<decimal>("Rate"))

Где y - группа строк. Давайте предположим, что группа была вычислена правильно; если он не был правильно рассчитан, то сначала исправьте эту проблему.

Данная проблема заключается в том, что лямбда-код может потерпеть неудачу, поскольку поле скорости может содержать нулевую, пустую или недесятичную строку. Цель состоит в том, чтобы отбросить таких записей до вычисления среднего.

Правильное решение здесь состоит в том, чтобы решить проблему несколькими небольшими, четко правильными шагами. Во-первых, получите данные в хорошем формате; мы будем sh "нулем, пустым, искаженным", чтобы все были представлены в виде нулевого десятичного числа:

// Consider making this an extension method!
static decimal? ToDecimal(string s)
{
  if (s == null) return null;
  decimal d;
  if (decimal.TryParse(s, out d))
    return d;
  return null;
}

Super. Теперь давайте использовать это. Следующий шаг - использовать этот инструмент, чтобы превратить y в последовательность строк:

y.Select(r => r.Field<string>("Rate"))

ОК, у нас есть последовательность строк. Теперь сделайте это в последовательности обнуляемых десятичных дробей:

 .Select(ToDecimal)

Если этот синтаксис кажется вам странным, вы всегда можете сказать

.Select(r => ToDecimal(r))

, если хотите.

Теперь у нас есть последовательность обнуляемых десятичных дробей. Откажитесь от нулей.

.Where(r => r.HasValue)

Теперь у нас есть последовательность ненулевых обнуляемых десятичных дробей. Сделайте последовательность десятичных знаков.

.Select(r => r.Value)

Теперь у нас есть последовательность ненулевых десятичных знаков. Возьмем среднее значение:

.Average()

И мы закончили.

Я бы повторил предостережение комментатора на посту, что вы должны быть осторожны, применяя Where к последовательности, которая затем перешел на Average. Среднее значение последовательности нулевого элемента не определено.

Также отмечу, что я оптимизировал этот ответ для педагогической ясности . Есть несколько способов решить эту проблему, и некоторые из них короче, чем я показал здесь. Преимущество этого решения состоит в том, что он представляет собой последовательность небольших шагов, каждый из которых приближает нас к решению.

3 голосов
/ 31 марта 2020

Вы должны отфильтровать любую строку, для которой Rate является нулевой или пустой строкой, поскольку допущение, что нулевая или пустая строка эквивалентна 0, может привести к неверному вычислению среднего значения. Тогда вы можете преобразовать свойство Rate в десятичное число:

var groupedRates = exchangeRatesTable.Rows.Cast<DataRow>()
.Where(r => !String.IsNullOrEmpty(r.Field<string>("Rate")))
.GroupBy(x => new
{
    OriginalCurrency = x.Field<string>("OriginalCurrency ").ToString(),
    TargetCurrency = x.Field<string>("TargetCurrency ").ToString(),
})
.Select(y => new
{
    OriginalCurrency = y.Key.OriginalCurrency ,
    TargetCurrency = y.Key.TargetCurrency ,
    AverageRate = y.Average(r => Convert.ToDecimal(r.Field<string>("Rate")))
});
1 голос
/ 31 марта 2020

Мой совет - преобразовать 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();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...