지구 날짜 => 화성 날짜

윤뿔소·2024년 7월 3일
0

Agorithm 문제풀이

목록 보기
6/7
post-thumbnail

화성 달력

지구의 날짜를 기준으로 일자를 구해 화성 날짜로 변환 및 해당 월 달력을 생성하는 구현 문제입니다.
또한 ANSICode로 로딩바 구현도 해야 합니다.

상세 요구사항 파악

이것만 보더라도 구현이 될 수 있게끔 전체 글을 읽고 정리해 보겠습니다. 요구사항을 파악하며 입출력 값도 자연스레 정의해 보겠습니다.

화성 달력 기준

  1. 1 화성년 = 668화성일, 2년마다 윤년으로 지정 및 669화성일.
  2. 1 화성일 : 1 솔(sol) = 1 지구일 (지구 하루와 화성 하루가 같다고 가정).
  3. 1주 화성일 : 7 화성일.
  4. 1 화성월 : 28 화성일( = 4주 화성일).
  5. 1 화성년 : 24 화성월로 구성.
    • 단, 24 × 28 = 672 화성일이라서 668 화성일보다 많으므로, 6 화성월마다 하루씩, 총 4일을 빼서 672 - 4 = 668로 맞춥니다.
    • 윤년의 경우 마지막 한 달에서 하루를 빼지 않고 669일로 맞춥니다.
  6. 화성 요일은 지구 요일과 동일한 기준으로 7개 요일에 이름을 붙임.
    • 솔 솔리스 (Sol Solis: 태양의 날), 솔 루나에 (Sol Lunae: 달의 날), 솔 마르티우스 (Sol Martius: 화성의 날), 솔 메르쿠리 (Sol Mercurii: 수성의 날), 솔 요비스 (Sol Jovis: 목성의 날), 솔 베네리스 (Sol Veneris: 금성의 날), 솔 사투르니 (Sol Saturni: 토성의 날)

입력 및 출력 조건

  • 지구에서 사용하는 그레고리안 달력을 기준으로 특정 날짜를 입력받습니다.
  • 입력된 지구 날짜를 화성 날짜로 변환해 출력합니다.
  • 변환된 화성 날짜를 포함하는 화성 월 달력을 출력합니다.
  • 로딩바는 5초 동안 진행 상황을 업데이트하면서 출력합니다. ANSICode를 사용해 구현합니다.

로딩바 구현

  • ANSICode를 사용해 콘솔 화면에 진행 현황을 업데이트하는 Progress Bar를 직접 구현합니다.
  • 5초 동안 적어도 10%마다 업데이트해 100%까지 진행 상황을 보여줍니다.

의사코드 작성

대략적으로 아래와 같이 생각했습니다.

  • 입력된 지구 날짜를 기준으로 총 지구 일수를 계산합니다.
  • 변환된 지구 일수를 기준으로 화성 날짜를 계산합니다. (하루를 봤을 때 지구 시간과 화성 시간이 동일하기 때문에)
  • 해당 화성 날짜가 포함된 화성 월 달력을 생성해 출력합니다.
  • 변환 중 ANSICode를 출력해 로딩바가 나오게 합니다.
함수 지구 날짜 계산 (입력값)
  날짜 변수 초기화
  작년까지의 일자 계산(윤년까지) 및 날짜 변수 더하기
  저번달까지의 일자 계산(2월-윤년 주의) 및 날짜 변수 더하기
  오늘까지의 날짜 더하기
  총 일자 return

함수 화성 날짜 계산 (지구 총 일자)
  화성 연도, 월, 일 변수 초기화
  올해 화성 년도 구하기
    규칙을 찾아 몫을 구하기 => 668 + 669를 더한 값으로 몫을 구하고 *2를 하는 등
    조건에 따라 +1을 해 올해 만들기
  올해 화성 월 / 일 구하기
    규칙을 찾아 몫을 구하기 => 28, 28, 28, 27이 반복 등
    +1을 해 이번달 만들기
    나머지로 오늘 날짜 구하기
  {year, month, day} return

함수 화성 달력 출력
  화성 달력 변수 초기화
  오늘 화성 일자를 보고 윤년인지, 하루가 부족한 월인지를 확인해 최대 일자 정의
  연도 월 배치해 화성 달력 변수에 push
  요일 배치 'Su Lu Ma Me Jo Ve Sa' 달력 변수에 push
  최대 일자까지 7일 씩 push
  화성 달력 변수 return

함수 로딩 바 구현 및 출력
  로딩, 지구일자, 화성일자, 달력 변수 초기화
  함수 지구 날짜, 화성 날짜, 달력 계산
    함수 지구 날짜 계산
    함수 화성 날짜 계산
    함수 화성 달력 출력
  함수 로딩바 출력할 5초 동안 반복
    로딩 += 10
    로딩 10% 씩 업데이트
    5초가 되면 = 로딩바 100%가 되면
      지구날 ~ 화성년 문자열 출력
      화성 달력 출력
  비동기적으로 실행 => 로딩바가 시작되면 계산 실행
  함수 지구 날짜, 화성 날짜, 달력 계산 실행
  setInterval 사용해 함수 로딩바 출력할 5초 동안 반복 사용

이 의사코드를 바탕으로 실제 구현을 시작해보겠습니다.

구현

흐름은 의사코드에 작성했으니 디테일 적인 부분만 작성했습니다.

지구 날짜 계산 함수

const getEarthDay = (date) => {
  let dayCount = 0;
  const [year, month, day] = date.split('-').map(Number);

  // 작년까지의 일자 수
  dayCount += 365 * (year - 1) + Math.floor((year - 1) / 4);

  // 이번 달까지의 일자 수
  // prettier-ignore
  const monthDay = [31, year % 4 ? 28 : 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  for (let i = 0; i < month - 1; i++) {
    dayCount += monthDay[i];
  }

  // 오늘까지 일자 수
  dayCount += day;

  return dayCount;
};
  • 날짜 분리 및 숫자 변환: const [year, month, day] = date.split('-').map(Number);
    • 입력받은 문자열 형식의 날짜를 split('-') 함수로 연도, 월, 일로 분리하고, map(Number)로 각 값을 숫자로 변환합니다.
    • 추후 계산을 용이하게 하기 위해 구조 분해 할당을 사용했습니다.
  • 윤년 계산: dayCount += 365 * (year - 1) + Math.floor((year - 1) / 4);
    • 전체 연도에 365를 곱하고, 4년마다 빠진 윤년을 채우기 위해 4를 나눈 몫을 더했습니다.
    • 2024년이라면 2023년까지의 일수를 채우기 위해 -1했습니다.
  • 월별 일수 배열 정의: const monthDay = [31, year % 4 ? 28 : 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    • 윤년일 경우 2월은 29일, 평년일 경우 28일로 설정합니다.
    • 규칙을 만들기 어렵기 때문에 배열로 하드코딩 했습니다.
  • 각 월의 일수를 더함: for (let i = 0; i < month - 1; i++) { dayCount += monthDay[i]; }
    • 입력된 월 이전까지의 모든 월의 일수를 더합니다. 만든

화성 날짜 계산 함수

const getMarsDate = (earthDay) => {
  let year = null;
  let month = null;
  let day = null;

  const marsRegularYear = 668;
  const marsLeapYear = 669;
  const marsYearLength = marsRegularYear + marsLeapYear;

  // 올해 화성 연도 구하기
  const fullMarsCycles = Math.floor(earthDay / marsYearLength);
  let remainingDays = earthDay % marsYearLength;
  if (remainingDays >= marsRegularYear) {
    year = fullMarsCycles * 2 + 1;
    remainingDays -= marsRegularYear;
  } else {
    year = fullMarsCycles * 2;
  }

  // 올해 화성 월 / 일 구하기
  for (let i = 1; i <= 24; i++) {
    let curMonthDays = i % 6 === 0 ? 27 : 28;
    if (i === 24 && year % 2 === 0) curMonthDays = 28;

    if (remainingDays > curMonthDays) {
      remainingDays -= curMonthDays;
    } else {
      month = i;
      day = remainingDays;
      break;
    }
  }

  return { year, month, day };
};
  • 화성 연도의 길이 설정: const marsRegularYear = 668; const marsLeapYear = 669; const marsYearLength = marsRegularYear + marsLeapYear;
    • 화성의 평년과 윤년의 길이를 정의하고, 두 해의 길이를 더한 값을 설정합니다.
    • 2년 마다 로테이션이 일어나기 때문에 둘을 더한 값을 나누고 *2를 할 생각으로 작성했습니다.
  • 화성 연도 계산: const fullMarsCycles = Math.floor(earthDay / marsYearLength);
    • 전체 지구 일수를 화성의 2년 주기로 나눠 화성의 전체 연수를 구합니다.
  • 남은 일수 계산 및 화성 연도 설정
    • let remainingDays = earthDay % marsYearLength; : 연도를 뽑아내고 남은 일자를 추출합니다.
    • if (remainingDays >= marsRegularYear) ~~ : 2년 분을 추출했기 때문에 만약 668일 보다 많다면 1년이 더 추가됩니다.
  • 화성 월 및 일 계산: for (let i = 1; i <= 24; i++) { let curMonthDays = i % 6 === 0 ? 27 : 28; if (i === 24 && year % 2 === 0) curMonthDays = 28; if (remainingDays > curMonthDays) { remainingDays -= curMonthDays; } else { month = i; day = remainingDays; break; } }
    • for (let i = 1; i <= 24; i++) : 24월까지 반복해 월, 일을 계산하려고 했습니다.
    • curMonthDays : 현재 화성 월에서 27일인지, 28일인지 정해서 할당하는 변수입니다. 윤달일 시 쉽게 변경되게 설정했습니다.
    • 화성의 각 월의 일수를 계산하여 남은 일수와 비교하고, 월과 일을 설정합니다.

화성 달력 출력 함수

const makeMarsCalendar = (year, month, day) => {
  const calendar = [];
  let maxDay = month % 6 === 0 ? 27 : 28;
  if (month === 24 && year % 2 === 0) maxDay = 28;

  calendar.push(`     ${year}${month}`);
  calendar.push('Su Lu Ma Me Jo Ve Sa');

  // 일자 찍어내기
  let currentDay = 1;
  for (let i = 0; i < 4; i++) {
    const dayArr = [];
    for (let j = 0; j < 7; j++) {
      if (currentDay <= maxDay) {
        if (currentDay === day) {
          dayArr.push(`\x1b[31m${String(currentDay).padStart(2)}\x1b[39m`); // 오늘 날짜를 빨간색으로 하이라이트
        } else {
          dayArr.push(String(currentDay).padStart(2));
        }
        currentDay++;
      } else {
        dayArr.push('  ');
      }
    }
    calendar.push(dayArr.join(' '));
  }

  return calendar;
};
  • 최대 일자 계산: let maxDay = month % 6 === 0 ? 27 : 28; if (month === 24 && year % 2 === 0) maxDay = 28;
    • 월별 최대 일수를 계산합니다. 6번째 월마다 27일, 윤년의 마지막 달은 28일로 설정합니다.
  • 날짜 삽입 및 하이라이팅
    • 4주를 돌며 7일을 돌게 이중 반복문을 썼습니다. 만일 28일이 없다면 공백을 넣었습니다.
    • if (currentDay === day) : ANSICode를 사용해 현재 일자가 오늘 날짜와 같으면 하이라이트합니다.
    • dayArr.push(String(currentDay).padStart(2)); : 그냥 넣었을 때, 1일과 10일 같이 자릿수가 차이나 보기 불편했습니다.padStart를 사용해 달력 자릿수를 정렬했습니다.
    • 일자 삽입 반복이 끝난 후 dayArrjoin으로 묶어 push 했습니다.

로딩 바 구현 및 출력

const displayMarsDateInfo = (input) => {
  let loading = 0;
  let earthDay = null;
  let marsDate = null;
  let marsCalendar = null;

  const calculateDate = () => {
    earthDay = getEarthDay(input);
    marsDate = getMarsDate(earthDay);
    marsCalendar = makeMarsCalendar(
      marsDate.year,
      marsDate.month,
      marsDate.day,
    );
  };

  const processLoading = () => {
    // 로딩 진행 중
    loading += 10;
    console.log(`${'▓'.repeat(loading / 10)}${'░'.repeat(10 - loading / 10)} 화성까지 여행 ${loading}%`); // prettier-ignore

    // 로딩이 끝나고 결과값 출력
    if (loading === 100) {
      clearInterval(loadingInterval);
      console.log(
        `지구날은 ${earthDay.toLocaleString('ko-KR')} => ${
          marsDate.year
        } 화성년 ${marsDate.month}${marsDate.day}일\n`,
      );
      marsCalendar.forEach((line) => {
        console.log(line);
      });
    }
  };

  // 로딩 진행 중 계산 실행 및 산출
  calculateDate();
  // 0.5초씩 반복하기, 재귀
  const loadingInterval = setInterval(processLoading, 500);
};
  • 로딩 진행 상태 출력: console.log(${'▓'.repeat(loading / 10)}${'░'.repeat(10 - loading / 10)} 화성까지 여행 ${loading}%);
    • 로딩 진행 상태를 10% 단위로 출력합니다. 로딩 + 그외 나머지는 빈칸 이런 식으로 만들어서 출력했습니다.
    • 로딩이 실행되면 날짜 계산 및 달력 출력 함수 calculateDate가 실행됩니다. 로딩이 시작되고 비동기적으로 출력이 나오는게 맞다고 생각해 분리했습니다.
  • 로딩 완료 후 결과 출력:
    • 로딩이 완료되면 => loading이 100이 되면 계산된 결과와 화성 달력을 출력합니다.
const readline = require('readline');

// 지구 날짜 계산 함수
const getEarthDay = (date) => {
  let dayCount = 0;
  const [year, month, day] = date.split('-').map(Number);

  // 작년까지의 일자 수
  dayCount += 365 * (year - 1) + Math.floor((year - 1) / 4);

  // 이번 달까지의 일자 수
  // prettier-ignore
  const monthDay = [31, year % 4 ? 28 : 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  for (let i = 0; i < month - 1; i++) {
    dayCount += monthDay[i];
  }

  // 오늘까지 일자 수
  dayCount += day;

  return dayCount;
};

// 화성 날짜 계산 함수
const getMarsDate = (earthDay) => {
  let year = null;
  let month = null;
  let day = null;

  const marsRegularYear = 668;
  const marsLeapYear = 669;
  const marsYearLength = marsRegularYear + marsLeapYear;

  // 올해 화성 연도 구하기
  const fullMarsCycles = Math.floor(earthDay / marsYearLength);
  let remainingDays = earthDay % marsYearLength;
  if (remainingDays >= marsRegularYear) {
    year = fullMarsCycles * 2 + 1;
    remainingDays -= marsRegularYear;
  } else {
    year = fullMarsCycles * 2;
  }
  // 만약 아예 0이라면 그레고리든 화성이든 0년은 없기 때문에 1년으로 지정
  if (fullMarsCycles === 0) {
    year = 1;
  }

  // 올해 화성 월 / 일 구하기
  for (let i = 1; i <= 24; i++) {
    let curMonthDays = i % 4 === 0 ? 27 : 28;
    if (i === 24 && year % 2 === 0) curMonthDays = 28;

    if (remainingDays > curMonthDays) {
      remainingDays -= curMonthDays;
    } else {
      month = i;
      day = remainingDays;
      break;
    }
  }

  return { year, month, day };
};

// 화성 달력 출력 함수
const makeMarsCalendar = (year, month, day) => {
  const calendar = [];
  let maxDay = month % 6 === 0 ? 27 : 28;
  if (month === 24 && year % 2 === 0) maxDay = 28;

  calendar.push(`     ${year}${month}`);
  calendar.push('Su Lu Ma Me Jo Ve Sa');

  // 일자 찍어내기
  let currentDay = 1;
  for (let i = 0; i < 4; i++) {
    const dayArr = [];
    for (let j = 0; j < 7; j++) {
      if (currentDay <= maxDay) {
        if (currentDay === day) {
          dayArr.push(`\x1b[31m${String(currentDay).padStart(2)}\x1b[39m`); // 오늘 날짜를 빨간색으로 하이라이트
        } else {
          dayArr.push(String(currentDay).padStart(2));
        }
        currentDay++;
      } else {
        dayArr.push('  ');
      }
    }
    calendar.push(dayArr.join(' '));
  }

  return calendar;
};

// 입력 및 출력 함수
const displayMarsDateInfo = (input) => {
  let loading = 0;
  let earthDay = null;
  let marsDate = null;
  let marsCalendar = null;

  const calculateDate = () => {
    earthDay = getEarthDay(input);
    marsDate = getMarsDate(earthDay);
    marsCalendar = makeMarsCalendar(
      marsDate.year,
      marsDate.month,
      marsDate.day,
    );
  };

  const processLoading = () => {
    // 로딩 진행 중
    loading += 10;

    console.log(
      `${'▓'.repeat(loading / 10)}${'░'.repeat(
        10 - loading / 10,
      )} 화성까지 여행 ${loading}%`,
    );

    // 로딩이 끝나고 결과값 출력
    if (loading === 100) {
      clearInterval(loadingInterval);
      console.log(
        `지구날은 ${earthDay.toLocaleString('ko-KR')} => ${
          marsDate.year
        } 화성년 ${marsDate.month}${marsDate.day}일\n`,
      );
      marsCalendar.forEach((line) => {
        console.log(line);
      });
    }
  };

  // 로딩 진행 중 계산 실행 및 산출
  calculateDate();
  // 0.5초씩 반복하기, 재귀
  const loadingInterval = setInterval(processLoading, 500);
};

// input output 함수
(async () => {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  rl.question('지구 날짜를 입력하세요.(YYYY-MM-DD) : ', (input) => {
    displayMarsDateInfo(input);
    rl.close();
  });
})();

// 예상 출력: 1106 화성년 8월 14일
// console.log(displayMarsDateInfo('2024-01-01'));

결과

결과를 살펴보겠습니다.

화면 기록 2024-07-03 20 48 57

프로세스는 잘 나옵니다. 거슬리는 건 3개입니다.

  1. 입력한 예상 값이 틀림.
  2. 로딩창이 줄바꿈이 자꾸 일어남.
  3. 입력할 때 에러 처리를 안함.

이 순서대로 디버깅을 실시해보겠습니다.

디버깅

1. 입력한 예상 값이 틀림

+ 이게.. 어떻게 해야 11일이 나와야하는지 잘 모르겠습니다. 이걸로 한 3시간 동안 고민하고 과정도 살펴봤는데 도무지 모르겠습니다. 더 개선할 예정입니다.
++ 제출 후 다른 동료분들도 비슷한 결과값이 나오는 걸 보고 제 문제만은 아닌가 생각해서 일단은 보류해놓은 상태입니다.

image

위 사진을 보면 8월 14일이 나와야하는데 11일이 나오고 있습니다. 지구 일수와 달력 형태는 맞는 걸 보니 화성 계산 날짜를 살펴보겠습니다.

log 찍기

const getMarsDate = (earthDay) => {
  let year = null;
  let month = null;
  let day = null;

  const marsRegularYear = 668;
  const marsLeapYear = 669;
  const marsYearLength = marsRegularYear + marsLeapYear;

  console.log(`지구 일수: ${earthDay}`);

  // 화성 연도 계산
  const fullMarsCycles = Math.floor(earthDay / marsYearLength);
  let remainingDays = earthDay % marsYearLength;
  console.log(`전체 화성 주기: ${fullMarsCycles}, 남은 일수: ${remainingDays}`);

  if (remainingDays >= marsRegularYear) {
    year = fullMarsCycles * 2 + 1;
    remainingDays -= marsRegularYear;
    console.log(`화성 연도 (윤년): ${year}, 남은 일수: ${remainingDays}`);
  } else {
    year = fullMarsCycles * 2;
    console.log(`화성 연도 (평년): ${year}, 남은 일수: ${remainingDays}`);
  }
  if (fullMarsCycles === 0) {
    year = 1;
    console.log(`화성 연도 (초기 0년 조정): ${year}`);
  }

  // 화성 월 및 일 계산
  for (let i = 1; i <= 24; i++) {
    let curMonthDays = i % 6 === 0 ? 27 : 28;
    if (i === 24 && year % 2 === 0) curMonthDays = 28;

    console.log(
      `현재 화성 월: ${i}, 현재 월의 일수: ${curMonthDays}, 남은 일수: ${remainingDays}`,
    );

    if (remainingDays > curMonthDays) {
      remainingDays -= curMonthDays;
    } else {
      month = i;
      day = remainingDays + 1; // 1일을 더해서 날짜를 보정
      console.log(
        `화성 날짜 계산 완료 - 연도: ${year}, 월: ${month}, 일: ${day}`,
      );
      break;
    }
  }

  return { year, month, day };
};

2. 로딩창이 줄바꿈이 자꾸 일어남.

image 이 사진을 보면 로딩창이 여러줄을 보여주고 있습니다. 이 로딩창을 1줄로만 끝내게 만들 예정입니다.

구글링을 해보니 stdout 메소드를 사용해 줄을 삭제하고, 다시 그리는 방법을 발견했습니다. 적용한 모습은 아래와 같습니다.

const processLoading = () => {
  // 로딩 진행 중
  loading += 10;
  process.stdout.clearLine(); // 이전 줄 삭제
  process.stdout.cursorTo(0); // 커서를 줄의 처음으로 이동
  process.stdout.write(`${'▓'.repeat(loading / 10)}${'░'.repeat(10 - loading / 10)} 화성까지 여행 ${loading}%`); // prettier-ignore
  // console.log(`${'▓'.repeat(loading / 10)}${'░'.repeat(10 - loading / 10)} 화성까지 여행 ${loading}%`);

  // 로딩이 끝나고 결과값 출력
  if (loading === 100) {
    clearInterval(loadingInterval);
    console.log(
      `지구날은 ${earthDay.toLocaleString('ko-KR')} => ${
        marsDate.year
      } 화성년 ${marsDate.month}${marsDate.day}일\n`,
    );
    marsCalendar.forEach((line) => {
      console.log(line);
    });
  }
};

stdout 메소드의 clearLine, cursorTo, write를 사용해봤습니다.

+실수 : cursorTo를 쓰지 않았더니 커서가 밀려 이상하게 출력됐습니다. cursorTo를 붙여 출력되게 해봤습니다.

결과

화면 기록 2024-07-03 21 07 46

마음이 편해집니다. 아주 깔끔합니다.

3. 입력할 때 에러 처리를 안함.

사용자가 입력 시 의도대로 입력하지 않은 경우가 발생할 거 같아 불편했습니다. 의도대로 입력될려면 아래와 같습니다.

  1. 숫자와 -의 조합으로 입력하기
  2. YYYY-MM-DD 형태로 입력하기
  3. 연도는 1~9999, 월은 1~12, 일자는 1~31로 입력하기

위 같은 조건을 가진 유효성 검사를 input 입력 칸에 넣어보겠습니다.

// input output 함수
(async () => {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  rl.question('지구 날짜를 입력하세요.(YYYY-MM-DD) : ', (input) => {
    const dateRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/;
    if (!dateRegex.test(input)) {
      console.log(
        '잘못된 날짜 형식입니다. YYYY-MM-DD 형식, 숫자로 입력해주세요.',
      );
      rl.close();
      return;
    }
    const [year, month, day] = input.split('-').map(Number);
    // prettier-ignore
    if (year < 1 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31) { 
      console.log(
        '잘못된 날짜입니다. 1년 1월 1일부터 9999년 12월 31일까지의 날짜를 입력해주세요.',
      );
      rl.close();
      return;
    }

    displayMarsDateInfo(year, month, day);
    rl.close();
  });
})();

결과

image

다음과 같이 나옵니다. 에러 처리를 통해 입력 값을 잘못 넣어 NaN이 안나오게 했습니다.

profile
코뿔소처럼 저돌적으로

0개의 댓글