1
votes

Convertir la chaîne en décimal dans la fonction LINQ Average tout en gérant les valeurs nulles possibles

J'ai une collection de taux de change, stockés dans un DataTable, que je voudrais regrouper tout en obtenant une moyenne des taux dans un groupe. Mon problème comporte deux parties.

  1. Le taux est stocké sous forme de chaîne et doit être converti en décimal avant de pouvoir être moyenné
  2. Le taux peut être manquant ou nul et si tel est le cas, je ne souhaite pas l'inclure dans la moyenne

Voici donc quelques exemples de données ...

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"))
});

Voici la déclaration LINQ sur laquelle j'ai travaillé ...

+------------------+----------------+---------+
| 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 |
+------------------+----------------+---------+

Le regroupement fonctionne, mais la Moyenne ne l'est pas, car je n'arrive pas à comprendre comment convertir la représentation string de "Rate" en un décimal , dans l'instruction LINQ . Je ne sais pas non plus comment gérer les lignes où le "Taux" est vide ou manquant.


12 commentaires

... réfléchi un peu. Où (...) filtrer les taux nuls ou vides?


C'est un bon point. Je n'ai pas pensé à Where () . Merci. Il suffit maintenant de comprendre la conversion dans l'instruction LINQ.


Au lieu de Rows.Cast () , utilisez .AsEnumerable () .


Notez que si y nul ou vide, Average jette une exception, je pense que la bonne façon est: y.Where (r => decimal.TryParse (r .Field ("Rate"), out _)). Select (r => decimal.Parse (r.Field ("Rate"))). DefaultIfEmpty (). Ave‌ rage (x => x);


Vous dites que vous devez gérer le cas où le champ de taux est une chaîne qui est (1) nulle, (2) vide, (3) une décimale, mais vous ne dites pas si vous devez gérer le cas où la valeur est une chaîne non vide, non nulle qui n'est pas non plus une décimale légale. Avez-vous besoin de gérer cette affaire?


@EricLippert Dans le cas où is n'est pas une valeur qui peut être convertie en décimale, je voudrais simplement lancer la valeur et ne pas l'inclure dans la moyenne, en la traitant essentiellement de la même manière que nulle ou zéro.


OK, aucune des réponses proposées jusqu'ici n'est correcte.


@EricLippert La réponse de @PabloCarballo serait-elle correcte puisqu'elle filtre les valeurs nulles ou vides avec la clause Where en premier?


Je pense que oui, mais si le where renvoie une liste vide, la moyenne lèvera une exception !?


@webworm: il ne filtre pas les champs où Convert.ToDecimal échoue car la chaîne n'est ni nulle, ni vide, ni sous forme de décimale légale.


@Sajid: C'est exact; c'est un problème distinct.


Je pense y.Where (r => decimal.TryParse (r.Field ("Rate"), out _)). Sélectionnez (r => decimal.Parse (r.Field (" Rate "))). DefaultIfEmpty (). Ave‌ rage (x => x); fonctionnera correctement


3 Réponses :


3
votes

Vous devez filtrer toute ligne dont le taux est nul ou une chaîne vide car supposer qu'une chaîne nulle ou vide équivaut à 0 peut entraîner un calcul de moyenne erroné. Ensuite, vous pouvez convertir le taux de propriété en décimal:

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 commentaires

Bien que la réponse de @EricLippert ait été très détaillée et expliquait très bien les choses, j'ai senti que cette réponse répond plus directement à la question de quelqu'un qui cherche un exemple.



4
votes

Revenons ici en arrière et reformulons le problème. Nous avons cette clause:

.Average()

y est un groupe de lignes. Supposons que le groupe a été calculé correctement; s'il n'a pas été calculé correctement, corrigez d'abord ce problème.

Le problème posé est que le lambda peut échouer car le champ de taux peut contenir une chaîne nulle, vide ou non décimale. L'objectif est de supprimer ces enregistrements avant de calculer la moyenne.

La bonne solution ici est de résoudre le problème en plusieurs petites étapes clairement correctes . Premièrement, mettez les données dans un bon format; nous souhaitons que "nul, vide, malformé" soit représenté par un décimal nul:

.Select(r => r.Value)

Super. Maintenant, utilisons-le. L'étape suivante consiste à utiliser cet outil pour transformer y en une séquence de chaînes:

.Where(r => r.HasValue)

OK, nous avons une séquence de chaînes. Maintenant, transformez cela en une séquence de décimales Nullables:

.Select(r => ToDecimal(r))

Si cette syntaxe vous semble étrange, vous pouvez toujours dire

 .Select(ToDecimal)

si vous préférez.

Nous avons maintenant une séquence de décimales nulles. Supprimez les valeurs nulles.

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

Nous avons maintenant une séquence de décimales nullables non nulles. Créez une séquence de décimales.

// 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;
}

Nous avons maintenant une séquence de décimales non nulles. Prenons la moyenne:

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

Et nous avons terminé.

Je voudrais faire écho à la mise en garde d'un commentateur sur le message que vous devez être prudent en appliquant Où à une séquence qui est ensuite passée à Average . La moyenne d'une séquence à zéro élément n'est pas définie.

De plus, je note ici que j'ai optimisé cette réponse pour clarté pédagogique . Il existe un certain nombre de façons de résoudre ce problème et certaines d'entre elles sont plus courtes que ce que j'ai montré ici. Cette solution a l'avantage d'être une série de petites étapes dont chacune nous rapproche d'une solution.


8 commentaires

Merci pour l'explication par étapes qui aide à comprendre ce qui se passe. Cela pourrait-il fonctionner dans une seule instruction LINQ ou devrais-je décomposer les étapes?


@webworm: Je garderais la méthode d'assistance ToDecimal en dehors de la requête, mais tout le reste peut certainement entrer dans une seule grande requête.


Merci @EricLippert. Cela a du sens car il s'agit d'une méthode d'extension. Il suffit de comprendre comment combiner les étapes de l'instruction LINQ. Merci encore!


@EricLippert vous n'avez pas besoin de si (s == null) renvoie null; dans la méthode. null n'est pas une valeur analysée, alors donnera false .


@Sajid: Je préfère que le code soit extrêmement clair sur la façon dont il gère les valeurs nulles. Est-il évident à partir du site d'appel que TryParse gère correctement un null? Ne faites pas ralentir le lecteur de la méthode pour se demander si c'est le cas ou non.


L'utilisation de r => r => r.Field ("Rate") est-elle intentionnelle? Je n'ai jamais vu l'opérateur lambda utilisé de cette manière. Ressemble à un paramètre et un opérateur lambda supplémentaires. Qu'est-ce que je rate?


@webworm: Erreur couper-coller, corrigé, merci. Cependant, c'est une utilisation légale des lambdas si vous modifiez les noms de paramètres formels. x => y => x + y est convertible en Func > . Autrement dit, nous pouvons dire Func > makeAdder = x => y => x + y; et ensuite nous pouvons dire Func addTen = makeAdder (10); int z = addTen (20); // 30


@webworm: Ou nous pourrions dire makeAdder (10) (20) et obtenir 30 directement. Cette technique est courante dans les langages fonctionnels; le processus consistant à séparer une fonction de deux arguments en une fonction d'un argument qui renvoie une fonction d'un argument est appelé "Currying" d'après Haskell Curry, le logicien qui a popularisé cette technique; il active une technique appelée "évaluation partielle", qui est montrée par ma fonction "makeAdder" - il évalue partiellement x + y en évaluant son argument et en le stockant comme valeur de x.



1
votes

Mon conseil serait de convertir les taux en décimales avant de commencer à les utiliser, ou en fait: de convertir tous les types dans le type qu'ils représentent réellement.

var DollarRates = exchangeRatesTable
       .ToExchangeRates()
       .Where(exchangeRate => exchangeRate.OriginalCurrency == "USD" 
                           || exchangeRate.TargetCurrency == "USD")
       .ToAverageExchangeRates()
       // if desired: continue with other LINQ statements
       .Where(...)
       .ToList();

Une valeur nulle dans un DataRow est un DbNull, Field le convertira automatiquement en null. Si vous avez également des chaînes vides qui doivent être converties en null, considérez:

DataTable exchangeRatesTable = ...
var exchangeRates = exchangeRatesTable.ToExchangeRates()
                                      .ToAverageExchangeRates();

moyen d'amélioration possible

Très souvent, les gens découplent les données de la façon dont cela les données sont sérialisées (stockées). Cela présente l'avantage que votre code est indépendant de la manière et de l'emplacement de stockage des données. Si par la suite vous décidez de stocker vos données au format CSV, JSon, dans SQLite ou dans un système de gestion de base de données lourd, votre code n'aura pas à changer.

Parce que vous rendez les données indépendantes de la façon dont les données sont stockées, il est plus facile de créer des données de test pour les tests unitaires.

De même, si vos tables changent, il n'y a qu'un seul endroit que vous devez changer la conversion de la table aux données qu'elle représente; un seul endroit où vous devez tester cette conversion.

Ceci est assez souvent fait dans une classe Repository. Le référentiel est une sorte de façade, ou adaptateur entre votre datatable et la séquence d'éléments que les lignes de la datatable représentent.

Très souvent, cette conversion est effectuée en utilisant une méthode d'extension. Cela le fera ressembler à une méthode LINQ. Voir méthodes d'extension démystifiées

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();
    });
}

(Ou utilisez l'alternative pour Rate).

Utilisation:

DataTable exchangeRatesTable = ...
var exchangeRates = exchangeRatesTable.ToExchangeRates();

Vous pouvez l'utiliser pour toutes les fonctions dans lesquelles vous prévoyez d'utiliser exchangeRatesTable. Vous n'avez plus besoin de taper la partie dataTable.Rows.Cast () .Select (row => new ...) encore et encore. De plus, il n'y a qu'un seul endroit où vous devez le tester.

Maintenant que nous maîtrisons les méthodes d'extension, créons également une méthode d'extension pour calculer les moyennes:

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")),
    }
}

Utilisation :

Rate = String.IsNullOrEmpty(row.Field<string>("Rate") ?
              (decimal?)null,                       // null if null or empty string
              row.Field<decimal?>("Rate")           // otherwise a decimal?

La bonne chose est que vous pouvez entrelacer ceci avec d'autres instructions LINQ:

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();
    });


0 commentaires