Finding sine of an angle in Java without using the Math class

Sine of an angle can be approximated using the following series:

sin(x)=x-{x^3/{3!}}+{x^5/{5!}}-...

where x is the angle in radians.

The above question had been asked in ISC(12) once and the method used by students usually use nested loops. Outer loop iterates for a certain no. of terms and inner loop calculates factorial. The code based on the above logic is as follows:

class Sine{
    public static float sine1(float x){
        float sin=0.0f;
        long fact;
        for(int i=1; i<=10; i++){
            fact=1;
            for(int j=1; j<=2*i-1; j++){
                fact=fact*j;
            }
            if(i%2==1){
                sin=sin+(float)(Math.pow(x,2*i-1)/fact);
            }else{
                sin=sin-(float)(Math.pow(x,2*i-1)/fact);
            }
        }
        return sin;
    }

    public static void main(String args[]){
        System.out.println("Sin(1.57079632679) is "+Math.sin(1.57079632679f)+" "+sine1(1.57079632679f));
    }
}

The above program displays the sine obtained from Math.sin() as well as out own sine1() method. The important point over here is that each successive term is smaller than the previous term because the denominator is increasing at a higher rate than the numerator. In the above program the outer loop is running up to 10 and is responsible for the accuracy of the sine we are calculating—the more the number of terms the better the approximation, however, don’t be under the impression that a very large number of iteration will give you a very accurate answer because we are calculating factorial inside the loop and factorials of large numbers won’t fit in an integer variable or for that matter into long as well.

The above method will give correct answer which would obviously be approximate due to the nature of the problem. Now let us see how we can improve the above solution. The accuracy in the above solution is by trial and error and we don’t know for sure the accuracy in terms of number of decimal places. Let’s we want the answer to be accurate to five decimal places that is 0.00001. This is easy to implement we can keep on computing new terrms till the value of the term is more than 0.00001 and once the value of the term falls below 0.00001 we can abort the loop. The basic idea is that a value of less than 0.00001 will not cause significant difference in the answer.

The term of the above series are being alternatively added and subtracted to the answer. Notice that at any stage the next numerator can be obtained by multiplying the current numerator by the square of x (that is x*x) and the next denominator can be obtained by multiplying the current denominator by (denominator+1)* (denominator+2). Let us see this with the help of an example—first term is x, multiplying by x2 will give us x3, which is the numerator of the second term; now the denominator of the first term is 1, multiplying 1 by (1+1) and (1+2) will give us 6 (1*2*3=6). This way, just by multiplying the numerator and the denominator, we would be able to save the time spent in calculating the factorial as well as some time spent due call of Math.pow(). The program after incorporating the above logic would be as follows:

class Sine{
    public static float sine1(float x){
        float sin=0.0f;
        long fact;
        for(int i=1; i<=10; i++){
            fact=1;
            for(int j=1; j<=2*i-1; j++){
                 fact=fact*j;
            }
            if(i%2==1){
                 sin=sin+(float)(Math.pow(x,2*i-1)/fact);
            }else{
                 sin=sin-(float)(Math.pow(x,2*i-1)/fact);
            }
        }
        return sin;
    }
    public static float sine2(float x){
        float sin=x,term, numerator=x, denominator=1, xsquare=x*x, factorial=1, sign=-1;
        do{
            numerator *= xsquare;
            denominator=denominator*(factorial+1)*(factorial+2);
            factorial=factorial+2;
            term=numerator/denominator;
            sin=sin+(sign*term);
            sign*=-1;
        }while(term>0.00001);
        return sin;
    }

    public static void main(String args[]){
        System.out.println("Sin(1.57079632679) is "+Math.sin(1.57079632679f)+" "+sine1(1.57079632679f)+" "+sine2(1.57079632679f));
    }
}

Note that now we have added one more function sine2() which employs the new logic. In order to alternate the terms between negative and positive we have taken an integer variable sign initialized to -1. The value of the variable sign will become +1 after being multiplied by -1 one and again -1 on next multiplication by -1. Due to this the value of the variable term will also alternate from negative to positive and positive to negative.

Let us also benchmark out programs so that we can actually see how efficient is our two sine function with respect to Math.sin. For details of how to determine the time taken for some piece of code one can see my earlier post Timing ‘Prime time’. The following program will call functions Math.sin(), sine1() and sine(2) and display the time taken to evaluate the sine of various angle in multiple of π/2:

class Sine{
    public static float sine1(float x){
        float sin=0.0f;
        long fact;
        for(int i=1; i<=10; i++){
            fact=1;
            for(int j=1; j<=2*i-1; j++){
                 fact=fact*j;
            }
            if(i%2==1){
                 sin=sin+(float)(Math.pow(x,2*i-1)/fact);
            }else{
                 sin=sin-(float)(Math.pow(x,2*i-1)/fact);
            }
        }
        return sin;
    }
    public static float sine2(float x){
        float sin=x,term, numerator=x, denominator=1, xsquare=x*x, factorial=1, sign=-1;
        do{
            numerator *= xsquare;
            denominator=denominator*(factorial+1)*(factorial+2);
            factorial=factorial+2;
            term=numerator/denominator;
            sin=sin+(sign*term);
            sign*=-1;
        }while(term>0.00001);
        return sin;
    }
    public static void benchmark(){
        long startTime,estimatedTime;
        float sineValue;
        float testcase[]={1.5707963268f, 3.1415926536f, 4.7123889804f, 6.2831853072f, 7.853981634f, 9.4247779607f, 10.9955742875f, 12.5663706143f, 14.1371669411f};
        System.out.println("n\t\tMath.sin\tsine1\t\tsine2");
        for(int i=0; i<testcase.length; i++){
            System.out.print(testcase[i]+"\t");

            startTime= System.nanoTime();
            sineValue=(float)Math.sin(testcase[i]);
            estimatedTime = System.nanoTime()- startTime;
            System.out.print(estimatedTime);

            startTime= System.nanoTime();
            sineValue=sine1(testcase[i]);
            estimatedTime = System.nanoTime()- startTime;
            System.out.print("\t\t"+estimatedTime);

            startTime= System.nanoTime();
            sineValue=sine2(testcase[i]);
            estimatedTime = System.nanoTime()- startTime;
            System.out.println("\t\t"+estimatedTime);

        }
    }

    public static void main(String args[]){
        benchmark();
    }
}

The output of the benchmark() function will vary from system to system and on my system it’s as follows:

n Math.sin sine1 sine2
1.5707964 2341 10698 3408
3.1415927 2007 10541 3635
4.712389 1628 10554 3899
6.2831855 1620 10390 4229
7.8539815 1657 10423 4339
9.424778 1684 10700 5329
10.995574 1807 10450 5436
12.566371 2533 6023 3408
14.137167 1492 6799 4282

As usual, the main function has purposely been kept to minimum and its expected that students will write appropriate input/output statements as per the requirement.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.