C#で固定長ファイルを作成する。バイト長で長さを指定する。シフトJISのファイルを作成する。

固定長ファイルをSJISのバイト指定で出力するためのクラスです。
SJIS以外の文字コードも指定可能です(※試してません)。
私が必要になった機能のみを実装したものです。

固定長ファイル出力クラスライブラリ
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace FixedText
{
    /// <summary>
    /// クラス、構造体の固定長ファイル出力定義用属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class FixedTextFileAttribute : Attribute
    {
        /// <summary>
        /// エンコード
        /// </summary>
        public string Encode;

        /// <summary>
        /// クラスまたは構造体に定義されたエンコーディングを取得する
        /// </summary>
        /// <typeparam name="T">対象のクラス、構造体</typeparam>
        /// <returns>エンコード</returns>
        public static Encoding GetEncoding<T>()
        {
            var encAttr = Attribute.GetCustomAttribute(
                typeof(T), typeof(FixedTextFileAttribute)) as FixedTextFileAttribute;

            return Encoding.GetEncoding(encAttr.Encode);
        }

        /// <summary>
        /// 指定された属性に従って固定長テキストを作成して返す
        /// </summary>
        /// <typeparam name="T">出力対象データクラス、構造体</typeparam>
        /// <param name="o">出力データが格納されたオブジェクト</param>
        /// <returns></returns>
        public static string ConvertFixedText<T>(object o)
        {
            var values = (T)o;

            //指定されたエンコードを取得する
            Encoding enc = GetEncoding<T>();

            //データを固定長文字列にする
            StringBuilder result = new StringBuilder();
            foreach (PropertyInfo info in typeof(T).GetProperties())
            {
                //出力対象除外項目判定:Reject属性が設定されている項目を除外する
                var reject = Attribute.GetCustomAttribute(info, typeof(RejectAttribute));
                if (reject != null)
                {
                    continue;
                }

                //固定長定義属性を取得する
                var fixedAttr = Attribute.GetCustomAttribute(
                    info, typeof(FixedAttribute)) as FixedAttribute;
                //属性が設定されていないならその項目を出力対象から除外する
                if(fixedAttr == null)
                {
                    continue;
                }

                //出力項目値を出力する
                string value = info.GetValue(values, null).ToString();

                //指定バイトを超えている場合、後ろから1文字づつ削って指定バイトに調整する
                while (fixedAttr.ByteLength < enc.GetByteCount(value))
                {
                    value = value.Substring(0, value.Length - 1);
                }

                //不足長を指定文字で埋める
                //※PadRight,PadLeftの指定桁は文字数であり、バイト数ではない。
                //  従って、不足バイト数ではなく不足文字数に換算して指定する
                int padLen = fixedAttr.ByteLength - (enc.GetByteCount(value) - value.Length);
                switch (fixedAttr.PadType)
                {
                    case EPadType.After:
                        result.Append(value.PadRight(padLen, fixedAttr.PadChar));
                        break;

                    case EPadType.Before:
                        result.Append(value.PadLeft(padLen, fixedAttr.PadChar));
                        break;
                }
            }

            return result.ToString();
        }

        /// <summary>
        /// 固定長でファイルに出力する
        /// </summary>
        /// <typeparam name="T">出力対象データクラス、構造体</typeparam>
        /// <param name="fileName">出力ファイル名(パス付き)</param>
        /// <param name="values">出力対象データ</param>
        public static void WriteFixedTextFile<T>(string fileName, IEnumerable<T> values)
        {
            //フォルダが存在しないなら作成する
            string path = Path.GetDirectoryName(fileName);
            if (!string.IsNullOrWhiteSpace(path))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(fileName));
            }

            //出力する固定長文字列を作成する
            StringBuilder builder = new StringBuilder();
            foreach (T value in values)
            {
                builder.AppendLine(ConvertFixedText<T>(value));
            }

            //作成した固定長データを指定されたエンコードのテキストファイルに出力する
            File.WriteAllText(fileName, builder.ToString(), GetEncoding<T>());
        }
    }

    /// <summary>
    /// 項目毎の固定長出力定義属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class FixedAttribute : Attribute
    {
        //バイト長
        public int ByteLength;
        //不足桁への文字埋めで使用する文字
        public char PadChar = ' ';
        //前埋め/後埋め
        public EPadType PadType = EPadType.After;
    }

    /// <summary>
    /// 固定長出力しない項目を指定するための属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class RejectAttribute : Attribute
    {
    }

    /// <summary>
    /// 不足桁文字埋めタイプ
    /// </summary>
    public enum EPadType
    {
        Before    //前埋め
        ,After    //後埋め
    }
}

使い方

出力データ定義クラス(または構造体)を作成する

出力データを格納するクラスまたは構造体を作成します。
そして、属性を使って文字コード、長さ、前埋め、後埋め、埋める文字を指定します。
(※項目はプロパティで宣言してください。フィールド属性への対応をしていません)

  • クラスの属性で文字コードを指定します

  • 各プロパティの属性で長さ、前埋め、後埋め、埋める文字を指定します

  • 出力対象外のプロパティがある場合はReject属性を使います

コード例:出力データを定義したクラス
[FixedTextFile(Encode = "Shift_JIS")]
public class Todofuken
{
    /// <summary>
    /// 都道府県コード
    /// </summary>
    [Fixed(ByteLength = 2, PadChar = '0', PadType = EPadType.Before)]
    public int Code { set; get; }

    /// <summary>
    /// 都道府県名
    /// </summary>
    [Fixed(ByteLength = 10, PadChar = ' ', PadType = EPadType.After)]
    public string Name { set; get; }

    /// <summary>
    /// 県庁所在地
    /// </summary>
    [Reject]
    public string PrefecturalCapital { set; get; }

    /// <summary>
    /// 面積
    /// </summary>
    [Fixed(ByteLength = 10, PadChar = ' ', PadType = EPadType.Before)]
    public decimal Area { set; get; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="code">都道府県コード</param>
    /// <param name="name">都道府県名</param>
    /// <param name="capital">県庁所在地</param>
    /// <param name="area">面積</param>
    public Todofuken(int code, string name, string capital, decimal area)
    {
        this.Code = code;
        this.Name = name;
        this.PrefecturalCapital = capital;
        this.Area = area;
    }
}

固定長ファイル出力メソッドを呼び出す

FixedTextFileAttribute の WriteFixedTextFile メソッドを使用して、固定長ファイルを作成します。

コード例:固定長ファイルを出力する
Todofuken[] data = {
    new Todofuken(1,"北海道", "札幌市", 83424.22m)
    , new Todofuken(2, "青森県", "青森市", 9645.40m)
    , new Todofuken(3, "岩手県", "盛岡市", 15275.01m)
    , new Todofuken(4, "宮城県", "仙台市", 7282.14m)
    , new Todofuken(5, "秋田県", "秋田市", 11637.54m)
};

//固定長ファイルを出力する
FixedTextFileAttribute.WriteFixedTextFile<Todofuken>(@"都道府県一覧.txt", data);
上記ソースで出力された固定長ファイルの中身
01北海道      83424.22
02青森県       9645.40
03岩手県      15275.01
04宮城県       7282.14
05秋田県      11637.54

C#で構造体やクラスに定義した属性の値を取得する

構造体やクラスのプロパティに定義した属性値を取得する方法を説明します。
例として、下記の構造体のプロパティに付加された 『FixedText』 の属性値を取得します。

FixedText属性を持った構造体
public struct Todofuken
{
    /// <summary>
    /// 都道府県コード
    /// </summary>
    [FixedText(Len = 2, PadChar = '0']
    public int Code { set; get; }

    /// <summary>
    /// 都道府県名
    /// </summary>
    [FixedText(Len = 10, PadChar = ' ']
    public string Name { set; get; }
}

なお、属性クラスは下記になります。詳しくは前回の記事をご覧ください。

FixedText属性クラス
[AttributeUsage(AttributeTargets.Property)]
public class FixedTextAttribute : System.Attribute
{
    public int Len { set; get; }
    public char PadChar { set; get; }
}

プロパティの属性を取得するには、プロパティのメタデータにアクセスするためのクラス 『System.Reflection.PropertyInfo』 が必要になります。
上記構造体の Code プロパティに対する PropertyInfo の取得方法は以下のようになります。

// Codeプロパティの PropertyInfo が必要なので取得する。Type の GetProperty メソッドで取得する
System.Reflection.PropertyInfo propertyInfo =  typeof(Todofuken).GetProperty("Code");

取得した PropertyInfo を使用して、以下のコードで FixedText 属性を取得します。

// Attribute の GetCustomAttribute メソッドでプロパティに設定された属性を取得する
FixedTextAttribute attribute = Attribute.GetCustomAttribute(
        propertyInfo, typeof(FixedTextAttribute)) as FixedTextAttribute;

あとは FixedText 属性のそれぞれの項目を取得するだけです。

// 属性に設定された値を取得する
Console.WriteLine(
    string.Format("Codeプロパティの属性値: Len={0} PadChar={1}"
        ,attribute.Len, attribute.PadChar));

上のコードをまとめると以下のようになります

/// <summary>
/// Todofuken構造体のプロパティ『Code』に設定された FixedText属性の値を取得する
/// </summary>
private void getAttributeValue_CodeProperty()
{
    // Codeプロパティの PropertyInfo が必要なので取得する。Type の GetProperty メソッドで取得する
    System.Reflection.PropertyInfo propertyInfo = typeof(Todofuken).GetProperty("Code");

    // Attribute の GetCustomAttribute メソッドで プロパティに設定された属性を取得する
    FixedTextAttribute attribute = Attribute.GetCustomAttribute(
        propertyInfo, typeof(FixedTextAttribute)) as FixedTextAttribute;

    // 属性に設定された値を取得する
    Console.WriteLine(
        string.Format("Codeプロパティの属性値: Len={0} PadChar={1}"
            ,attribute.Len, attribute.PadChar));
}

同様に Name プロパティに設定された属性を以下のコードで取得します。

/// <summary>
/// Todofuken構造体のプロパティ『Name』に設定された FixedText属性の値を取得する
/// </summary>
private void getAttributeValue_NameProperty()
{
    // Nameプロパティの PropertyInfo が必要なので取得する。Type の GetProperty メソッドで取得する
    System.Reflection.PropertyInfo propertyInfo = typeof(Todofuken).GetProperty("Name");

    // Attribute の GetCustomAttribute メソッドで プロパティに設定された属性を取得する
    FixedTextAttribute attribute = Attribute.GetCustomAttribute(
        propertyInfo, typeof(FixedTextAttribute)) as FixedTextAttribute;

    // 属性に設定された値を取得する
    Console.WriteLine(
        string.Format("Nameプロパティの属性値: Len={0} PadChar={1}"
            ,attribute.Len, attribute.PadChar));
}

このやりかただと、構造体のプロパティ名を予め知っておく必要がありますので、汎用化するには難があります。
そこで、以下のようにして構造体のプロパティすべてを取得する方法に変更することにします。

/// <summary>
/// 構造体に設定された属性の値を取得する
/// </summary>
public void getAttributeValue()
{
    foreach (PropertyInfo propertyInfo in typeof(Todofuken).GetProperties())
    {
        var attribute = Attribute.GetCustomAttribute(
            propertyInfo, typeof(FixedTextAttribute)) as FixedTextAttribute;

        //属性が定義されたプロパティだけを参照するため、fixedAttrがnullなら処理の対象外
        if (attribute != null)
        {
            Console.WriteLine(
                string.Format("{0}プロパティの属性値: Len={1} PadChar={2}"
                , propertyInfo.Name, attribute.Len, attribute.PadChar)
                );
        }
    }
}

更に Tofofuken 構造体以外の構造体やクラスでも使用できるように下記のように改造します。

/// <summary>
/// 構造体やクラスに設定された属性の値を取得する
/// </summary>
/// <typeparam name="T">属性が定義された構造体またはクラス</typeparam>
public void getAttributeValue<T>()
{
    foreach (PropertyInfo propertyInfo in typeof(T).GetProperties())
    {
        var attribute = Attribute.GetCustomAttribute(
            propertyInfo, typeof(FixedTextAttribute)) as FixedTextAttribute;

        //属性が定義されたプロパティだけを参照するため、fixedAttrがnullなら処理の対象外
        if(attribute != null)
        {
            Console.WriteLine(
                string.Format("{0}プロパティの属性値: Len={1} PadChar={2}"
                    , propertyInfo.Name, attribute.Len, attribute.PadChar)
                );
        }
    }
}

この関数は以下のように呼び出します。

getAttributeValue<Todofuken>();

C#で属性を作成する

属性を作成する

下記に示すような構造体のプロパティに付加する属性 『FixedText』 を作成していきます。

FixedText属性を持った構造体
public struct Todofuken
{
    /// <summary>
    /// 都道府県コード
    /// </summary>
    [FixedText(Len = 2, PadChar = '0']
    public int Code { set; get; }

    /// <summary>
    /// 都道府県名
    /// </summary>
    [FixedText(Len = 10, PadChar = ' ']
    public string Name { set; get; }
}

1.まずは System.Attribute を継承したクラスを作成します。ここで作成したクラス名が、属性名となります。

public class FixedTextAttribute : System.Attribute
{
}

クラス名に Attribute を付加した場合、Attributeを除いた属性名も有効になります。
つまり、下記の属性名は両方とも有効で、同じ属性を表します。

  • [FixedText]

  • [FixedTextAttribute]

クラス名に Attribute をつけなかった場合、先頭に@をつけた属性名も有効になります。
この場合、下記の属性名が有効になります。

  • [FixedText]

  • [@FixedText]

2.作成する属性がプロパティに対する属性であることを指定します。

AttributeUsage属性を用いて、何に対する属性なのかを指定します。
『何に対する』とは、例えば次のようなものがあります。(これら以外にもあります。)

  • クラスに対する属性

  • 構造体に対する属性

  • フィールドに対する属性

  • プロパティに対する属性

  • メソッドに対する属性

プロパティの場合は、AttributeUsege 属性に AttributeTargets.Property を指定します。

[AttributeUsage(AttributeTargets.Property)]
public class FixedTextAttribute : System.Attribute
{
}

複数の項目に対する属性であることを示す場合は、| で結合します。
例えば、プロパティとフィールドの両方に対する属性であることを指定する場合、次のようになります。

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class FixedTextAttribute : System.Attribute
{
}

3.属性に項目を持たせる

FixedText属性は、長さ(Len)と付加文字(PadChar)の2つの項目を持っています。
これらをプロパティとして追加します。

[AttributeUsage(AttributeTargets.Property)]
public class FixedTextAttribute : System.Attribute
{
    public int Len { set; get; }
    public char PadChar { set; get; }
}

以上で、属性の作成は完了です。
次は、クラスや構造体に定義された属性を取得する方法を掲載していきます。


C#の属性という便利な機能

例えば、次のような固定長のテキストを出力する仕様があるとします。

Table 1. 固定長項目
No 列名 長さ 出力形式

1

都道府県コード

2

前ゼロ埋め

2

都道府県名

10

後ろ半角スペース埋め

また、以下の構造体に都道府県のデータが格納されているとします。

構造体
public struct Todofuken
{
    /// <summary>
    /// 都道府県コード
    /// </summary>
    public int Code { set; get; }

    /// <summary>
    /// 都道府県名
    /// </summary>
    public string Name { set; get; }
}

仕様を満たすため、以下のようなコーディングをすると思います。
(あくまでも一例です)

/// <summary>
/// 引数の都道府県データを固定長のテキストにして返す
/// </summary>
/// <param name="data">都道府県データ</param>
/// <returns>固定長テキスト</returns>
private string getFixedText(Todofuken data)
{
    StringBuilder sb = new StringBuilder();

    //都道府県コード 前0埋め2桁
    sb.Append(data.Code.ToString().PadLeft(2, '0'));

    //都道府県名後ろスペース埋め
    sb.Append(data.Name.PadRight(10));

    return sb.ToString();
}

これだと 『どの項目が何桁で、桁埋めで使用する文字は何か』 という情報がソースの中に埋もれてしまいます。
そこで属性の出番です。
属性の機能を使用すると 『どの項目が何桁で、桁埋めで使用する文字は何か』 という情報を構造体に記述できるようになります。
ソースを追う必要がなくなり、『どの項目が何桁で、桁埋めで使用する文字は何か』が一目瞭然になります。

もちろん、属性を利用するためにはいろいろとコーディングが必要になります。
その内容は次回以降に掲載します。
固定長情報を属性に記述するようにした構造体
public struct Todofuken
{
    /// <summary>
    /// 都道府県コード
    /// </summary>
    [FixedText(Len = 2, PadChar = '0', PadType = EPadType.Before)]
    public int Code { set; get; }

    /// <summary>
    /// 都道府県名
    /// </summary>
    [FixedText(Len = 10, PadChar = ' ', PadType = EPadType.After)]
    public string Name { set; get; }
}