選択UIなどで、配列のインデックスを循環させることがよくあります。
以下の計算が有名です。
int i; // インデックス
int diff; // 変化させる値
int len; // 配列の長さ
i = (i + diff) % len;
i = (i - diff + len) % len;
しかし私としては、この処理は汎用性が低いと感じています。
なぜなら、負数に対する剰余がちょっと癖強いからです。
成功ケース
int i = 2;
int diff = 4;
int len = 5;
i = (i + diff) % len; // 1
i = (i - diff + len) % len; // 3
失敗ケース
int i = 2;
int diff = 10;
int len = 5;
i = (i + diff) % len; // 2
i = (i - diff + len) % len; // -3 (-3 % 5 != 2)
負数への対応を強化する場合、次のように出来るでしょう。
失敗ケース リベンジ!
int i = 2;
int diff = 10;
int len = 5;
i = (i + diff) % len; // 2
i = ((i - diff) % len + len) % len; // 2
もうちょっと汎用性を高めてみます。
先ほどまでは [0, len) の区間でしたが、
これを [begin, end) の任意区間に対応させてみます。
考え方としては、
- begin を引いて 一旦 [0, end-begin) の区間にずらし、
- 上述の計算を行った後、
- begin を足して [begin, end) の区間に戻す
感じです。
それと、もう diff は見なくていいですね。任意整数に対しての計算が出来るので。
任意区間に対応したバージョン
int i = 12
int begin = 4;
int end = 7;
// 0. 区間の長さを求めておく
int len = end - begin;
// 1. begin を引く
// この後剰余を取るので、ここでは必要ない
i -= begin;
// 2. 上述の計算
i = (i % len + len) % len;
// この段階で 0 <= i < (end-begin) になる
// 3. begin を足す
// begin <= i < end になるので、更なる剰余の計算は必要ない
i += begin; // つまり i = (i + begin) % len としなくて良い
// 計算結果 : 6
これまで述べた内容を、拡張メソッドにしてみました。
Pure C# でも同様に使えると思います。
完成版
using System.Runtime.CompilerServices;
public static class MathUtils
{
/// <summary>
/// 値を循環的に制限する
/// </summary>
/// <param name="value">値</param>
/// <param name="begin">最小値</param>
/// <param name="end">最大値 + 1</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ModClamped(this int value, int begin, int end)
{
int len = end - begin;
return ((value - begin) % len + len) % len + begin;
}
}
使用例
_ = -1.ModClamped(0, 5); // 4
_ = 6.ModClamped(1, 5); // 2