ディクショナリのキーに正規表現を使えるようにしてみる

寒いから説明は後日

```csharp
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace RegexMatchingDictionary
{
    /// <summary>
    /// 正規表現ディクショナリ
    /// 
    /// 正規表現をキーとして使えるディクショナリ
    /// </summary>
    /// <typeparam name="TValue">ディクショナリ内の値の型</typeparam>
    /// <remarks>
    /// 一般的なディクショナリと違って複数のキーにマッチするので注意。
    /// 一つのディクショナリインスタンスに同一キー(同一の正規表現パターン)は登録不可。
    /// 正規表現のオプション(RegexOptions)はディクショナリインスタンスごとに共通。(キーごとに別のオプションは需要が少なそうな割に複雑になるから)
    /// </remarks>
    public class RegexMatchingDictionary<TValue> : IDictionary<string, TValue>
    {
        /// <summary>Key-Valueリスト</summary>
        protected readonly List<(Regex RegexPattern, TValue Value)> regexValuePairs;
        /// <summary>正規表現オプション</summary>
        private readonly RegexOptions regexOptions;

        #region Constructors
        /// <summary>基底コンストラクタ</summary>
        /// <param name="options">正規表現オプション</param>
        public RegexMatchingDictionary(RegexOptions options)
        {
            regexValuePairs = new List<(Regex, TValue)>();
            regexOptions = options;
        }

        /// <summary>デフォルトコンストラクタ</summary>
        public RegexMatchingDictionary() : this(RegexOptions.None) { }

        /// <summary>初期値リストつきコンストラクタ</summary>
        /// <param name="collention">初期値としてセットするパターンと値のコレクション</param>
        /// <param name="options">正規表現オプション</param>
        public RegexMatchingDictionary(IEnumerable<(string, TValue)> collention, RegexOptions options = RegexOptions.None)
            : this(options)
        {
            foreach ((string, TValue) pv in collention)
            { this.Add(pv.Item1, pv.Item2); }
        }
        #endregion

        #region IDictionary implements
        /// <summary>正規表現パターン文字列のリストを取得。</summary>
        public ICollection<string> Keys => regexValuePairs.Select(rv => rv.RegexPattern.ToString()).ToArray();

        /// <summary>値のリストを取得。</summary>
        public ICollection<TValue> Values => regexValuePairs.Select(rv => rv.Value).ToArray();

        /// <summary>指定した文字列にマッチした要素の値を取得。</summary>
        /// <param name="key">検索対象文字列</param>
        /// <returns>一致したパターンに対応する値</returns>
        /// <remarks>
        /// 複数のパターンに一致する場合は先に登録したパターンに対応する値を返す。
        /// setは不可。
        /// </remarks>
        public TValue this[string key]
        {
            get
            {
                if (key == null)
                { throw new ArgumentNullException(); }
                int idx = regexValuePairs.FindIndex(rv => rv.RegexPattern.IsMatch(key));
                if (idx < 0)
                { throw new KeyNotFoundException(); }
                return regexValuePairs[idx].Value;
            }
            set { throw new NotImplementedException(); }
        }

        /// <summary>指定した正規表現パターンと対応する値のセットを登録する。</summary>
        /// <param name="pattern">正規表現パターン文字列</param>
        /// <param name="value">追加する要素の値</param>
        /// <remarks>リスト末尾に追加されるので既存のパターンより高い優先度にしたい場合はInsert()を使用する。</remarks>
        public void Add(string pattern, TValue value)
        {
            //空文字も正規表現としては無意味なのでnull扱いにする。
            if (string.IsNullOrEmpty(pattern))
            { throw new ArgumentNullException(); }
            //同一パターンを登録済みならエラー
            if (regexValuePairs.Any(rv => rv.RegexPattern.ToString() == pattern))
            { throw new ArgumentException(); }
            regexValuePairs.Add((new Regex(pattern, regexOptions), value));
        }

        /// <summary>正規表現パターンが登録済みかチェックする。</summary>
        /// <param name="pattern">正規表現パターン文字列</param>
        /// <returns>同一パターンが登録されていればtrue</returns>
        public bool ContainsKey(string pattern)
        {
            return regexValuePairs.Any(rv => rv.RegexPattern.IsMatch(pattern));
        }

        /// <summary>指定したパターンと対応する値を削除する。</summary>
        /// <param name="pattern">正規表現パターン文字列</param>
        /// <returns>指定したパターンが存在していればtrue</returns>
        public bool Remove(string pattern)
        {
            if (string.IsNullOrEmpty(pattern))
            { throw new ArgumentNullException(); }
            int idx = regexValuePairs.FindIndex(rv => rv.RegexPattern.ToString() == pattern);
            if (idx < 0)
            { return false; }
            regexValuePairs.RemoveAt(idx);
            return true;
        }

        /// <summary>指定した文字列と一致したパターンがあれば対応付けられた値を返す。</summary>
        /// <param name="key">検索対象文字列</param>
        /// <returns>一致したパターンに対応する値</returns>
        /// <returns>一致したパターンがあればtrue</returns>
        public bool TryGetValue(string key, out TValue value)
        {
            int idx = regexValuePairs.FindIndex(rv => rv.RegexPattern.IsMatch(key));
            if (idx < 0)
            {
                value = default;
                return false;
            }
            value = regexValuePairs[idx].Value;
            return true;
        }
        #endregion

        #region ICollention<T> implements
        /// <summary>登録されている要素数</summary>
        public int Count => regexValuePairs.Count;
        /// <summary>読み取り専用かどうか</summary>
        public bool IsReadOnly => false;

        /// <summary>指定した正規表現パターンと対応する値のセットを登録する。</summary>
        /// <param name="item">登録する正規表現パターンと値のペア</param>
        public void Add(KeyValuePair<string, TValue> item)
            => Add(item.Key, item.Value);

        /// <summary>登録内容をすべて消去する</summary>
        public void Clear()
            => regexValuePairs.Clear();

        /// <summary>指定した正規表現パターンと対応する値のセットが登録済みかチェックする。</summary>
        /// <param name="item">存在チェックする正規表現パターンと値のペア</param>
        /// <returns>登録されていればtrue</returns>
        /// <remarks>ICollentionの実装を満たすために必要なだけで、使うことはまずないはず。</remarks>
        public bool Contains(KeyValuePair<string, TValue> item)
            => regexValuePairs.Any(rv => rv.RegexPattern.ToString() == item.Key && object.Equals(rv.Value, item.Value));

        /// <summary>リストに登録された内容を配列として取り出す。</summary>
        /// <param name="array">正規表現パターン文字列と値のペアを格納する配列</param>
        /// <param name="arrayIndex">抽出開始位置のインデックス</param>
        /// <remarks>ICollentionの実装を満たすために必要なだけで、使うことはまずないはず。</remarks>
        public void CopyTo(KeyValuePair<string, TValue>[] array, int arrayIndex)
        {
            if (array == null)
            { throw new ArgumentNullException(); }
            if (arrayIndex < 0)
            { throw new ArgumentOutOfRangeException(); }
            if (arrayIndex + array.Length > regexValuePairs.Count)
            { throw new ArgumentException(); }

            Array.Copy(regexValuePairs.Select(rv => new KeyValuePair<string, TValue>(rv.RegexPattern.ToString(), rv.Value)).ToArray(), 0,
                array, arrayIndex, regexValuePairs.Count);
        }

        /// <summary>指定したパターンと対応する値を削除する。</summary>
        /// <param name="item">削除するパターン文字列と値のペア</param>
        /// <returns>指定したペアが存在していればtrue</returns>
        /// <remarks>ICollentionの実装を満たすために必要なだけで、使うことはまずないはず。</remarks>
        public bool Remove(KeyValuePair<string, TValue> item)
        {
            int idx = regexValuePairs.FindIndex(rv => rv.RegexPattern.ToString() == item.Key && object.Equals(rv.Value, item.Value));
            if (idx < 0)
            { return false; }
            regexValuePairs.RemoveAt(idx);
            return true;
        }
        #endregion

        #region IEnumerable implements
        /// <summary>列挙子を返す。</summary>
        /// <remarks>IEnumerable<T>実装</remarks>
        public IEnumerator<KeyValuePair<string, TValue>> GetEnumerator()
            => new RegexMatchingDictionaryEnumerator(this);

        /// <summary>列挙子を返す。</summary>
        /// <remarks>IEnumerable実装</remarks>
        IEnumerator IEnumerable.GetEnumerator()
            => this.GetEnumerator();

        /// <summary>列挙子クラス</summary>
        private class RegexMatchingDictionaryEnumerator : IEnumerator<KeyValuePair<string, TValue>>
        {
            /// <summary>内部リストの列挙子</summary>
            private IEnumerator<(Regex RegexPattern, TValue Value)> enumerator;

            /// <summary>コンストラクタ</summary>
            internal RegexMatchingDictionaryEnumerator(RegexMatchingDictionary<TValue> source)
            {
                enumerator = source.regexValuePairs.GetEnumerator();
            }

            /// <summary>現在の要素</summary>
            public KeyValuePair<string, TValue> Current => new KeyValuePair<string, TValue>(enumerator.Current.RegexPattern.ToString(), enumerator.Current.Value);
            /// <summary>現在の要素</summary>
            object IEnumerator.Current => new KeyValuePair<string, TValue>(enumerator.Current.RegexPattern.ToString(), enumerator.Current.Value);

            public void Dispose()
            {
                enumerator?.Dispose();
                enumerator = null;
            }

            public bool MoveNext()
                => enumerator.MoveNext();
            public void Reset()
                => enumerator.Reset();
        }
        #endregion

        //IListではないが、検索順序操作のためにIList風プロパティとメソッドも実装する
        #region IList like implements
        /// <summary>指定したインデックスの値を取得する。(readonly)</summary>
        /// <param name="index">登録リストのインデックス</param>
        /// <returns>登録されている値</returns>
        public TValue this[int index]
            => regexValuePairs[index].Value;

        /// <summary>指定した正規表現パターン文字列のインデックス番号を返す。</summary>
        /// <param name="pattern">正規表現パターン文字列</param>
        /// <returns>該当なしなら-1</returns>
        public int IndexOf(string pattern)
            => regexValuePairs.FindIndex(rv => rv.RegexPattern.ToString() == pattern);

        /// <summary>リスト内の指定した位置に正規表現パターンと対応する値を挿入する。</summary>
        /// <param name="index">挿入位置のインデックス番号</param>
        /// <param name="pattern">正規表現パターン文字列</param>
        /// <param name="value">対応する値</param>
        public void Insert(int index, string pattern, TValue value)
            => regexValuePairs.Insert(index, (new Regex(pattern, this.regexOptions), value));

        /// <summary>リスト内の指定した位置の正規表現パターンと対応する値を削除する。</summary>
        /// <param name="index">削除する要素のインデックス番号</param>
        public void RemoveAt(int index)
            => regexValuePairs.RemoveAt(index);
        #endregion

        #region Original impliments
        /// <summary>登録されている正規表現オブジェクトの列挙する。</summary>
        public IEnumerable<Regex> Regexs => regexValuePairs.Select(rv => rv.RegexPattern);

        /// <summary>正規表現パターン文字列に対応する値を取得する。</summary>
        /// <param name="pattern">正規表現パターン文字列</param>
        /// <param name="value">対応する値</param>
        public TValue GetValueByPattern(string pattern)
        {
            int idx = regexValuePairs.FindIndex(rv => rv.RegexPattern.ToString() == pattern);
            if (idx < 0)
            { return default; }
            return regexValuePairs[idx].Value;
        }

        /// <summary>正規表現パターン文字列に対応する値を取得する。</summary>
        /// <param name="pattern">正規表現パターン文字列</param>
        /// <param name="value">対応する値</param>
        /// <returns>該当するパターン文字列があればtrue</returns>
        public bool TryGetValueByPattern(string pattern, out TValue value)
        {
            int idx = regexValuePairs.FindIndex(rv => rv.RegexPattern.ToString() == pattern);
            if (idx < 0)
            {
                value = default;
                return false;
            }
            value = regexValuePairs[idx].Value;
            return true;
        }
        #endregion
    }


    /// <summary>
    /// 正規表現アクションディクショナリ
    /// 
    /// 文字列を渡すと正規表現パターンに一致したら登録されているアクションを実行する。
    /// </summary>
    /// <typeparam name="TResult">アクションの返り値型</typeparam>
    /// <remarks>
    /// IDictionaryなのでアクションは値を返す必要がある。返り値が不要なら最後に必ず 'return true;' でもつければOK。
    /// 最初に一致したパターンのアクションだけを実行するので注意。
    /// </remarks>
    public class RegexMatchingActionDictionary<TResult> : RegexMatchingDictionary<Func<Match, string, TResult>>
    {
        /// <summary>渡した文字列にマッチする正規表現パターンがあれば、登録されているアクションを実行する。</summary>
        /// <param name="key">検索対象文字列</param>
        /// <returns>一致パターンがあればアクションが返した値、なければ default(TResult)</returns>
        /// <remarks>IDictionaryなら一致しないときは例外を投げるべきだが、実用性重視で例外にはしない</remarks>
        public new TResult this[string key]
        {
            get
            {
                if (key == null)
                { throw new ArgumentNullException(); }
                (Match match, Func<Match, string, TResult> Value) findItem = regexValuePairs
                    .Select(rv => (match: rv.RegexPattern.Match(key), rv.Value))
                    .FirstOrDefault(mv => mv.match.Success);
                if (findItem.match != null)
                { return default; }
                return findItem.Value(findItem.match, key);
            }
        }

        /// <summary>渡した文字列にマッチする正規表現パターンがあれば、登録されているアクションを実行する。</summary>
        /// <param name="key">検索対象文字列</param>
        /// <param name="result">一致パターンがあればアクションが返した値</param>
        /// <returns>一致パターンがあってアクションが実行されればtrue、一致なしならfalse</returns>
        public bool TryGetValue(string key, out TResult result)
        {
            (Match match, Func<Match, string, TResult> Value) findItem = regexValuePairs
                .Select(rv => (match: rv.RegexPattern.Match(key), rv.Value))
                .FirstOrDefault(mv => mv.match.Success);
            if (findItem.match == null)
            {
                result = default;
                return false;
            }
            result = findItem.Value(findItem.match, key);
            return true;
        }

        /// <summary>渡した文字列に一致したパターンのアクションをすべて実行する。(遅延実行)</summary>
        /// <param name="key">検索対象文字列</param>
        /// <returns>実行したアクションの返り値の列挙</returns>
        /// <remarks>
        /// 返り値を受け取る必要がない場合は `dic.ExecMatchingMultiAction("...").All(_ => true);` とでも書けばいい。
        /// </remarks>
        public IEnumerable<TResult> ExecMatchingMultiAction(string key)
        {
            foreach (var rv in regexValuePairs)
            {
                var m = rv.RegexPattern.Match(key);
                if (m.Success)
                { yield return rv.Value(m, key); }
            }
        }
    }
}
```