코테 리팩토링) [3차] 방금그곡

EBAB!·2024년 5월 17일
0
post-custom-banner

목표

  • 기존에 푸는데만 집중했던 문제들을 클린 코드, 객체 지향 방식으로 리팩토링
  • 완벽한 효율보다는 다양한 스타일로 작성

문제

코테 문제) https://school.programmers.co.kr/learn/courses/30/lessons/17683

네오는 자신이 기억한 멜로디를 가지고 방금그곡을 이용해 음악을 찾는다. 그런데 라디오 방송에서는 한 음악을 반복해서 재생할 때도 있어서 네오가 기억하고 있는 멜로디는 음악 끝부분과 처음 부분이 이어서 재생된 멜로디일 수도 있다. 반대로, 한 음악을 중간에 끊을 경우 원본 음악에는 네오가 기억한 멜로디가 들어있다 해도 그 곡이 네오가 들은 곡이 아닐 수도 있다. 그렇기 때문에 네오는 기억한 멜로디를 재생 시간과 제공된 악보를 직접 보면서 비교하려고 한다. 다음과 같은 가정을 할 때 네오가 찾으려는 음악의 제목을 구하여라.

  • 방금그곡 서비스에서는 음악 제목, 재생이 시작되고 끝난 시각, 악보를 제공한다.
  • 네오가 기억한 멜로디와 악보에 사용되는 음은 C, C#, D, D#, E, F, F#, G, G#, A, A#, B 12개이다.
  • 각 음은 1분에 1개씩 재생된다. 음악은 반드시 처음부터 재생되며 음악 길이보다 재생된 시간이 길 때는 음악이 끊김 없이 처음부터 반복해서 재생된다. 음악 길이보다 재생된 시간이 짧을 때는 처음부터 재생 시간만큼만 재생된다.
  • 음악이 00:00를 넘겨서까지 재생되는 일은 없다.
  • 조건이 일치하는 음악이 여러 개일 때에는 라디오에서 재생된 시간이 제일 긴 음악 제목을 반환한다. 재생된 시간도 같을 경우 먼저 입력된 음악 제목을 반환한다.
  • 조건이 일치하는 음악이 없을 때에는 “(None)”을 반환한다.

입력 형식
입력으로 네오가 기억한 멜로디를 담은 문자열 m과 방송된 곡의 정보를 담고 있는 배열 musicinfos가 주어진다.

  • m은 음 1개 이상 1439개 이하로 구성되어 있다.
  • musicinfos는 100개 이하의 곡 정보를 담고 있는 배열로, 각각의 곡 정보는 음악이 시작한 시각, 끝난 시각, 음악 제목, 악보 정보가 ','로 구분된 문자열이다.
  • 음악의 시작 시각과 끝난 시각은 24시간 HH:MM 형식이다.
  • 음악 제목은 ',' 이외의 출력 가능한 문자로 표현된 길이 1 이상 64 이하의 문자열이다.
  • 악보 정보는 음 1개 이상 1439개 이하로 구성되어 있다.




기존 코드

def rep(s):
    # '#'음정이 있는 음을 한 문자로 줄이기
    d = {'C#':'c','D#':'d','F#':'f','G#':'g','A#':'a'}
    # 딕셔너리를 돌며 모든 #문자를 축소
    for k,v in d.items():
        s = s.replace(k,v)
    return s

def solution(m, musicinfos):
    # 주어진 m 치환
    m = rep(m)
    # 제목 , 재생시간을 저장할 튜플
    answer = ('',0)
    for musicinfo in musicinfos:
        # 음악 시작, 끝, 제목, 음 으로 나눈다.
        s,e,title,music = musicinfo.split(',')
        # 재생시간을 구해준다
        run_time = int(e[:2])*60+int(e[3:]) - int(s[:2])*60 - int(s[3:])
        # 음에서 '#'음정 치환
        music = rep(music)
        
        # 음악 길이가 재생시간보다 길어질 때 까지 늘려준 다음 재생시간만큼 자른다.
        while len(music)<=run_time:
            music*=2
        music = music[:run_time]
        # 음악 안에 찾는 멜로디 m이 없으면 넘어가고, 있으면 재생시간이 큰 쪽을 answer에 저장
        if music.find(m)==-1:
            continue
        else:
            answer = (title,run_time) if answer[1] < run_time else answer
    # answer의 첫 원소가 초기화상태 그대로면, 없을 시 문구 리턴
    if answer[0] =='':
        return '(None)'
    
    # 있다면 title 리턴
    return answer[0]


수정코드

class TimeUtils:
    @staticmethod
    def time_to_minutes(time: str) -> int:
        hours, minutes = map(int, time.split(':'))
        return hours * 60 + minutes
    
    @staticmethod
    def calculate_playtime(start: str, end: str) -> int:
        return TimeUtils.time_to_minutes(end) - TimeUtils.time_to_minutes(start)

    
class Music:
    def __init__(self, info: list[str]):
        start, end, title, melody = info.split(',')
        
        self.playtime = TimeUtils.calculate_playtime(start, end)
        self.title = title
        self.melody = melody

    @property
    def played_melody(self) -> str:
        conv_melody: str = convert_sharp_to_lowercase(self.melody)
        repeat: int = self.playtime // len(conv_melody) + 1
        
        return (repeat * conv_melody)[:self.playtime]


class MusicFinder:
    def __init__(self):
        self.music_list = []
    
    def append(self, music: Music) -> None:
        self.music_list.append(music)
    
    def find(self, melody: str) -> Music:
        target_melody = convert_sharp_to_lowercase(melody)
        found_music = None
        max_playtime = 0
        
        for music in self.music_list:
            if (
                target_melody in music.played_melody
                and music.playtime > max_playtime
            ):
                found_music = music
                max_playtime = music.playtime
        
        return found_music

    
def convert_sharp_to_lowercase(s: str) -> str:
    SHARP = '#'
    converted_melody = []
    idx = len(s) - 1

    while idx >= 0:
        if s[idx] == SHARP:
            converted_melody.append(s[idx - 1].lower())
            idx -= 2
        else:
            converted_melody.append(s[idx])
            idx -= 1

    return ''.join(reversed(converted_melody))

    
def solution(m: str, musicinfos: list) -> str:
    music_finder: MusicFinder = MusicFinder()
    
    for music_info in musicinfos:
        music_finder.append(Music(music_info))
    
    found_music = music_finder.find(m)
    
    return found_music.title if found_music else "(None)"
    

개선점

  • PEP8 가이드라인에 따라 작성된 코드
  • 타입 힌트 추가
  • 지나친 주석 제거
  • 객체 지향적 설계를 통해 코드의 재사용성과 유지보수성 증가
    • TimeUtils, Music, MusicFinder 객체를 통한 역할 분리
  • 길이 2인 음정 변환 로직 개선.
    • 기존 방식: 길이 2인 음정들을 반복 탐색하며 변환 (시간복잡도 O(N: 음정길이 * M: 길이 2인 음정갯수))
    • 개선 방식: 뒤에서부터 순차 탐색하며 #이 나오면 문자 압축 (시간복잡도 O(N))
  • 명확한 변수 및 메서드 명명과 중복 코드 제거를 통해 가독성과 명확성 향상.
profile
공부!
post-custom-banner

0개의 댓글