寒いから説明は後日
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); }
}
}
}
}