본문 바로가기
알고리즘/기초

금액 표기시 천 단위로 숫자에 컴마를 찍는 6가지 방식 (자바 구현 코드)

by Renechoi 2023. 6. 21.

천 단위로 금액을 계수하기 쉽도록 컴마를 찍는 표기법을 자바 코드로 구현하는 방법을 알아본다. 

 

- 1000 -> 1,000

- 123456789 -> 123,456,789 

 

아이디어의 몇 가지 포인트들은 다음과 같다. 

- 숫자형을 받아 문자형으로 리턴해야 한다.

- 3자리 수에 착안한다.

- 자릿수 정보를 이용하여 앞에서부터 붙이거나, 자릿수 정보 없이 뒤에서부터 붙이거나. 

- 나눗셈과 나머지 연산을 사용할 수 있다. 

- Regex를 이용할 수 있다. 

 

크게 나머지 연산을 이용한 풀이법과 Regex를 이용한 풀이법으로 나뉜다. 1~5는 나머지 연산을 이용한 코드이고 마지막 코드는 Regex를 이용한다. 

 

 

1. 반복문과 나눗셈을 이용한 방식 1

 

private static String convert(long money) {
   int digit = calculateDigit((int)money);
   StringBuilder answer = new StringBuilder();
   int count = 0;
   for (int i = 1; i < digit; i++) {
      count++;
      answer.insert(0, money % 10);
      money /= 10;
      if (count % 3 == 0) {
         answer.insert(0, ",");
      }
   }

   answer.insert(0, money);

   return answer.toString();
}

 

 

동작 원리 

주어진 숫자를 문자열로 변환하여 역순으로 추가하고, 각 자리마다 숫자를 추가할 때마다 count 변수를 증가시키며 3의 배수일 때마다 쉼표를 추가하는 방식이다. 최종 리턴 값은 문자열을 뒤집어서 반환한다.

 

- 주어진 숫자를 문자열로 변환하여 StringBuilder 객체 answer에 역순으로 추가한다.
- 각 자리마다 숫자를 추가할 때마다 count 변수를 증가시킨다.
- count 변수가 3의 배수일 때마다 ,를 추가하여 천 단위마다 쉼표를 넣는다.
- 마지막으로 남은 숫자를 answer에 추가하고 문자열로 변환하여 반환한다.

 

예를 들어, "1234567"이 "1,234,567"이 되는 과정은 다음과 같다. 

 

1) 첫 번째 반복 (i = 1)
count 증가: count = 1
answer 역순 추가: answer = "7"
money 갱신: money = 123456

 

2) 두 번째 반복 (i = 2):
count 증가: count = 2
answer 역순 추가: answer = "76"
money 갱신: money = 12345

 

3) 세 번째 반복 (i = 3):
count 증가: count = 3
answer 역순 추가: answer = "567"
count가 3의 배수이므로 쉼표 추가: answer = "765,"
money 갱신: money = 1234

 

4) 네 번째 반복 (i = 4):
count 증가: count = 4
answer 역순 추가: answer = "765,4"
money 갱신: money = 123

 

5) 다섯 번째 반복 (i = 5):
count 증가: count = 5
answer 역순 추가: answer = "765,43"
money 갱신: money = 12

 

6) 여섯 번째 반복 (i = 6):
count 증가: count = 6
answer 역순 추가: answer = "765,432"
count가 3의 배수이므로 쉼표 추가: answer = "765,432,"
money 갱신: money = 1

 

7) 반복문 종료 후:
answer에 남은 money 추가: answer = "765,432,1"
최종 결과를 뒤집어서 반환: "1,234,567"

 

 

시간 복잡도 

반복문은 자릿수에 비례하는 횟수만큼 실행되므로 O(n)이다. 

 

 

장점 

- 익숙하다. 간단하다.

 

단점 

- 역순으로 문자열을 만들기 때문에 뒤집어서 보내주어야 하는 작업이 필요하다.
- 반복문과 나머지 연산을 사용하여 비효율적일 수 있다.

 

 

 

 

2. 반복문과 나눗셈을 이용한 방식 2

 

private static String convert2(long money) {
   int digit = calculateDigit((int)money);
   StringBuilder answer = new StringBuilder();
   Consumer<Long> consumer = n -> {
      if (answer.length() % 4 == 3) {
         answer.append(",");
      }
      answer.append(n);
   };

   reduceDigits(money, digit, consumer);

   return answer.reverse().toString();
}
private static void reduceDigits(long money, int digit, Consumer<Long> consumer) {
   for (int i = 1; i <= digit; i++) {
      consumer.accept(money % 10);
      money /= 10;
   }
}

동작 원리 

 

1번 방식과 유사하지만 두 가지 다른 특징을 갖는다. 

 

- 함수형 인터페이스를 이용하기

- count 변수를 사용하지 않고 String 길이를 활용하여 계수하기 

 

반복문과 나눗셈 연산을 이용하는 아이디어 큰 틀은 같다.

 

- 주어진 숫자를 문자열로 변환하여 역순으로 추가하는데, 각 자리마다 숫자를 추가할 때마다 StringBuilder 객체 answer의 길이를 확인하여 4의 배수일 때마다 쉼표를 추가한다.
- reduceDigits() 메서드는 주어진 숫자를 자릿수 별로 나누어 consumer를 통해 처리한다. consumer는 각 자리 숫자를 받아 answer에 추가하는 역할을 한다.
- convert2 메서드에서 reduceDigits 메서드를 호출하고, reduceDigits 메서드에서는 주어진 숫자를 나누고 각 자릿수를 consumer로 전달하여 처리한다.
- 최종적으로 answer를 뒤집어 문자열로 변환하여 반환한다.

 

 

 

시간 복잡도 

반복문은 자릿수에 비례하는 횟수만큼 실행되므로 O(n)이다.

 

장점 

- Consumer 인터페이스를 사용하여 함수형 프로그래밍의 장점을 활용할 수 있다.
- count 변수를 사용하지 않고 문자열의 길이를 활용하여 계수하는 방식이 간결하다.

 

단점 

- 역순으로 문자열을 만들기 때문에 뒤집어서 보내주어야 하는 작업이 필요하다.
- 반복문과 나머지 연산을 사용하여 비효율적일 수 있다.

 

 

 

 

 

3. Stream과 나머지 연산을 이용하기 

 

private static String convert3(long money) {
   StringBuilder reversedMoney = new StringBuilder(String.valueOf(money)).reverse();
   String formattedMoney = IntStream.range(0, reversedMoney.length())
      .mapToObj(i -> reversedMoney.charAt(i) + ((i + 1) % 3 == 0 && i + 1 != reversedMoney.length() ? "," : ""))
      .collect(Collectors.joining());
   return new StringBuilder(formattedMoney).reverse().toString();
}

 

동작 원리 

 

반복문과 나눗셈을 이용한 방식이지만 다음 두 가지 특징을 갖는다. 

 

- count 변수를 사용하지 않는다.

- String 변수를 사용하지 않는다. 

 

Stream을 사용하므로 보다 간결하고 직관적인 방식으로 변환 작업을 수행한다. Stream의 특징에 따라 지연된 연산을 사용하기 때문에 별도의 String 변수를 사용하지 않고 연산된 내용을 조합할 수 있어 훨씬 간결한 코드를 작성하게 해준다. 

 

사용된 로직의 핵심적인 부분은 mapToObj() 부분이다. 각 인덱스에 대해 문자와 쉼표(3자리수 -> 천 단위 구분)를 조합하여 스트림을 돌면서 3의 배수이면서 마지막 인덱스가 아닌 경우에만 쉼표를 추가한다. 

 

((i + 1) % 3 == 0 && i + 1 != reversedMoney.length() ? "," : "")

이와 같은 코드에서  (i + 1) % 3 == 0는 현재 인덱스 i의 다음 인덱스가 3의 배수인지를 체크하는 조건이다. i + 1을 사용하는 이유는 인덱스가 0부터 시작하기 때문이다.

i + 1 != reversedMoney.length()는 현재 인덱스 i의 다음 인덱스가 마지막 인덱스가 아닌지를 체크하는 조건이다. 마지막 인덱스인 경우에는 쉼표를 추가하지 않아야 하므로 해당 조건을 추가하여 처리한다.

 

마지막 인덱스를 체크해주어야 하는 이유는 각 자릿수에 대한 처리를 수행할 때 3의 배수인 경우에만 쉼표를 추가하기 때문이다. 예를 들어, "200000"을 변환할 때 마지막 인덱스가 3의 배수인데도 쉼표를 추가하게 되면 ",200,000"과 같은 결과가 나타난다. 마지막 인덱스가 3의 배수인 경우에는 쉼표를 추가하면 안 되기 때문에 해당 조건을 체크해준다. 

 

 

시간 복잡도 

주어진 숫자의 자릿수에 비례하는 O(n) 시간 복잡도를 갖는다.

 

장점 

- Stream을 활용하여 코드가 간결하고 가독성이 좋다.
- count 변수나 중간 문자열 변수를 사용하지 않으므로 메모리 사용에서 이점이 있다. 

 

단점 

- 역순으로 문자열을 만들기 때문에 뒤집어서 보내주어야 하는 작업이 필요하다.
- Stream을 사용하는 과정에서 성능 손실이 있을 수 있다. 

 

 

 

 

 

 

 

 

4. 반복문과 나눗셈을 이용한 방식 2

 

private static String convert4(long money) {
   String reversedMoney = new StringBuilder(String.valueOf(money)).reverse().toString();
   char[] reversedChars = reversedMoney.toCharArray();
   StringBuilder formattedMoney = new StringBuilder();
   for (int i = 0; i < reversedChars.length; i++) {
      formattedMoney.append(reversedChars[i]);
      if ((i + 1) % 3 == 0 && i + 1 != reversedChars.length) {
         formattedMoney.append(",");
      }
   }
   return formattedMoney.reverse().toString();
}

동작 원리 

 

검증이 되는 주요 로직 부분은 위의 3번에서 사용한 연산과 동일하다. Stream을 사용하지 않고 캐릭터 배열을 만들어서 해당 배열에 추가하는 방식으로 formatted를 구성한다. 

 

 

시간 복잡도 

주어진 숫자의 자릿수에 비례하는 O(n) 시간 복잡도를 갖는다.

장점 

- 그런대로 단순하다. 

 

단점 

- StringBuilder도 사용하고 배열도 사용하면서 코드의 구성이 복잡하다. 

 

 

 

 

5. java.text의 DecimalFormat API를 이용하기 

private static String convert5(long money) {
   DecimalFormat decimalFormat = new DecimalFormat("#,###");
   String formattedMoney = decimalFormat.format(money);
   return Stream.of(formattedMoney.split(""))
      .reduce("", (accumulator, value) -> accumulator + value);
}

동작 원리 

 

먼저, DecimalFormat 클래스를 사용하여 원하는 형식으로 숫자를 포맷팅한다. 여기서는 "#,###" 형식을 사용하여 천 단위로 쉼표를 추가한다. 포맷팅된 숫자는 formattedMoney 변수에 저장된다. 그런 다음, formattedMoney 문자열을 split("") 메서드를 사용하여 각 문자로 분할하고 Stream으로 변환한다. 이후, reduce() 메서드를 사용하여 각 문자를 순회하면서 빈 문자열에 차례대로 추가하여 최종 변환된 문자열을 얻는다.

 

시간 복잡도 

java.text.DecimalFormat의 format() 메서드의 시간 복잡도는 입력된 숫자의 자릿수에 비례한다. 문자열 분할 및 Stream 연산에는 O(n) 시간이 소요된다.

 

장점 

코드가 간결하고 직관적이며, DecimalFormat를 이용하므로 포맷 문자열을 유연하게 조정할 수 있다.

 

단점 

- 복잡한 형식의 포맷팅이 필요한 경우, DecimalFormat 클래스로 모든 요구 사항을 충족시키기 어려울 수 있다.

- DecimalFormat 클래스를 사용하기 위해 추가적인 객체 생성과 초기화가 필요하다.

 

 

 

 

6. 정규표현식(Regex)를 이용하기 

 

public static String convert6(long money) {
   String strAmount = String.valueOf(money);
   String regex = "(\\d)(?=(\\d{3})+$)";
   String replacement = "$1,";
   return strAmount.replaceAll(regex, replacement);
}

동작 원리 

 

위의 5가지 방식이 연산을 이용해 변환하는 방식이었다면 이 방식은 정규표현식을 사용하여 세 자리마다 쉼표(,)를 추가하는 기능을 수행한다.

 

- (\\d): 숫자에 매칭된다

- (?=(\\d{3})+$): 세 자리의 숫자 그룹에 매칭된다. 금액의 끝에서부터 거꾸로 세 자리마다 쉼표를 추가하기 위한 기준이다.

- "$1,"은 대체 문자열로 사용된다. 여기서 $1은 첫 번째 그룹에 매칭된 값을 나타낸다. 따라서 각 숫자 뒤에 쉼표가 추가된다.

 

정규표현식인 "(\d)(?=(\d{3})+$)"은 숫자(\d)가 뒤따라오는 세 자리 수(\d{3})들에 대해 긍정형 전방 탐색을 수행한다. 즉, 해당 숫자들을 찾아낸 뒤, 쉼표를 삽입할 위치로 인식하는 역할을 한다. 쉼표 삽입은 "$1,"를 통해 이루어지는데, 여기서 $1은 정규표현식 그룹의 첫 번째 매칭을 의미하며, 매칭된 숫자 그대로 유지하고 쉼표를 추가함을 의미한다.

 

마지막으로, replaceAll() 메서드를 사용하여 정규표현식에 맞는 부분을 쉼표가 추가된 문자열로 대체하여 변환된 금액을 반환한다. replaceAll() 함수는 문자열 내에서 정규표현식에 매칭되는 부분을 찾아 대체 문자열로 치환하는 역할을 한다. 

 

 

시간 복잡도 

정규표현식의 패턴 매칭 작업은 입력된 문자열을 한 번씩 순회하면서 수행되므로, 입력된 금액의 자릿수에 따라 처리 시간이 증가한다. 즉, 문자열의 길이에 선형적으로 비례한다. 따라서 시간 복잡도는 O(n)이다. 

 

장점 

- 간결하고 효율적인 코드 작성을 가능하게 한다. 한 줄의 코드로 처리할 수 있으며, 반복문 등을 사용하는 번거로움을 줄일 수 있다.

 

단점 

- 정규표현식을 레퍼런스 없이 암기하여 사용하기가 쉽지 않다. 

 

 

 

 

 

 

 

전체 코드 

package basic.string;

import java.text.DecimalFormat;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;


public class MoneyFormatter {

   @Test
   public static void test() {

      System.out.println(convert3(1000));


      Assertions.assertEquals("1,000", convert(1000));
      Assertions.assertEquals("1,000", convert2(1000));
      Assertions.assertEquals("1,000", convert3(1000));
      Assertions.assertEquals("1,000", convert4(1000));
      Assertions.assertEquals("1,000", convert5(1000));
      Assertions.assertEquals("1,000", convert6(1000));

      Assertions.assertEquals("1,001", convert(1001));
      Assertions.assertEquals("1,001", convert2(1001));
      Assertions.assertEquals("1,001", convert3(1001));
      Assertions.assertEquals("1,001", convert4(1001));
      Assertions.assertEquals("1,001", convert5(1001));
      Assertions.assertEquals("1,001", convert6(1001));

      Assertions.assertEquals("1,234", convert(1234));
      Assertions.assertEquals("1,234", convert2(1234));
      Assertions.assertEquals("1,234", convert3(1234));
      Assertions.assertEquals("1,234", convert4(1234));
      Assertions.assertEquals("1,234", convert5(1234));
      Assertions.assertEquals("1,234", convert6(1234));

      Assertions.assertEquals("3,999", convert(3999));
      Assertions.assertEquals("3,999", convert2(3999));
      Assertions.assertEquals("3,999", convert3(3999));
      Assertions.assertEquals("3,999", convert4(3999));
      Assertions.assertEquals("3,999", convert5(3999));
      Assertions.assertEquals("3,999", convert6(3999));

      Assertions.assertEquals("99,999", convert(99999));
      Assertions.assertEquals("99,999", convert2(99999));
      Assertions.assertEquals("99,999", convert3(99999));
      Assertions.assertEquals("99,999", convert4(99999));
      Assertions.assertEquals("99,999", convert5(99999));
      Assertions.assertEquals("99,999", convert6(99999));

      Assertions.assertEquals("200,000", convert(200000));
      Assertions.assertEquals("200,000", convert2(200000));
      Assertions.assertEquals("200,000", convert3(200000));
      Assertions.assertEquals("200,000", convert4(200000));
      Assertions.assertEquals("200,000", convert5(200000));
      Assertions.assertEquals("200,000", convert6(200000));

      Assertions.assertEquals("1,234,567", convert(1234567));
      Assertions.assertEquals("1,234,567", convert2(1234567));
      Assertions.assertEquals("1,234,567", convert3(1234567));
      Assertions.assertEquals("1,234,567", convert4(1234567));
      Assertions.assertEquals("1,234,567", convert5(1234567));
      Assertions.assertEquals("1,234,567", convert6(1234567));

      Assertions.assertEquals("9,999,999", convert(9999999));
      Assertions.assertEquals("9,999,999", convert2(9999999));
      Assertions.assertEquals("9,999,999", convert3(9999999));
      Assertions.assertEquals("9,999,999", convert4(9999999));
      Assertions.assertEquals("9,999,999", convert5(9999999));
      Assertions.assertEquals("9,999,999", convert6(9999999));

      Assertions.assertEquals("123,456,789", convert(123456789));
      Assertions.assertEquals("123,456,789", convert2(123456789));
      Assertions.assertEquals("123,456,789", convert3(123456789));
      Assertions.assertEquals("123,456,789", convert4(123456789));
      Assertions.assertEquals("123,456,789", convert5(123456789));
      Assertions.assertEquals("123,456,789", convert6(123456789));
   }

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

   private static String convert(long money) {
      int digit = calculateDigit((int)money);
      StringBuilder answer = new StringBuilder();
      int count = 0;
      for (int i = 1; i < digit; i++) {
         count++;
         answer.insert(0, money % 10);
         money /= 10;
         if (count % 3 == 0) {
            answer.insert(0, ",");
         }
      }

      answer.insert(0, money);

      return answer.toString();
   }

   private static String convert2(long money) {
      int digit = calculateDigit((int)money);
      StringBuilder answer = new StringBuilder();
      Consumer<Long> consumer = n -> {
         if (answer.length() % 4 == 3) {
            answer.append(",");
         }
         answer.append(n);
      };

      reduceDigits(money, digit, consumer);

      return answer.reverse().toString();
   }

   private static void reduceDigits(long money, int digit, Consumer<Long> consumer) {
      for (int i = 1; i <= digit; i++) {
         consumer.accept(money % 10);
         money /= 10;
      }
   }

   private static String convert3(long money) {
      StringBuilder reversedMoney = new StringBuilder(String.valueOf(money)).reverse();
      String formattedMoney = IntStream.range(0, reversedMoney.length())
         .mapToObj(i -> reversedMoney.charAt(i) + ((i + 1) % 3 == 0 && i + 1 != reversedMoney.length() ? "," : ""))
         .collect(Collectors.joining());
      return new StringBuilder(formattedMoney).reverse().toString();
   }

   private static String convert4(long money) {
      String reversedMoney = new StringBuilder(String.valueOf(money)).reverse().toString();
      char[] reversedChars = reversedMoney.toCharArray();
      StringBuilder formattedMoney = new StringBuilder();
      for (int i = 0; i < reversedChars.length; i++) {
         formattedMoney.append(reversedChars[i]);
         if ((i + 1) % 3 == 0 && i + 1 != reversedChars.length) {
            formattedMoney.append(",");
         }
      }
      return formattedMoney.reverse().toString();
   }

   private static String convert5(long money) {
      DecimalFormat decimalFormat = new DecimalFormat("#,###");
      String formattedMoney = decimalFormat.format(money);
      return Stream.of(formattedMoney.split(""))
         .reduce("", (accumulator, value) -> accumulator + value);
   }



   public static String convert6(long money) {
      String strAmount = String.valueOf(money);
      String regex = "(\\d)(?=(\\d{3})+$)";
      String replacement = "$1,";
      return strAmount.replaceAll(regex, replacement);
   }


   private static int calculateDigit(int number) {
      if (number == 0) {
         return 1;
      }
      return (int)(Math.log10(number) + 1);

   }
}



 

반응형