简体   繁体   English

将值包装到 [min,max] 范围内而不进行除法

[英]Wrap value into range [min,max] without division

Is there any way in C# to wrap a given value x between x_min and x_max.在 C# 中有什么方法可以在 x_min 和 x_max 之间包装给定的值 x。 The value should not be clamped as in Math.Min/Max but wrapped like a float modulus.该值不应该像Math.Min/Max那样被Math.Min/Max而是像float模数一样包裹。

A way to implement this would be:实现这一点的一种方法是:

x = x - (x_max - x_min) * floor( x / (x_max - x_min));

However, I am wondering if there is an algorithm or C# method that implements the same functionality without divisions and without the likely float-limited-precision issues that may arise when the value lies far away from the desired range.但是,我想知道是否有一种算法或 C# 方法可以在没有除法的情况下实现相同的功能,并且没有在值远离所需范围时可能出现的浮点限制精度问题。

You can wrap it using two modulo operations, which is still equivalent to a division .您可以使用两个模运算来包装它,这仍然相当于一个除法 I don't think there is a more efficient way of doing this without assuming something about x .我不认为有更有效的方法来做到这一点而不对x做一些假设。

x = (((x - x_min) % (x_max - x_min)) + (x_max - x_min)) % (x_max - x_min) + x_min;

The additional sum and modulo in the formula are to handle those cases where x is actually less than x_min and the modulo might come up negative.公式中的附加和和模数用于处理x实际上小于x_min并且模数可能为负的情况。 Or you could do this with an if , and a single modular division:或者你可以用一个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);

Unless x is not far from x_min and x_max , and is reachable with very few sums or subtractions (think also error propagation ), I think the modulo is your only available method.除非xx_minx_max ,并且可以通过很少的总和或减法达到(也考虑误差传播),我认为模数是您唯一可用的方法。

Without division无除法

Keeping in mind that error propagation might become relevant, we can do this with a cycle:记住错误传播可能会变得相关,我们可以用一个循环来做到这一点:

d = x_max - x_min;
if (abs(d) < MINIMUM_PRECISION) {
    return x_min; // Actually a divide by zero error :-)
}
while (x < x_min) {
    x += d;
}
while (x > x_max) {
    x -= d;
}

Note on probabilities关于概率的说明

The use of modular arithmetic has some statistical implications (floating point arithmetic also would have different ones).模运算的使用有一些统计意义(浮点运算也有不同的意义)。

For example say we wrap a random value between 0 and 5 included (eg a six-sided dice result) into a [0,1] range (ie a coin flip).例如,假设我们将包含的 0 和 5 之间的随机值(例如六面骰子结果)包装到 [0,1] 范围内(即抛硬币)。 Then然后

0 -> 0      1 -> 1
2 -> 0      3 -> 1
4 -> 0      5 -> 1

if the input has flat spectrum, ie, every number (0-5) has 1/6 probability, the output will also be flat, and each item will have 3/6 = 50% probability.如果输入具有平坦的频谱,即每个数字 (0-5) 都有 1/6 的概率,则输出也将是平坦的,并且每个项目都有 3/6 = 50% 的概率。

But if we had a five-sided dice (0-4), or if we had a random number between 0 and 32767 and wanted to reduce it in the (0, 99) range to get a percentage, the output would not be flat, and some number would be slightly (or not so slightly) more likely than others.但是,如果我们有一个五面骰子 (0-4),或者如果我们有一个介于 0 和 32767 之间的随机数并希望将其减少到 (0, 99) 范围内以获得百分比,则输出将不会平坦,并且某些数字会比其他数字稍微(或不那么轻微)更有可能。 In the five-sided dice to coin-flip case, heads vs. tails would be 60%-40%.在五面骰子掷硬币的情况下,正面与反面的概率为 60%-40%。 In the 32767-to-percent case, percentages below 67 would be CEIL(32767/100)/FLOOR(32767/100) = 0.3% more likely to come up than the others.在 32767 到百分比的情况下,低于 67 的百分比将是 CEIL(32767/100)/FLOOR(32767/100) = 比其他百分比高 0.3%。

(To see this more clearly, consider the number to be from "00000" to "32767": once every 328 throws, the first three digits of the number will be "327". When this happens, the last two digits can only go from "00" to "67", they cannot be "68" to "99" because 32768 is out of range. So, digits from 00 to 67 are slightly less more likely. (为了更清楚地看到这一点,考虑从“00000”到“32767”的数字:每抛出一次328,数字的前三位数字将是“327”。发生这种情况时,最后两位数字只能去从“00”到“67”,它们不能是“68”到“99”,因为 32768 超出范围。因此,从 00 到 67 的数字的可能性要小一些。

So, if one wanted a flat output, one would have to ensure that (max-min) was a divisor of the input range.因此,如果想要平坦的输出,则必须确保 (max-min) 是输入范围的除数。 In the case of 32767 and 100, the input range would have to be truncated at the nearest hundred (minus one), 32699, so that (0-32699) contained 32700 outcomes.在 32767 和 100 的情况下,输入范围必须在最接近的百位(减一)处截断,即 32699,以便 (0-32699) 包含 32700 个结果。 Whenever the input was >= 32700, the input function would have to be called again to obtain a new value:每当输入 >= 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
        }
    }
#endif
    return x_min + (
             (
               (x - x_min) % d
             ) + d
           ) % d;

When (INPUTRANGE%OUTPUTRANGE)/(INPUTRANGE) is significant, the overhead might be considerable (eg reducing 0-197 to 0-99 requires making roughly twice as many calls).当 (INPUTRANGE%OUTPUTRANGE)/(INPUTRANGE) 很重要时,开销可能相当大(例如,将 0-197 减少到 0-99 需要进行大约两倍的调用)。

If the input range is less than the output range (eg we have a coin flipper and we want to make a dice tosser), multiply (do not add) using Horner's algorithm as many times as required to get an input range which is larger.如果输入范围小于输出范围(例如,我们有一个硬币翻转器并且我们想要掷骰子),则使用霍纳算法乘(不加)多少次以获得更大的输入范围。 Coin flip has a range of 2, CEIL(LN(OUTPUTRANGE)/LN(INPUTRANGE)) is 3, so we need three multiplications:硬币翻转的范围是 2,CEIL(LN(OUTPUTRANGE)/LN(INPUTRANGE)) 是 3,所以我们需要三个乘法:

for (;;) {
    x = ( flip() * 2 + flip() ) * 2 + flip();
    if (x < 6) {
        break;
    }
}

or to get a number between 122 and 221 (range=100) out of a dice tosser:或者从掷骰子中得到一个 122 到 221(范围=100)之间的数字:

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;

Modulo works fine on floating point, so how about: Modulo 在浮点上运行良好,那么如何:

x = ((x-x_min) % (x_max - x_min) ) + x_min;

It's still effectively a divide though, and you need to tweak it for values less < min...尽管如此,它仍然是一个有效的鸿沟,你需要调整它的值小于 < min ...

You are worrying about accuracy when the number is far away from the range.当数字远离范围时,您会担心准确性。 However this is not related to the modulo operation, however it is performed, but is a property of floating point.然而,这与模运算无关,无论它是如何执行的,而是浮点的一个属性。 If you take a number between 0 and 1, and you add a large constant to it, say to bring it into the range 100 to 101, it will lose some precision.如果你取一个介于 0 和 1 之间的数字,并给它加上一个很大的常数,比如把它放到 100 到 101 的范围内,它会失去一些精度。

Are min and max fixed values? min 和 max 是固定值吗? If so, you could figure out their range and the inverse of that in advance:如果是这样,您可以提前计算出它们的范围及其倒数:

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);
}

The multiplication should perform somewhat better than division.乘法应该比除法表现得更好一些。

x = x<x_min?  x_min:
    x>x_max?  x_max:x;

Its a little convoluted, and you can definitely break it into a pair of if statements.. But I don't see the need for division to begin with.它有点令人费解,你绝对可以把它分解成一对 if 语句..但我认为没有必要开始进行除法。

Edit:编辑:

I seem to have missunderstood, le我好像误会了,le

x = x<x_min?  x_max - (x_min - x):
    x>x_max?  x_min + (x - x_max):x;

This would work if your value of x does not vary too much.. which might work depending on the use case.如果您的 x 值变化不大,这将起作用。这可能会根据用例起作用。 Else for a more robust version I expect you need divide or repeated (recursive?) subtraction atleast.否则,对于更强大的版本,我希望您至少需要除法或重复(递归?)减法。

This should be a more robust version which keeps performing the above calculation until x is stable.这应该是一个更健壮的版本,它会继续执行上述计算,直到 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;
}

How about using an extension method on IComparable .如何在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;
    }
}

And a unit test:还有一个单元测试:

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));

    }
}

LinqPad SAMPLE CODE (Restricted to 3 decimal places) 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;
    }
} 

RESULTS结果

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

If you're able to add the constraint of a min value of 0, simplifying LSerni's answer above is: x = ((x % x_max) + x_max) % x_max如果您能够添加最小值为 0 的约束,则简化上面 LSerni 的答案是: x = ((x % x_max) + x_max) % x_max

The first x % x_max operation will always be negative when x is less than the 0 min value.x小于 0 最小值时,第一个x % x_max操作将始终为负。 This allows the second modulus operation of that simplification to be replaced with a less than 0 comparison.这允许用小于 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;
}

For the very specific case of range 0..1 this seems to work:对于范围 0..1 的非常特殊的情况,这似乎有效:

float wrap(float n) {
    if (n > 1.0) {
        return n - floor(n);
    }
    if (n < 0.0) {
        return n + ceil(abs(n));
    }
    return n;
}

use Wouter de Kort 's answer but change使用Wouter de Kort的答案但改变

if (value.CompareTo(max) > 0) return max;

to

if (value.CompareTo(max) > 0) return min;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 MVC日期范围介于最小值和最大值之间 - MVC date range between min and max value Azure搜索:价格范围 - 最小值和最大值计算 - Azure Search: price range - min & max value calculation 禁用Wpf日期选择器中所有月份和年份的特定日期,没有最小和最大日期范围 - Disabling Particular days in Wpf datepicker for all months and years without min and max date range 范围滑块的逻辑: min(min, max) 有效,但 max(min, max) 无效 - Logic for range sliders: min(min, max) works but max(min, max) doesn't C# 列出最小和最大范围 - C# list min and max range 清单值的平均值,最小值和最大值 - Average, Min and Max of List value 在没有数组或任何循环的情况下从用户那里获取 5 个值并找到最大值和最小值 - Get 5 values from user in single veriable without array or any loop and find the max and min value C#中的数字列表并获取最小值和最大值,而不保留先前循环中存储的变量 - List of numbers in C# and getting min and max value without retaining variables stored in previous loop 更改最小刻度而不更改图表上的最大值 - Change the min scale without changing the max on a chart 在xtraGridControl中的可编辑列上设置最小值和最大值 - set min and max value on editable column in xtraGridControl
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM