<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: lxb dev</title>
    <description>The latest articles on DEV Community by lxb dev (@lxb_dev).</description>
    <link>https://dev.to/lxb_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4236211%2F92061e47-368c-46d8-95d4-895b92ab9015.png</url>
      <title>DEV Community: lxb dev</title>
      <link>https://dev.to/lxb_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lxb_dev"/>
    <language>en</language>
    <item>
      <title>Merging CSV and Excel files without breaking them: encodings, leading zeros, and header drift (.NET notes)</title>
      <dc:creator>lxb dev</dc:creator>
      <pubDate>Tue, 10 Nov 2026 03:37:16 +0000</pubDate>
      <link>https://dev.to/lxb_dev/merging-csv-and-excel-files-without-breaking-them-encodings-leading-zeros-and-header-drift-net-517g</link>
      <guid>https://dev.to/lxb_dev/merging-csv-and-excel-files-without-breaking-them-encodings-leading-zeros-and-header-drift-net-517g</guid>
      <description>&lt;p&gt;"Just concatenate the files" works until the first real-world export shows up. When I was building a small desktop tool in .NET/WPF that merges and splits spreadsheets, three problems caused most of the bugs. Here they are, with the fix I ended up using for each.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. "CSV" is not one encoding
&lt;/h3&gt;

&lt;p&gt;In practice you'll see at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UTF-8&lt;/li&gt;
&lt;li&gt;UTF-8 with a BOM (common from Excel's "CSV UTF-8" option)&lt;/li&gt;
&lt;li&gt;GBK / GB18030 (still very common in exports from Chinese finance and e-commerce systems)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open a GBK file as UTF-8 and every Chinese name turns into garbage. A simple detection order that works well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check for a BOM. If there is one, trust it.&lt;/li&gt;
&lt;li&gt;Try decoding as &lt;strong&gt;strict&lt;/strong&gt; UTF-8, so invalid bytes throw an error instead of being silently replaced.&lt;/li&gt;
&lt;li&gt;If that fails, fall back to GB18030 (a superset of GBK).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .NET Core / .NET 5+: legacy code pages need this once at startup&lt;/span&gt;
&lt;span class="nc"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RegisterProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CodePagesEncodingProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Encoding&lt;/span&gt; &lt;span class="nc"&gt;DetectEncoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xEF&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xBB&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xBF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UTF8Encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UTF8Encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;throwOnInvalidBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UTF8Encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DecoderFallbackException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetEncoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GB18030"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain ASCII is valid UTF-8, so step 2 won't misdetect it. Still give users a manual override, because detection can guess wrong on short files.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Leading zeros are data
&lt;/h3&gt;

&lt;p&gt;Employee IDs, phone numbers, postal codes, and some order numbers look like numbers but aren't. Once &lt;code&gt;00123&lt;/code&gt; becomes &lt;code&gt;123&lt;/code&gt;, lookups and joins quietly fail.&lt;/p&gt;

&lt;p&gt;Rules that saved me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read every CSV cell as a &lt;strong&gt;string&lt;/strong&gt;. Don't infer types during the merge.&lt;/li&gt;
&lt;li&gt;When writing .xlsx, write ID-like columns as text cells, not numbers.&lt;/li&gt;
&lt;li&gt;In a preview grid, show the raw string, so users can spot damage before they export.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Header drift
&lt;/h3&gt;

&lt;p&gt;File A has &lt;code&gt;Dept&lt;/code&gt;, file B has &lt;code&gt;Department&lt;/code&gt;, and file C added &lt;code&gt;CostCenter&lt;/code&gt; halfway through the year. You need to pick a strategy on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Align to the first file:&lt;/strong&gt; the output keeps file 1's columns. Extra columns in later files are dropped, and it's worth reporting which ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Union of headers:&lt;/strong&gt; keep every column you see and leave missing values empty. You lose nothing, but you get sparse columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever you choose, trim whitespace and compare headers case-insensitively, because &lt;code&gt;Name&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; are a classic trap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: splitting
&lt;/h3&gt;

&lt;p&gt;Splitting one sheet into one file per value of a column is mostly about file names: strip characters Windows doesn't allow (&lt;code&gt;\ / : * ? " &amp;lt; &amp;gt; |&lt;/code&gt;), handle empty values (for example, a &lt;code&gt;_blank&lt;/code&gt; file), and watch out for values that differ only by case. Windows treats &lt;code&gt;Shanghai.xlsx&lt;/code&gt; and &lt;code&gt;shanghai.xlsx&lt;/code&gt; as the same file.&lt;/p&gt;




&lt;p&gt;How do you handle header drift? Do you normalize headers in a script first, or keep a mapping table?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: this post was written with AI assistance and reviewed by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>excel</category>
      <category>tutorial</category>
      <category>csv</category>
    </item>
  </channel>
</rss>
