ようこそ。睡眠不足なプログラマのチラ裏です。

C#で和暦について本気出して考えてみた【初級〜中級編】IFormattable、ICustomFormatter、IFormatProviderインターフェイスを実装してみよう。てゆうか、プロのプログラマなら当然正規表現を使いたいよね!

思いつきで「C#で和暦について本気出して考えてみた」の不定期連載を開始します。
タイトルが長すぎというツッコミは、心の中にそっとしまっておいてください。


予定している連載内容

【初級〜中級編】

  • 和暦構造体を作ろう。
  • IFormattable、ICustomFormatter、IFormatProviderインターフェイスを実装してみよう。
  • プロのプログラマなら正規表現を使いたいよね!

【中級〜上級編】

  • JapaneseCalendarクラスをハックしてみよう。
  • DateTimeFormatInfoクラスをハックしてみよう。
  • 明治より前の和暦元号だって使いたいよね。Microsoftったらもー空気読めなさすぎ!

【上級編】

【番外編】

  • YOU、自作カレンダーとか作ってみちゃいなよ。


予定は未定ですが、このような感じで計4回の連載を予定しています。
では、さっそく【初級〜中級編】をお送りいたします。


C#で和暦について本気出して考えてみた【初級〜中級編】

まずはお試しコードとその実行結果を見ていただきましょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using ClassLibrary1;

namespace ConsoleApplication1
{
    static class Program
    {
        static void Main(string[] args)
        {
            List<Wareki> list = new List<Wareki>()
            {  
                 new Wareki(1868, 9, 8)
                ,new Wareki(1912, 7, 30)
                ,new Wareki(1926, 12, 25)
                ,new Wareki(1989, 1, 8)
                ,Wareki.Now
                ,new Wareki(9999, 12, 31)
            };

            const string strformat = @"    干支(\k\e  )  \g\g\g(\g\g,\g)\y\y年\M\M月\d\d日 \d\d\d\d (\d\d\d) \t\t(\t)\h\h:\m\m:\s\s.\f\f\f " + "\r\n"
                                    + " -> 干支(ke) ggg(gg,g)yy年MM月dd日 dddd(ddd) tt(t)hh:mm:ss.fff";//"ggyy/MM/dd";

            var fmt = new WarekiFormatInfo();

            foreach (var d in list.Select((n, i) => new { Value = n, Index = i }))
            {
                if (d.Index != 0) Console.WriteLine(d.Value.AddDays(-1).ToString(strformat, fmt));
                Console.WriteLine(d.Value.ToString(strformat, fmt));
            }

            Console.ReadKey();
        }
    }
}


実行結果

    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(戊辰) 明治(明,M)010908日 火曜日(火) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(壬子) 明治(明,M)450729日 月曜日(月) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(壬子) 大正(大,T)010730日 火曜日(火) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(丙寅) 大正(大,T)151224日 金曜日(金) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(丙寅) 昭和(昭,S)011225日 土曜日(土) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(己巳) 昭和(昭,S)640107日 土曜日(土) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(己巳) 平成(平,H)010108日 日曜日(日) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(己丑) 平成(平,H)210216日 月曜日(月) PM(P)09:26:04.875
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(己丑) 平成(平,H)210217日 火曜日(火) PM(P)09:26:04.875
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(己亥) 平成(平,H)80111230日 木曜日(木) AM(A)00:00:00.000
    干支(ke  )  ggg(gg,g)yy年MM月dd日 dddd (ddd) tt(t)hh:mm:ss.fff
 -> 干支(己亥) 平成(平,H)80111231日 金曜日(金) AM(A)00:00:00.000


Wareki(和暦構造体)について、独自の書式フォーマット(WarekiFormatInfo)を用いて
結果が出力されていることがわかると思います。これはWareki(和暦構造体)がIFormattableを実装していて、
且つWarekiFormatInfoクラスがICustomFormatterインターフェイスおよび
IFormatProviderインターフェイスを実装することで実現されています。


Wareki(和暦構造体)を作る

Wareki(和暦構造体)はDateTime(日付型)をオブジェクトコンポジションすることをベースとした構造体です。
カルチャに"ja-JP"を適用し、JapaneseCalendarを利用することで、和暦の元号情報を取得しています。
また、AbbreviatedEnglishEraNamesプロパティにリフレクションを利用してアクセスすることで、
和暦のアルファベット略称(平成はH、昭和はSなど)も利用できるようにしています。
これは、.NET Frameworkでは推奨されていないプロパティに対するアクセスなので、
今後、.NET Frameworkがバージョンアップすることで利用できなくなる可能性があることを留意しましょう。


和暦構造体の注目点は、IFormattableインターフェイスのメンバの実装についてです。
指定されたFormatProviderに応じた書式フォーマットを適用するように実装しています。
今回は、独自に作成するフォーマットであるWarekiFormatInfoと、
日付型用のフォーマットであるDateTimeFormatInfoおよびNumberFormatInfoで書式設定ができるようにしています。
つまり、IFormattableインターフェイスでは、抽象的なFormatProviderでフォーマットできることを定義することになります。
他にも、DateTime(日付型)が実装しているインターフェイスについてすべて実装しています。
おまけで干支の情報もプロパティとして持つようにしてみました。
パーフェクトと言える実装にはなっていないような気もしますが、参考にしてみてください。

using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;

namespace ClassLibrary1
{
    /// <summary>
    /// 和暦構造体
    /// </summary>
    [Serializable()]
    public struct Wareki : IComparable, IFormattable, IConvertible, 
    ISerializable, IComparable<Wareki>, IEquatable<Wareki>
    {
        private static readonly string[] _kan = new string[] { "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" };
        private static readonly string[] _shi = new string[] { "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" };

        /// <summary>
        /// DateTime
        /// </summary>
        private DateTime _date;

        private static readonly CultureInfo _cultureInfo = new System.Globalization.CultureInfo("ja-JP");
        private static string[] abbrevEnglishEraNames = null;

        /// <summary>
        /// Wareki の最大有効値を表します。このフィールドは読み取り専用です。 
        /// </summary>
        public static readonly DateTime MaxValue = new DateTime(9999, 12, 31);

        /// <summary>
        /// Wareki の最小有効値を表します。このフィールドは読み取り専用です。 
        /// </summary>
        public static readonly DateTime MinValue = new DateTime(1868, 9, 8);
        
        #region コンストラクタ
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="date"></param>
        public Wareki(DateTime date)
        {
            _date = date;

            if (date < MinValue) throw new ArgumentOutOfRangeException("範囲外です。MinValueより小さい値は設定できません。");
            if (date > MaxValue) throw new ArgumentOutOfRangeException("範囲外です。MaxValueより大きい値は設定できません。");

            _cultureInfo.DateTimeFormat.Calendar  = new JapaneseCalendar();            
            Type t = typeof(System.Globalization.DateTimeFormatInfo);

            //Privateメンバへのアクセス
            //公開されていなので、いずれ名前が変更されたり、
            //なくなっても一切文句は言えないが、.NET Framework3.5までは、いまのところとりあえず大丈夫みたいだよ
            PropertyInfo pi = t.GetProperty("AbbreviatedEnglishEraNames", BindingFlags.NonPublic | BindingFlags.Instance);
            abbrevEnglishEraNames = (string[])pi.GetValue(_cultureInfo.DateTimeFormat, null);
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="year"></param>
        /// <param name="month"></param>
        /// <param name="day"></param>
        public Wareki(int year, int month, int day):this(new DateTime(year,month,day)){}

        /// <summary>
        /// デシリアル化用のコンストラクタ
        /// </summary>
        /// <param name="info"></param>
        /// <param name="context"></param>
        public Wareki(SerializationInfo info, StreamingContext context)
        {
            _date = info.GetDateTime("_date");
            abbrevEnglishEraNames = (string[])info.GetValue("abbrevEnglishEraNames",typeof(string[]));
        }
        
        #endregion

        #region プロパティ

        /// <summary>
        /// このインスタンスの日付の部分を取得します。 
        /// </summary>
        public DateTime Date { get { return _date;} }

        /// <summary>
        /// このインスタンスで表される月の日付を取得します。 
        /// </summary>
        public int Day { get { return _date.Day; } }

        /// <summary>
        /// このインスタンスで表される曜日を取得します。 
        /// </summary>
        public DayOfWeek DayOfWeek { get { return _date.DayOfWeek; } }

        /// <summary>
        /// このインスタンスで表される年間積算日を取得します。
        /// </summary>
        public int DayOfYear { get { return _date.DayOfYear; } }

        /// <summary>
        /// このインスタンスで表される日付の時間の部分を取得します。 
        /// </summary>
        public int Hour { get { return _date.Hour; } }

        /// <summary>
        /// このインスタンスが表す時刻の種類 (現地時刻、世界協定時刻 (UTC)、または、そのどちらでもない) を示す値を取得します。 
        /// </summary>
        public DateTimeKind Kind { get { return _date.Kind; } }

        /// <summary>
        /// このインスタンスで表される日付のミリ秒の部分を取得します。
        /// </summary>
        public int Millisecond { get { return _date.Millisecond; } }

        /// <summary>
        /// このインスタンスで表される日付の分の部分を取得します。
        /// </summary>
        public int Minute { get { return _date.Minute; } }

        /// <summary>
        /// このインスタンスで表される日付の月の部分を取得します。 
        /// </summary>
        public int Month { get { return _date.Month; } }

        /// <summary>
        /// コンピュータ上の現在の日時を現地時刻で表した Wareki オブジェクトを取得します。 
        /// </summary>
        public static Wareki Now { get { return new Wareki(DateTime.Now); } }

        /// <summary>
        /// このインスタンスで表される日付の秒の部分を取得します。 
        /// </summary>
        public int Second { get { return _date.Second; } }

        /// <summary>
        /// このインスタンスの日付と時刻を表すタイマ刻み数を取得します。 
        /// </summary>
        public long Ticks { get { return _date.Ticks; } }

        /// <summary>
        /// このインスタンスの時刻を取得します。 
        /// </summary>
        public TimeSpan TimeOfDay { get { return _date.TimeOfDay; } }

        /// <summary>
        /// 現在の日付を取得します。
        /// </summary>
        public static DateTime Today { get { return DateTime.Today; } }

        /// <summary>
        /// コンピュータ上の現在の日時を世界協定時刻 (UTC) で表した Wareki オブジェクトを取得します。
        /// </summary>
        public static Wareki UctNow { get { return new Wareki(DateTime.UtcNow); } }

        /// <summary>
        /// 元号アルファベットを取得します。
        /// </summary>
        public string EnglishEraName
        {
            get
            {
                int era = _cultureInfo.DateTimeFormat.Calendar.GetEra(_date);
                return abbrevEnglishEraNames[era - 1];
            }
        }

        /// <summary>
        /// 元号名を取得します。
        /// </summary>
        public string Era
        {
            get
            {
                var era = _cultureInfo.DateTimeFormat.Calendar.GetEra(_date);
                return _cultureInfo.DateTimeFormat.GetEraName(era);
            }
        }

        /// <summary>
        /// 元号略名を取得します。
        /// </summary>
        public string AbbreviatedEraName
        {
            get
            {
                var era = _cultureInfo.DateTimeFormat.Calendar.GetEra(_date);
                return _cultureInfo.DateTimeFormat.GetAbbreviatedEraName(era);
            }
        }

        /// <summary>
        /// 元号に対する年を取得します。
        /// </summary>
        public int Year
        {
            get { return _cultureInfo.DateTimeFormat.Calendar.GetYear(_date); }
        }

        /// <summary>
        /// 曜日名を取得します。
        /// </summary>
        public string DayName
        {
            get
            {
                var weekNames = _cultureInfo.DateTimeFormat.DayNames;
                return weekNames[((int)DayOfWeek)];
            }
        }

        public string ShortestDayName
        {
            get
            {
                var weekNames = _cultureInfo.DateTimeFormat.ShortestDayNames;
                return weekNames[(int)DayOfWeek];
            }
        }

        /// <summary>
        /// 十干を取得します。
        /// </summary>
        public string Kan
        {
            get { return GetKan(_date.Year); }
        }

        /// <summary>
        /// 十二支を取得します。
        /// </summary>
        public string Shi
        {
            get { return GetShi(_date.Year); }        
        }

        /// <summary>
        /// 閏年かどうかを取得します。(グレゴリオ暦における閏年)
        /// </summary>
        /// <param name="year"></param>
        /// <returns></returns>
        public bool IsLeapYear
        {
            get { return DateTime.IsLeapYear(_date.Year); }
        }

        /// <summary>
        /// ユリウス暦での日付を取得します。
        /// </summary>
        public string JulianDate
        {
            get { return Utility.GregorianToJulian(_date); }
        }

        #endregion

        #region オーバーライド

        /// <summary>
        /// ToString
        /// 
        /// 元号xx年xx月xx日
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return this.ToString("gggyy年MM月dd日");
        }

        #endregion

        #region メソッド

        /// <summary>
        /// このインスタンスの値について、指定した書式の日付の文字列形式に変換します。
        /// 使用するFormatProviderはWarekiFormatInfoです。
        /// </summary>
        /// <param name="format"></param>
        /// <returns></returns>
        public string ToString(string format)
        {
            return this.ToString(format, new WarekiFormatInfo());
        }

        /// <summary>
        /// このインスタンスの値に指定した TimeSpan の値を加算します。
        /// </summary>
        public Wareki Add(TimeSpan value) 
        {
            _date = _date.Add(value);
            return this;
        }

        /// <summary>
        /// このインスタンスの値に、指定した日数を加算します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddDays(int value)
        {
            _date = _date.AddDays(value);
            return this;
        }

        /// <summary>
        /// このインスタンスの値に指定した時間数を加算します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddHours(double value)
        {
            _date = _date.AddHours(value);
            return this;
        }

        /// <summary>
        /// このインスタンスの値に指定したミリ秒数を加算します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddMilliseconds(double value)
        {
            _date = _date.AddMilliseconds(value);
            return this;
        }
        
        /// <summary>
        /// このインスタンスの値に指定した分数を加算します。  
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddMinutes(double value)
        {
            _date = _date.AddMinutes(value);
            return this;
        }

        /// <summary>
        /// このインスタンスの値に指定した月数を加算します。  
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddMonths(int value)
        {
            _date = _date.AddMonths(value);
            return this;
        }

        /// <summary>
        /// このインスタンスの値に指定した秒数を加算します。  
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddSeconds(double value)
        {
            _date = _date.AddSeconds(value);
            return this;
        }

        /// <summary>
        /// このインスタンスの値に指定したタイマ刻み数を加算します。  
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddTicks(long value)
        {
            _date = _date.AddTicks(value);
            return this;
        }

        /// <summary>
        /// このインスタンスの値に指定した年数を加算します。  
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki AddYears(int value)
        {
            _date = _date.AddYears(value);
            return this;
        }

        /// <summary>
        /// Wareki の 2 つのインスタンスを比較し、これらの相対値を示す値を返します。  
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static int Compare(Wareki w1, Wareki w2)
        {
            return DateTime.Compare(w1.Date, w2.Date);
        }

        /// <summary>
        /// 指定した月および年の日数を返します。
        /// </summary>
        /// <param name="year"></param>
        /// <param name="month"></param>
        /// <returns></returns>
        public static int DaysInMonth(int year, int month)
        {
            return DateTime.DaysInMonth(year, month);
        }

        /// <summary>
        /// 64 ビットのバイナリ値を逆シリアル化し、元のシリアル化 Wareki オブジェクトを再構築します。  
        /// </summary>
        /// <param name="dateData"></param>
        /// <returns></returns>
        public static Wareki FromBinary(long dateData)
        {
            return new Wareki(DateTime.FromBinary(dateData));
        }

        /// <summary>
        /// 指定された Windows ファイル時刻を同等の現地時刻に変換します。  
        /// </summary>
        /// <param name="filetime"></param>
        /// <returns></returns>
        public static Wareki FromFileTime(long fileTime)
        {
            return new Wareki(DateTime.FromFileTime(fileTime));
        }

        /// <summary>
        /// 指定された Windows ファイル時刻を同等の UTC 時刻に変換します。  
        /// </summary>
        /// <param name="filetime"></param>
        /// <returns></returns>
        public static Wareki FromFileTimeUtc(long fileTime)
        {
            return new Wareki(DateTime.FromFileTimeUtc(fileTime));
        }

        /// <summary>
        /// 指定した OLE オートメーション日付と等しい Wareki を返します。  
        /// </summary>
        /// <param name="d"></param>
        /// <returns></returns>
        public static Wareki FromOADate(double d)
        {
            return new Wareki(DateTime.FromOADate(d));
        }

        /// <summary>
        /// このインスタンスの値を、標準 DateTime 書式指定子によってサポートされるすべての文字列形式に変換します。
        /// </summary>
        /// <returns></returns>
        public string[] GetDateTimeFormats() 
        {
            return _date.GetDateTimeFormats();
        }

        /// <summary>
        /// このインスタンスの値を、指定した標準 DateTime 書式指定子によってサポートされる
        /// すべての文字列形式に変換します。
        /// </summary>
        /// <param name="format"></param>
        /// <returns></returns>
        public string[] GetDateTimeFormats(char format)
        {
            return _date.GetDateTimeFormats(format);
        }

        /// <summary>
        /// このインスタンスの値を、標準 DateTime 書式指定子および
        /// 指定したカルチャ固有の書式情報によってサポートされる、すべての文字列形式に変換します。
        /// </summary>
        /// <param name="provider"></param>
        /// <returns></returns>
        public string[] GetDateTimeFormats(IFormatProvider provider) 
        {
            return _date.GetDateTimeFormats(provider);
        }

        /// <summary>
        /// このインスタンスの値を、指定した標準 DateTime 書式指定子および
        /// カルチャ固有の書式情報によってサポートされる、すべての文字列形式に変換します。
        /// </summary>
        /// <param name="format"></param>
        /// <param name="provider"></param>
        /// <returns></returns>
        public string[] GetDateTimeFormats(char format,IFormatProvider provider)
        {
            return _date.GetDateTimeFormats(format, provider);
        }

        /// <summary>
        /// オーバーライドされます。 このインスタンスのハッシュ コードを返します。 
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode() 
        {
            return 29 * _date.GetHashCode();
        }

        /// <summary>
        /// 現在のインスタンスの Type を取得します。  (Object から継承されます。)
        /// </summary>
        /// <returns></returns>
        public new Type GetType() { return typeof(Wareki); }

        /// <summary>
        /// Wareki のインスタンスが、現在のタイム ゾーンの夏時間の期間内であるどうかを示します。  
        /// </summary>
        /// <returns></returns>
        public bool IsDaylightSavingTime() 
        {
            return _date.IsDaylightSavingTime();
        }

        /// <summary>
        /// 指定した日付と時刻に指定した時間間隔を加算して、新しい日付と時刻を作成します。
        /// </summary>
        /// <param name="w"></param>
        /// <param name="t"></param>
        /// <returns></returns>
        public static Wareki operator +(Wareki w, TimeSpan t) 
        {
            return new Wareki(w.Date + t);
        }

        /// <summary>
        /// Wareki の 2 つの指定したインスタンスが等しいかどうかを判断します。
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator ==(Wareki w1, Wareki w2)
        {
            return w1.Equals(w2);  
        }

        /// <summary>
        /// 指定した Wareki が、指定したもう 1 つの Wareki より大きいかどうかを判断します。
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator >(Wareki w1, Wareki w2) 
        {
            return w1.Date > w2.Date;
        }

        /// <summary>
        /// 指定した Wareki が、指定したもう 1 つの Wareki 以上かどうかを判断します。
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator >=(Wareki w1, Wareki w2)
        {
            return w1.Date >= w2.Date;
        }

        /// <summary>
        /// Wareki の 2 つの指定したインスタンスが等しくないかどうかを判断します。  
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator !=(Wareki w1, Wareki w2) 
        {
            return !w1.Equals(w2);
        }

        /// <summary>
        /// 指定した Wareki が、指定したもう 1 つの Wareki より小さいかどうかを判断します。
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator <(Wareki w1, Wareki w2) 
        {
            return w1.Date < w2.Date;
        }

        /// <summary>
        /// 指定した Wareki が、指定したもう 1 つの Wareki 以下かどうかを判断します。
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator <=(Wareki w1, Wareki w2) 
        {
            return w1.Date <= w2.Date;
        }

        /// <summary>
        /// Nullable型に関する考慮
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator <=(Wareki? w1, Wareki? w2)
        {
            if (w1 == null) return (w2 == null);
            return w1.Value <= w2.Value;
        }

        /// <summary>
        /// Nullable型に関する考慮
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static bool operator >=(Wareki? w1, Wareki? w2)
        {
            if (w1 == null) return (w2 == null);
            return w1.Value >= w2.Value;
        }


        /// <summary>
        /// 指定した日付と時刻から指定したもう 1 つの日付と時刻を減算して、時間間隔を作成します。
        /// </summary>
        /// <param name="w1"></param>
        /// <param name="w2"></param>
        /// <returns></returns>
        public static TimeSpan operator -(Wareki w1, Wareki w2) 
        {
            return w1.Date - w2.Date;
        }

        /// <summary>
        /// 指定した日付と時刻から指定した時間間隔を減算して、新しい日付と時刻を作成します。
        /// </summary>
        /// <param name="w"></param>
        /// <param name="t"></param>
        /// <returns></returns>
        public static Wareki operator -(Wareki w, TimeSpan t)
        {
            return new Wareki(w.Date - t);
        }

        /// <summary>
        /// 指定した文字列形式の日付と時刻を等価の Wareki の値に変換します。
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public static Wareki Parse(string s)
        {
            return new Wareki(DateTime.Parse(s, CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// 指定したカルチャ固有の書式情報を使用して、指定した日付と時刻の文字列形式を等価の Wareki の値に変換します。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="provider"></param>
        /// <returns></returns>
        public static Wareki Parse(string s, IFormatProvider provider)
        {
            return new Wareki(DateTime.Parse(s, provider));
        }

        /// <summary>
        /// 指定したカルチャ固有の書式情報と書式スタイルを使用して、指定した日付と時刻の文字列形式を等価の Wareki の値に変換します。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="provider"></param>
        /// <param name="styles"></param>
        /// <returns></returns>
        public static Wareki Parse(string s, IFormatProvider provider, DateTimeStyles styles)
        {
            return new Wareki(DateTime.Parse(s, provider, styles));
        }

        /// <summary>
        /// 指定した書式とカルチャ固有の書式情報を使用して、
        /// 指定した日付と時刻の文字列形式を等価の Wareki の値に変換します。
        /// 文字列形式の書式は、指定した書式と完全に一致する必要があります。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="format"></param>
        /// <param name="provider"></param>
        /// <returns></returns>
        public static Wareki ParseExact(string s, string format, IFormatProvider provider)
        {
            return new Wareki(DateTime.ParseExact(s, format, provider));
        }

        /// <summary>
        /// 指定した書式、カルチャ固有の書式情報、およびスタイルを使用して、
        /// 指定した日付と時刻の文字列形式を等価の Wareki に変換します。
        /// 文字列形式の書式は、指定した書式と完全に一致する必要があります。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="format"></param>
        /// <param name="provider"></param>
        /// <param name="style"></param>
        /// <returns></returns>
        public static Wareki ParseExact(string s, string format, IFormatProvider provider, DateTimeStyles style)
        {
            return new Wareki(DateTime.ParseExact(s, format, provider, style));
        }
        
        /// <summary>
        /// 指定した書式の配列、カルチャ固有の書式情報、およびスタイルを使用して、
        /// 指定した日付と時刻の文字列形式を等価の Wareki に変換します。
        /// 文字列形式の書式は、指定した書式の少なくとも 1 つと完全に一致する必要があります。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="formats"></param>
        /// <param name="provider"></param>
        /// <param name="style"></param>
        /// <returns></returns>
        public static Wareki ParseExact(string s, string[] formats, IFormatProvider provider, DateTimeStyles style) 
        {
            return new Wareki(DateTime.ParseExact(s, formats, provider, style));
        }

        /// <summary>
        /// 指定した複数の Object インスタンスが同一かどうかを判断します。
        /// </summary>
        /// <param name="objA"></param>
        /// <param name="objB"></param>
        /// <returns></returns>
        public new static bool ReferenceEquals(object objA, object objB) 
        {
            return object.ReferenceEquals(objA, objB);
        }

        /// <summary>
        /// 指定された Wareki と同じ時刻を表す新しい DateTime オブジェクトを、
        /// DateTimeKind 値 (現地時刻、世界協定時刻 (UTC)、または、そのいずれでもないことを示す) に基づいて作成します。
        /// </summary>
        /// <param name="value"></param>
        /// <param name="kind"></param>
        /// <returns></returns>
        public static Wareki SpecifyKind(Wareki value, DateTimeKind kind) 
        {
            return new Wareki(DateTime.SpecifyKind(value.Date, kind));
        }

        /// <summary>
        /// このインスタンスから指定した日付と時刻を減算します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public TimeSpan Subtract(Wareki value) 
        {
            return _date.Subtract(value.Date);
        }

        /// <summary>
        /// このインスタンスから指定した存続時間を減算します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public Wareki Subtract(TimeSpan value) 
        {
            return new Wareki(_date.Subtract(value));
        }

        /// <summary>
        /// DateTime オブジェクトを 64 ビットのバイナリ値にシリアル化します。
        /// 後で、この値を使って、Wareki オブジェクトを再構築できます。
        /// </summary>
        /// <returns></returns>
        public long ToBinary() 
        {
            //実はDateTime.ToBinary()なだけ
            return _date.ToBinary();
        }

        /// <summary>
        /// 現在の Wareki オブジェクトの値を Windows ファイル時刻に変換します。
        /// </summary>
        /// <param name="fileTime"></param>
        /// <returns></returns>
        public long ToFileTime()
        {
            return _date.ToFileTime();
        }

        /// <summary>
        /// 現在の Wareki オブジェクトの値を Windows ファイル時刻に変換します。
        /// </summary>
        /// <returns></returns>
        public long ToFileTimeUtc() 
        {
            return _date.ToFileTimeUtc();
        }

        /// <summary>
        /// 現在の Wareki オブジェクトの値を現地時刻に変換します。
        /// </summary>
        /// <returns></returns>
        public Wareki ToLocalTime() 
        { 
            return new Wareki(_date.ToLocalTime());
        }

        /// <summary>
        /// このインスタンスの値を、それと等価な長い形式の日付の文字列形式に変換します。
        /// </summary>
        /// <returns></returns>
        public string ToLongDateString() 
        {
            return this.ToString("gggyy年MM月dd");
        }

        /// <summary>
        /// このインスタンスの値を、それと等価の長い時刻書式の文字列形式に変換します。
        /// </summary>
        /// <returns></returns>
        public string ToLongTimeString() 
        {
            return _date.ToLongTimeString();
        }

        /// <summary>
        /// このインスタンスの値をそれと等価な OLE オートメーション日付に変換します。
        /// (ユリウス日)
        /// </summary>
        /// <returns></returns>
        public double ToOADate() 
        {
            return _date.ToOADate();
        }

        /// <summary>
        /// このインスタンスの値を、それと等価な短い形式の日付の文字列形式に変換します。
        /// </summary>
        /// <returns></returns>
        public string ToShortDateString()
        {
            return this.ToString("gyy/MM/dd");
        }

        /// <summary>
        /// このインスタンスの値を、それと等価の短い時刻書式の文字列形式に変換します。
        /// </summary>
        /// <returns></returns>
        public string ToShortTimeString()
        {
            return _date.ToShortTimeString();
        }

        /// <summary>
        /// 現在の Wareki オブジェクトの値を世界協定時刻 (UTC) に変換します。
        /// </summary>
        /// <returns></returns>
        public Wareki ToUniversalTime()
        {
            return new Wareki(_date.ToUniversalTime());
        }

        /// <summary>
        /// 指定した文字列形式の日付と時刻を等価の Wareki の値に変換します。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public static bool TryParse(string s, out Wareki result) 
        { 
            var d = default(DateTime);
            var b = DateTime.TryParse(s, out d);
            result = new Wareki(d);
            return b;
        }

        /// <summary>
        /// 指定したカルチャ固有の書式情報と書式スタイルを使用して、
        /// 指定した日付と時刻の文字列形式を等価の Wareki の値に変換します。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="provider"></param>
        /// <param name="styles"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public static bool TryParse(string s, IFormatProvider provider
                                  , DateTimeStyles styles, out Wareki result) 
        {
            var d = default(DateTime);
            var b = DateTime.TryParse(s, provider, styles, out d);
            result = new Wareki(d);
            return b;
        }

        /// <summary>
        /// 指定した書式、カルチャ固有の書式情報、およびスタイルを使用して、
        /// 指定した日付と時刻の文字列形式を等価の Wareki に変換します。文字列形式の書式は、
        /// 指定した書式と完全に一致する必要があります。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="format"></param>
        /// <param name="provider"></param>
        /// <param name="style"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public static bool TryParseExact(string s, string format, IFormatProvider provider
                                       , DateTimeStyles style, out Wareki result)
        {
            var d = default(DateTime);
            var b = DateTime.TryParseExact(s, format, provider, style, out d);
            result = new Wareki(d);
            return b;
        }

        /// <summary>
        /// 指定した書式の配列、カルチャ固有の書式情報、およびスタイルを使用して、
        /// 指定した日付と時刻の文字列形式を等価の Wareki に変換します。
        /// 文字列形式の書式は、指定した書式の少なくとも 1 つと完全に一致する必要があります。
        /// </summary>
        /// <param name="s"></param>
        /// <param name="formats"></param>
        /// <param name="provider"></param>
        /// <param name="style"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public static bool TryParseExact(string s, string[] formats, IFormatProvider provider
                                       , DateTimeStyles style, out Wareki result) 
        {
            var d = default(DateTime);
            var b = DateTime.TryParseExact(s, formats, provider, style, out d);
            result = new Wareki(d);
            return b;
        }

        /// <summary>
        /// 対象のインスタンスが、指定したオブジェクトに等しいかどうかを示す値を返します。
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (!(obj is Wareki)) return false;
            return this.Equals((Wareki)obj);
        }

        /// <summary>
        /// 対象年の十干を取得します。
        /// </summary>
        /// <param name="year"></param>
        /// <returns></returns>
        public static string GetKan(int year)
        {
            return _kan[(year - 594) % 10];
        }

        /// <summary>
        /// 対象年の十二支を取得します。
        /// </summary>
        /// <param name="year"></param>
        /// <returns></returns>
        public static string GetShi(int year)
        {
            return _shi[(year - 592) % 12];
        }

        #endregion

        #region IComparable メンバ

        /// <summary>
        /// 指定したオブジェクトとこのインスタンスを比較し、これらの相対値を示す値を返します。
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public int CompareTo(object obj)
        {
            if (obj is Wareki) return this.CompareTo((Wareki)obj);
            return this.Date.CompareTo(obj);
        }

        #endregion

        #region IComparable<Wareki> メンバ

        /// <summary>
        /// Wareki オブジェクトとこのインスタンスを比較し、これらの相対値を示す値を返します。
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public int CompareTo(Wareki other)
        {
            return this.Date.CompareTo(other.Date);
        }

        #endregion

        #region IEquatable<Wareki> メンバ

        /// <summary>
        /// このインスタンスが、指定した Wareki インスタンスと等しいかどうかを示す値を返します。
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public bool Equals(Wareki other)
        {
            return _date.Equals(other.Date);
        }

        #endregion

        #region ISerializable メンバ

        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("_date", _date);
            info.AddValue("abbrevEnglishEraNames", abbrevEnglishEraNames);
            
        }

        #endregion
        
        #region IFormattable メンバ

        public string ToString(string format, IFormatProvider formatProvider)
        {
            if (format == null || formatProvider == null) return this.ToString();
            var wf = formatProvider.GetFormat(this.GetType()) as WarekiFormatInfo ;
            if (wf != null) return wf.Format(format, this, formatProvider);
            var df = formatProvider.GetFormat(typeof(DateTimeFormatInfo)) as DateTimeFormatInfo;
            if (df != null) return _date.ToString(df);
            var nf = formatProvider.GetFormat(typeof(NumberFormatInfo)) as NumberFormatInfo;
            if (nf != null) return _date.ToString(nf);
            return this.ToString();
        }

        #endregion

        #region IConvertible メンバ

        public string ToString(IFormatProvider provider)
        {
            return this.ToString(null, provider);
        }

        public DateTime ToDateTime(IFormatProvider provider)
        {
            DateTime date = default(DateTime);
            if (provider != null)
                date = (DateTime)provider.GetFormat(typeof(DateTime));
            return date;
        }

        /// <summary>
        /// DateTime 値型の TypeCode を返します。
        /// </summary>
        /// <returns></returns>
        public TypeCode GetTypeCode()
        {
            return TypeCode.DateTime;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        bool IConvertible.ToBoolean(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        byte IConvertible.ToByte(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        char IConvertible.ToChar(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        decimal IConvertible.ToDecimal(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        double IConvertible.ToDouble(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        short IConvertible.ToInt16(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        int IConvertible.ToInt32(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        long IConvertible.ToInt64(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        sbyte IConvertible.ToSByte(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        float IConvertible.ToSingle(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        object IConvertible.ToType(Type conversionType, IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        ushort IConvertible.ToUInt16(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        uint IConvertible.ToUInt32(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        ulong IConvertible.ToUInt64(IFormatProvider provider)
        {
            throw new InvalidCastException();
        }

        #endregion

    }
}


和暦の書式フォーマットを定義するWarekiFormatInfoクラス

まずは、残念な実装例を見てみましょう。

using System;
using System.Globalization;
using System.Text.RegularExpressions;

namespace ClassLibrary1
{
    /// <summary>
    /// WarekiFormatInfo
    /// 
    /// 和暦構造体の書式とか
    /// </summary>
    public sealed class WarekiFormatInfo : ICustomFormatter, IFormatProvider
    {
        private Wareki _w = default(Wareki);

        public object GetFormat(Type formatType)
        {
            if (formatType == typeof(Wareki)) return this;
            return CultureInfo.CurrentCulture.GetFormat(formatType);
        }

        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            if (arg.GetType() != typeof(Wareki))
            {
                IFormattable formatable = arg as IFormattable;
                if (formatable != null)
                    return formatable.ToString(format, formatProvider);

                return arg.ToString();
            }

            _w = (Wareki)arg;

            switch (format)
            {
                case "gyy/MM/dd":
                    return _w.EnglishEraName +
                           _w.Year.ToString().PadLeft(2, '0') + "/" +
                           _w.Month.ToString().PadLeft(2, '0') + "/" +
                           _w.Day.ToString().PadLeft(2, '0');
                case "ggyy/MM/dd":
                    return _w.AbbreviatedEraName +
                           _w.Year.ToString().PadLeft(2, '0') + "/" +
                           _w.Month.ToString().PadLeft(2, '0') + "/" +
                           _w.Day.ToString().PadLeft(2, '0');
                case "gggyy/MM/dd":
                    return _w.Era +
                           _w.Year.ToString().PadLeft(2, '0') + "/" +
                           _w.Month.ToString().PadLeft(2, '0') + "/" +
                           _w.Day.ToString().PadLeft(2, '0');
                case "gyy年MM月dd日":
                    return _w.EnglishEraName +
                           _w.Year.ToString().PadLeft(2, '0') + "年" +
                           _w.Month.ToString().PadLeft(2, '0') + "月" +
                           _w.Day.ToString().PadLeft(2, '0') + "日";
                case "ggyy年MM月dd日":
                    return _w.AbbreviatedEraName +
                           _w.Year.ToString().PadLeft(2, '0') + "年" +
                           _w.Month.ToString().PadLeft(2, '0') + "月" +
                           _w.Day.ToString().PadLeft(2, '0') + "日";
                case "gggyy年MM月dd日":
                    return _w.Era +
                           _w.Year.ToString().PadLeft(2, '0') + "年" +
                           _w.Month.ToString().PadLeft(2, '0') + "月" +
                           _w.Day.ToString().PadLeft(2, '0') + "日";
                default: 
                    return "";
            }
        }
    }
}


はい、この実装は非常に残念と言わざるを得ません。
プログラマの経験不足がコードにそのまま表れています。


新人さん、あるいは正規表現に造詣の浅いプログラマにありがちな実装と言えるでしょう。
switch文を使って、直にフォーマットの書式を振り分けるかたちで実装されているので、
保守性を失うだけではなく、多様な書式へ対応することも難しくなってしまっています。
ICustomFormatterインターフェイスを実装するような場合は、
正規表現を用いて多様な書式へ対応できるように実装するのがベターではないでしょうか。


以下に正規表現を用いたICustomFormatterインターフェイスの実装例を示します。

using System;
using System.Globalization;
using System.Text.RegularExpressions;

namespace ClassLibrary1
{
    /// <summary>
    /// WarekiFormatInfo
    /// 
    /// 和暦構造体の書式とか
    /// </summary>
    public sealed class WarekiFormatInfo : ICustomFormatter, IFormatProvider
    {
        private Wareki _w = default(Wareki);

        public object GetFormat(Type formatType)
        {
            if (formatType == typeof(Wareki)) return this;
            return CultureInfo.CurrentCulture.GetFormat(formatType);
        }

        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            if (arg.GetType() != typeof(Wareki))
            {
                IFormattable formatable = arg as IFormattable;
                if (formatable != null)
                    return formatable.ToString(format, formatProvider);

                return arg.ToString();
            }

            _w = (Wareki)arg;

            string result = format;

            result = ReplaceKan(result);
            result = ReplaceShi(result);
            result = ReplaceYear(result);
            result = ReplaceMonth(result);
            result = ReplaceWeek(result);
            result = ReplaceDay(result);
            result = ReplaceAmPm(result);
            result = ReplaceHour(result);
            result = ReplaceMinute(result);
            result = ReplaceSecond(result);
            result = ReplaceMillisecond(result);
            result = ReplaceEra(result);
            result = ReplaceEscape(result);

            return result;
        }

        /// <summary>
        /// 年に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceYear(string target)
        {
            var result = target;

            var ryyyy = @"(?<!\\)(?<!\\y{1,}])yyyy(?![y\\])";
            while (Regex.IsMatch(result, ryyyy))
                result = Regex.Replace(result, ryyyy, _w.Date.Year.ToString().PadLeft(4, '0'));

            var ryy = @"(?<!\\)(?<!\\y{1,}])yy(?![y\\])";
            while (Regex.IsMatch(result, ryy))
                result = Regex.Replace(result, ryy, _w.Year.ToString().PadLeft(2, '0'));

            return result;
        }

        /// <summary>
        /// 月に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceMonth(string target)
        {
            var result = target;

            var rMM = @"(?<!\\)(?<!\\M{1,}])MM(?![M\\])";
            while (Regex.IsMatch(result, rMM))
                result = Regex.Replace(result, rMM, _w.Month.ToString().PadLeft(2, '0'));

            var rM = @"(?<!\\)(?<!\\M{1,}])M(?![M\\])";
            while (Regex.IsMatch(result, rM))
                result = Regex.Replace(result, rM, _w.Month.ToString());

            return result;
        }

        /// <summary>
        /// 曜日に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceWeek(string target)
        {
            var result = target;

            var rdddd = @"(?<!\\)(?<!\\d{1,}])dddd(?![d\\])";
            while (Regex.IsMatch(result, rdddd))
                result = Regex.Replace(result, rdddd, _w.DayName);

            var rddd = @"(?<!\\)(?<!\\d{1,}])ddd(?![d\\])";
            while (Regex.IsMatch(result, rddd))
                result = Regex.Replace(result, rddd, _w.ShortestDayName);

            return result;
        }

        /// <summary>
        /// 日に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceDay(string target)
        {
            var result = target;

            var rdd = @"(?<!\\)(?<!\\d{1,}])dd(?![d\\])";
            while (Regex.IsMatch(result, rdd))
                result = Regex.Replace(result, rdd, _w.Day.ToString().PadLeft(2, '0'));

            var rd = @"(?<!\\)(?<!\\d{1,}])d(?![d\\])";
            while (Regex.IsMatch(result, rd))
                result = Regex.Replace(result, rd, _w.Day.ToString());

            return result;
        }

        /// <summary>
        /// A.M.指定子あるいはP.M指定子に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceAmPm(string target)
        {
            var result = target;

            var ampm = _w.Hour < 12 ? System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat.AMDesignator 
                                    : System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat.PMDesignator;

            var rtt = @"(?<!\\)(?<!\\t{1,}])tt(?![t\\])";
            while (Regex.IsMatch(result, rtt))
                result = Regex.Replace(result, rtt, ampm);

            var rt = @"(?<!\\)(?<!\\t{1,}])t(?![t\\])";
            while (Regex.IsMatch(result, rt))
                result = Regex.Replace(result, rt, ampm.Substring(0,1));

            return result;
        }

        /// <summary>
        /// 時に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceHour(string target)
        {
            var result = target;
           
            var rHH = @"(?<!\\)(?<!\\H{1,}])HH(?![H\\])";
            while (Regex.IsMatch(result, rHH))
                result = Regex.Replace(result, rHH, _w.Hour.ToString().PadLeft(2, '0'));

            var rH = @"(?<!\\)(?<!\\H{1,}])H(?![H\\])";
            while (Regex.IsMatch(result, rH))
                result = Regex.Replace(result, rH, _w.Hour.ToString());

            var ampmHour = _w.Hour > 12 ? _w.Hour - 12 : _w.Hour; 

            var rhh = @"(?<!\\)(?<!\\h{1,}])hh(?![h\\])";
            while (Regex.IsMatch(result, rhh))
                result = Regex.Replace(result, rhh, ampmHour.ToString().PadLeft(2, '0'));

            var rh = @"(?<!\\)(?<!\\h{1,}])h(?![h\\])";
            while (Regex.IsMatch(result, rh))
                result = Regex.Replace(result, rh, ampmHour.ToString());

            return result;
        }

        /// <summary>
        /// 分に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceMinute(string target)
        {
            var result = target;

            var rmm = @"(?<!\\)(?<!\\m{1,}])mm(?![m\\])";
            while (Regex.IsMatch(result, rmm))
                result = Regex.Replace(result, rmm, _w.Minute.ToString().PadLeft(2, '0'));

            var rm = @"(?<!\\)(?<!\\m{1,}])m(?![m\\])";
            while (Regex.IsMatch(result, rm))
                result = Regex.Replace(result, rm, _w.Minute.ToString());

            return result;
        }

        /// <summary>
        /// 秒に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceSecond(string target)
        {
            var result = target;

            var rss = @"(?<!\\)(?<!\\s{1,}])ss(?![s\\])";
            while (Regex.IsMatch(result, rss))
                result = Regex.Replace(result, rss, _w.Second.ToString().PadLeft(2, '0'));

            var rs = @"(?<!\\)(?<!\\s{1,}])s(?![s\\])";
            while (Regex.IsMatch(result, rs))
                result = Regex.Replace(result, rs, _w.Second.ToString());

            return result;
        }

        /// <summary>
        /// ミリ秒に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceMillisecond(string target)
        {
            var result = target;

            var rfff = @"(?<!\\)(?<!\\f{1,}])fff(?![f\\])";
            while (Regex.IsMatch(result, rfff))
                result = Regex.Replace(result, rfff, _w.Millisecond.ToString().PadLeft(3, '0'));

            var rf = @"(?<!\\)(?<!\\f{1,}])f(?![f\\])";
            while (Regex.IsMatch(result, rf))
                result = Regex.Replace(result, rf, _w.Millisecond.ToString());

            return result;
        }

        /// <summary>
        /// 元号に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceEra(string target)
        {
            var result = target;

            var rggg = @"(?<!\\)(?<!\\g{1,}])ggg(?![g\\])";
            while (Regex.IsMatch(result, rggg))
                result = Regex.Replace(result, rggg, _w.Era);

            var rgg = @"(?<!\\)(?<!\\g{1,}])gg(?![g\\])";
            while (Regex.IsMatch(result, rgg))
                result = Regex.Replace(result, rgg, _w.AbbreviatedEraName);

            var rg = @"(?<!\\)(?<!\\g{1,}])g(?![g\\])";
            while (Regex.IsMatch(result, rg))
                result = Regex.Replace(result, rg, _w.EnglishEraName);

            return result;
        }

        /// <summary>
        /// 十干に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceKan(string target)
        {
            var result = target;

            var rkkk = @"(?<!\\)(?<!\\k{1,}])k(?![k\\])";
            while (Regex.IsMatch(result, rkkk))
                result = Regex.Replace(result, rkkk, _w.Kan);

            return result;
        }

        /// <summary>
        /// 十二支に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceShi(string target)
        {
            var result = target;

            var reee = @"(?<!\\)(?<!\\e{1,}])e(?![e\\])";
            while (Regex.IsMatch(result, reee))
                result = Regex.Replace(result, reee, _w.Shi);

            return result;        
        }

        /// <summary>
        /// エスケープした文字に関する置換
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private string ReplaceEscape(string target)
        {
            var result = target;
            result = Regex.Replace(result, @"\\g", "g");
            result = Regex.Replace(result, @"\\y", "y");
            result = Regex.Replace(result, @"\\M", "M");
            result = Regex.Replace(result, @"\\d", "d");
            result = Regex.Replace(result, @"\\t", "t");
            result = Regex.Replace(result, @"\\H", "H");
            result = Regex.Replace(result, @"\\h", "h");
            result = Regex.Replace(result, @"\\m", "m");
            result = Regex.Replace(result, @"\\s", "s");
            result = Regex.Replace(result, @"\\f", "f");
            result = Regex.Replace(result, @"\\k", "k");
            result = Regex.Replace(result, @"\\e", "e");
            return result;
        }
    }
}

前読み、先読みを用いた正規表現により、自由度の高い書式フォーマットが実現されています。
また、「\k」などの記述で、書式指定文字である「k」のエスケープ等にも対応しています。
もっとキレイにやる方法もあるとは思うのですが、私も言うほど正規表現が得意というわけではないので、
今日のところはこの程度の実装で大目に見てください。また、この実装が何かの参考になれば幸いです。


ちょっと余談

PerlPHPプログラマというか、いわゆるWeb系な人であれば、利用シーンもそれなりに多いためか、
正規表現が得意なイメージがあります。それに対して業務系やパッケージ系プログラマである、
VB.NETerおよびC#erは正規表現が苦手な人が多いのではないでしょうか。少なくとも自分の周りではそうです。


正規表現は何も実際のプログラミングにだけ使える道具ではなく、
あらゆるテキスト編集作業に威力を発揮するので、必ず、それも出来るだけ早いうちに習得するとよいと思う。


正規表現っておいしいの?意味わかんないよ」そんな時期が俺にもありました。
正規表現は新人教育に必ず入れるべき。これはガチ。



最後にテーマ曲を。

ポルノグラフィティ - 幸せについて本気出して考えてみた


ポルノグラフィティ - 「幸せについて本気出して考えてみた」歌詞