Java에서 소수(Decimal)을 분수(Fraction)로 표현하는 방법에 대해서 알아보자.
프로그래밍에서는 대부분 분수 표현을 사용하지 않고 소수표현을 사용합니다.
표현 방식은 소수의 경우 0.6와 같이 사용하고 분수의 경우 3/5처럼 주로 표현합니다.
생각외로 분수로 표현하는 방식이 까다로움이 있어 참고 사이트를 발췌하여 정리합니다.
지금부터 소수를 분수로 변경하는 몇가지 방법을 사용해 보도록 하겠습니다.
10의 거듭제곱을 활용하는 방법
소수를 분수로 변환하는 간단한 방법 중 하나는 소수에 10의 거듭제곱을 곱한 다음 결과 분자와 분모를 분수로 사용하는 것입니다. 즉, 소수를 정수로 변환하기 위해 소수점이하 자리수를 곱하여 분모에 곱한 수를 위치하는 것입니다.
public static String convertDecimalToFractionUsingMultiplyingWithPowerOf10(double decimal) {
String decimalStr = String.valueOf(decimal);
int decimalPlaces = decimalStr.length() - decimalStr.indexOf('.') - 1;
long denominator = (long) Math.pow(10, decimalPlaces);
long numerator = (long) (decimal * denominator);
return numerator + "/" + denominator;
}
이를 테스트를 해보면 다음과 같은 결과를 얻을 수 있습니다.
@Test
void convertDecimalToFractionUsingMultiplyingWithPowerOf10() {
Assertions.assertEquals("5/10", DeceimalToFraction.convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.5));
Assertions.assertEquals("1/10", DeceimalToFraction.convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.1));
Assertions.assertEquals("6/10", DeceimalToFraction.convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.6));
Assertions.assertEquals("85/100", DeceimalToFraction.convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.85));
Assertions.assertEquals("125/100", DeceimalToFraction.convertDecimalToFractionUsingMultiplyingWithPowerOf10(1.25));
Assertions.assertEquals("1333333333/1000000000", DeceimalToFraction.convertDecimalToFractionUsingMultiplyingWithPowerOf10(1.333333333));
}
하지만 변환된 결과물을 확인해 보면 기약분수(더이상 약분할 수 없는 분수)가 아닐 경우가 존재하게 됩니다.
예를 들면 5/10의 경우 1/2로 기약분수로 만들수 있습니다.
최대 공약수 활용
위의 방법만을 사용하게 뒤면 0.5의 경우 5/10으로 기약분수가 아닌 분수로 표현이 되고 있습니다.
이를 위해서 최대 공약수(Greatest Common Divisor)를 구하여 분모와 분자에 적용하면 기약분수를 산출 할 수 있습니다.
public static String convertDecimalToFractionUsingGCD(double decimal) {
String decimalStr = String.valueOf(decimal);
int decimalPlaces = decimalStr.length() - decimalStr.indexOf('.') - 1;
long denominator = (long) Math.pow(10, decimalPlaces);
long numerator = (long) (decimal * denominator);
long gcd = gcd(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
return numerator + "/" + denominator;
}
// 최대 공약수 구하기
public static long gcd(long a, long b) {
if (b == 0) {
return a;
} else {
return gcd(b, a % b);
}
}
위 매서드를 테스트하면 다음과 같은 결과를 유추할 수 있습니다.
@Test
void convertDecimalToFractionUsingGCD() {
Assertions.assertEquals("1/2", DeceimalToFraction.convertDecimalToFractionUsingGCD(0.5));
Assertions.assertEquals("1/10", DeceimalToFraction.convertDecimalToFractionUsingGCD(0.1));
Assertions.assertEquals("3/5", DeceimalToFraction.convertDecimalToFractionUsingGCD(0.6));
Assertions.assertEquals("17/20", DeceimalToFraction.convertDecimalToFractionUsingGCD(0.85));
Assertions.assertEquals("5/4", DeceimalToFraction.convertDecimalToFractionUsingGCD(1.25));
Assertions.assertEquals("1333333333/1000000000", DeceimalToFraction.convertDecimalToFractionUsingGCD(1.333333333));
}
하지만 만약 큰 수를 계산하게 된다면 최대 공약수를 산출하는 과정에서 재귀호출과 %연산에 의하여 계산 비용이 늘어나 성능이 저하 될 수 있습니다. 또한 순환소수와 같은 1.3333333과 유형들도 제대로 산출하지 못함을 알 수 있습니다.
Apache Commons Math 라이브러리 활용
Apache Commons Math와 같은 라이브러리를 사용하여 소수를 분수로 변환할 수도 있습니다. 이 경우 Apache Commons Math의 Fraction 클래스를 활용하여 소수를 분수로 변환합니다.
소수를 분수로 변환하려면 소수 값을 생성자에 전달하여 Fraction 개체를 만듭니다.
소수 값을 나타내는 Fraction 객체가 있으면 toString() 메서드를 호출하여 분수의 문자열 표현을 얻을 수 있습니다.
public static String convertDecimalToFractionUsingApacheCommonsMath(double decimal) {
Fraction fraction = new Fraction(decimal);
return fraction.toString();
}
위 매서드를 테스트하면 다음과 같은 결과를 볼 수 있습니다.
@Test
void convertDecimalToFractionUsingApacheCommonsMath() {
Assertions.assertEquals("1 / 2", DeceimalToFraction.convertDecimalToFractionUsingApacheCommonsMath(0.5));
Assertions.assertEquals("1 / 10", DeceimalToFraction.convertDecimalToFractionUsingApacheCommonsMath(0.1));
Assertions.assertEquals("3 / 5", DeceimalToFraction.convertDecimalToFractionUsingApacheCommonsMath(0.6));
Assertions.assertEquals("17 / 20", DeceimalToFraction.convertDecimalToFractionUsingApacheCommonsMath(0.85));
Assertions.assertEquals("5 / 4", DeceimalToFraction.convertDecimalToFractionUsingApacheCommonsMath(1.25));
Assertions.assertEquals("4 / 3", DeceimalToFraction.convertDecimalToFractionUsingApacheCommonsMath(1.333333333));
}
결과를 보면 알 수 있듯이 1.333333333과 같은 순환소수도 분수로 변환하는 것을 볼 수 있습니다.
가장 좋은 결과를 가져다 주고 있습니다.
순환소수 처리 방법
1.333333333과 같은 순환소수의 경우 Apache Commons Math라이브러리 처럼 처리 할 수 있을까요?
우선 반복되는 소수를 분수로 변환하려면 먼저 소수점 뒤에 무한히 나타나는 반복되는 숫자를 식별합니다.
public static String extractRepeatingDecimal(String fractionalPart) {
int length = fractionalPart.length();
for (int i = 1; i <= length / 2; i++) {
String sub = fractionalPart.substring(0, i);
boolean repeating = true;
for (int j = i; j + i <= length; j += i) {
if (!fractionalPart.substring(j, j + i).equals(sub)) {
repeating = false;
break;
}
}
if (repeating) {
return sub;
}
}
return "";
}
만약 반복되는 소수점이 감지되면 다음 주요 속성을 결정하여 진행합니다.
- n : 반복되는 시퀀스의 숫자 길이.
- repeatingValue : 반복되는 숫자의 숫자 값입니다.
- 정수부분 : 소수점 앞의 소수점에서 추출된 정수
- 분모 : 분모는 10을 n 승하고 1을 빼서 도출됩니다.
- 분자: 분자는 반복되는 값 과 정수부분 과 분모를 곱한 값을 더하여 계산됩니다 .
public static String convertDecimalToFractionUsingGCDRepeating(double decimal) {
String decimalStr = String.valueOf(decimal);
int indexOfDot = decimalStr.indexOf('.');
String afterDot = decimalStr.substring(indexOfDot + 1);
// 순환소수 판별
String repeatingNumber = extractRepeatingDecimal(afterDot);
if ("".equals(repeatingNumber)) {
// 기존 기약분수 로직
return convertDecimalToFractionUsingGCD(decimal);
} else {
// 순환소수일 경우 로직
int n = repeatingNumber.length();
int repeatingValue = Integer.parseInt(repeatingNumber);
int integerPart = Integer.parseInt(decimalStr.substring(0, indexOfDot));
int denominator = (int) Math.pow(10, n) - 1;
int numerator = repeatingValue + (integerPart * denominator);
long gcd = gcd(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
return numerator + "/" + denominator;
}
}
이 매서드를 테스트하면 다음과 같은 결과를 얻을 수 있습니다.
@Test
void convertDecimalToFractionUsingGCDRepeating() {
Assertions.assertEquals("1/2", DeceimalToFraction.convertDecimalToFractionUsingGCDRepeating(0.5));
Assertions.assertEquals("17/20", DeceimalToFraction.convertDecimalToFractionUsingGCDRepeating(0.85));
Assertions.assertEquals("5/4", DeceimalToFraction.convertDecimalToFractionUsingGCDRepeating(1.25));
Assertions.assertEquals("4/3", DeceimalToFraction.convertDecimalToFractionUsingGCDRepeating(1.333333333));
Assertions.assertEquals("7/9", DeceimalToFraction.convertDecimalToFractionUsingGCDRepeating(0.777777));
}
결과값과 같이 1.3333333 및 0.777777과 같은 순환소수도 4/3, 7/9와 같이 분수로 표현되고 있음을 확인 할 수 있습니다.
다만, 반복되는 부분과 반복되지 않는 부분이 모두 포함된 소수(예: 0.1123123123)의 경우 계산 되지 않을 경우도 존재합니다.
읽어 주셔서 감사합니다.
참고: https://www.baeldung.com/java-decimal-fraction-conversion
'Development > Java' 카테고리의 다른 글
[JAVA] 2차원 배열에서 최대값, 최소값 구하기(for, stream api) (0) | 2024.07.31 |
---|---|
[Spring Boot] Spring Boot Test에서 @Autowried와 @InjectMocks 사용 (0) | 2024.07.29 |
[JAVA] Stream API에서 NoSuchElementException을 방지하는 방법 (3) | 2024.07.22 |
JAVA에서 IPv4 사용하기 (0) | 2019.07.24 |
[Spring Boot] Gradle 라이브러리 추가후 반드시 해야하는 작업 (0) | 2017.07.27 |