반응형

최근 프로그래머스 문제를 풀면서 문자열을 가공해서 사용해야하는 문제가 많이 있다고 생각했고, String과 한번 친해져보자는 생각으로 글을 작성하게 되었다. 적어도 이정도는 까먹지 말자라는 느낌으로... 시작해보자

 

예시로 들 문제는 아래와 같다.

https://school.programmers.co.kr/learn/courses/30/lessons/72410

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

프로그래머스에 2021 KAKAO BLIND RECRUITMENT의 레벨 1문제이다. String과 친해지기 정말 좋은 문제라고 생각된다.

 

문제를 짧게 설명해 보면 다음과 같다.

1단계 new_id의 모든 대문자를 대응되는 소문자로 치환합니다.

2단계 new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.

3단계 new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.

4단계 new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.

5단계 new_id가 빈 문자열이라면, new_id에 "a"를 대입합니다.

6단계 new_id의 길이가 16자 이상이면, new_id의 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다. 만약 제거 후 마침표(.)가 new_id의 끝에 위치한다면 끝에 위치한 마침표(.) 문자를 제거합니다.

7단계 new_id의 길이가 2자 이하라면, new_id의 마지막 문자를 new_id의 길이가 3이 될 때까지 반복해서 끝에 붙입니다.

문자열 new_id가 입력되는데, 해당 문자열을 위와 같이 7단계를 거쳐가며 가공한다. 최종 결과를 반환하면 되는 문제이다. 문제를 해결하면서 정규표현식으로도 풀 수 있고, 직접 문자열을 하나하나 확인하며 해결할 수도 있다. 여기서는 정규표현식을 주로 사용하며 문제를 해결해보자.

 

 

# 1단계 : 모든 대문자를 소문자로 변경

new_id = new_id.toLowerCase();

아주 간단한 String의 메소드인 toLowerCase()를 사용해주면 모든 대문자가 소문자로 변경된다. 그럼 반대로 소문자를 대문자로 바꾸기 위해서는 toUpperCase()를 사용해주면 된다. 

 

 

 

# 2단계 : 영문 소문자, 숫자, 마침표(.), 빼기(-), 밑줄(_)을 제외한 모든 특수문자를 제거

여기서부터 정규표현식(regex)이 사용된다. 그리고 우리는 String의 replaceAll() 메소드를 사용할 것이다. replace()도 있지 않냐라고 물어본다면, 두 메소드 다 원하는 문자열을 찾아서 어떤 문자열로 교체해주는 역할을 한다. 하지만 replaceAll()의 경우 target으로 정규표현식을 사용할 수 있고, replace는 사용하지 못한다는 특징이 존재한다. 이제 사용해보자.

 

영문 소문자, 숫자, 마침표(.), 빼기(-), 밑줄(_)은 다 어떻게 표현할 수 있을까?

영어 소문자 => [a-z]
숫자 => [0-9]
마침표(.) => [.]
밑줄(_) => [_]
빼기(-) => [-]

위 처럼 []을 이용해서 특수문자 또는 문자, 숫자의 범위로 표현할 수 있다. 이 부분을 replaceAll()의 첫 번째 인자로 넣어주면 해당 정규표현식에 포함되는 문자를 원하는 문자로 변경할 수 있다.

 

하지만 우리가 하고 싶은 것은 영어 소문자, 숫자, 마침표, 밑줄, 빼기가 아닌 다른 문자들을 변경시키고 싶은 것이다. 그렇기 때문에 정규표현식에서 부정(반대)의 표시는 앞에 ^을 붙이는 것이다. 예를 하나 들어보자.

영어 소문자가 아닌 것들 => [^a-z]
숫자가 아닌 것들 => [^0-9]
마침표(.)가 아닌 것들 => [^.]
밑줄(_)이 아닌 것들 => [^_]
빼기(-)가 아닌 것들 => [^-]

이제 거의 완성이 되어간다. 우리는 위에 있는 모든 조건을 포함할 수 있는 정규표현식을 만들어야 한다. 그럼 이제 2단계를 완료해보자.

new_id = new_id.replaceAll("[^-_.a-z0-9]", "");

이렇게 작성해주면 빼기(-), 밑줄(_), 마침표(.), 영어 소문자, 숫자를 제외한 다른 문자는 모두 제외된다. 하지만 여기서 주의해야할 점이 있다. 빼기(-)는 a-z처럼 문자의 범위에 사용되기 때문에 아래와 같이 작성하면 빼기(-)는 제거되지 않을 거다.

new_id = new_id.replaceAll("[^.-_a-z0-9]", "");

뭐가 다른건지 찾아보면 [-_.]대신 [.-_]이 들어갔다. 이게 뭔 차이냐라고 물어보신다면 빼기(-)가 가운데 들어가는 순간 문자의 범위를 나타내게 된다. 즉, [.-_]의 의미는 문자(.)부터 문자(_)까지 범위에 있는 모든 문자를 표현하는 정규표현식이 되는 것이다.

 

아스키 코드표를 보면 마침표(.)는 46번이고, 밑줄(_)은 95번이다. 그리고 빼기(-)는 45번이기 때문에 [.-_]에는 빼기(-)가 포함되지 않는다. 그래서 아래 코드와 같이 작성하면 테스트에 통과하지 못할 것이다. 빼기(-) 기호를 정규표현식에서 사용할 때는 신경써서 사용해야 한다.

 

 

 

# 3단계 : 마침표(.)가 2개이상 반복되는 부분은 모두 마침표(.) 하나로 변경

여기서도 정규 표현식이 사용된다. 마침표가 여러번 반복되는지는 어떻게 알 수 있을까? 이때 사용하는 것이 {}이다. 중괄호는 정규표현식에서 해당 문자의 빈도수 또는 범위를 나타낼 수 있다. 이렇게 말로만 하면 이해하기 어려우니 코드로 살펴보자.

.이 2번 이상 반복되는 것을 찾고 싶다 => [.]{2,}
.이 3번 이상 반복되는 것을 찾고 싶다 => [.]{3,}
.이 2번만 반복되는 것을 찾고 싶다 => [.]{2}
.이 2번 이상 6번 이하로 반복되는 것을 찾고 싶다 => [.]{2,6}

이렇게 범위를 지정해서 찾을 수 있다. 여기서 주의해야할 점은 {}안에 범위 지정할 때, {2, 6} 이런식으로 공백이 있으면 PatternSyntaxException 예외가 발생하기 때문에 주의하길 바란다.

 

그럼 이제 3단계도 마무리를 해보자.

new_id = new_id.replaceAll("[.]{2,}", ".");

이제 마침표(.)가 2개 이상 반복되는 모든 부분을 찾아서 마침표(.) 하나로 변경해줄 것이다.

 

 

 

# 4단계 : 마침표(.)가 처음이나 끝에 있으면 제거한다.

여기서는 여러가지 방법을 생각할 수도 있다. 예를 들어 String의 startsWith()나 endsWith()를 사용하는 방법이다. 

if (new_id.startsWith(".")) {
	// .으로 시작하면 첫 번째 .을 제거하는 로직 실행
}

if (new_id.endsWith(".")) {
    // .으로 끝나면 마지막 .을 제거하는 로직 실행
}

하지만 이왕하는거 정규 표현식으로 해결을 해보자. 처음 시작 문자와 마지막 끝 문자를 확인하는 방법이 있을까? 물론 정규 표현식에 존재한다. 코드로 한번 살펴보자.

문자열의 첫 번째 문자 => ^.
문자열의 마지막 문자 => .$

이렇게 보면 조금 어려울 수 있기 때문에 간단한 예시를 하나 들어보자.

String str = "아녕하세용";

// 문자열의 첫 번째 문자를 "안"로 변경하고 싶다.
str = str.replaceAll("^.", "안"); // 안녕하세용

// 문자열의 마지막 문자를 "요"로 변경하고 싶다.
str = str.replaceAll(".$", "요"); // 안녕하세요

이제 이해가 됐을 거라고 생각한다. 하지만 우리가 해야할 것은 첫 번째, 마지막 문자를 무조건 바꾸는게 아니고 어떠한 조건(. 인가)에 대해서 참이면 바꿔 줘야 한다. 이때 사용할 수 있는 정규 표현식도 존재한다. 

// 문자열의 첫 문자가 "아"면 "안"으로 바꿔줘
str = str.replaceAll("^[아]", "안");

// 문자열의 첫 문자가 "."이라면 ""으로 바꿔줘
str = str.replaceAll("^[.]", "");

// 문자열의 마지막 문자가 "용"이면 "요"로 바꿔줘
str = str.replaceAll("[용]$", "요");

// 문자열의 마지막 문자가 "."이면 ""로 바꿔줘
str = str.replaceAll("[.]$", "");

그럼 이제 우리는 4단계도 마무리할 수 있다.

new_id = new_id.replaceAll("^[.]", ""); // 첫 문자 . 제거
new_id = new_id.replaceAll("[.]$", ""); // 마지막 문자 . 제거

근데 위 코드를 한 줄로 표현할 수는 없을까? 이때 정규표현식에서는 or 표현 (|)으로 여러 조건을 한 번에 해결할 수 있다. 한 줄로 표현해보자.

new_id = new_id.replaceAll("^[.]|[.]$", "");

이제 앞 뒤에 마침표(.)가 있으면 모두 제거될 것이다.

 

 

 

# 5단계 : 비어있는 문자열("")이라면 "a"를 추가

이 부분은 매우 싶고, 2가지 방법이 있습니다.

// 첫 번째 방법
if (new_id.length() == 0) {
    new_id = "a";
}

// 두 번째 방법
if (new_id.isEmpty()) {
    new_id = "a";
}

저는 가독성도 좋고, 이왕이면 String의 메소드를 활용하기 위해서 isEmpty() 메소드를 사용했습니다. isEmpty()는 비어있는 문자열이면 true를 반환하고, 아니면 false를 반환합니다.

 

 

 

# 6단계 (1) : 문자열의 길이가 16자 이상이 넘어가면 앞에부터 15번째 문자열만 남기고 뒤쪽은 모두 제거합니다.

문자열의 길이가 16자 이상인지 확인하고, 만약 넘어간다면 String의 index 0부터 14까지만 살려되면 되는 작업입니다. 이때는 String을 원하는 범위만큼 자를 수 있는 substring()이라는 메소드를 사용해서 해결했습니다.

if (new_id.length() >= 16) {
    new_id = new_id.substring(0, 15); // index가 0이상 15미만 (0 ~ 14) 만큼 잘라냄
}

6단계의 다른 조건으로 이제 넘어가 봅니다.

 

 

 

# 6단계 (2) : 6 -1 단계에서 제거한 문자열의 마지막이 마침표(.)라면 그 마침표는 제거합니다.

이 부분은 이미 해본 것이기 때문에 넘어가겠습니다.

 

 

 

# 7단계 : 문자열의 길이가 2 이하라면 문자열의 마지막 문자를 뒤에 이어붙입니다. 이때 길이가 3이되면 종료

이어 붙일 때는 StringBuilder의 append()를 사용해도 되고, 그냥 String에 더해줘도 됩니다. 반복횟수는 많아도 3번이기 때문에 그냥 String에 더하는 방법을 사용하겠습니다.

while (new_id.length() <= 2) {
    new_id += new_id.charAt(new_id.length() - 1);
}

문자열의 길이가 2이하면 new_id에 마지막 문자를 더해주는 로직을 작성하고, 모든 단계가 마무리 되었습니다.

반응형

+ Recent posts