Different length arrays to one CSV

Viewed 68

If you have multiple arrays of different length, how can you export these to a single csv in powershell?

Array1 = 1,2,3 
Array2 = Bob,smithy,Alex,Jeremy 
Array3 = yes,no

Output CSV

Number  Name  Valid 
———————————————————  
1      Bob    Yes  
2      Smithy no  
3      Alex
       Jeremy

Basically each array would be in its own header column.

Tried lines like

Array1 | Select-Object Number | export-csv -Path C:\Path

This works for singular arrays to singular csv files

But if I try

Array1, Array2, Array3 | Select-Object Number, Name, Valid | export-csv -Path C:\Path

I just get the header names and no values in the columns

3 Answers

One way to do it is with a for loop.

$Array1 = 1, 2, 3
$Array2 = 'Joe Bloggs', 'John Doe', 'Jane Doe'
$Array3 = 'Yes', 'No'

$export = for($i = 0; $i -lt [Linq.Enumerable]::Max([int[]] ($Array1.Count, $Array2.Count, $Array3.Count)); $i++) {
    [pscustomobject]@{
        Number = $Array1[$i]
        Name   = $Array2[$i]
        Valid  = $Array3[$i]
    }
}
$export | Export-Csv path\to\csv.csv -NoTypeInformation

Another example using a function, the logic is more or less the same except that there is more overhead involved, since this function can handle an indefinite amount of arrays coming from the pipeline.

function Join-Array {
    [CmdletBinding()]
    param(
        [parameter(ValueFromPipeline, Mandatory)]
        [object[]] $InputObject,

        [parameter(Mandatory)]
        [string[]] $Columns
    )

    begin {
        $inputDict = [ordered]@{}
        $index     = 0
    }
    process {
        try {
            if($MyInvocation.ExpectingInput) {
                return $inputDict.Add($Columns[$index++], $InputObject)

            }
            foreach($item in $InputObject) {
                $inputDict.Add($Columns[$index++], $item)
            }
        }
        catch {
            if($_.Exception.InnerException -is [ArgumentNullException]) {
                $PSCmdlet.ThrowTerminatingError(
                    [Management.Automation.ErrorRecord]::new(
                        [Exception] 'Different count between input arrays and Columns.',
                        'InputArrayLengthMismatch',
                        [Management.Automation.ErrorCategory]::InvalidOperation,
                        $InputObject
                    )
                )
            }
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    end {
        foreach($pair in $inputDict.GetEnumerator()) {
            $count = $pair.Value.Count
            if($count -gt $max) {
                $max = $count
            }
        }
        for($i = 0; $i -lt $max; $i++) {
            $out = [ordered]@{}
            foreach($column in $inputDict.PSBase.Keys) {
                $out[$column] = $inputDict[$column][$i]
            }
            [pscustomobject] $out
        }
    }
}

The usage would be pretty easy, the arrays can be passed through the pipeline:

$Array1 = 1, 2, 3
$Array2 = 'Joe Bloggs', 'John Doe', 'Jane Doe'
$Array3 = 'Yes', 'No'
$Array4 = 'hello', 'world', 123, 456

$Array1, $Array2, $Array3, $Array4 | Join-Array -Columns Number, Name, Valid, Test |
    Export-Csv path\to\csv.csv -NoTypeInformation

Or via Named Parameter / Positional Binding:

Join-Array $Array1, $Array2, $Array3, $Array4 -Columns Number, Name, Valid, Test | 
    Export-Csv path\to\csv.csv -NoTypeInformation

For quiet some time I am maintaining a Join-Object script/Join-Object Module (see also: In Powershell, what's the best way to join two tables into one?). It's main purpose is joining tables (aka lists of objects) based on a related column (aka property) defined by the -on parameter using an user (aka scripter) friendly syntax which as much joining options as possible. This has lead to a few implicated features that could be used in your specific request:

  • If there is no relation defined (the -on parameter is omitted), it is assumed that you want to do a side-by-side join of the tables (aka lists)
    • If the tables (or lists) are not equal in size, a common (inner) join will end when either lists ends. A full join (e.g. FullJoin-Object alias FullJoin or a left - or right join) at the other hand, will join the complete respective lists
  • An array of scalars (e.g. strings rather then -custom- objects) is joined as being a list of objects where the (default) column (aka property) name is Value
    • If both sides contain a list a scalars, the output will be a [Collections.ObjectModel.Collection[psobject]] list containing each left - and right item
  • Join commands can be chained: ... |Join ... |Join ...
  • If the columns (aka properties) provided in the tables (aka lists) overlap, they will be merged in an array ( <property name> = <left property value>, <right property value>) by default, which items could also be assigned to a specific name (prefix) using the -Name parameter. This parameter can be used with every join commando in the chain.

All together this means that you might reach your requirement with the following command line:

$Array1 |FullJoin $Array2 |FullJoin $Array3 -Name Number, Name, Valid
Number Name       Valid
------ ----       -----
     1 Joe Bloggs Yes
     2 John Doe   No
     3 Jane Doe

You need to restructure the data to pivot it into one array (presented here in pseudo-json):

[ 
  {"1", "Bob", "Yes"}, 
  {"2", "Smithy", "no"}, 
  {"3", "Alex", ""}, 
  {"", "Jeremy", ""} 
]

Note how the missing fields still appear with empty placeholders.

Once you've pivoted your data this way, writing it to a CSV file is trivial, as are many other tasks (this is the better way to structure things in the first place).

Related