I was going to say “yes”… but…
After taking another look at the MeanAngle Rosetta page, mean angle is a more complex problem than I thought. The example questions get at common corner cases on either general floating point precision or the behavior of common atan2() implementations.
The most common answers given are 0.0, -90.0, and 20.0 – but even though most languages agree, the second answer is clearly a strange artifact, when a more meaningful answer would be either 0 or NaN. A comment on the XPL0 answer explains:
The second example is interesting because it computes ATan2(0.,0.), which is undefined in mathematics (like division by zero), but the floating point processor in IBM-PC-type computers defines it as zero. The reason -90 gets displayed instead is due to small rounding errors (and another extra-mathematical concept, -0). In fact almost any angle can result because of slight rounding errors as Y and X both approach zero.
Some of the answers special-case any very small values as equivalent to atan2(0,0)
and manually return something else. But it seems like the Julia answer gets it right – in Julia they implement two built-in methods, sind()
and cosd()
, which use a piecewise design to return good sin and cos values for degrees:
In particular this eliminates floating point error on common values such as:
- sin(180) degrees: 0.0, not -8.742278E-8
- sin(360) degrees: 0.0, not 1.7484555E-7
I feel like the need for a good piece-wise solution comes up over and over with winding. Anyway… I did a quick line-by-line translation of the Julia → processing:
// function sind(x::Real)
float sind(float x) {
// if isinf(x)
// return throw(DomainError(x, "`x` cannot be infinite."))
if (x==Float.POSITIVE_INFINITY || x==Float.NEGATIVE_INFINITY) {
throw new IllegalArgumentException("x cannot be infinite");
}
// elseif isnan(x)
// return oftype(x,NaN)
// end
else if (Float.isNaN(x)) {
throw new IllegalArgumentException("x cannot be NaN");
}
// rx = copysign(float(rem(x,360)),x)
float rx = x % 360;
// arx = abs(rx)
float arx = abs(rx);
// if rx == zero(rx)
// return rx
if (rx == 0.0 || rx == -0.0 ) {
return rx;
}
// elseif arx < oftype(rx,45)
// return sin_kernel(deg2rad_ext(rx))
else if (arx < 45) {
return sin(radians(rx));
}
// elseif arx <= oftype(rx,135)
// y = deg2rad_ext(oftype(rx,90) - arx)
// return copysign(cos_kernel(y),rx)
else if (arx <= 135) {
float y = abs(cos(radians(90 - arx)));
if (rx < 0) return -y;
return y;
}
// elseif arx == oftype(rx,180)
// return copysign(zero(rx),rx)
else if (arx == 180) {
if (rx < 0) return -0.0;
return 0.0;
}
// elseif arx < oftype(rx,225)
// y = deg2rad_ext((oftype(rx,180) - arx)*sign(rx))
// return sin_kernel(y)
else if (arx < 255) {
float y = radians(180.0 - arx);
if(rx < 0) y = y * -1;
return sin(y);
}
// elseif arx <= oftype(rx,315)
// y = deg2rad_ext(oftype(rx,270) - arx)
// return -copysign(cos_kernel(y),rx)
else if (arx <= 315) {
float y = abs(cos(radians(270.0 - arx)));
if (rx < 0) return y;
return -y;
}
// else
// y = deg2rad_ext(rx - copysign(oftype(rx,360),rx))
// return sin_kernel(y)
// end
else {
float y;
if (rx < 0) y = radians(rx - 360);
else y = radians(rx + 360);
return sin(y);
}
}
// end
…and the final Processing function looks like this:
float sind(float x) {
if (x==Float.POSITIVE_INFINITY || x==Float.NEGATIVE_INFINITY) {
throw new IllegalArgumentException("x cannot be infinite");
}
else if (Float.isNaN(x)) {
throw new IllegalArgumentException("x cannot be NaN");
}
float rx = x % 360;
float arx = abs(rx);
if (rx == 0.0 || rx == -0.0 ) {
return rx;
}
else if (arx < 45) {
return sin(radians(rx));
}
else if (arx <= 135) {
float y = abs(cos(radians(90 - arx)));
if (rx < 0) return -y;
return y;
}
else if (arx == 180) {
if (rx < 0) return -0.0;
return 0.0;
}
else if (arx < 255) {
float y = radians(180.0 - arx);
if(rx < 0) y = y * -1;
return sin(y);
}
else if (arx <= 315) {
float y = abs(cos(radians(270.0 - arx)));
if (rx < 0) return y;
return -y;
}
else {
float y;
if (rx < 0) y = radians(rx - 360);
else y = radians(rx + 360);
return sin(y);
}
}
Test it with:
float[] tests = {0, 180, 360, -360,
Float.POSITIVE_INFINITY,
Float.NEGATIVE_INFINITY,
Float.NaN
};
void setup(){
for (float test : tests) {
try {
float good = sind(test);
float bad = sin(radians(test));
println(good, bad);
} catch (IllegalArgumentException e) {
println(e);
}
}
}
Output:
0.0 0.0
0.0 -8.742278E-8
0.0 1.7484555E-7
-0.0 -1.7484555E-7
java.lang.IllegalArgumentException: x cannot be infinite
java.lang.IllegalArgumentException: x cannot be infinite
java.lang.IllegalArgumentException: x cannot be NaN
So that looks great.
I believe that if getMeanAngle()
uses sind()
and cosd()
instead of sin()
and cos()
it will give better answers. But I’ll finish that up a little later.