Home > front end >  How does one edit a list of hashtables in powershell?
How does one edit a list of hashtables in powershell?

Time:07-24

I am trying to find a better way to edit a list of hashtables in powershell. I have successfully looped though the list to find duplicates, but I am not sure how to edit the hashtable and update the list. Does anyone know how to do this at scale?

The test data is formated like this : ( I know it is terrible, but that is the way of the world! )

$TEST_DATA = @(
  "CA>TORONTO>MAP-LAYOUT-1",        `
  "US>NEW-YORK>MAP-LAYOUT-25",      `
  "US>DENVER>MAP-LAYOUT-2",         `
  "UK>LODNON>MAP-LAYOUT-2",         `
  "EU>PARIS>MAP-LAYOUT-25",         `
  "EU>VENICE>MAP-LAYOUT-2",         `
  "EU>BERLIN>MAP-LAYOUT-8",         `
  "EU>PARIS>MAP-LAYOUT-30",         `
  "EU>VENICE>NEW-MAP-LAYOUT-2",     `
  "EU>ATHENS>NEW-MAP-LAYOUT-8",     `
  "CA>TORONTO>NEW-MAP-LAYOUT-1",    `
  "CA>VANCOUVER>MAP-LAYOUT-1",      `
  "US>MIAMI>MAP-LAYOUT-25",         `
  "RU>MOSCOW>MAP-LAYOUT-2",         `
  "RU>ST-PETERSBURG>MAP-LAYOUT-2",  `
  "JP>TOKYO>MAP-LAYOUT-25",         `
  "EU>PARIS>MAP-LAYOUT-100",        `
  "AU>SYDNEY>MAP-LAYOUT-3",         `
  "AU>PERTH>MAP-LAYOUT-3",          `
  "MX>MEXICO-CITY>MAP-LAYOUT-5",    `
  "MX>TIJAUNA>MAP-LAYOUT-1",        `
  "CN>SHANGHAI>MAP-LAYOUT-8",       `
  "CA>CALGARY>MAP-LAYOUT-1" 
  )

and my little powershell script is like this :

$MAPS = @()

$TEST_DATA | Foreach {
  $COUNTRY = ($_).split('>')[0]
  $CITY = ($_).split('>')[1]
  $MAP = ($_).split('>')[2] 

  If ( $MAPS.count -gt 0) {
    $MAPS | Foreach {
      $MAPS_ROW = $_
      Write-Host $MAPS_ROW
      If (( $MAPS_ROW.COUNTRY -eq $COUNTRY ) -and ( $MAPS_ROW.CITY -eq $CITY )) {
        Write-Host '--------' -ForegroundColor Red
        Write-Host "CONFLICT DETECTED ! "
        write-Host '--------' -ForegroundColor Red
        Write-Host ""
        Write-Host "CONFLICT MAP : $MAPS_ROW"
        Write-Host ""
        If (($MAP.lenght -gt $MAPS_ROW.MAP.length) -or ( $MAP.lenght -gt ($MAPS_ROW.MAP.trim("NEW-")).length)) {
          $MAPS.Remove($MAPS_ROW)
          $NEW_MAP = New-Object PSObject -property @{ COUNTRY="$COUNTRY"; CITY="$CITY"; MAP="$MAP"; }
          $MAPS  = $NEW_MAP
        } else {
          Write-Host '--------' -ForegroundColor Yellow
          Write-Host "FALSE MAP CONFLICT DETECHED !  $MAPS_ROW "
          Write-Host '--------' -ForegroundColor Yellow
        }
      } else {
        Write-Host '--------' -ForegroundColor BLue
        Write-Host "NOT IN : 'MAPS'"
        Write-Host '--------' -ForegroundColor Blue
      }
    }
    $NEW_MAP = New-Object PSObject -property @{ COUNTRY="$COUNTRY"; CITY="$CITY"; MAP="$MAP"; }
    $MAPS  = $NEW_MAP
  } else { 
    Write-Host '--------' -ForegroundColor BLue
    Write-Host "First MAP Entry"
    Write-Host '--------' -ForegroundColor Blue
    $NEW_MAP = New-Object PSObject -property @{ COUNTRY="$COUNTRY"; CITY="$CITY"; MAP="$MAP"; }
    $MAPS  = $NEW_MAP
  } 
}
$SORTED_MAPS = $MAPS | Sort-Object { $_.COUNTRY },  { $_.CITY }, { $_.MAP } | Select-Object @{n="COUNTRY";e={$_.COUNTRY}}, @{n="CITY";e={$_.CITY}}, @{n="MAP";e={$_.MAP}}

Write-Host '--------' -ForegroundColor Blue
$SORTED_MAPS
Write-Host '--------' -ForegroundColor Blue

and the looping sort of works and I get a nice clean sorted list at the end.

COUNTRY CITY          MAP              
------- ----          ---              
AU      PERTH         MAP-LAYOUT-3     
AU      SYDNEY        MAP-LAYOUT-3     
CA      CALGARY       MAP-LAYOUT-1     
CA      TORONTO       MAP-LAYOUT-1     
CA      TORONTO       NEW-MAP-LAYOUT-1 
CA      VANCOUVER     MAP-LAYOUT-1     
CN      SHANGHAI      MAP-LAYOUT-8     
EU      ATHENS        NEW-MAP-LAYOUT-8 
EU      BERLIN        MAP-LAYOUT-8     
EU      PARIS         MAP-LAYOUT-100   
EU      PARIS         MAP-LAYOUT-25    
EU      PARIS         MAP-LAYOUT-30
EU      VENICE        MAP-LAYOUT-2     
EU      VENICE        NEW-MAP-LAYOUT-2 
JP      TOKYO         MAP-LAYOUT-25    
MX      MEXICO-CITY   MAP-LAYOUT-5     
MX      TIJAUNA       MAP-LAYOUT-1     
RU      MOSCOW        MAP-LAYOUT-2     
RU      ST-PETERSBURG MAP-LAYOUT-2     
UK      LODNON        MAP-LAYOUT-2     
US      DENVER        MAP-LAYOUT-2     
US      MIAMI         MAP-LAYOUT-25    
US      NEW-YORK      MAP-LAYOUT-25    

and my problems are :

  1. How to remove the duplicates from the list?

I tried to sort with the unique switch but coudn't get it to work properly...

  1. How to make sure that ther is one unique entry for each city with the most correct map name?

I tried to do a check on country and city fields, but that doesn't seem to allow me to add and remove the hashtable from the array.

  1. Is it better to just create a new arraylist and try to to sort the map bit ?

with conditions like .

  1. If the map contains "new-" or some other tag it has a highest preference. eg: "EU VENICE NEW-MAP-LAYOUT-2 " would be the result for venice.

  2. If the has the highest increment value would be the second highest priority... eg: "EU PARIS MAP-LAYOUT-100 " would be the result for paris.

Thanks in advance for any advice on how to tackle these issues...

Ta,

X

CodePudding user response:

I'd break the code into 2 parts - the first part to process the input lines and extract some extended properties like "IsNew" and "Priority" from the map name, and the second part to apply your selection logic.

That way it's much clearer to see what the "business logic" is for determining the priority of maps, versus the more mechanical code to parse your input.

Part 1 - Pre-process input lines

# convert all the rows first
# (error handling omitted for brevity)
$TEST_MAPS = $TEST_DATA | foreach-object {
    $parts = $_.Split(">");
    write-output ([pscustomobject] ([ordered] @{
        #"TEXT"     = $_
        "COUNTRY"  = $parts[0]
        "CITY"     = $parts[1]
        "MAP"      = $parts[2]
        #"BASENAME" = if( $parts[2].StartsWith("NEW-") ) { $parts[2].Substring("NEW-".Length) } else { $parts[2] }
        "ISNEW"    = $parts[2].StartsWith("NEW-")
        "PRIORITY" = [int] $parts[2].Substring($parts[2].LastIndexOf("-")   1)
    }));
}

If we output $TEST_MAPS it looks like this:

$TEST_MAPS | ft

COUNTRY CITY          MAP              ISNEW PRIORITY
------- ----          ---              ----- --------
CA      TORONTO       MAP-LAYOUT-1     False        1
US      NEW-YORK      MAP-LAYOUT-25    False       25
US      DENVER        MAP-LAYOUT-2     False        2
UK      LODNON        MAP-LAYOUT-2     False        2
EU      PARIS         MAP-LAYOUT-25    False       25
EU      VENICE        MAP-LAYOUT-2     False        2
EU      BERLIN        MAP-LAYOUT-8     False        8
EU      PARIS         MAP-LAYOUT-30    False       30
EU      VENICE        NEW-MAP-LAYOUT-2 True         2
EU      ATHENS        NEW-MAP-LAYOUT-8 True         8
CA      TORONTO       NEW-MAP-LAYOUT-1 True         1
CA      VANCOUVER     MAP-LAYOUT-1     False        1
US      MIAMI         MAP-LAYOUT-25    False       25
RU      MOSCOW        MAP-LAYOUT-2     False        2
RU      ST-PETERSBURG MAP-LAYOUT-2     False        2
JP      TOKYO         MAP-LAYOUT-25    False       25
EU      PARIS         MAP-LAYOUT-100   False      100
AU      SYDNEY        MAP-LAYOUT-3     False        3
AU      PERTH         MAP-LAYOUT-3     False        3
MX      MEXICO-CITY   MAP-LAYOUT-5     False        5
MX      TIJAUNA       MAP-LAYOUT-1     False        1
CN      SHANGHAI      MAP-LAYOUT-8     False        8
CA      CALGARY       MAP-LAYOUT-1     False        1

Part 2 - Apply map selection logic for each city

All we need to do now is group the maps into cities and decide which map in each group has the highest precedence.

By the way, you didn't specifically say what "wins" if a map with NEW- has a lower "Priority" than one without - e.g. NEW-MAP-LAYOUT-1 vs MAP-LAYOUT-200, so for the sake of argument I've assumed "Priority" takes precedence (i.e. MAP-LAYOUT-200 beats NEW-MAP-LAYOUT-1).

# group the maps by city 
$MAP_GROUPS = $TEST_MAPS | group-object -Property COUNTRY, CITY;

# select the "most correct" map name for each group
$MAPS = $MAP_GROUPS | foreach-object {
    write-output (
        $_.Group
            | sort-object -Property PRIORITY, ISNEW -Descending
            | select-object -First 1
    );
}

which gives the following results:

$MAPS | sort-object -Property COUNTRY, CITY | ft

COUNTRY CITY          MAP              ISNEW PRIORITY
------- ----          ---              ----- --------
AU      PERTH         MAP-LAYOUT-3     False        3
AU      SYDNEY        MAP-LAYOUT-3     False        3
CA      CALGARY       MAP-LAYOUT-1     False        1
CA      TORONTO       NEW-MAP-LAYOUT-1 True         1
CA      VANCOUVER     MAP-LAYOUT-1     False        1
CN      SHANGHAI      MAP-LAYOUT-8     False        8
EU      ATHENS        NEW-MAP-LAYOUT-8 True         8
EU      BERLIN        MAP-LAYOUT-8     False        8
EU      PARIS         MAP-LAYOUT-100   False      100
EU      VENICE        NEW-MAP-LAYOUT-2 True         2
JP      TOKYO         MAP-LAYOUT-25    False       25
MX      MEXICO-CITY   MAP-LAYOUT-5     False        5
MX      TIJAUNA       MAP-LAYOUT-1     False        1
RU      MOSCOW        MAP-LAYOUT-2     False        2
RU      ST-PETERSBURG MAP-LAYOUT-2     False        2
UK      LODNON        MAP-LAYOUT-2     False        2
US      DENVER        MAP-LAYOUT-2     False        2
US      MIAMI         MAP-LAYOUT-25    False       25
US      NEW-YORK      MAP-LAYOUT-25    False       25

You'll lose a lot of your imperative-style logging with this approach, but I've assumed that was mainly for debugging your existing code. If you specifically need the logging output in that format then this answer probably won't help much :-).

CodePudding user response:

Mr. E,

I's suggest the use of an array of Custom Objects as follows.

Clear-Host 

$Maps = @()
  
$MapsArgs = @{PropertyNames = "Country", "City", "Map"
             Delimiter = '>'}  
$Test_Data.GetEnumerator() | 
   ForEach-Object  {
     $Obj = $_ | ConvertFrom-String @MapsArgs
     $Maps  = $obj
  }

$Maps2 = $Maps | Sort-Object -Unique -Property Country,City
$Maps2

Output:


Country City          Map             
------- ----          ---             
AU      PERTH         MAP-LAYOUT-3    
AU      SYDNEY        MAP-LAYOUT-3    
CA      CALGARY       MAP-LAYOUT-1    
CA      TORONTO       NEW-MAP-LAYOUT-1
CA      VANCOUVER     MAP-LAYOUT-1    
CN      SHANGHAI      MAP-LAYOUT-8    
EU      ATHENS        NEW-MAP-LAYOUT-8
EU      BERLIN        MAP-LAYOUT-8    
EU      PARIS         MAP-LAYOUT-30   
EU      VENICE        MAP-LAYOUT-2    
JP      TOKYO         MAP-LAYOUT-25   
MX      MEXICO-CITY   MAP-LAYOUT-5    
MX      TIJAUNA       MAP-LAYOUT-1    
RU      MOSCOW        MAP-LAYOUT-2    
RU      ST-PETERSBURG MAP-LAYOUT-2    
UK      LODNON        MAP-LAYOUT-2    
US      DENVER        MAP-LAYOUT-2    
US      MIAMI         MAP-LAYOUT-25   
US      NEW-YORK      MAP-LAYOUT-25   

Be aware that if you have a larger list you will probably want to use something other than the = to build the array. Like a dictionary where you can use an AddItem() property for efficiency.

BTW, you do not need the line continuation characters in your $Test_data the comma's serve to tell PS that more is coming as does the (.

CodePudding user response:

Your array defines a CSV with data delimited by >, so here's an approach based on that:

$TEST_DATA = "CA>TORONTO>MAP-LAYOUT-1",
  "US>NEW-YORK>MAP-LAYOUT-25",
  "US>DENVER>MAP-LAYOUT-2",
  "UK>LODNON>MAP-LAYOUT-2",
  "EU>PARIS>MAP-LAYOUT-25",
  "EU>VENICE>MAP-LAYOUT-2",
  "EU>BERLIN>MAP-LAYOUT-8",
  "EU>PARIS>MAP-LAYOUT-30",
  "EU>VENICE>NEW-MAP-LAYOUT-2",
  "EU>ATHENS>NEW-MAP-LAYOUT-8",
  "CA>TORONTO>NEW-MAP-LAYOUT-1",
  "CA>VANCOUVER>MAP-LAYOUT-1",
  "US>MIAMI>MAP-LAYOUT-25",
  "RU>MOSCOW>MAP-LAYOUT-2",
  "RU>ST-PETERSBURG>MAP-LAYOUT-2",
  "JP>TOKYO>MAP-LAYOUT-25",
  "EU>PARIS>MAP-LAYOUT-100",
  "AU>SYDNEY>MAP-LAYOUT-3",
  "AU>PERTH>MAP-LAYOUT-3",
  "MX>MEXICO-CITY>MAP-LAYOUT-5",
  "MX>TIJAUNA>MAP-LAYOUT-1",
  "CN>SHANGHAI>MAP-LAYOUT-8",
  "CA>CALGARY>MAP-LAYOUT-1"

$result = $TEST_DATA | ConvertFrom-Csv -Delimiter '>' -Header 'Country','City','Map' | 
    Sort-Object Country, City |   # sort on Country, then City
    Group-Object Country, City |  # group on Country, then City
    ForEach-Object {
        # see if we have an item (or perhaps multiple items) starting with 'NEW' 
        $newMap = $_.Group | Where-Object {$_.Map -match '^NEW'}
        if ($newMap) { 
            # take the top-numbered item from the 'NEW-' item(s)
            $newMap | Sort-Object { [int]($_.Map -replace '\D ') } -Descending | 
                      Select-Object -First 1 | Select-Object *, @{Name = 'IsNew'; Expression = {$true}}
        }
        else {
            # no 'NEW' items found, so just take the top-numbered item in the group
            $_.Group | Sort-Object { [int]($_.Map -replace '\D ') } -Descending | 
                       Select-Object -First 1 | Select-Object *, @{Name = 'IsNew'; Expression = {$false}}
        }
    }

# show on screen
$result | Format-Table -AutoSize

# output as CSV file
$result | Export-Csv -Path 'X:\Somewhere\SortedMap.csv' -NoTypeInformation

Output:

Country City          Map              IsNew
------- ----          ---              -----
AU      PERTH         MAP-LAYOUT-3     False
AU      SYDNEY        MAP-LAYOUT-3     False
CA      CALGARY       MAP-LAYOUT-1     False
CA      TORONTO       NEW-MAP-LAYOUT-1  True
CA      VANCOUVER     MAP-LAYOUT-1     False
CN      SHANGHAI      MAP-LAYOUT-8     False
EU      ATHENS        NEW-MAP-LAYOUT-8  True
EU      BERLIN        MAP-LAYOUT-8     False
EU      PARIS         MAP-LAYOUT-100   False
EU      VENICE        NEW-MAP-LAYOUT-2  True
JP      TOKYO         MAP-LAYOUT-25    False
MX      MEXICO-CITY   MAP-LAYOUT-5     False
MX      TIJAUNA       MAP-LAYOUT-1     False
RU      MOSCOW        MAP-LAYOUT-2     False
RU      ST-PETERSBURG MAP-LAYOUT-2     False
UK      LODNON        MAP-LAYOUT-2     False
US      DENVER        MAP-LAYOUT-2     False
US      MIAMI         MAP-LAYOUT-25    False
US      NEW-YORK      MAP-LAYOUT-25    False
  • Related