Guides · Excel and CSV
Excel CSV Import Drops Leading Zeros: Keep IDs as Text
Published · Tested with Microsoft 365 Excel 16.0 (build 20430), PowerShell 7.6.6 and Windows PowerShell 5.1 (build 26100) on Windows 11 Pro
A report export has an order ID 00123 and a ZIP code 02134. Someone opens the CSV in Excel, fixes one cell and saves. The IDs are now 123 and 2134, nothing complains, and the lookup that joins this file to another system finds no match. This guide reproduces the loss with desktop Excel, shows that saving makes it permanent, and tests four ways to load the file. One of them, Workbooks.OpenText with a text setting for the column, does nothing at all on a .csv file. Two scripts at the end do the safe version: one checks a CSV for values Excel would change and exits with code 1, the other loads it as text into an .xlsx.
Before you start
- The data is invented.
orders.csvhas five columns and four rows chosen to hit each rule:order_id(00123,00456,01007and a 20-digit ID),zip(02134,10001,90210,07030),part(1E5,007E2,0042,12-34),ship_date(ISO dates) andqty(a plain number). - How we tested. Desktop Excel from Microsoft 365, version 16.0, build 20430, driven from PowerShell through COM, each time in a separate Excel process that the script ended. Every case ran three times and the three runs matched. The two scripts below also ran three times in PowerShell 7.6.6 and three times in Windows PowerShell 5.1.26100.9444, with the same results. Nothing was run in the Excel you may have open.
- This PC uses Spanish (Spain) regional settings. That matters in two ways. Excel’s own CSV opener splits on the Windows list separator, which here is a semicolon, so the loss cases below use a copy of the file with
;as the delimiter (the methods that name the delimiter themselves read the comma version). And Excel shows numbers and dates in the regional style:1,00E+05anddic-34here, where an English setting shows a point and “Dec-34”. The stored values are the same; we did not run the cases under an English setting. - Excel’s rules. Microsoft lists the conversions Excel applies on its own, under “Set automatic data conversions” (Microsoft Support). It names four: removing leading zeros and converting to a number, keeping only the first 15 digits of long numbers, converting digits around the letter “E” to scientific notation, and converting date-like letter-and-number combinations to a date.
- Related. If the file comes from a script, PowerShell JSON to CSV covers columns that go missing before the file exists. To run these scripts from a scheduler without orphan Excel processes, see Close Only the Excel Your Script Opened.
What Excel does to the file
The call that matches “open the CSV”, Workbooks.Open, on the semicolon version of orders.csv. The file holds text. Excel reads each field and keeps what it recognises as a number or a date:
| Column | In the file | In the cell | Stored as |
|---|---|---|---|
order_id | 00123 | 123 | number |
order_id | 12345678901234567890 | 1,23E+19 | number, digits past 15 lost |
zip | 02134 | 2134 | number |
zip | 07030 | 7030 | number |
part | 1E5 | 1,00E+05 | number (100000) |
part | 007E2 | 7,00E+02 | number (700) |
part | 0042 | 42 | number |
part | 12-34 | dic-34 | a date (December 1934) |
ship_date | 2026-01-05 | ######## in a narrow column | a date (a number with a date format) |
- Nothing marks the change. The file opened without a message and every cell looks like a plausible value.
12-34is a part number here; Excel stored December 1934. - The ISO dates are a different case. Turning
2026-01-05into a date is usually what you want. The hashes only mean the column is too narrow for the date. - Saving makes it permanent. We opened the file with
Workbooks.Openand saved it as CSV without touching a cell. The saved file, three runs:
The zeros are gone,order_id;zip;part;ship_date;qty 123;2134;1,00E+05;05/01/2026;3 456;10001;7,00E+02;03/02/2026;12 1007;90210;42;04/03/2026;1 1,23457E+19;7030;dic-34;05/04/2026;71E5is now a different string, the 20-digit ID is rounded and the dates are written in the regional style, not ISO. The original values cannot be rebuilt from this file. - Formatting afterwards is too late. Setting a column to Text after the file is open only changes how a number is shown. The zeros were removed when the cell was read.
Four ways to load it
The loaders that let you say “this column is text” are Workbooks.OpenText, a QueryTable and Power Query. Results on the same data, three runs each:
| Method | Result |
|---|---|
Workbooks.Open on the .csv | Numbers and dates, the table above |
Workbooks.OpenText on the .csv, every column set to text | Same as Open. The setting is ignored and no error is raised |
Workbooks.OpenText on a copy named .txt, every column set to text | Text, all values as in the file |
QueryTables.Add on the .csv with TextFileColumnDataTypes | Text, all values as in the file |
Power Query (Csv.Document and Table.TransformColumnTypes with type text) | Text, all values as in the file |
OpenTextis the trap. It is the method every example on the web uses, and with a.csvpath it gave the same numbers as plainOpen. The same call on a copy of the file with a.txtextension kept the text. We did not find this behaviour in Microsoft’s documentation, so treat it as what we measured on this build. The delimiter flags were ignored too: on the comma file,OpenTextwithCommaset to true left each whole line in column A.- A
QueryTableworks on the.csvas it is. You pass one type per column: 2 for text, 1 for General. That is what the script in the last section uses. - Power Query keeps the text because you say so. From the interface it is Data, From Text/CSV, Transform Data, then change the column type to Text. The M behind it, which we also ran from code, is:
Type names every column you care about. A column you leave out is whatever Power Query detects.let Source = Csv.Document(File.Contents("C:\data\orders.csv"), [Delimiter=",", Encoding=65001, QuoteStyle=QuoteStyle.Csv]), Headers = Table.PromoteHeaders(Source, [PromoteAllScalars=true]), Typed = Table.TransformColumnTypes(Headers, {{"order_id", type text}, {"zip", type text}, {"part", type text}, {"ship_date", type text}, {"qty", Int64.Type}}) in Typed
The Automatic Data Conversion option
Microsoft 365 and Excel 2024 have a switch for this. Under File, Options, Data, Automatic Data Conversion, Microsoft lists “Remove leading zeros and convert to number” and the other three conversions, and says they apply when opening a .csv or .txt file. It also says the options “don’t directly affect data imported by using Power Query” (Microsoft Support).
- What we saw. With this Excel’s settings untouched,
Workbooks.Openapplied the conversions in the table above, so on this install the options were on, as they are by default. - What we did not do. We did not switch the options off. They are per-user Excel settings on the PC, which is outside what a guide should change, and we did not find a documented way to set them from code. So we cannot say how
Workbooks.Openbehaves with them off. - Why it is not the fix for a pipeline. A setting on your PC does not travel with the file. The colleague who opens the CSV, and the server that runs your script, have their own values. Loading the column as text, or checking the file before it is opened, does not depend on anyone’s options.
Check the file before Excel sees it
Every cell that Excel changes has a value in the file that looks like what it will change. The script reads the CSV as plain text, no Excel needed, and reports each column that has a value matching one of four patterns: digits with a leading zero, 16 or more digits, digits around an E, and two groups of digits joined by - or /. It exits with 1 when it finds any, so a scheduled job or a CI step stops. An empty file or a missing one is an error with code 2, not a pass. Save it as Test-CsvTextColumns.ps1:
# Test-CsvTextColumns.ps1: find CSV columns that Excel would change when it opens the file.
# Exit 0 = no risky values, 1 = at least one column at risk, 2 = file unreadable or empty.
# Windows PowerShell 5.1 and PowerShell 7.
param(
[Parameter(Mandatory)] [string] $Path,
[string] $Delimiter = ','
)
$ErrorActionPreference = 'Stop'
# Each rule: the name shown in the report and the pattern of a value Excel would change.
$rules = @(
@{ Name = 'leading zeros dropped'; Pattern = '^0\d+$' },
@{ Name = 'digits past 15 rounded'; Pattern = '^\d{16,}$' },
@{ Name = 'read as scientific notation'; Pattern = '^\d+[eE][+-]?\d+$' },
@{ Name = 'read as a date'; Pattern = '^\d{1,2}[-/]\d{1,4}$' }
)
try {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "File not found: $Path" }
$rows = @(Import-Csv -LiteralPath $Path -Delimiter $Delimiter -Encoding UTF8)
if ($rows.Count -eq 0) { throw 'No data rows. An empty file is not the same as a clean one.' }
} catch {
[Console]::Error.WriteLine("ERROR: $($_.Exception.Message)")
exit 2
}
$findings = foreach ($column in $rows[0].PSObject.Properties.Name) {
foreach ($rule in $rules) {
$hits = @($rows | Where-Object { "$($_.$column)".Trim() -match $rule.Pattern })
if ($hits.Count -gt 0) {
[pscustomobject]@{
Column = $column
Risk = $rule.Name
Rows = $hits.Count
Example = "$($hits[0].$column)".Trim()
}
}
}
}
if ($findings) {
$findings | Format-Table -AutoSize | Out-String -Width 200 | Write-Host
Write-Host "RISK: $(@($findings | Select-Object -ExpandProperty Column -Unique).Count) column(s) would change in Excel. Import them as text."
exit 1
}
Write-Host "OK: $($rows.Count) rows, no value Excel would change."
exit 0
On the invented file, in PowerShell 7.6.6 and in Windows PowerShell 5.1, in every run:
PS> .\Test-CsvTextColumns.ps1 -Path .\orders.csv
Column Risk Rows Example
------ ---- ---- -------
order_id leading zeros dropped 3 00123
order_id digits past 15 rounded 1 12345678901234567890
zip leading zeros dropped 2 02134
part leading zeros dropped 1 0042
part read as scientific notation 2 1E5
part read as a date 1 12-34
RISK: 3 column(s) would change in Excel. Import them as text.
PS> $LASTEXITCODE
1
A file with values like A1 and Z9 printed OK: 2 rows, no value Excel would change. and exited with 0. A file with a header and no rows, and a path that does not exist, both exited with 2.
- It is a list of patterns, not a copy of Excel. It flags what we saw Excel change. It does not know every conversion Excel makes, and a value it flags may be fine for your use. Add a rule when you find a new case.
- It is also useful on its own. A leading-zero column is a sign that the column is an identifier. If your contract says it is text, a number is a defect to catch at the file, not in the report.
Load it as text from PowerShell
The second script uses the QueryTable from the table above. It counts the columns from the CSV, sets every one to text except the ones you list in -NumericColumns (numbered from 1), saves an .xlsx, and ends only the Excel process it started. Exit codes: 0 saved, 2 bad input, 3 Excel failed. Save it as Import-CsvAsText.ps1:
# Import-CsvAsText.ps1: load a CSV into a new .xlsx with every column as text,
# except the ones you list in -NumericColumns (1-based). Uses its own Excel process
# and ends only that process. Exit 0 = saved, 2 = bad input, 3 = Excel failed.
# Windows PowerShell 5.1 and PowerShell 7. Needs desktop Excel.
param(
[Parameter(Mandatory)] [string] $Csv,
[Parameter(Mandatory)] [string] $Xlsx,
[string] $Delimiter = ',',
[int[]] $NumericColumns = @()
)
$ErrorActionPreference = 'Stop'
Add-Type -Namespace Win32 -Name Native -MemberDefinition @'
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
'@
try {
$Csv = (Resolve-Path -LiteralPath $Csv).ProviderPath
$Xlsx = [IO.Path]::GetFullPath($Xlsx)
$columnCount = @((Import-Csv -LiteralPath $Csv -Delimiter $Delimiter -Encoding UTF8) | Select-Object -First 1 |
ForEach-Object { $_.PSObject.Properties }).Count
if ($columnCount -eq 0) { throw 'No data rows or no header.' }
} catch {
[Console]::Error.WriteLine("ERROR: $($_.Exception.Message)")
exit 2
}
# xlGeneralFormat = 1, xlTextFormat = 2
$types = 1..$columnCount | ForEach-Object { if ($NumericColumns -contains $_) { 1 } else { 2 } }
$excel = $null; $excelPid = 0
try {
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $false
[void][Win32.Native]::GetWindowThreadProcessId([IntPtr]$excel.Hwnd, [ref]$excelPid)
$book = $excel.Workbooks.Add()
$sheet = $book.Worksheets.Item(1)
$query = $sheet.QueryTables.Add("TEXT;$Csv", $sheet.Range('A1'))
$query.TextFilePlatform = 65001 # UTF-8
$query.TextFileParseType = 1 # xlDelimited
$query.TextFileOtherDelimiter = $Delimiter
$query.TextFileColumnDataTypes = [int[]]$types
[void]$query.Refresh($false)
$query.Delete() # keep the values, drop the live link
$book.SaveAs($Xlsx, 51) # xlOpenXMLWorkbook
$book.Close($false)
Write-Host "Saved $Xlsx ($columnCount columns, text except: $(if ($NumericColumns) { $NumericColumns -join ',' } else { 'none' }))"
$code = 0
} catch {
[Console]::Error.WriteLine("ERROR: Excel failed: $($_.Exception.Message)")
$code = 3
} finally {
if ($excel) { try { $excel.Quit() } catch { } ; [void][Runtime.InteropServices.Marshal]::ReleaseComObject($excel) }
if ($excelPid) { Start-Sleep -Milliseconds 500; Get-Process -Id $excelPid -ErrorAction SilentlyContinue | Stop-Process -Force }
}
exit $code
Run on orders.csv with column 5 numeric, three times in each PowerShell version:
PS> .\Import-CsvAsText.ps1 -Csv .\orders.csv -Xlsx .\orders.xlsx -NumericColumns 5
Saved C:\data\orders.xlsx (5 columns, text except: 5)
PS> $LASTEXITCODE
0
We read the saved .xlsx back without Excel, by unzipping it. The shared strings hold 00123, 02134, 1E5, 007E2, 0042, 12345678901234567890, 07030 and 12-34 exactly as in the CSV, and the qty cells are numbers (<c r="E2"><v>3</v></c>). No Excel process was left behind.
- The dates stay text.
ship_dateis2026-01-05as a string. Add its number to-NumericColumnsif you want Excel’s date conversion for it; the region then decides how it is read. - Run the check first.
Test-CsvTextColumns.ps1 -Path x.csvtells you which columns need the text treatment.-Delimiter ';'is a parameter of both scripts; we ran both with the semicolon version of the file, three times in each PowerShell version, with the same results.
Limits
- One machine and one build. Windows 11 Pro with Spanish (Spain) regional settings, Microsoft 365 Excel 16.0 build 20430, PowerShell 7.6.6 and Windows PowerShell 5.1.26100.9444. We did not test other Excel versions, Excel for Mac, Excel 2019 or earlier, other regional settings, or the Automatic Data Conversion options switched off.
- Method, not mouse. We tested
Workbooks.Open, which is the call behind opening a file from code. We did not test a double click in Explorer or File, Open in the interface, which we expect to behave the same but did not measure. We did not test Data, From Text/CSV with Excel’s own type detection. - The
OpenTextresult is a measurement. It ignored the text setting on a.csvand obeyed it on a.txtcopy in our runs, and we found no Microsoft page that says why. - Text is not a number. A column loaded as text will not sum, sort numerically or match a numeric column in a lookup. That is the point for identifiers; check it for anything else.
- The check reads the first sign, not every case. It looks at the four patterns above. Phone numbers with a plus, percentages, fractions and other date forms are not covered, and we did not test them.
- Tricks inside the CSV were not tried. We did not test quotes, a leading apostrophe or an
="00123"formula in the file. The methods above avoid the question by not letting Excel guess. - An existing
.xlsxis replaced without a prompt. The import script turns Excel’s alerts off, so a file at-Xlsxis overwritten. Point it at a new name if you need the old one. - Desktop Excel only. Both the import script and the methods table need the installed application. The check needs only PowerShell.
- It checks the file, not the business meaning. A text column with the right zeros can still hold the wrong ZIP code.
The code on this page was written for this guide and tested on the versions above. The data is invented.