LoginSignup
4
3

More than 5 years have passed since last update.

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

Posted at

寒いから説明は後日

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); }
            }
        }
    }
}
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3