TIPS-製作固定欄寬資料檔
1 |
昨天小談了固定欄寬資料檔解析程式的寫法,有人問到解析之餘要如何產生固定欄寬資料,並提了一些常見但有點機車的需求,例如: 針對數字欄位要能指定數值靠右左方補零(不知為什麼,某些"阿公"很愛用這種不補空白要補零的規格,每次要肉眼除錯時,十來個數字欄位夾雜零零相連到天邊,數位置數到眼睛都快出血了)、文字過長要能自動截切,還要避免切成半個中文字...
禁不住這團機車小需求的刺激,忽然一陣熱血衝腦... 待神智恢復時,發現自己端坐在電腦前,VS2010開啟中的專案有一段小程式...
以下的類別是昨天FixedWidthTextParser的進化版(敢情昨天是皮卡丘,今天是雷丘就是了?),主要改了幾個地方:
- FieldDef擴充了PaddingChar、IsRightAlign、AutoTrim三個屬性欄位:
IsRightAlign預設為false,表示向左對齊,當需要向右對齊時,要設為true。
PaddingChar預設為空白(' '),但可指定'0'或其他用來填補不足長度的字元。
AutoTrim預設為false,當資料長度超過欄位長度限制時,一律丟出錯誤;若希望程式自動將過長部分截掉,則可將其設為true。 - FixedWidthTextParser重新命名為FixWidthColTextHelper,ParseLine()原本的Trim()修改為依IsRightAlign決定用TrimStart()或TrimEnd(),而要Trim的字元以PaddingChar為準。
- 增加string DumpData(Dictionary<string, object> data)方法,傳入存有資料的Dictionary,依欄位定義組裝出固定欄寬資料字串。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
/// <summary>
/// 欄位定義
/// </summary>
public class FieldDef
{
/// <summary>
/// 欄位名稱
/// </summary>
public string FieldName;
/// <summary>
/// 起啟位置
/// </summary>
public int StartPos = -1;
/// <summary>
/// 欄位長度
/// </summary>
public int Length;
/// <summary>
/// 填補字元(一般為空白或0)
/// </summary>
public char PaddingChar = ' ';
/// <summary>
/// 是否向右靠齊向左填補
/// </summary>
public bool IsRightAlign = false;
/// <summary>
/// 是否自動截斷過長部分,設為false遇過長時則?出例外
/// </summary>
public bool AutoTrim = false;
/// <summary>
/// 建構式
/// </summary>
/// <param name="fldName">欄位名稱</param>
/// <param name="len">欄位長度</param>
/// <param name="paddingChar">長度不足填補字元,預設為空白</param>
/// <param name="rightAlign">是否靠右對齊,預設為否</param>
public FieldDef(string fldName, int len,
bool rightAlign = false, char paddingChar = ' ')
{
FieldName = fldName;
Length = len;
PaddingChar = paddingChar;
IsRightAlign = rightAlign;
}
}
/// <summary>
/// 固定寬度文件解析
/// </summary>
public class FixWidthColTextHelper
{
List<FieldDef> fields = new List<FieldDef>();
Encoding encoding;
int lineLength = 0;
/// <summary>
/// 建構式,傳入文件定義
/// </summary>
/// <param name="enc">文字編碼</param>
/// <param name="def">欄位定義</param>
public FixWidthColTextHelper(
Encoding enc,
params FieldDef[] def)
{
encoding = enc;
int startPos = 0;
foreach (FieldDef fd in def)
{
fd.StartPos = startPos;
fields.Add(fd);
startPos += fd.Length;
lineLength += fd.Length;
}
}
/// <summary>
/// 傳入單行資料字串,解析為多欄值
/// </summary>
/// <param name="line">原始單行資料字串</param>
/// <returns>各欄位值之雜湊表</returns>
public Dictionary<string, string> ParseLine(string line)
{
//資料字串轉為byte[]
byte[] data = encoding.GetBytes(line);
//檢查長度是否吻合?
if (data.Length != lineLength)
throw new ApplicationException(
string.Format("字串長度({0}Bytes)不符要求({1}Bytes)!",
data.Length, lineLength));
//宣告雜湊結構存放資料
var result = new Dictionary<string, string>();
//依欄位位置、長度逐一取值
foreach (var fd in fields)
{
//由指定位置取得內容
string val = encoding.GetString(data, fd.StartPos, fd.Length);
//依靠左靠右做不同處理
if (fd.IsRightAlign)
val = val.TrimStart(fd.PaddingChar);
else
val = val.TrimEnd(fd.PaddingChar);
//以欄位名稱為Key存入
result.Add(fd.FieldName, val);
}
return result;
}
/// <summary>
/// 解析多行固定欄寬資料
/// </summary>
/// <param name="text">多行文字資料</param>
/// <returns>解析結果</returns>
public List<Dictionary<string, string>> Parse(string text)
{
var all = new List<Dictionary<string, string>>();
using (StringReader sr = new StringReader(text))
{
string line = null;
while ((line = sr.ReadLine()) != null)
all.Add(ParseLine(line));
}
return all;
}
/// <summary>
/// 解析固定欄寬資料檔
/// </summary>
/// <param name="path">檔案路徑</param>
/// <returns>解析結果</returns>
public List<Dictionary<string, string>> ParseFile(string path)
{
if (!File.Exists(path))
throw new ApplicationException(path + "不存在!");
return Parse(File.ReadAllText(path, encoding));
}
/// <summary>
/// 傳入資料欄位,依欄位定義匯出成為字串
/// </summary>
/// <param name="data">以雜湊方式保存的欄位值</param>
/// <returns>固定欄寬之資料字串</returns>
public string DumpData(Dictionary<string, object> data)
{
StringBuilder sb = new StringBuilder();
foreach (var fd in fields)
{
string val = data.ContainsKey(fd.FieldName) ?
Convert.ToString(data[fd.FieldName]) : "";
//計算資料長度
byte[] buff = encoding.GetBytes(val);
int len = buff.Length;
//超過長度且不允許自動截斷時,丟出例外
if (len > fd.Length)
{
if (!fd.AutoTrim)
throw new ApplicationException(
string.Format("欄位[{0}]內容過長!(長度={1} 限制={2})",
fd.FieldName, len, fd.Length));
else
{
//自動截除過長部分
val = encoding.GetString(buff, 0, fd.Length);
//若切到半個中文時會產生"?"(0x3f),加入以下邏輯避免
if (val.EndsWith("?") && buff[fd.Length - 1] != 0x3f)
val = val.Remove(val.Length - 1) + " ";
}
}
//決定左補或是右補
if (len < fd.Length)
{
//因需配合Encoding算長度,不能直接用PaddingLeft()
string padding =
new string(fd.PaddingChar, fd.Length - len);
if (fd.IsRightAlign) //靠右對齊時補左邊
val = padding + val;
else //靠左對齊時補右邊
val += padding;
}
sb.Append(val);
}
return sb.ToString();
}
}
以下是用來測試/驗證功能的程式碼。分別測試了左靠/右靠比一比、補空補零補星星、自動截斷切中文、切成半個也沒關係... 等等。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
FixWidthColTextHelper fwct =
new FixWidthColTextHelper(
Encoding.GetEncoding(950),
//左靠左補空
new FieldDef("姓名", 10),
//右靠左補零
new FieldDef("積分", 6, true, '0'),
//右靠左補空
new FieldDef("獎金", 5, true),
//左靠右補星號
new FieldDef("等級", 10, false, '*'),
//左靠過長自動截斷
new FieldDef("註記", 6) { AutoTrim = true }
);
//建立測試資料
var data = new Dictionary<string, object>();
data.Add("姓名", "黑暗執行緒");
data.Add("積分", 1688);
data.Add("獎金", 8888);
data.Add("等級", "初心者");
data.Add("註記", "中文備註");
string test = fwct.DumpData(data);
Console.WriteLine(test);
//測試解析時可否正確去除左補零及右補星
var result = fwct.ParseLine(test);
Console.WriteLine("積分={0} 等級={1}",
result["積分"], result["等級"]);
//測試自動截斷切到半個中文字
data["註記"] = "ABC中文";
Console.WriteLine(fwct.DumpData(data));
//驗證自動截斷能正確保留"?"
data["註記"] = "ABC中?";
Console.WriteLine(fwct.DumpData(data));
//測試未設自動截斷時,過長會出錯
data["姓名"] = "黑暗執行緒很酷";
try
{
Console.WriteLine(fwct.DumpData(data));
}
catch (Exception ex)
{
Console.WriteLine("Eror=" + ex.Message);
}
Console.Read();
}
}
}
測試結果如下,Check It Out!
黑暗執行緒001688 8888初心者****中文備 積分=1688 等級=初心者 黑暗執行緒001688 8888初心者****ABC中 黑暗執行緒001688 8888初心者****ABC中? Eror=欄位[姓名]內容過長!(長度=12 限制=10)
Comments
# by Money
時隔10年, 現今還留存著祖父級別的系統需要這種格式的輸出 XD , 感謝黑暗大大!!