JDK-8051808 : Math.toRadians and Math.toDegrees could be more accurate (and faster)
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.lang
  • Affected Version: 8u11
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: windows_7
  • CPU: x86
  • Submitted: 2014-07-23
  • Updated: 2019-03-04
  • Resolved: 2014-07-23
Related Reports
Duplicate :  
Relates :  
Description
A DESCRIPTION OF THE REQUEST :
In java.lang.Math, the methods toRadians and toDegrees are not as accurate as they could be. This is the current implementation:

    public static double toRadians(double angdeg) {
        return angdeg / 180.0 * PI;
    }
    
    public static double toDegrees(double angrad) {
        return angrad * 180.0 / PI;
    }

The following simple change would produce a result that is generally more accurate:

    public static double toRadians(double angdeg) {
        return angdeg * DEG_TO_RAD;
    }
    
    public static double toDegrees(double angrad) {
        return angrad / DEG_TO_RAD;
    }
    
    /** Constant used by toRadians and toDegrees. */
    private static final double DEG_TO_RAD = 0.017453292519943295;

The attached test case shows this in detail. It tests three techniques for converting radians to degrees and degrees to radians.

(1) Using the standard Math methods ("standard").
(2) Using a single double constant DEG_TO_RAD in the way shown above ("better").
(3) Using BigDecimal for computation, then converting back to double ("best").

It tests a large number of random values and compares how often the "better" technique beats the standard technique at producing a result that is as close as possible to the one computed by BigDecimal. It shows that:

- The better technique is more accurate than the standard technique about 24% of the time.
- The better technique is less accurate than the standard technique about 3% of the time.

(The rest of the time, it makes no difference.)

Not only would this change make the Math methods slightly more accurate, it would be faster, because it only does a single arithmetic operation instead of two. An equivalent change could benefit StrictMath.

JUSTIFICATION :
It's better, it's faster, and it's an easy change to implement.


---------- BEGIN SOURCE ----------
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;

class MathRadiansTest {
    static final BigDecimal PI = new BigDecimal(
        "3.14159265358979323846264338327950288419716939937510" +
        "5820974944592307816406286208998628034825342117067982");
    
    static final double DEG_TO_RAD = PI.divide(new BigDecimal(180), 100, RoundingMode.HALF_UP).doubleValue();
    static final double RAD_TO_DEG = new BigDecimal(180).divide(PI, 100, RoundingMode.HALF_UP).doubleValue();
    
    
    public static void main(String[] args) {
        Random random = new Random(0);
        
        int toRadBetterTechniqueWins = 0;
        int toRadBetterTechniqueLoses = 0;
        int toDegBetterTechniqueWins = 0;
        int toDegBetterTechniqueLoses = 0;
        
        for (int i = 0; i < 10000; i++) {
            double degrees = random.nextInt(360) + random.nextDouble();
            
            double standard = Math.toRadians(degrees);
            double better = degrees * DEG_TO_RAD;
            double best = new BigDecimal(degrees)
                .divide(new BigDecimal(180), 100, RoundingMode.HALF_UP)
                .multiply(PI)
                .doubleValue();
            
            double standardError = Math.abs(best - standard);
            double betterError   = Math.abs(best - better);
            
            if (betterError < standardError) {
                toRadBetterTechniqueWins++;
            } else if (betterError > standardError) {
                toRadBetterTechniqueLoses++;
            }
            
            if (!(standard == better && better == best)) {
                System.out.println(
                    degrees + " => " +
                    "Standard: " + standard + "; " +
                    "Better: " + better + "; " +
                    "Best: " + best);
            }
        }
        
        for (int i = 0; i < 10000; i++) {
            double radians = random.nextDouble() * (2 * Math.PI);
            
            double standard = Math.toDegrees(radians);
            
            // seemingly more accurate to divide than to multiply by the reciprocal:
            double better = radians / DEG_TO_RAD;
            //double better = radians * RAD_TO_DEG;
            
            double best = new BigDecimal(radians)
                .multiply(new BigDecimal(180))
                .divide(PI, 100, RoundingMode.HALF_UP)
                .doubleValue();
            
            double standardError = Math.abs(best - standard);
            double betterError   = Math.abs(best - better);
            
            if (betterError < standardError) {
                toDegBetterTechniqueWins++;
            } else if (betterError > standardError) {
                toDegBetterTechniqueLoses++;
            }
            
            if (!(standard == better && better == best)) {
                System.out.println(
                    radians + " => " +
                    "Standard: " + standard + "; " +
                    "Better: " + better + "; " +
                    "Best: " + best);
            }
        }
        
        System.out.println("When converting from degrees to radians:");
        System.out.println("Better technique wins: " + toRadBetterTechniqueWins);
        System.out.println("Better technique loses: " + toRadBetterTechniqueLoses);
        
        System.out.println("When converting from radians to degrees:");
        System.out.println("Better technique wins: " + toDegBetterTechniqueWins);
        System.out.println("Better technique loses: " + toDegBetterTechniqueLoses);
        
        //System.out.println("DEG_TO_RAD = " + DEG_TO_RAD);
        //System.out.println("RAD_TO_DEG = " + RAD_TO_DEG);
    }
}
---------- END SOURCE ----------


Comments
Closing as a duplicate of JDK-4477961.
23-07-2014

Note however this comment in the test // seemingly more accurate to divide than to multiply by the reciprocal: double better = radians / DEG_TO_RAD; //double better = radians * RAD_TO_DEG; which running the test bears out to be true, i.e., multiplying by the reciprocal appears to be less accurate (with respect to BigDecimal) than dividing.
23-07-2014

I would use private static final double DEG_TO_RAD = Math.PI / 180.0; private static final double RAD_TO_DEG = 180.0 / Math.PI; These are compile-time constants so they have no runtime computation overhead. As symbolic constants, they're more meaningful than the decimal fraction literal. Finally, use of RAD_TO_DEG in toDegrees() allows floating point multiplication instead of division, which should be faster and no less accurate. (Though the accuracy needs to be checked.)
23-07-2014

I have used this technique for the performance benefit though I had also noticed it produced more accurate results as well.
23-07-2014