POWERQUERY :: Importer un fichier .csv avec construction de critères dynamiques (5 max) de type texte et sélection entre deux dates possible - V2.0

oguruma

XLDnaute Occasionnel
Bonjour, ce poste fait suite à :
https://excel-downloads.com/threads...election-dynamique-entre-deux-dates.20083419/ et à https://excel-downloads.com/threads...0-000-de-lignes.20083249/page-6#post-20648819 (qui a eu beaucoup de verbosité ;))

Cette version 2.0 propose désormais de placer des critères de sélection entre deux dates ainsi que des critères de sélection de type texte UNIQUEMENT. 5 critères maximum.

Recommandation : il est conseillé de placer vos critères dans l'ordre 1....5 pour éviter des dysfonctionnements de la requête.
Un bon nombre de cas on été testés mais je suis peut-être passé au travers de certains qui provoqueraient des erreurs majeures. Me le faire savoir en réponse suite à vos tests.

Description des paramètres

1719160001020.png


Certains parlent d'eux-mêmes, je vais donc passer très vite.

- SEPARATEUR : séparateur de colonne - saisie libre
- ENCODAGE : une liste est proposée
1719160129600.png

- QUOTE_STYLE : liste
1719160165438.png

- LIGNE_ENTETE : votre fichier comporte-t-il une ligne d'entête ou pas (OUI/NON)
- SEPARATEUR_ENREGISTREMENTS :
1719160272191.png

Ce paramètre est utilisé pour importer les colonnes du fichier (j'y reviendrai plus bas)
- CULTURE :
1719160337551.png

Il est utilisé pour formater une colonne date selon le pays
- CHAMP_DATE : dans la version précédente il "fallait aller à la pêche" dans le fichier texte pour identifier ce champ. Ceci est désormais simplifié par une liste déroulante qui propose la liste des colonnes se trouvant dans le fichier texte
1719160482774.png


- DATE_DEB et DATE_FIN : sont les dates de début et de fin
- Nous avons ensuite les critères de type texte
1719160547248.png


CHAMP_x : propose la liste déroulante des colonnes
CRITERE_x : saisie libre du critère
OPERATEUR_x :
1719160616689.png


Exemple de critère :
1719160638159.png


CONNECTEUR : and, or : par défaut si rien n'est renseigné ce sera and

Après avoir renseigner ces paramètres : Importer les colonnes
1719160713833.png
1719160686971.png

- Le paramètre SEPARATEUR_ENREGRISTREMENT a ici toute son importance.


En effet pour importer les colonnes du fichier texte
1719160834890.png

on passe par l'importation du fichier en tant que binaire "inutile d'activer toute l'artillerie Pwq" pour lire les deux 1ères lignes.
Voici le code :
PowerQuery:
let
    //***********************************************************************************************************
    // On récupère les paramètres de la table TB_PARAMS
    //***********************************************************************************************************
    FileName=fnGetParameters("TB_PARAMS","CHEMIN_COMPLET"), 
    t_Separator=fnGetParameters("TB_PARAMS","SEPARATEUR"),
    Separator=if t_Separator is null then ";" else t_Separator,
    bPromote=if fnGetParameters("TB_PARAMS","LIGNE_ENTETE")="OUI" then true else false, 
    t1_Separator_Enreg=fnGetParameters("TB_PARAMS","SEPARATEUR_ENREGISTRMENTS"), 
    t2_Separator_Enreg = if t1_Separator_Enreg is null then "CR/LF" else t1_Separator_Enreg,
    Separator_Enreg = if t2_Separator_Enreg = "CR/LF"
                      then
                            "#(cr)#(lf)"
                      else if t2_Separator_Enreg = "CR"
                      then
                            "#(cr)"
                      else if t2_Separator_Enreg = "LF"
                      then
                            "#(lf)"
                      else "#(cr)#(lf)",

    //Separator_Enreg =  "#(cr)#(lf)",
    //***********************************************************************************************************
    // Importation des données
    //***********************************************************************************************************
    isFileExists=fnFileExists(FileName), // Le fichier existe ?
    ListColumns = if isFileExists
             then
                let
                    Src=try
                    // La source de données est chargée en l'état en mode binaire
                    // Inutile de lancer le formatage Pwq pour récupèrer les deux premières lignes texte
                    List.Buffer(Text.Split(Text.FromBinary(Binary.Buffer(File.Contents(FileName))),Separator_Enreg))
                    otherwise "#ERROR",                 
                    L1=Src{0}, // ligne 1
                    L2=Src{1}, // ligne 2
                    // et on prend l'une des deux lignes pour placer les titres de colonnes
                    L3=if bPromote then Text.Split(L1,Separator) else Text.Split(L2,Separator)
                in
                    L3
             else
                "#ERROR#"
 
in
    ListColumns

On voit donc ici l'importance de la saisie du paramètre SEPRATEUR_ENREGISTREMENT :
PowerQuery:
    Separator_Enreg = if t2_Separator_Enreg = "CR/LF"

                      then

                            "#(cr)#(lf)"

                      else if t2_Separator_Enreg = "CR"

                      then

                            "#(cr)"

                      else if t2_Separator_Enreg = "LF"

                      then

                            "#(lf)"

                      else "#(cr)#(lf)",

Le chargement du binaire se fait ici : List.Buffer(Text.Split(Text.FromBinary(Binary.Buffer(File.Contents(FileName))),Separator_Enreg))

Après avoir importer les colonnes (si besoin de les connaître afin de placer des filtres) il suffit de cliquer sur
1719161152541.png
pour importer ce fichier.

Organisation et explication du code PowerQuery/M


Pour des raisons de simplicité et de relecture le code a été divisé en deux parties :
PowerQuery:
let
  
    //******************************************************************************************************
    // On va s'appuyer le résultat de la requête de recherche entre deux dates pour appliquer le filtre
    // Le résultat de la recherche entre deux dates est dans TB_DATA_SELECT_DATE
    // Puis on applique les filtres sur les autres colonnes
    //******************************************************************************************************
    rcFunc=fnGetStringCriteria(),
  
    //******************************************************************************************************
    // Si aucun critère de sélection préciser on renvoie la totalité de la table des données
    //******************************************************************************************************
    QUERY_FILTER_STRING = if rcFunc="" then TB_DATA_SELECT_DATE else Table.SelectRows(TB_DATA_SELECT_DATE, fnGetStringCriteria())

in
    QUERY_FILTER_STRING

Partie 1 : On applique le critère de sélection entre date et on lance requête TB_DATA_SELECT_DATE
TB_DATA_SELECT_DATE : c'est la requête maîtresse qui est issue de la requête initiale de chargement du fichier : TB_DATA_SOURCE

En effet on applique une sélection sur les dates à partir de la source afin de produire TB_DATA_SELECT_DATE
Partie 2 : Construction de la requête permettant de filtrer sur les colonnes texte.
Attention les colonnes doivent être uniques (c'est une des contraintes). En effet en critères 1 et 2 on ne peut pas avoir le même nom champ. En effet ils sont mémorisés dans une liste PowerQuery.


L'enchainement est donc le suivant :
1) TB_DATA_SOURCE
2) TB_DATE_SELECT_DATE
3) Application des critères de type texte

1) TB_DATA_SOURCE

PowerQuery:
let
    //***********************************************************************************************************
    // On récupère les paramètres de la table TB_PARAMS
    //***********************************************************************************************************
    FileName=fnGetParameters("TB_PARAMS","CHEMIN_COMPLET"), 
    t_FileEncoding=fnGetParameters("TB_PARAMS","ENCODAGE"),
    FileEncoding=if t_FileEncoding is null then 65001 else Number.From(Text.Split(t_FileEncoding,"|"){1}),
    t_Separator=fnGetParameters("TB_PARAMS","SEPARATEUR"),
    Separator=if t_Separator is null then ";" else t_Separator,
    bPromote=if fnGetParameters("TB_PARAMS","LIGNE_ENTETE")="OUI" then true else false, 
    t_QuoteStyle=fnGetParameters("TB_PARAMS","QUOTE_STYLE"),
    QuoteStyle=if t_QuoteStyle is null then 0 else Number.From(Text.Split(t_QuoteStyle,"|"){1}),
    //***********************************************************************************************************
    // Importation des données
    //***********************************************************************************************************
    isFileExists=fnFileExists(FileName), // Le fichier existe ?
    TbData = if isFileExists
             then
                let
                    Src1=try
                    Csv.Document(File.Contents(FileName),[Delimiter=Separator, Columns=null, Encoding=FileEncoding, QuoteStyle=QuoteStyle])
                    otherwise "#ERROR",
                    //***********************************************************************************************************
                    // Gestion de la ligne titre des colonnes
                    //***********************************************************************************************************
                    Src2=try
                    if bPromote then Table.PromoteHeaders(Src1, [PromoteAllScalars=true]) else Src1
                    otherwise "#ERROR#"
                in
                    Src2
             else
                "#ERROR#"
in
    TbData

2) TB_DATE_SELECT_DATE

PowerQuery:
let

    //***********************************************************************************************************
    // On récupère les paramètres de la table TB_PARAMS
    //*********************************************************************************************************** 
    FieldDate=fnGetParameters("TB_PARAMS","CHAMP_DATE"),
    Field=fnGetParameters("TB_PARAMS","FIELD"),
    Criteria=fnGetParameters("TB_PARAMS","CRITERIA"),
    t_DATE_DEB=fnGetParameters("TB_PARAMS","DATE_DEB"),
    t_DATE_FIN=fnGetParameters("TB_PARAMS","DATE_FIN"), 
    Region=fnGetParameters("TB_PARAMS","CULTURE"), 
    bPromote=if fnGetParameters("TB_PARAMS","PROMOTE")="OUI" then true else false, 

    //***********************************************************************************************************
    // Contrôle sur le format des dates saisies - on supprimer les espaces éventuels
    //***********************************************************************************************************
    DateDeb=if t_DATE_DEB is text then  Text.Trim(t_DATE_DEB) else t_DATE_DEB,
    DateFin=if t_DATE_FIN is text then  Text.Trim(t_DATE_FIN) else t_DATE_FIN, 
  
    //***********************************************************************************************************
    // On détermine l'interval de dates à retenir
    //***********************************************************************************************************
    ToDateDateDeb=if DateDeb <> null then #date(Date.Year(DateDeb),Date.Month(DateDeb),Date.Day(DateDeb)) else null,
    ToDateDateFin=if DateFin <> null then #date(Date.Year(DateFin),Date.Month(DateFin),Date.Day(DateFin)) else null,
    isDateBetween=if ToDateDateDeb is date and ToDateDateFin is date then true else false,
    isDateLessOrEqual=if ToDateDateDeb is null and ToDateDateFin is date then true else false,
    isDateGreaterOrEqual=if ToDateDateDeb is date and ToDateDateFin is null then true else false,

    //***********************************************************************************************************
    // Un champ date est-il renseigné dans les paramètres ?
    //***********************************************************************************************************
    isFieldDateExists = if Text.Trim(FieldDate) is null then false else true,

    //***************************************************************************************************************
    // Cas où la date n'est pas transmise dans les paramètres on prend toutes les données au moment de la sélection
    //***************************************************************************************************************
    isFullSource=if ToDateDateDeb is null and ToDateDateFin is null then true else false, 

    //***********************************************************************************************************
    // Gestion de la ligne titre des colonnes - Source = TB_DATA_SOURCE
    //***********************************************************************************************************
    TableToPromote = try
                        TB_DATA_SOURCE
                    otherwise "#ERROR#", 

    //***********************************************************************************************************
    // On récupère la liste des colonnes à partir de la liste déjà calculée
    //***********************************************************************************************************
    ListColumns=LISTE_DES_COLONNES,
    PosField=List.PositionOf(ListColumns,FieldDate),

    //***********************************************************************************************************
    // Nettoyage du champ date et formatage type date si au moins un champ date est renseigné
    // Le champ date spécifié doit exister dans les colonnes de la table
    //***********************************************************************************************************
    TableToSelect = try
                        if isFieldDateExists and PosField <> -1
                        then
                            let
                                CleanText = Table.TransformColumns(TableToPromote,{{FieldDate, Text.Clean, type text}}),
                                DelSpaces = Table.TransformColumns(CleanText,{{FieldDate, Text.Trim, type text}}),
                                // On applique les paramètres de formatage régionaux
                                TableTypeRegion =  Table.TransformColumnTypes(DelSpaces, {{FieldDate, type date}}, Region)
                            in
                                TableTypeRegion
                        else
                            TableToPromote
                    otherwise "#ERROR",

    //***********************************************************************************************************
    // On prépare la sélection entre les dates pour les évaluer dans Expression.Evaluate
    //***********************************************************************************************************
    StrEvalFilterLessOrEqual="each [" & FieldDate & "] <= ToDateDateFin",
    StrEvalFilterGreaterOrEqual="each [" & FieldDate &"] >= ToDateDateDeb", 
    StrEvalFilterBetween="each [" & FieldDate &"] >= ToDateDateDeb and [" & FieldDate & "] <= ToDateDateFin",

    //***********************************************************************************************************
    // Calcul sur quelle tranche de date on doit appliquer la sélection
    //***********************************************************************************************************
    StrEval = try if isDateBetween
              // Deux dates ont été saisies
              then  StrEvalFilterBetween
              else
                    // Seule la date de début a été saisie
                    if isDateGreaterOrEqual
                    then  StrEvalFilterGreaterOrEqual
                    else
                    // Seule la date de fin a été saisie
                        if isDateLessOrEqual
                        then  StrEvalFilterLessOrEqual
                        else  "#N/A"
                otherwise "#ERROR#",

    //***********************************************************************************************************
    // On applique la sélection en tenant compte si des dates sont renseignées
    //***********************************************************************************************************
    //*************************************************************************************************************************************
    // si aucune sélection de date, si le champ date est inconnu, si on ne précise pas de champ date alors on renvoie la table complète
    // sinon on applique le filtre sur les dates
    //*************************************************************************************************************************************
    SelectToTable = try if isFullSource or not isFieldDateExists or PosField=-1 or StrEval is null or StrEval = "#ERROR#"
                    then TableToSelect
                    else
                        if StrEval = "#N/A"
                            then #table({},{})
                            else let
                                    EvalFilter=Expression.Evaluate(StrEval, [Table.SelectRows=Table.SelectRows, ToDateDateDeb=ToDateDateDeb, ToDateDateFin=ToDateDateFin, FieldDate=FieldDate]),
                                    Select = try Table.SelectRows(TableToSelect, EvalFilter) otherwise "#ERROR#",
                                    Sort = try Table.Sort(Select,{{FieldDate, Order.Ascending}}) otherwise "#ERROR#"
                                in
                                    Sort
                    // Et en cas d'impossibilité d'effectuer la requête on renvoie ce message
                    otherwise "Erreur de traitement. Vérifier vos paramètres." 
in
    SelectToTable

3) Application des critères de type texte

Pour cette dernière étape on passe par une fonction qui va construire le code nécessaire M à l'application des filtres. Ce code est largement documenté surtout pour les non initiés au langage M

PowerQuery:
let fnGetStringCriteria = () =>
        let

            //***********************************************************************************************************************
            // Définition des constantes
            //***********************************************************************************************************************
            GUI="""",
            SEP_LIST_1="§",
            SEP_LIST_2="µ",
            RECORD_FUNCTIONS=[Text.Contains=Text.Contains, Text.StartsWith=Text.StartsWith,Text.EndsWith=Text.EndsWith],
            LIST_STR_OPERATORS=List.Buffer({"Egal","Différent","Contient","Ne contient pas","Commence par","Fini par","Ne comment pas par","Ne fini pas par"}),
            LIST_FUNC_OPERATORS=List.Buffer({"=","<>","Text.Contains","not Text.Contains","Text.StartsWith","Text.EndsWith","not Text.StartsWith","not Text.EndsWith"}), 

            //***********************************************************************************************************************
            // On récupère les critères des colonnes - 5 maxi
            //***********************************************************************************************************************
            f_CHAMP_1=fnGetParameters("TB_PARAMS","CHAMP_1"),
            f_CRITERE_1=fnGetParameters("TB_PARAMS","CRITERE_1"),
            f_OPERATEUR_1=fnGetParameters("TB_PARAMS","OPERATEUR_1"),

            f_CHAMP_2=fnGetParameters("TB_PARAMS","CHAMP_2"),
            f_CRITERE_2=fnGetParameters("TB_PARAMS","CRITERE_2"),
            f_OPERATEUR_2=fnGetParameters("TB_PARAMS","OPERATEUR_2"),

            f_CHAMP_3=fnGetParameters("TB_PARAMS","CHAMP_3"),
            f_CRITERE_3=fnGetParameters("TB_PARAMS","CRITERE_3"),
            f_OPERATEUR_3=fnGetParameters("TB_PARAMS","OPERATEUR_3"),

            f_CHAMP_4=fnGetParameters("TB_PARAMS","CHAMP_4"),
            f_CRITERE_4=fnGetParameters("TB_PARAMS","CRITERE_4"),
            f_OPERATEUR_4=fnGetParameters("TB_PARAMS","OPERATEUR_4"),

            f_CHAMP_5=fnGetParameters("TB_PARAMS","CHAMP_5"),
            f_CRITERE_5=fnGetParameters("TB_PARAMS","CRITERE_5"),
            f_OPERATEUR_5=fnGetParameters("TB_PARAMS","OPERATEUR_5"),

            //***********************************************************************************************************************
            // Le connecteur ET (and) OU(or)
            //***********************************************************************************************************************
            t_Connector=fnGetParameters("TB_PARAMS","CONNECTEUR"),
            Connector=if t_Connector is null then "and" else fnGetParameters("TB_PARAMS","CONNECTEUR"),

            //***********************************************************************************************************************
            // Sécurisation si un critère n'est pas renseigné on force à NULL en caractère pour éviter d'avoir une liste vide
            //***********************************************************************************************************************
            t_Criteria1=if f_CHAMP_1 is null or f_CRITERE_1 is null or f_OPERATEUR_1 is null  then "NULL"  else f_CHAMP_1 & SEP_LIST_1 & f_CRITERE_1 & SEP_LIST_1 & f_OPERATEUR_1,
            t_Criteria2=if f_CHAMP_2 is null or f_CRITERE_2 is null or f_OPERATEUR_2 is null  then "NULL"  else f_CHAMP_2 & SEP_LIST_1 & f_CRITERE_2 & SEP_LIST_1 & f_OPERATEUR_2,
            t_Criteria3=if f_CHAMP_3 is null or f_CRITERE_3 is null or f_OPERATEUR_3 is null  then "NULL"  else f_CHAMP_3 & SEP_LIST_1 & f_CRITERE_3 & SEP_LIST_1 & f_OPERATEUR_3,
            t_Criteria4=if f_CHAMP_4 is null or f_CRITERE_4 is null or f_OPERATEUR_4 is null  then "NULL"  else f_CHAMP_4 & SEP_LIST_1 & f_CRITERE_4 & SEP_LIST_1 & f_OPERATEUR_4,
            t_Criteria5=if f_CHAMP_5 is null or f_CRITERE_5 is null or f_OPERATEUR_5 is null  then "NULL"  else f_CHAMP_5 & SEP_LIST_1 & f_CRITERE_5 & SEP_LIST_1 & f_OPERATEUR_5,

            //***********************************************************************************************************************
            // Constitution de la liste des critères en chaine de caractères
            //***********************************************************************************************************************
            t_Criterias=t_Criteria1 & SEP_LIST_2 & t_Criteria2 & SEP_LIST_2 & t_Criteria3 & SEP_LIST_2 & t_Criteria4 & SEP_LIST_2 & t_Criteria5,
            ListCriterias=List.Buffer(Text.Split(t_Criterias,SEP_LIST_2)),   

            //***********************************************************************************************************************
            // Construction du RECORD de Expression.Evaluate pour alimenter le contexte PowerQuery #shared
            //***********************************************************************************************************************
            StrAccRec=List.Accumulate(
                ListCriterias,
                {},
                (state,current)=> if current = "NULL"
                                then state
                                else
                                        let
                                                Ndx=List.PositionOf(ListCriterias,current),
                                                ListCurrent=Text.Split(current,SEP_LIST_1),
                                                Col=ListCurrent{0},
                                                StrRec= state & {Col}
                                        in
                                                StrRec
            ),
          
            RecFromListCol=Record.FromList(StrAccRec,StrAccRec),
            RecCombine=Record.Combine({RecFromListCol, RECORD_FUNCTIONS}),

            //***********************************************************************************************************************
            // On va lister les critères 1 à 1 en prenant les colonnes sur lesquelles il faut appliquer les filtres
            //***********************************************************************************************************************
            StrAccCrit=List.Accumulate(
                ListCriterias,
                "",
                                  // Si le champ/colonne n'est pas renseigné on passe au suivant
                (state,current)=> if current = "NULL"
                                then state // Contenu de l'accumulateur
                                else
                                        let
                                            // On construit le code pour le critère de la colonne en cours de traitement
                                            Ndx=List.PositionOf(ListCriterias,current),
                                            ListCurrent=List.Buffer(Text.Split(current,SEP_LIST_1)),
                                            Col=ListCurrent{0},  // colonne
                                            Crit=ListCurrent{1}, // critère de sélection
                                            Oper=ListCurrent{2}, // opérateur de sélection
                                            PosOper=List.PositionOf(LIST_STR_OPERATORS,Oper), // On recherche la colonne associée à la fonction de recherche PowerQuery
                                            OpFunction=LIST_FUNC_OPERATORS{PosOper},  // On recherche la fonction PowerQuery
                                            StrSelect=if PosOper=0 or PosOper=1
                                                    // Cas de la 1ère colonne
                                                    then "[" & Col & "]" & " " & OpFunction & GUI & Crit & GUI
                                                    // Cas des colonnes suivante
                                                    else " " & OpFunction & "(" & "[" & Col & "]" & "," & GUI & Crit & GUI & ")",
                                            AccStrSelect= if Ndx = 0
                                                            // Nous sommes sur la 1ère colonne
                                                        then StrSelect
                                                            // Nous sommes sur les colonnes suivantes
                                                        else state & " " & Connector & " " & StrSelect,
                                            AccStrEval= AccStrSelect
                                        in
                                            AccStrEval
            ),
          
            Result= if StrAccCrit=""
                                then
                                        ""
                                else
                                        let
                                            //*****************************************************************************************
                                            // Si le critère est placé en position 5
                                            // *** IL EST RECOMMANDE DE LES PLACER DANS L'ORDRE POUR EVITER LES DYSFONCTIONNEMENTS ****
                                            //*****************************************************************************************
                                            EachCheckAndOr=if Text.StartsWith(StrAccCrit," or ")
                                                            then
                                                                    Text.Replace(StrAccCrit,"or","")                                                     
                                                            else
                                                                    if Text.StartsWith(StrAccCrit," and ")
                                                                    then
                                                                        Text.Replace(StrAccCrit,"and","")                                                     
                                                                    else
                                                                        StrAccCrit,                                         
                                            Each= " each " & EachCheckAndOr, // On termine le code PowerQuery/M avec le each
                                            EvalStrAccumulateCritEach=Expression.Evaluate(Each,RecCombine)
                                        in
                                            EvalStrAccumulateCritEach

        in
            Result
in
    fnGetStringCriteria

J'ai laissé cette requête TB_TEST_fnGetCriteria (à voir dans l'environnement Pwq) qui permet de débugger la fonction sur les critères de types texte.

En conclusion, pour me répéter une nouvelle fois, ce code est très pédagogique pour les non initiés. Par ailleurs je dois avouer que j'y ai passé un plus de temps que prévu afin de sécuriser au maximum pour éviter les "plantages".

J'admets aussi qu'il n'est pas à la portée de ceux qui débutent. En résumé et pour l'avoir déjà dit, coder en M, quand on arrive à des choses complexes qui demandent de la réflexion, c'est de la programmation au pur sens du terme et la rubrique algorithmie
est loin d'être facultative : elle est même de RIGUEUR. :).

Je vous livre le fichier Excel ainsi que le fichier exemple afin de le tester.
 

Pièces jointes

  • Consolider2M_V2.0_GoldRelease.xlsm
    291.1 KB · Affichages: 2
  • fr-en-liste-diplomes-professionnels.zip
    17.6 KB · Affichages: 3
Dernière édition:

Discussions similaires

Statistiques des forums

Discussions
314 422
Messages
2 109 449
Membres
110 483
dernier inscrit
Laanvy