Question [Fonction] Inverser une hashtable

Plus d'informations
il y a 11 mois 1 semaine - il y a 11 mois 1 semaine #29863 par Laurent Dardenne
On peut parfois souhaiter déclarer une hashtable (clé=valeur) et tout en créant l'inverse ( valeur=clé). Par exemple :
$h=@{
'un'=1
}

#et 

$h2=@{
1='un'
}
Ce qui permet de manipuler une information soit d'après son nom soit d'après sa valeur qui lui est attribuée :
$key='un'
$H.$key
$key='1'
$H2.$key
L'inconvénient ici est que cette approche doit créer 2 variables, là où le C# par exemple propose des indexers .
Les classes Powershell permettent ce type de construction (voir chapitre 6.5 Indexeur) mais cela nécessite qq connaissances supplémentaires.
De plus on peut encore trouver des environnements en PS v3.0 ou v4.0 ( ce qui est mon cas et je ne peux ni charger de dll ni utiliser Add-Type).

On reprend donc les bases de Powershell, ici ETS, tout en codant simplement. Ce qui nous donne ceci:
<?xml version="1.0" encoding="utf-8" ?>
<Types>
 <Type>
     <Name>System.Collections.Hashtable</Name>
     <Members>
       <ScriptMethod>
         <Name>Reverse</Name>
          <!-- Les types primitifs sont :
            Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double et Single. 
          -->
         <Script>
            $ReverseHashtable=@{}

            Foreach ($Current in $this.GetEnumerator())
            {
              $Key=$Current.Key
              $Value=$Current.Value
              if ($null -eq $Value)
              { Throw "Impossible to reverse the hashtable. The key '$Key' has a value null.The value of a key cannot be null." }
              $ValueType=$Value.Gettype()
              if ( ($ValueType -isnot [string]) -and $ValueType.isPrimitive)
              { 
                try {
                  $ReverseHashtable.Add($Current.Value,$Key) 
                } catch [System.ArgumentException] {
                  Throw "Impossible to reverse the hashtable. Key values '$($Current.Value)' are duplicated."
                }
              }
              else
              { Throw "Impossible to reverse the hashtable. The value '$Key' is not a scalar or a string :'$ValueType'." }
            }
            return ,$ReverseHashtable
         </Script>
       </ScriptMethod>
     </Members>
  </Type>
    <Type>
     <Name>System.Collections.Specialized.OrderedDictionary</Name>
     <Members>
       <ScriptMethod>
         <Name>Reverse</Name>
         <Script>
            if ($this.IsReadOnly)
            { $ReverseHashtable=[ordered]@{} }
            else
            { $ReverseHashtable=@{} }

            Foreach ($Current  in $this.GetEnumerator())
            {
              $Key=$Current.Key
              $Value=$Current.Value
              if ($null -eq $Value)
              { Throw "Impossible to reverse the hashtable. The key the key '$Key' has a value null.The value of a key cannot be null." }              
              $ValueType=$Value.Gettype()

              if ( ($ValueType -isnot [string]) -and $ValueType.isPrimitive)
              { 
                try {
                  $ReverseHashtable.Add($Current.Value,$Key) 
                } catch [System.ArgumentException] {
                  Throw "Impossible to reverse the hashtable. Key values '$($Current.Value)' are duplicated."
                }
              }
              else
              { Throw "Impossible to reverse the hashtable. The value of the key '$Key' is not a scalar or a string :'$ValueType'." }
            }

            if ($this.IsReadOnly)
            { return ,$ReverseHashtable.AsReadOnly() }
            return ,$ReverseHashtable
         </Script>
       </ScriptMethod>
     </Members>
  </Type>
</Types>
Ce qui reste accessible je pense.

On duplique le code car on doit déclarer les types 'System.Collections.Hashtable' et 'System.Collections.Specialized.OrderedDictionary'.
La présence du second type est liée à cette astuce .

Ce qui autorise, à partir d'un hashtable en ReadOnly, la création d'un hashtable inversée et toujours en ReadOnly.

Une fois enregistré ce fichier, on l'utilise ainsi :
$File='..\System.Collections.Hashtable.ps1xml'
Update-TypeData -PrependPath $File

$h=@{
    'un'=1
    'deux'=2
    'trois'=3
}

$h.Reverse()
# Name                           Value
# ----                           -----
# 3                              trois
# 2                              deux
# 1                              un
Les cas d'erreur étant :
$h=@{
    'un'=1
    'deux'=2
    'trois'=$Null
}
$h2=$h.Reverse()
#Exception lors de l'appel de «Reverse» avec «0» argument(s): «Impossible to reverse the hashtable. 
#The key 'trois' has a value null.The value of a key cannot be null.»
#
#Une clé de hashtable ne peut $etre $null

$h=@{
    'un'=1
    'deux'=2
    'trois'=1
}
$h2=$h.Reverse()
#Exception lors de l'appel de «Reverse» avec «0» argument(s): «Impossible to reverse the hashtable. 
#Key values '1' are duplicated.»
#
#les valeurs devenant des clés on ne peut dupliquer un nom de clé

$h=@{
    'un'=1
    'deux'=(get-item 'G:\PS\Hashtable\Reverse.Tests.ps1') #$PSCommandPath)
    'trois'=3
}
$h2=$h.Reverse()
#Exception lors de l'appel de «Reverse» avec «0» argument(s): «Impossible to reverse the hashtable. 
#The value 'deux' is not a scalar or a string :'System.IO.FileInfo'.»
#
#Pour un usage sous Powershell on évite ce type de construction.

Une fonction identique qui ne s'appuie pas sur ETS :
function New-ReversedHashtable {
    param($Hashtable)
    
    if(-not  ( ($Hashtable -is [System.Collections.Hashtable]) -OR ($Hashtable-is [System.Collections.Specialized.OrderedDictionary])) )
    { Throw "The argument `$Hashtable([$($Hashtable.GetType().Fullname)]) the argument must be one of the following types : [System.Collections.Hashtable], [System.Collections.Specialized.OrderedDictionary]" }
    $isReadOnly=$Hashtable.IsReadOnly
   
    if ($isReadOnly)
    { $ReverseHashtable=[ordered]@{} }
    else
    { $ReverseHashtable=@{} }  
    
    Foreach ($Current in $Hashtable.GetEnumerator())
    {
       $Key=$Current.Key
       $Value=$Current.Value
       if ($null -eq $Value) #ArgumentNullException
       { Throw "Impossible to reverse the hashtable. The key '$Key' has a value null.The value of a key cannot be null." }
       $ValueType=$Value.Gettype()
   
       if ( ($ValueType -isnot [string]) -and $ValueType.isPrimitive)
       { 
           try {
               $ReverseHashtable.Add($Current.Value,$Key) 
           } catch [System.ArgumentException] { #ArgumentException
               Throw "Impossible to reverse the hashtable. Key values '$($Current.Value)'' are duplicated."
           }
       }
       else
       { Throw "Impossible to reverse the hashtable. The value of the key '$Key' is not a scalar or a string :'$ValueType'." }
    }
    
    if ($isReadOnly)
    { return ,$ReverseHashtable.AsReadOnly() }
    return ,$ReverseHashtable
}
Mais j’entends une personne au fond de la salle près du radiateur qui me dit "Comment inverser une hashtable générique ?"
Exemple :
$h = new-object 'System.Collections.Generic.Dictionary[String,Int]'
$h.'Un'=1
$h.'Deux'=2
$h.'Trois'=3
Bha, c'est un autre sujet (*).
;-)
$KeyDelegate =  [Func[[System.Collections.Generic.KeyValuePair[String,Int]],String]]{ $args[0].Key }
$ValueDelegate =  [Func[[System.Collections.Generic.KeyValuePair[String,Int]],Int]]{ $args[0].Value}
$Result=[Linq.Enumerable]::ToDictionary($h, $ValueDelegate,$KeyDelegate)
$Result.GetType().Fullname
$Result
Powershell c'est facile, mais ce n'est pas tout les jours facile...

Note: je n'ai pas de lien sur ETS (Extended Type System) à proposer car MS joue au bonneteau avec la doc de Powershell et surtout avec ce sujet.

*
Le sujet étant d'inverser n'importe quel type de hashtable générique, voir d'utiliser d'autres types avec la méthode [Linq.Enumerable]::ToDictionary
---

Tutoriels PowerShell
Dernière édition: il y a 11 mois 1 semaine par Laurent Dardenne. Raison: Balise code

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 3 mois 3 semaines #30344 par Laurent Dardenne
En attendant un tutoriel sur l'usage des génériques avec Powershell, voici une première ébauche :
function Inverse{
    param(
        $InputObject
    )

     #On manipule des informations du type
    $SourceType=$InputObject.GetType()

    if ($SourceType.IsGenericType -eq $false)
    { Throw 'InputObject doit être un type générique.' }
    
     # On distingue les deux erreurs
    if ($SourceType.IsGenericTypeDefinition -eq $true)
    { Throw 'InputObject doit être un type générique fermé.' }

     #On cherche à savoir si $inputObject implémente l'interface générique
     # IDictionary<TKey,TValue> ( qui elle même implémente System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey,TValue>>)

    $isIDictionaryImplemented=$InputObject.GetType().GetInterfaces().Where({
        if ($_.isGenericType)
        { $_.GetGenericTypeDefinition().fullname -eq 'System.Collections.Generic.IDictionary`2'}
    }).Count -eq 1

    # Note MsDoc : 
    #   L'interface IDictionary<TKey,TValue> est l’interface de base pour les collections génériques de paires clé/valeur.
    #   Chaque élément est une paire clé/valeur stockée dans un KeyValuePair<TKey,TValue> objet.

    if ($isIDictionaryImplemented -eq $false)
    { Throw 'InputObject n''est pas une collection générique de paires clé/valeur (IDictionary<TKey,TValue>).' }

     #On récupère les types utilisés pour créer la 'hashtable' générique ( TODO : le type peut être différent)
    $GenericArguments=$InputObject.GetType().GetGenericArguments()

    #Crée un type ouvert à partir de la classe System.Collections.Generic.KeyValuePair
    #Ici on sait que ce type générique à deux arguments de type; cf. 'System.Collections.Generic.IDictionary`2'
    $KeyValuePairType=[Type]'System.Collections.Generic.KeyValuePair`2'
    Write-debug "KeyValuePairType : $($KeyValuePairType.ToString())"
    Write-debug "KeyValuePairType est un type ouvert ? $($KeyValuePairType.IsGenericTypeDefinition -eq $true)"

     #Crée un type fermé (exemple: System.Collections.Generic.KeyValuePair<String,Int> ) à partir des arguments de type de $InputObject
     #C'est une classe générique différente mais les types des arguments sont identiques
    $DelegateInputParameterType=$KeyValuePairType.MakeGenericType($GenericArguments)
    Write-debug "DelegateInputParameter : $($DelegateInputParameterType.ToString())"
    Write-debug "DelegateInputParameter est un type fermé ? $($DelegateInputParameterType.IsGenericTypeDefinition -eq $false)"

    #On crée les foncteurs nécessaires pour manipuler les argument de types de $InputObject
    #on sait, d'après 'System.Collections.Generic.IDictionary`2', que le functor à deux arguments de type
    $FunctorType=[Type]'Func`2'

     #L'ordre de la liste des paramètres correspond à celui
     # indiqué lors de la création de l'objet
    [Type[]] $ParametersType=@($DelegateInputParameterType,$GenericArguments[0])
    $KeyDelegateType=$FunctorType.MakeGenericType($ParametersType)
    Write-debug "Foncteur pour KeyDelegateType : $($KeyDelegateType)"

    [Type[]] $ParametersType=@($DelegateInputParameterType,$GenericArguments[1])
    $ValueDelegateType=$FunctorType.MakeGenericType($ParametersType)
    Write-debug "Foncteur pour ValueDelegateType : $($ValueDelegateType)"

     #Cast nécessaire des scriptblocks
    $KeyDelegate =  { $args[0].Key } -as $KeyDelegateType
    $ValueDelegate ={ $args[0].Value} -as $ValueDelegateType

    #Bien que le paramètre $InputObject ne soit pas typé dans l'entête de la fonction, on manipule un PSObject
    #on doit donc récupérer l'objet encapsulé via la propriété PsObject.BaseObject
    #Sinon on a une erreur d'appel
    #
    #L'ordre des délégués est ici inversé puisque c'est l'objectif : la valeur devient la clé et inversement
    $Result=[Linq.Enumerable]::ToDictionary($InputObject.PsObject.BaseObject, $ValueDelegate,$KeyDelegate)
    ,$Result
}

$h = new-object 'System.Collections.Generic.Dictionary[String,Int]'
$h.'Un'=1
$h.'Deux'=2
$h.'Trois'=3

$Result=Inverse $h
$Result
# Key Value
# --- -----
#   1 Un
#   2 Deux
#   3 Trois

$h2 = new-object 'System.Collections.Generic.Dictionary[Int,String]'
$h2.Add(1,'Un')
$h2.Add(2,'Deux')
$h2.Add(3,'Trois')
$Result=Inverse $h2
$Result
# Key   Value
# ---   -----
# Un        1
# Deux      2
# Trois     3

#Similaire à [Ordered]}
$SL = new-object 'System.Collections.Generic.SortedList[String,Int]'
$SL.Add("Un", 1)
$SL.Add("Deux", 2)
$SL.Add("Trois", 3)

$Result=Inverse $SL
$Result
Le dernier résultat n'est pas du type attendu (system.collections.Generic.SortedList[Int,String]), un jeu de test Pester serait le bienvenue :-)
Si ce sujet vous intéresse et en attendant la finalisation du tutoriel sur les génériques, vous pouvez consulter cet article : www.red-gate.com/simple-talk/dotnet/net-...nce-powershell-linq/

A suivre :-)

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 3 mois 3 semaines #30353 par Laurent Dardenne
Une solution au dernier point, convertir le résultat d'appel de [Linq.Enumerable]::ToDictionary dans le type d'origine en inversant ses arguments.
On peut utiliser cette mécanique de conversion .
Car si on consulte la documentation des classes génériques implémentant IDictionary<TKey,TValue>, on constate qu'elles proposent toutes un constructeur attendant un IDictionnary générique.

Pour cette partie on utilise une string contenant le nom de type :
$Result=[Linq.Enumerable]::ToDictionary($InputObject.PsObject.BaseObject, $ValueDelegate,$KeyDelegate)
if ("$SourceType" -notmatch '^System\.Collections\.Generic\.Dictionary\[')
{
    #On récupère le 'nom court' du type : FullClassName[String,Int]
    # On n'utilise pas ici le système de réflexion de dotNet.
   $TargetTypeName="$SourceType"

    #On inverse <TKey,TValue> pour avoir <TValue,TKey>
    #Note:  on suppose que TKey,TValue ne sont pas des types génériques
   $TargetTypeName=$TargetTypeName -replace '^(.*?)\[(.*?),(.*)\]$','$1[$3,$2]'

   Write-Debug "Tente un transtypage à la Powershell vers le type $TargetTypeName"
   #Peut appeler un constructeur utilisant un dictionnaire générique en paramètre.
   #Cf. https://devblogs.microsoft.com/powershell/understanding-powershells-type-conversion-magic/
   $Result=$Result -as [Type]$TargetTypeName

   If ($null -eq $Result)
   { Throw "Impossible to cast the type '$($Result.Gettype())' to  '$TargetTypeName'." }
}
La version modifiée :
function Inverse{
    param(
        $InputObject
    )

     #On manipule des informations du type
    $SourceType=$InputObject.GetType()

    if ($SourceType.IsGenericType -eq $false)
    { Throw 'InputObject doit être un type générique.' }

     # On distingue les deux erreurs
    if ($SourceType.IsGenericTypeDefinition -eq $true)
    { Throw 'InputObject doit être un type générique fermé.' }

     #On cherche à savoir si $inputObject implémente l'interface générique
     # IDictionary<TKey,TValue> ( qui elle même implémente System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey,TValue>>)
    $isIDictionaryImplemented=$InputObject.GetType().GetInterfaces().Where({
        if ($_.isGenericType)
        { $_.GetGenericTypeDefinition().fullname -eq 'System.Collections.Generic.IDictionary`2'}
    }).Count -eq 1
    # Note MsDoc :
    #   L'interface IDictionary<TKey,TValue> est l'interface de base pour les collections génériques de paires clé/valeur.
    #   Chaque élément est une paire clé/valeur stockée dans un KeyValuePair<TKey,TValue> objet.

    if ($isIDictionaryImplemented -eq $false)
    { Throw 'InputObject n''est pas une collection générique de paires clé/valeur (IDictionary<TKey,TValue>).' }

     #On récupère les types utilisés pour créer la 'hashtable' générique ( TODO : le type peut être différent)
    $GenericArguments=$InputObject.GetType().GetGenericArguments()

    #Crée un type ouvert à partir de la classe System.Collections.Generic.KeyValuePair
    #Ici on sait que ce type générique à deux arguments de type; cf. 'System.Collections.Generic.IDictionary`2'
    $KeyValuePairType=[Type]'System.Collections.Generic.KeyValuePair`2'
    Write-debug "KeyValuePairType : $($KeyValuePairType.ToString())"
    Write-debug "KeyValuePairType est un type ouvert ? $($KeyValuePairType.IsGenericTypeDefinition -eq $true)"

     #Crée un type fermé (exemple: System.Collections.Generic.KeyValuePair<String,Int> ) à partir des arguments de type de $InputObject
    #C'est une classe générique différente mais les types des arguments sont identiques
    $DelegateInputParameterType=$KeyValuePairType.MakeGenericType($GenericArguments)
    Write-debug "DelegateInputParameter : $($DelegateInputParameterType.ToString())"
    Write-debug "DelegateInputParameter est un type fermé ? $($DelegateInputParameterType.IsGenericTypeDefinition -eq $false)"

    #On crée les foncteurs nécessaires pour manipuler les argument de types de $InputObject
    #on sait, d'après 'System.Collections.Generic.IDictionary`2', que le functor à deux arguments de type
    $FunctorType=[Type]'Func`2'

     #L'ordre de la liste des paramètres correspond à celui
     # indiqué lors de la création de l'objet
    [Type[]] $ParametersType=@($DelegateInputParameterType,$GenericArguments[0])
    $KeyDelegateType=$FunctorType.MakeGenericType($ParametersType)
    Write-debug "Foncteur pour KeyDelegateType : $($KeyDelegateType)"

    [Type[]] $ParametersType=@($DelegateInputParameterType,$GenericArguments[1])
    $ValueDelegateType=$FunctorType.MakeGenericType($ParametersType)
    Write-debug "Foncteur pour ValueDelegateType : $($ValueDelegateType)"

     #Cast nécessaire des scriptblocks
    $KeyDelegate =  { $args[0].Key } -as $KeyDelegateType
    If ($null -eq $KeyDelegate)
    { Throw "Impossible to cast the 'Key' scriptblock to '$KeyDelegateType'."}

    $ValueDelegate ={ $args[0].Value} -as $ValueDelegateType
    If ($null -eq $ValueDelegate)
    { Throw "Impossible to cast the 'Value' scriptblock to '$ValueDelegate'."}


    #Bien que le paramétre $InputObject ne soit pas typé dans l'entête de la fonction, on manipule un PSObject
    #on doit donc récupérer l'objet encapsulé via la propriété PsObject.BaseObject
    #Sinon on a une erreur d'appel
    #
    #L'ordre des délégués est ici inversé puisque c'est l'objectif : la valeur devient la clé et inversement
    $Result=[Linq.Enumerable]::ToDictionary($InputObject.PsObject.BaseObject, $ValueDelegate,$KeyDelegate)
    if ("$SourceType" -notmatch '^System\.Collections\.Generic\.Dictionary\[')
    {
        #On récupère le 'nom court' du type : FullClassName[String,Int]
        # On n'utilise pas ici le système de réflexion de dotNet.
       $TargetTypeName="$SourceType"
        #On inverse <TKey,TValue> pour avoir <TValue,TKey>
        #Note:  on suppose que TKey,TValue ne sont pas des types génériques
        #       IDictionary<typeof(T), T> est impossible.
       $TargetTypeName=$TargetTypeName -replace '^(.*?)\[(.*?),(.*)\]$','$1[$3,$2]'

       Write-Debug "Tente un transtypage à la Powershell vers le type $TargetTypeName"
       #Peut appeler un constructeur utilisant un dictionnaire génèrique en paramètre.
       #Cf. https://devblogs.microsoft.com/powershell/understanding-powershells-type-conversion-magic/
       $Result=$Result -as [Type]$TargetTypeName
       If ($null -eq $Result)
       { Throw "Impossible to cast the type '$($Result.Gettype())' to  '$TargetTypeName'." }
    }
    Write-Output $Result -NoEnumerate
}

$h = new-object 'System.Collections.Generic.Dictionary[String,Int]'
$h.'Un'=1
$h.'Deux'=2
$h.'Trois'=3

$Result=Inverse $h
$Result
# Key Value
# --- -----
#   1 Un
#   2 Deux
#   3 Trois
"$($Result.GetType())"
#System.Collections.Generic.Dictionary[int,string]

$h2 = new-object 'System.Collections.Generic.Dictionary[Int,String]'
$h2.Add(1,'Un')
$h2.Add(2,'Deux')
$h2.Add(3,'Trois')
$Result=Inverse $h2
$Result
# Key   Value
# ---   -----
# Un        1
# Deux      2
# Trois     3
"$($Result.GetType())"
#System.Collections.Generic.Dictionary[string,int]


#Similaire à [Ordered]
#ou a System.Collections.Generic.SortedDictionary<TKey,TValue>
$SL = new-object 'system.collections.Generic.SortedList[String,Int]'
$SL.Add("Un", 1)
$SL.Add("Deux", 2)
$SL.Add("Trois", 3)

$Result=Inverse $SL
$Result
"$($Result.GetType())"
#System.Collections.Generic.SortedList[int,string]

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Temps de génération de la page : 0.147 secondes
Propulsé par Kunena