[C#] 제곱, 제곱근, 역수 수식 표시 & 계산기 정밀도 높이기(부동 소수점 계산 오류)

2024. 1. 11. 16:15[C#]/[C# 윈폼] 혼자해보는 계산기 만들기

1. 역수, 제곱, 제곱근에 대한 수식 표시 기능을 만들었다.

1) 역수 처리 

        // 역수 처리
        private void OneOverXBtn_Click(object sender, EventArgs e)
        {
            double num = double.Parse(NumScreen.Text);
            double inverse = 1.0 / num;
			NumScreen.Text = inverse.ToString("#,##0.#########");
            
            if (!isNewNum)
            {
                string lastExpression = expressionScreen.Text.Substring(expressionScreen.Text.LastIndexOf(' ') + 1);
                if (lastExpression.Contains("1/("))
                {
                    expressionScreen.Text = expressionScreen.Text.Substring(0, expressionScreen.Text.LastIndexOf("1/("));
                }
                else
                {
                    string currentExpression = expressionScreen.Text;
                    expressionScreen.Text = currentExpression + " ";
                }
            }
            else
            {
                expressionScreen.Text = "";
            }

            expressionScreen.Text += "1/(" + NumScreen.Text + ")";
            isNewNum = true;
            Opt = Operators.None;
 
        }

 

2) 제곱 처리

        //제곱 처리
        private void SqrBtn_Click(object sender, EventArgs e)
        {
            double num = double.Parse(NumScreen.Text);
            double result = num * num;
            NumScreen.Text = result.ToString("#,##0.#########");
            Result = result;

            if (!isNewNum)
            {
                string lastExpression = expressionScreen.Text.Substring(expressionScreen.Text.LastIndexOf(' ') + 1);
                if (lastExpression.Contains("²"))
                {
                    expressionScreen.Text = expressionScreen.Text.Substring(0, expressionScreen.Text.LastIndexOf("²"));
                }
                else
                {
                    string currentExpression = expressionScreen.Text;
                    expressionScreen.Text = currentExpression + " ";
                }
            }
            else
            {
                expressionScreen.Text = "";
            }

            expressionScreen.Text += num + "²";
            isNewNum = true;
            Opt = Operators.None; // 사칙 연산 초기화
        }


    
3) 제곱근 처리

        //제곱근 처리
        private void rootBtn_Click(object sender, EventArgs e)
        {
            double num = double.Parse(NumScreen.Text);
            double result = Math.Sqrt(num); // 제곱근 계산
            NumScreen.Text = result.ToString("#,##0.#########");
            Result = result;


            if (!isNewNum)
            {
                string lastExpression = expressionScreen.Text.Substring(expressionScreen.Text.LastIndexOf(' ') + 1);
                if (lastExpression.Contains("²√"))
                {
                    expressionScreen.Text = expressionScreen.Text.Substring(0, expressionScreen.Text.LastIndexOf("²√"));
                }
                else
                {
                    string currentExpression = expressionScreen.Text;
                    expressionScreen.Text = currentExpression + " ";
                }
            }
            else
            {
                expressionScreen.Text = "";
            }

            expressionScreen.Text += "²√" + num;
            isNewNum = true;
            Opt = Operators.None; // 사칙 연산 초기화
        }
  • 이렇게 만들고 보니 계산법과 수식만 다르고 같은 로직으로 돌아간다는 게 보였다.
  • 그래서 하나의 함수로 만들어줬다.

변경 후

// 역수 처리
private void OneOverXBtn_Click(object sender, EventArgs e)
{
    SetExpressionText("1/(");
}

//제곱 처리
private void SqrBtn_Click(object sender, EventArgs e)
{
    SetExpressionText("²");
}

//제곱근 처리
private void rootBtn_Click(object sender, EventArgs e)
{
    SetExpressionText("²√");
}

 

SetExpressionText 함수

   private void SetExpressionText(string sign)
   {
       double num = double.Parse(NumScreen.Text);
       double result = 0.0;
       
       // 수식 추가 및 초기화 로직
       if (!isNewNum)
       {
           string lastExpression = expressionScreen.Text.Substring(expressionScreen.Text.LastIndexOf(' ') + 1);
           if (lastExpression.Contains(sign))
           {
               expressionScreen.Text = expressionScreen.Text.Substring(0, expressionScreen.Text.LastIndexOf(sign));
           }
           else
           {
               expressionScreen.Text += " ";
           }
       }
       else
       {
           expressionScreen.Text = "";
       }

       // 연산 로직
       switch (sign)
       {
           case "²":
               result = num * num;
               expressionScreen.Text = $"{num}²";
               break;
           case "²√":
               result = Math.Sqrt(num);
               expressionScreen.Text = $"²√{num}";
               break;
           case "1/(":
               result = 1.0 / num;
               expressionScreen.Text = $"1/({num})";
               break;
       }

       NumScreen.Text = result.ToString("#,##0.#########");

       isNewNum = true;
       Opt = Operators.None;
   }
  • sign값을 받아와 각 sign 별 처리를 해줬다.
  • 이렇게 해주니 한눈에 보기도 편하고 나중에 유지보수도 쉬워질 것 같았다.

2. 계산 정밀도 높히기(feat. 부동 소수점)

12의 제곱근을 계산한 뒤 다시 제곱을 하면 11.999999999라는 숫자가 나오는 것을 볼 수 있었다.

 

전부 실패하고 100% 정확도는 아니지만 거의 근사한 방법으로 시도했다.

  • 소수점에 9 갯수와 0 갯수를 Count해서 올림/반올림

시도해 본 방법

Heron's method (x)
Newton's method (x)
소수점 반올림 (x)
소수점에 9 갯수와 0 갯수를 Count해서 올림/반올림 (채택)

 

 

가장 정확한 계산이 나올 때 까지 알고리즘을 계속해서 변경해줬다. 

  1. 우선 9자리까지 표현해주던 수를 소수점 12자리로 변경했다.
  2. 처음에는 소수점 아래 "9"를 7개 이상 포함하면 올림 처리, "0"을 7개 이상 포함하면 내림 처리를 해줬다.
  3. 하지만 12.8과 같은 소수점이 있는 수를 입력 후 제곱, 제곱근을 계산하면 반올림 처리가 되서 제곱근을 하다가 제곱을 하면 13이 나오는 오류가 발생했다..
   private double RoundCustom(double value, int decimals)
   {
       string stringValue = value.ToString($"F{decimals}");
       int indexOfDecimal = stringValue.IndexOf('.');

       if (indexOfDecimal != -1)
       {
           string decimalPart = stringValue.Substring(indexOfDecimal + 1);

           // 내림 처리
           if (decimalPart.Count(c => c == '0') >= 7)
           {
               return Math.Floor(value);
           }

           // 올림 처리
           if (decimalPart.Count(c => c == '9') >= 7)
           {
               return Math.Ceiling(value);
           }
       }

       return Math.Round(value, decimals);
   }

 

 

그래서 어떻게 하면 더 정확도를 높힐까 생각했다.

  1. 연속되는 0000가 있으면 그 위치까지 내림 처리를 해준다.
  2. 연속되는 9999가 있으면 그 위에 소수점까지 올림 처리를 해준다.
  3. 소수점 아래 숫자가 "9"를 7개 이상 포함하면 정수 올림 처리를 해준다.
  4. 소수점 아래 숫자가 "0"을 7개 이상 포함하면 내림 처리를 해준다. 
 private double RoundCustom(double value, int decimals)
 {
     string stringValue = value.ToString($"F{decimals}");
     int indexOfDecimal = stringValue.IndexOf('.');

     if (indexOfDecimal != -1)
     {
         string decimalPart = stringValue.Substring(indexOfDecimal + 1);

         // 내림 처리
         if (decimalPart.Contains("0000"))
         {
             return Math.Floor(value * Math.Pow(10, decimals - 4)) / Math.Pow(10, decimals - 4);
         }

         // 올림 처리
         if (decimalPart.Contains("9999"))
         {
             return Math.Ceiling(value * Math.Pow(10, decimals - 4)) / Math.Pow(10, decimals - 4);
         }
     }

     // 정수에 대한 올림 처리
     if (value >= 0 && value % 1 == 0 && value.ToString().Length >= 7)
     {
         return Math.Ceiling(value);
     }

     // 정수에 대한 내림 처리
     if (value < 0 && value % 1 == 0 && value.ToString().Length >= 8)
     {
         return Math.Floor(value);
     }

     return Math.Round(value, decimals);
 }

 

이게 지금까지는 정확도가 꽤 맞는 편이라고 생각한다.

물론 100% 정확도는 아니라서 오류가 있긴 하지만 어지간하면 근사치에 가장 가까운 값이 나오도록 했다.

12의 제곱근 10번 연산한 결과
다시 10번 제곱해준 결과

 

 

12.82를 10번 제곱근 한 결과
다시 10번 제곱해준 결과


 

 

부동 소수점이 계산에서 꽤나 까다로운 에러를 발생시킨다는 것을 깨달았다.

나중에 큰 프로젝트를 참여하게 된다면 해당 공부를 훨씬 정밀하게 해야겠다고 느꼈다.

반응형