Format-Csv.ps1

<#PSScriptInfo
.VERSION 1.0.0
.GUID 19631007-263d-4567-b726-665727c076ce
.AUTHOR iRon
.COMPANYNAME
.COPYRIGHT
.TAGS Csv format align columns readable
.LICENSE https://github.com/iRon7/Format-Csv/LICENSE
.PROJECTURI https://github.com/iRon7/Format-Csv
.ICON
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
.PRIVATEDATA
#>


<#
.SYNOPSIS
Formats (aligns) a Csv table
 
.DESCRIPTION
This cmdlet makes a Csv file or list better human readable by aligning the columns in a way that the resulted
Csv format is still a valid as input for the ConvertFrom-Csv cmdlet.
 
.INPUTS
Csv (here) string or object list
 
.OUTPUTS
String[]
 
.PARAMETER InputObject
    Specifies the CSV strings to be formatted or the objects that are converted to CSV formatted strings.
    You can also pipe objects to ConvertTo-CSV.
 
.PARAMETER Align
    Specifies the alignment of the columns
    * Left - to align all the columns to the left
    * Right - to align all the columns to the Right
    * Auto - to autmatically align each cell depending on the column and cell contents
 
    When autmatic alignment is set, the whole column is aligned to the right if all cells are numeric. Besides,
    each individual cell that contains a number type (e.g. integer) will also aligned to the right.
 
.PARAMETER Delimiter
    Specifies the delimiter to separate the property values in CSV strings. The default is a comma (,).
    Enter a character, such as a colon (:). To specify a semicolon (;) enclose it in single quotation marks.
 
.PARAMETER Quote
    Quotes all the headers and values. If the Quote switch is set, all the delimeters are aligned.
    (By default, each value is directly followed by a delimiter for compatibility reasons.)
 
.PARAMETER Tight
    This switch will remove the leading space (only added when the -Quote switch is set) and trailing space
    attached to each delimiter.
 
.EXAMPLE
    $Csv = @'
    "Name","Number","Object","Remark"
    "One","1","Text","Normal"
    "Two","2","123","Number"
    "Three","3","Te,xt","Comma in Text"
    "Four","4","Te""xt","Double quote in text"
    ,,,"Empty ($Null)"
    "Five","5","More","Normal"
    '@
 
    $Csv |Format-Csv
    Name, Number, Object, Remark
    One, 1, Text, Normal
    Two, 2, 123, Number
    Three, 3, "Te,xt", "Comma in Text"
    Four, 4, "Te""xt", "Double quote in text"
    , , , "Empty ($Null)"
    Five, 5, More, Normal
 
.LINK
    https://github.com/iRon7/Format-Csv
#>

[CmdletBinding(DefaultParameterSetName='Html')][OutputType([Object[]])] param(
    [Parameter(ValueFromPipeLine = $True, Mandatory = $True)]$InputObject,
    [Char]$Delimiter = ',',
    [ValidateSet('Auto', 'Left', 'Right')][String]$Align = 'Auto',
    [Switch]$Quote,
    [Switch]$Tight
)

begin {
    function Quote([String]$String) {
        if ($String.Contains('"')) { '"' + $String.Replace('"', '""') + '"' }
        elseif ($Quote -or $String.Contains($Delimiter) -or ($String -match '\s')) { '"' + $String + '"' } else { $String }
    }
    function IsNumeric([String]$String) {
        [double]::TryParse($String, [ref]$Null)
    }
    $Separator = if ($Tight) { "$Delimiter" } elseif ($Quote) { " $Delimiter " } else { "$Delimiter " }
    $Column = [Ordered]@{}
    $List = [System.Collections.Generic.List[psobject]]::new()
}

process {
    if ($List.get_Count() -or $InputObject) { $List.Add($InputObject) } # Skip leading empty objects
}

end {
    if ($List.get_Count()) {
        if ($List[0] -is [String]) { $List = $List |ConvertFrom-Csv -Delimiter $Delimiter }
        $Names = @($List[0].PSObject.Properties.Where{ $_.MemberType -eq 'NoteProperty' }.Name)
        if (!$Names) { $Names = @($List[0].PSObject.Properties.Where{ $_.MemberType -eq 'Property' }.Name) }
        foreach ($Name in $Names) {
            $Header = Quote $Name
            $IsNumber = $Name -isnot [String] -and (IsNumeric $Name) # Numeric type
            $Column[$Name] = @{
                Data     = [System.Collections.Generic.List[string]]$Header
                IsNumber = [System.Collections.Generic.List[bool]]@($IsNumber) # https://github.com/PowerShell/PowerShell/issues/17731
                Width    = $Header.Length
                Left     = if ($Align -eq 'Left') { $True } elseif ($Align -eq 'Right') { $False } else { $Null }
            }
        }
        foreach ($Object in $List) {
            foreach ($Name in $Names) {
                $Value = $Object.$Name
                $IsNumber = $Value -isnot [String] -and (IsNumeric $Value) # Numeric type
                $Column[$Name].IsNumber.Add($IsNumber)
                $String = "$Value"
                if ($Null -eq $Column[$Name].Left -and -not ($String -eq "" -or (IsNumeric $String))) { $Column[$Name].Left = $True } # Numeric string
                $String = Quote $String
                if ($Column[$Name].Width -lt $String.Length) { $Column[$Name].Width = $String.Length }
                $Column[$Name].Data.Add($String)
            }
        }
        for ($y = 0; $y -lt $Column[0].Data.Count; $y++) {
            -Join @(
                for ($x = 0; $x -lt $Names.Count; $x++) {
                    $Left = $Align -eq 'Left' -or ($Align -eq 'auto' -and $Column[$x].Left -and -not $Column[$x].IsNumber[$y])
                    $String = $Column[$x].Data[$y]
                    $Width = $Column[$x].Width
                    if ($Left) {
                        if ($x -eq $Names.Count - 1) { $String }
                        elseif ($Quote) { $String.PadRight($Width, ' ') + $Separator }
                        else { ($String + $Separator).PadRight($Width + $Separator.Length, ' ') }
                    }
                    else {
                        if ($x -eq $Names.Count - 1) { $String.PadLeft($Width, ' ') }
                        else { $String.PadLeft($Width, ' ') + $Separator }
                    }
                }
            )
        }
    }
}