I want to calculate the ratio of intersection between two intervals; the length of the intersection divided by the length of the shorter interval.
If two intervals do not intersect, the ratio is 0. If one interval is fully contained in the other, the ratio is 1, otherwise it is a number between 0 and 1.
These intervals are angular intervals that lie on the same circle. So \$[-10°, 10°]\$ or \$[350°, 370°]\$ or \$[359990°, 360010°]\$ all represent the same interval.
Consequently, my method AngleIntervalIntersectionRatio()
should return 1 when called with
\$[-10°, 10°]\$ and \$[350°, 370°]\$, since both intervals are contained in one another
Some other example values:
[-10°, 10°] and [0°, 360°]: return 1; (= intersection:20° / shortest interval:20°) [-10°, 10°] and [0°, 180°]: return 0.5; (= intersection:10° / shortest interval:20°) [-10°, 10°] and [90°, 180°]: return 0; [-10°, 10°] and [350°, 360°]: return 1; [-20°, 20°] and [10°, 350°]: return 0.5; (= intersection:20° / shortest interval:40°) [-10°, 10°] and [-3600090°, -3600000°]: return 0.5;
I have two methods for this: IntervalIntersectionRatio()
just calculates the intersection ratio of two intervals on an infinite line.
AngleIntervalIntersectionRatio()
uses this function to calculate the intersection ratio of two intervals on a circle.
I am unhappy with my current code, mainly with two parts:
- The code I use to normalize the angles to be between \$0°\$ and \$360°\$ uses some while loops. Anything I try with modulo just becomes very hard to understand at a glance.
- Since the end of the interval can be above \$360°\$ after normalization, I have to call
IntervalIntersectionRatio()
3 times to cover all possible cases of intersection. For example, with intervals such as \$[-20°, 20°]\$ and \$[10°, 350°]\$ (becomes \$[340°, 380°]\$ and \$[10°, 350°]\$ after normalizing, and the intersection is \$20°\$, not \$10°\$).
Is there anything I could to to make it more beautiful? I am not primarily concerned with performance.
I am also unsure whether I should put all this into a full-blown class hierarchy of intervals and angular-intervals, just for these two functions.
/// <summary> returns the length of the intersection between two angular intervals on the same circle, divided by the length of the shorter interval </summary>
/// <param name="i1Start">start of first interval in degrees</param>
/// <param name="i1End">end of first interval in degress</param>
/// <param name="i2Start">start of second interval in degrees</param>
/// <param name="i2End">end of second interval in degrees</param>
/// <returns>ratio between 0 and 1 (0: no intersection; 1: one interval is a subset of the other</returns>
public static double AngleIntervalIntersectionRatio(double a1start, double a1end, double a2start, double a2end)
{
// the start angle is always smaller than the end angle
Debug.Assert(a1start < a1end);
Debug.Assert(a2start < a2end);
// the intervals can cover at most a full circle
Debug.Assert(a1start + 360 >= a1end);
Debug.Assert(a2start + 360 >= a2end);
// the angles may be larger than 360° or smaller than 0°:
// [-10°, 10°] or [350°, 370°] are both valid representations of the same interval
// ugly code to normalize the angular intervals
//so that the start of each interval is definitely between 0° and 360°
while (a1start >= 360)
{
a1start -= 360;
a1end -= 360;
}
while (a1start < 0)
{
a1start += 360;
a1end += 360;
}
while (a2start >= 360)
{
a2start -= 360;
a2end -= 360;
}
while (a2start < 0)
{
a2start += 360;
a2end += 360;
}
// ugly code to cover all 3 possible intersection types.
// (Since intervals are at most 360° big, this should cover all possibilities)
// at most two of these ratios can be !=0 at the same time
double ratio1 = IntervalIntersectionRatio(a1start, a1end, a2start, a2end);
double ratio2 = IntervalIntersectionRatio(a1start, a1end, a2start + 360, a2end + 360);
double ratio3 = IntervalIntersectionRatio(a1start + 360, a1end + 360, a2start, a2end);
return ratio1 + ratio2 + ratio3;
}
/// <summary> returns the length of the intersection between two intervals, divided by the length of the shorther interval
/// </summary>
/// <param name="i1Start">start of first interval</param>
/// <param name="i1End">end of first interval</param>
/// <param name="i2Start">start of second interval</param>
/// <param name="i2End">end of second interval</param>
/// <returns>ratio between 0 and 1 (0: no intersection; 1: one interval is a subset of the other</returns>
public static double IntervalIntersectionRatio(double i1Start, double i1End, double i2Start, double i2End)
{
double intersectionStart = Math.Max(Math.Min(i1Start, i1End), Math.Min(i2Start, i2End));
double intersectionEnd = Math.Min(Math.Max(i1Start, i1End), Math.Max(i2Start, i2End));
double intersectionLength = intersectionEnd - intersectionStart;
if (intersectionLength <= 0) return 0;
double length1 = Math.Abs(i1End - i1Start);
double length2 = Math.Abs(i2End - i2Start);
double minLength = Math.Min(length1, length2);
// minLength is >0 if intersectionLength is >0, so no divide by zero here
double ratio = intersectionLength / minLength;
ratio = Math.Max(0, ratio);
ratio = Math.Min(1, ratio);
return ratio;
}
(-10, 370)
equivalent to(-10, 10)
or will it force a ratio of 1 for all other non-empty intervals? \$\endgroup\$decimal
instead ofdouble
. \$\endgroup\$