Format-Csv.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<#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 }
                    }
                }
            )
        }
    }
}