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);
elseval = 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 , 感謝黑暗大大!!