Значение 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;