Значение Wrap в диапазон [min, max] без деления
есть ли способ в C# обернуть заданное значение x между x_min и x_max. Значение не должно быть зажато, как в Math.Min/Max
но завернутый как float
модуль.
способ реализовать это было бы:
x = x - (x_max - x_min) * floor( x / (x_max - x_min));
однако мне интересно, есть ли алгоритм или метод C#, который реализует ту же функциональность без делений и без вероятных проблем с ограниченной точностью, которые могут возникнуть, когда значение находится далеко от желаемого диапазона.
8 ответов
вы можете обернуть его с помощью двух операций по модулю, что по-прежнему эквивалентно делению. Я не думаю, что есть более эффективный способ сделать это, не предполагая что-то о x
.
x = (((x - x_min) % (x_max - x_min)) + (x_max - x_min)) % (x_max - x_min) + x_min;
дополнительная сумма и по модулю в Формуле должны обрабатывать те случаи, когда x
на самом деле меньше, чем x_min
и модуль может быть отрицательным. Или вы можете сделать это с помощью if
и один модульный разделение:
if (x < x_min)
x = x_max - (x_min - x) % (x_max - x_min);
else
x = x_min + (x - x_min) % (x_max - x_min);
если x
Не далеко от x_min
и x_max
, и достижимо с очень немногими суммами или вычитаниями (подумайте также распространения ошибок), я думаю, что модуль - ваш единственный доступный метод.
без разделения
имея в виду, что распространение ошибок может стать актуальным, мы можем сделать это с циклом:
d = x_max - x_min;
if (abs(d) < MINIMUM_PRECISION) {
return x_min; // Actually a divide by zero error :-)
}
while (x < x_min) {
x += (x_max - x_min);
}
while (x > x_max) {
x -= (x_max - x_min);
}
примечание о вероятностях
использование модульной арифметики некоторые статистические последствия (арифметика с плавающей запятой также будет иметь разные).
например, скажем, мы обертываем случайное значение от 0 до 5 (например, результат шестигранной кости) в диапазон [0,1] (т. е. сальто монеты). Тогда
0 -> 0 1 -> 1
2 -> 0 3 -> 1
4 -> 0 5 -> 1
если вход имеет плоский спектр, т. е. каждое число (0-5) имеет вероятность 1/6, выход также будет плоским, и каждый элемент будет иметь вероятность 3/6 = 50%.
но если бы у нас были пятисторонние кости (0-4), или если бы у нас было случайное число от 0 до 32767 и мы хотели уменьшить его в диапазоне (0, 99), чтобы получить процент, выход не был бы плоским, и некоторое число было бы немного (или не так немного) более вероятным, чем другие. В пятистороннем кубике для монетного флипа, Орел против решки будет 60%-40%. В 32767-процентном случае процент ниже 67 будет CEIL(32767/100)/FLOOR(32767/100) = 0,3% с большей вероятностью, чем другие.
Итак, если бы вы хотели плоский выход, вы бы необходимо убедиться, что (max-min) является делителем входного диапазона. В случае 32767 и 100 входной диапазон должен быть усечен на ближайшей сотне (минус один), 32699, так что (0-32699) содержит 32700 исходов. Всякий раз, когда вход был >= 32700, функция ввода должна была быть вызвана снова, чтобы получить новое значение:
function reduced() {
#ifdef RECURSIVE
int x = get_random();
if (x > MAX_ALLOWED) {
return reduced(); // Retry
}
#else
for (;;) {
int x = get_random();
int d = x_max - x_min;
if (x > MAX_ALLOWED) {
continue; // Retry
}
return x_min + (
(
(x - x_min) % d
) + d
) % d;
}
#endif
когда(INPUTRANGE%OUTPUTRANGE)/(INPUTRANGE) является значительным, накладные расходы могут быть значительными (например, сокращение 0-197 до 0-99 требует примерно в два раза больше звонков).
если входной диапазон меньше выходного диапазона (например, у нас есть монетный Флиппер, и мы хотим сделать кубик), умножьте (не добавляйте), используя алгоритм Хорнера столько раз, сколько требуется, чтобы получить входной диапазон, который больше. Сальто монетки имеет ряд 2, CEIL(LN(OUTPUTRANGE)/LN (INPUTRANGE)) 3, поэтому нам нужно 3 умножения:
for (;;) {
x = ( flip() * 2 + flip() ) * 2 + flip();
if (x < 6) {
break;
}
}
или получить число между 122 и 221 (диапазон=100) из кости tosser:
for (;;) {
// ROUNDS = 1 + FLOOR(LN(OUTPUTRANGE)/LN(INPUTRANGE)) and can be hardwired
// INPUTRANGE is 6
// x = 0; for (i = 0; i < ROUNDS; i++) { x = 6*x + dice(); }
x = dice() + 6 * (
dice() + 6 * (
dice() /* + 6*... */
)
);
if (x < 200) {
break;
}
}
// x is now 0..199, x/2 is 0..99
y = 122 + x/2;
по модулю отлично работает с плавающей запятой, так что как насчет:
x = ((x-x_min) % (x_max - x_min) ) + x_min;
это по-прежнему эффективно разделение, и вам нужно настроить его для значений меньше
вы беспокоитесь о точности, когда число находится далеко от диапазона. Однако это не связано с операцией по модулю, однако она выполняется, но является свойством с плавающей запятой. Если вы берете число от 0 до 1 и добавляете к нему большую константу, скажем, чтобы привести его в диапазон от 100 до 101, он потеряет некоторую точность.
являются ли min и max фиксированными значениями? Если это так, вы могли бы выяснить их диапазон и обратный этому заранее:
const decimal x_min = 5.6m;
const decimal x_max = 8.9m;
const decimal x_range = x_max - x_min;
const decimal x_range_inv = 1 / x_range;
public static decimal WrapValue(decimal x)
{
return x - x_range * floor(x * x_range_inv);
}
умножение должно работать несколько лучше, чем деление.
как насчет использования метода расширения на IComparable
.
public static class LimitExtension
{
public static T Limit<T>(this T value, T min, T max)
where T : IComparable
{
if (value.CompareTo(min) < 0) return min;
if (value.CompareTo(max) > 0) return max;
return value;
}
}
и модульный тест:
public class LimitTest
{
[Fact]
public void Test()
{
int number = 3;
Assert.Equal(3, number.Limit(0, 4));
Assert.Equal(4, number.Limit(4, 6));
Assert.Equal(1, number.Limit(0, 1));
}
}
x = x<x_min? x_min:
x>x_max? x_max:x;
его немного запутанный, и вы можете определенно разбить его на пару операторов if.. Но я не вижу необходимости в разделении с самого начала.
Edit:
кажется, я не понял, Ле
x = x<x_min? x_max - (x_min - x):
x>x_max? x_min + (x - x_max):x;
Это будет работать, если ваше значение x не слишком сильно меняется.. что может сработать в зависимости от варианта использования. Еще для более надежной версии я ожидаю, что вам нужно разделить или повторить (рекурсивно?) вычитание хотяб.
Это должно мощная версия, которая держит выполнении вышеуказанных расчетов до X является стабильным.
int x = ?, oldx = x+1; // random init value.
while(x != oldx){
oldx = x;
x = x<x_min? x_max - (x_min - x):
x>x_max? x_min + (x - x_max):x;
}
пример кода LinqPad (ограничен 3 десятичными знаками)
void Main()
{
Test(int.MinValue, 0, 1,0.1f, "value = int.MinValue");
Test(int.MinValue, -2,- 1,0.1f, "value = int.MinValue");
Test(int.MaxValue, 0, 1,0.1f, "value = int.MaxValue");
Test(int.MaxValue, -2,- 1,0.1f, "value = int.MaxValue");
Test(-2,-2,-1,0.1f, string.Empty);
Test(0,0,1,0.1f, string.Empty);
Test(1,1,2,0.1f, string.Empty);
Test(int.MinValue, 0, 1, -0.1f, "value = int.MinValue");
Test(int.MinValue, -2,- 1, -0.1f, "value = int.MinValue");
Test(int.MaxValue, 0, 1, -0.1f, "value = int.MaxValue");
Test(int.MaxValue, -2,- 1, -0.1f, "value = int.MaxValue");
Test(-2,-2,-1, -0.1f, string.Empty);
Test(0,0,1, -0.1f, string.Empty);
Test(1,1,2, -0.1f, string.Empty);
}
private void Test(float value, float min ,float max, float direction, string comment)
{
"".Dump(" " + min + " to " + max + " direction = " + direction + " " + comment);
for (int i = 0; i < 11; i++)
{
value = (float)Math.Round(min + ((value - min) % (max - min)), 3);
string.Format(" {1} -> value: {0}", value, i).Dump();
value = value + direction < min && direction < 0 ? max + direction : value + direction;
}
}
результаты
0 to 1 direction = 0.1 value = int.MinValue
0 -> value: 0
1 -> value: 0.1
2 -> value: 0.2
3 -> value: 0.3
4 -> value: 0.4
5 -> value: 0.5
6 -> value: 0.6
7 -> value: 0.7
8 -> value: 0.8
9 -> value: 0.9
10 -> value: 0
-2 to -1 direction = 0.1 value = int.MinValue
0 -> value: -2
1 -> value: -1.9
2 -> value: -1.8
3 -> value: -1.7
4 -> value: -1.6
5 -> value: -1.5
6 -> value: -1.4
7 -> value: -1.3
8 -> value: -1.2
9 -> value: -1.1
10 -> value: -2
0 to 1 direction = 0.1 value = int.MaxValue
0 -> value: 0
1 -> value: 0.1
2 -> value: 0.2
3 -> value: 0.3
4 -> value: 0.4
5 -> value: 0.5
6 -> value: 0.6
7 -> value: 0.7
8 -> value: 0.8
9 -> value: 0.9
10 -> value: 0
-2 to -1 direction = 0.1 value = int.MaxValue
0 -> value: -2
1 -> value: -1.9
2 -> value: -1.8
3 -> value: -1.7
4 -> value: -1.6
5 -> value: -1.5
6 -> value: -1.4
7 -> value: -1.3
8 -> value: -1.2
9 -> value: -1.1
10 -> value: -2
-2 to -1 direction = 0.1
0 -> value: -2
1 -> value: -1.9
2 -> value: -1.8
3 -> value: -1.7
4 -> value: -1.6
5 -> value: -1.5
6 -> value: -1.4
7 -> value: -1.3
8 -> value: -1.2
9 -> value: -1.1
10 -> value: -2
0 to 1 direction = 0.1
0 -> value: 0
1 -> value: 0.1
2 -> value: 0.2
3 -> value: 0.3
4 -> value: 0.4
5 -> value: 0.5
6 -> value: 0.6
7 -> value: 0.7
8 -> value: 0.8
9 -> value: 0.9
10 -> value: 0
1 to 2 direction = 0.1
0 -> value: 1
1 -> value: 1.1
2 -> value: 1.2
3 -> value: 1.3
4 -> value: 1.4
5 -> value: 1.5
6 -> value: 1.6
7 -> value: 1.7
8 -> value: 1.8
9 -> value: 1.9
10 -> value: 1
0 to 1 direction = -0.1 value = int.MinValue
0 -> value: 0
1 -> value: 0.9
2 -> value: 0.8
3 -> value: 0.7
4 -> value: 0.6
5 -> value: 0.5
6 -> value: 0.4
7 -> value: 0.3
8 -> value: 0.2
9 -> value: 0.1
10 -> value: 0
-2 to -1 direction = -0.1 value = int.MinValue
0 -> value: -2
1 -> value: -1.1
2 -> value: -1.2
3 -> value: -1.3
4 -> value: -1.4
5 -> value: -1.5
6 -> value: -1.6
7 -> value: -1.7
8 -> value: -1.8
9 -> value: -1.9
10 -> value: -2
0 to 1 direction = -0.1 value = int.MaxValue
0 -> value: 0
1 -> value: 0.9
2 -> value: 0.8
3 -> value: 0.7
4 -> value: 0.6
5 -> value: 0.5
6 -> value: 0.4
7 -> value: 0.3
8 -> value: 0.2
9 -> value: 0.1
10 -> value: 0
-2 to -1 direction = -0.1 value = int.MaxValue
0 -> value: -2
1 -> value: -1.1
2 -> value: -1.2
3 -> value: -1.3
4 -> value: -1.4
5 -> value: -1.5
6 -> value: -1.6
7 -> value: -1.7
8 -> value: -1.8
9 -> value: -1.9
10 -> value: -2
-2 to -1 direction = -0.1
0 -> value: -2
1 -> value: -1.1
2 -> value: -1.2
3 -> value: -1.3
4 -> value: -1.4
5 -> value: -1.5
6 -> value: -1.6
7 -> value: -1.7
8 -> value: -1.8
9 -> value: -1.9
10 -> value: -2
0 to 1 direction = -0.1
0 -> value: 0
1 -> value: 0.9
2 -> value: 0.8
3 -> value: 0.7
4 -> value: 0.6
5 -> value: 0.5
6 -> value: 0.4
7 -> value: 0.3
8 -> value: 0.2
9 -> value: 0.1
10 -> value: 0
1 to 2 direction = -0.1
0 -> value: 1
1 -> value: 1.9
2 -> value: 1.8
3 -> value: 1.7
4 -> value: 1.6
5 -> value: 1.5
6 -> value: 1.4
7 -> value: 1.3
8 -> value: 1.2
9 -> value: 1.1
10 -> value: 1
если вы можете добавить ограничение минимального значения 0, упрощая ответ LSerni выше:x = ((x % x_max) + x_max) % x_max
первый x % x_max
операция всегда будет отрицательным, когда x
меньше значения 0 мин. Это позволяет заменить вторую операцию модуля этого упрощения на сравнение менее 0.
float wrap0MinValue(float x, float x_max)
{
int result = toWrap % maxValue;
if (result < 0) // set negative result back into positive range
result = maxValue + result;
return result;
}
использовать Ваутер де Кортответ, но изменить
if (value.CompareTo(max) > 0) return max;
to
if (value.CompareTo(max) > 0) return min;