#なぜこのメソッドがない?
LINQを使っていてMax、Averageなどの統計値を出したい、という場面がよくありますが、
なぜこれが標準で計算できないんだ!
と個人的に思う処理に
- メディアン (Median)
- 四分位点
- 標準偏差 (Std)
等がありました。
なので、まずは最も使用頻度が高いメディアンを実装してみました。
##やりたいこと
その1:シンプルかつ違和感のない処理
例えば和を求めたい場合は、下のコードのように標準ライブラリのSum()を使って
List<int> iList = new List<int> { 1, 2, 3, 4, 5 };
int sum = iList.Sum()
のような形で実現可能です。
これに合わせて
List<int> iList = new List<int> { 1, 2, 3, 4, 5 };
int median = iList.Median()
のようにシンプルに算出したいです。
その2:型依存性なし
入力がint型のとき、double型のとき、long型のとき・・
と全て実装すると可読性が著しく落ちるので、
色んな型を一括で処理できるようにしたいです。
##実現するには?
その1を実現するためには「拡張メソッド」
その2を実現するためには「ジェネリック」
を使用すれば良いらしいです
(恥ずかしながら今回調べて初めて知りました)
こちらを参考にさせて頂きました
##コード
アルゴリズム自体はこちらを参照させて頂きました。
本体(DateTime型とそれ以外で処理を分けています)
public static class LinQCustomMethods
{
// メディアン算出メソッド(Generics)
public static T Median<T>(this IEnumerable<T> src)
{
//ジェネリックの四則演算用クラス
var ao = new ArithmeticOperation<T>();
//昇順ソート
var sorted = src.OrderBy(a => a).ToArray();
if (!sorted.Any())
{
throw new InvalidOperationException("Cannot compute median for an empty set.");
}
int medianIndex = sorted.Length / 2;
//要素数が偶数のとき、真ん中の2要素の平均を出力
if (sorted.Length % 2 == 0)
{
//四則演算可能な時のみ算出
if (ao.ArithmeticOperatable(typeof(T)))
{
return ao.Divide(ao.Add(sorted[medianIndex], sorted[medianIndex - 1]), (T)(object)2.0);
}
else throw new InvalidOperationException("Cannot compute arithmetic operation");
}
//奇数のときは、真ん中の値を出力
else
{
return sorted[medianIndex];
}
}
// メディアン算出(DateTime型のみ別メソッド)
public static DateTime Median(this IEnumerable<DateTime> src)
{
//昇順ソート
var sorted = src.OrderBy(a => a).ToArray();
if (!sorted.Any())
{
throw new InvalidOperationException("Cannot compute median for an empty set.");
}
int medianIndex = sorted.Length / 2;
//要素数が偶数のとき、真ん中の2要素の平均を出力
if (sorted.Length % 2 == 0)
{
return sorted[medianIndex] + new TimeSpan((sorted[medianIndex - 1] - sorted[medianIndex]).Ticks / 2);
}
//奇数のときは、真ん中の値を出力
else
{
return sorted[medianIndex];
}
}
}
ジェネリックの四則演算用クラス
こちらを参考にさせて頂きました
//ジェネリック四則演算用クラス
public class ArithmeticOperation<T>
{
/// <summary>
/// 四則演算適用可能かを判定
/// </summary>
/// <param name="src">判定したいタイプ</param>
/// <returns></returns>
public bool ArithmeticOperatable(Type srcType)
{
//四則演算可能な型の一覧
var availableT = new Type[]
{
typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(long), typeof(ulong), typeof(byte),
typeof(decimal), typeof(double)
};
if (availableT.Contains(srcType)) return true;
else return false;
}
/// <summary>
/// 四則演算可能なクラスに対しての処理
/// </summary>
public ArithmeticOperation()
{
var availableT = new Type[]
{
typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(long), typeof(ulong), typeof(byte),
typeof(decimal), typeof(double)
};
if (!availableT.Contains(typeof(T)))
{
throw new NotSupportedException();
}
var p1 = Expression.Parameter(typeof(T));
var p2 = Expression.Parameter(typeof(T));
Add = Expression.Lambda<Func<T, T, T>>(Expression.Add(p1, p2), p1, p2).Compile();
Subtract = Expression.Lambda<Func<T, T, T>>(Expression.Subtract(p1, p2), p1, p2).Compile();
Multiply = Expression.Lambda<Func<T, T, T>>(Expression.Multiply(p1, p2), p1, p2).Compile();
Divide = Expression.Lambda<Func<T, T, T>>(Expression.Divide(p1, p2), p1, p2).Compile();
Modulo = Expression.Lambda<Func<T, T, T>>(Expression.Modulo(p1, p2), p1, p2).Compile();
Equal = Expression.Lambda<Func<T, T, bool>>(Expression.Equal(p1, p2), p1, p2).Compile();
GreaterThan = Expression.Lambda<Func<T, T, bool>>(Expression.GreaterThan(p1, p2), p1, p2).Compile();
GreaterThanOrEqual = Expression.Lambda<Func<T, T, bool>>(Expression.GreaterThanOrEqual(p1, p2), p1, p2).Compile();
LessThan = Expression.Lambda<Func<T, T, bool>>(Expression.LessThan(p1, p2), p1, p2).Compile();
LessThanOrEqual = Expression.Lambda<Func<T, T, bool>>(Expression.LessThanOrEqual(p1, p2), p1, p2).Compile();
}
public Func<T, T, T> Add { get; private set; }
public Func<T, T, T> Subtract { get; private set; }
public Func<T, T, T> Multiply { get; private set; }
public Func<T, T, T> Divide { get; private set; }
public Func<T, T, T> Modulo { get; private set; }
public Func<T, T, bool> Equal { get; private set; }
public Func<T, T, bool> GreaterThan { get; private set; }
public Func<T, T, bool> GreaterThanOrEqual { get; private set; }
public Func<T, T, bool> LessThan { get; private set; }
public Func<T, T, bool> LessThanOrEqual { get; private set; }
}
##結果
List<int> iList = new List<int> { 1, 2, 3, 4, 5 };
List<double> dList = new List<double> { 3.2, 3.5, 3.6, 4 };
List<DateTime> dtList = new List<DateTime> { new DateTime(2020, 3, 24), new DateTime(2020, 3, 25), new DateTime(2020, 3, 26), new DateTime(2020, 3, 27) };
//メディアン(int)
Console.WriteLine(iList.Median().ToString());
//メディアン(double)
Console.WriteLine(dList.Median().ToString());
//メディアン(DateTime)
Console.WriteLine(dtList.Median().ToString());
3
3.55
2020/03/25 12:00:00
正常にメディアンが出せていそうです
##苦労したところ
その1. Generics(T)型の足し算
上のコード中の「要素数が偶数のとき、真ん中の2要素の平均を出力」とありますが、
平均を出すためには足し算、割り算等の四則演算が必要です。
Generics型は「a + b」みたいな形で四則演算ができないので、
こちらを参考に、四則演算クラスを追加しました。
※Average()メソッドを使えばGenerics型のまま平均を出せますが、
今後Generic型の処理で四則演算を使いたい場面が多そうなので、クラスとして作成しました。
その2. DateTime型の扱い
DateTime型は上記四則演算クラスが適用できないので、別途メソッドを作りました。
DateTime型はMath.Maxクラスも使えなかったりと、色々不便なところが多いですね。
その3. Genericsのキャスト
平均を出す際に「2」で割る必要がありますが、
そのままだとGenerics(T)型をint型で割れずにエラーが出るため、
「2」をGenerics(T)型にキャストする必要があります。
こちらを参考に、
「2.0」としてdouble型で宣言 → object型にキャスト → Generics(T)型にキャスト
の順で、Generics(T)型へのキャストが実現できました。
##そして
上記を実装したあとに、こんなものがあることに気付きました(泣)
リンク