[英]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.除非
x
离x_min
和x_max
,并且可以通过很少的总和或减法达到(也考虑误差传播),我认为模数是您唯一可用的方法。
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;
}
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.