昨天小談了固定欄寬資料檔解析程式的寫法,有人問到解析之餘要如何產生固定欄寬資料,並提了一些常見但有點機車的需求,例如: 針對數字欄位要能指定數值靠右左方補零(不知為什麼,某些"阿公"很愛用這種不補空白要補零的規格,每次要肉眼除錯時,十來個數字欄位夾雜零零相連到天邊,數位置數到眼睛都快出血了)、文字過長要能自動截切,還要避免切成半個中文字...

禁不住這團機車小需求的刺激,忽然一陣熱血衝腦... 待神智恢復時,發現自己端坐在電腦前,VS2010開啟中的專案有一段小程式...

以下的類別是昨天FixedWidthTextParser的進化版(敢情昨天是皮卡丘,今天是雷丘就是了?),主要改了幾個地方:

  1. FieldDef擴充了PaddingChar、IsRightAlign、AutoTrim三個屬性欄位:
    IsRightAlign預設為false,表示向左對齊,當需要向右對齊時,要設為true。
    PaddingChar預設為空白(' '),但可指定'0'或其他用來填補不足長度的字元。
    AutoTrim預設為false,當資料長度超過欄位長度限制時,一律丟出錯誤;若希望程式自動將過長部分截掉,則可將其設為true。
  2. FixedWidthTextParser重新命名為FixWidthColTextHelper,ParseLine()原本的Trim()修改為依IsRightAlign決定用TrimStart()或TrimEnd(),而要Trim的字元以PaddingChar為準。
  3. 增加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 , 感謝黑暗大大!!

Post a comment