티스토리 뷰



프로그래밍은 환상적인 "코딩" 보다는 완벽한 "문제 해결"에 마음을 두어야 할 것입니다. 요구사항(문제)을 잘 분석해서 핵심을 파악하는 것이 시간과 노력을 아끼는 첩경입니다. 초보 개발자 일수록 빨리 끝내려는 조급함이 오히려 함정에 빠지는 기폭제가 될 수 있으므로 "어떻게 문제를 풀 것인가?" 하는 설계가 잘 나오도록 마음을 두어야 합니다. 이번 문제는 간단하지만 방심하면 곳곳에 빈틈이 생길 수 있습니다. 완성도를 높일 수 있도록 노력해야 할 것입니다.


■ 문제

1. 프로그램 형태 및 규칙

- 콘솔 프로그램 형태이며 프로그램 아규먼트로 년도 및 월을 입력받아 해당 월의 달력을 표준 출력(화면)으로 출력합니다.

- 1년 1월 1일은 월요일. 4년마다 윤년으로 2월을 29로 처리

- 4년마다 윤년이지만 100년마다는 윤년이 아니고 400년마다는 윤년으로 처리


2. 입력

- calendar 2006  또는 calendar 2006/1의 형태로 년도만 또는 년월을 프로그램 아규먼트로 입력 받음

- 프로그램 이름 다음에 /k 옵션을 추가할 수 있음(위치는 가변적, 년월 다음에 올 수도 있음).

- 프로그램 아규먼트 오류 입력시 사용법 출력


3. 출력

- 년도만 입력시 해당 년도의 전체 월을 출력하고 년월 입력시 해당월만 출력

- 각 월마다 년/월을 제목으로 출력하고 영문 약자로 요일 제목 표시

- /k 옵션 부여시 요일 제목을 한글로 출력






■ 코드와 해설

프로그램은 프로그램 아규먼트를 분석 및 검사하는 부분과 각 월별 달력을 출력하는 부분으로 크게 나뉠 수 있습니다. 각 월별 달력 출력을 위해서는 해당 월의 시작일이 무슨 요일인지를 구하는 부분과 해당 년도가 윤년인지를 판단하는 부분을 구현 해야 하는데 유사한 로직이므로 아래의 코드 내용을 참고합니다.

#include <stdio.h>
#include <string.h>

int limitday[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

// year, pmonth : 출력할 년도와 월
// pday : 요일(0-일요일), flagkon : 0-영문, 1-한글
int printmonth(int year, int pmonth, int pday, int  flagkon)
{
    int i;
    char *monname[] = { "January", "February", "March", "April", "May", "June", 
                    "July", "August", "September", "October", "November", "December" };
    
    if (flagkon)
        printf("\n   %d년 %d월", year, pmonth);
    else
        printf("\n   %d %s %d", pmonth, monname[pmonth], year);
    printf("\n ------------------------------------\n", year, pmonth);
    
    if (flagkon)
        printf("   일   월   화   수   목   금   토   \n");    
    else
        printf("   SUN  MON  TUE  WED  THU  FRI  SAT  \n");
    
    for( i = 0; i < pday; i++) printf("     ");
    
    for( i = 1; i <= limitday[pmonth]; i++)
    {
        printf("%5d", i );
        
        //토요일에 줄 구분
        if( pday == 6 ) printf(" \n");
        
        pday = ++ pday % 7 ;    
    }
    printf("\n");
    
    return(pday);
}

void UsageAndExit(char *msg)
{
    if (msg != NULL) printf("\n%s", msg);
    printf("\ncalendar 사용법 : calendar [/k] 년도[/월]");
    printf("\n\t지정한 년도 또는 년도/월에 해당하는 달력을 출력합니다.");
    printf("\n\t/k\t한국어 달력을 출력합니다.");
    printf("\n\t년도만 입력하면 해당 년도의 1~12월 달력을 출력합니다.");
    printf("\n\t년도/월을 입력하면 지정한 월의 달력을 출력합니다.");
    exit(0);
}

int main(int argc, char** argv)
{    
    
    char *searchindexk = "/k", *ptr, *slash = "/";
    int day, i, frommonth = 1, tomonth = 12, year, lastyear, monthon = 0, flagkon = 0;
    
    if (argc < 2 || argc > 3 ) UsageAndExit("프로그램 아규먼트 오류!");
    
    for(i = 1; i < argc ; i++)
    {
        
        if((strcmp(strlwr(argv[i]), searchindexk))==0)
        {   // /k 옵션 
            flagkon = 1;
        }
        else
        {    
            ptr = strstr(argv[i], slash);
            if(ptr == NULL)
            {   //년도만 지정
                year = atoi(argv[i]);
            }
            else
            {   //년도/월 지정
                *ptr = '\0';
                year = atoi(argv[i]);            
                frommonth = atoi(++ptr);
                tomonth = frommonth;
            }
        }
    }
    
    if (year < 1 || frommonth < 1 || tomonth > 12) UsageAndExit("년도 및 월 입력 오류!");

    //윤년 처리
    if(year%4==0 && (year%100 != 0 || year%400 == 0)) limitday[2]=29;
    
    //올해의 시작 요일
    lastyear = year - 1 ;
    day = (lastyear + (lastyear / 4) - (lastyear / 100) + (lastyear / 400) + 1) % 7;

    //지정 월의 시작요일
    for( i = 1; i < frommonth; i++) day += limitday[i];
    day %= 7;
             
    for(i = frommonth; i <= tomonth; i++)
    {
        day = printmonth( year, i, day, flagkon ); 
    }
    printf("\n");
    
    return 0;
}
  • 전역 변수와 지역 변수 :
    C언어에서 main()함수를 포함하여 모든 함수들은 함수 내부에서만 사용할 수 있는 지역(Local) 변수와 모든 함수에서 동일하게 참조할 수 전역(Global) 변수로 나뉩니다. 전역 변수를 사용하면 함수 호출 과정의 파라미터 전달로 인한 성능 감소를 줄이고 편리하게 프로그래밍 할 수 있는 측면이 있으나 함수의 재사용성을 떨어트리고 결과적으로 프로그램의 안정성을 해치는 요소로 작용 가능성이 높습니다.  위의 예제에서 limitday는 각 월별 날 수를 가지고 있는 배열로 윤년인 경우 2월의 날 수를 29로 바꾸기는 하지만 상수(Constant)의 성격으로 활용합니다. 이처럼 전역변수는 제한적인 용도로 사용해야 합니다.

  • 입력 아규먼트의 대소문자 처리 :
    /k 옵션이 입력되었는지 검사하는 로직을 보면 strcmp()로 두개의 문자열을 비교해서 내용이 같은 경우(==0) 플래그를 켜는 작업을 하고 있습니다. 그런데 사용자가 소문자로 /k와 같이 입력하면 다행이지만 대문자로 /K와 같이 입력하는 경우에는 어떻게 처리해야 하는지를 검토해야만 합니다. 코드에서는 대문자로 입력해도 소문자 /k와 동일한 결과를 얻기 위해서 strlwr() 함수를 사용하고 있습니다. strlwr() 함수의 프로토타입은 char *strlwr( char *str ); 인데 대상 문자열 포인터인 str 앞에 const가 없으므로 strlwr()함수는 포인터가 가리키고 있는 위치를 검사해서 소문자로 직접 변경하는 작업을 수행합니다. 원본 문자열을 그대로 유지하고 싶다면 다른 곳에 복사해서 함수를 호출해야 합니다. 리턴은 원본 문자열 포인터입니다. 대문자로 바꿀때는 strupr()을 사용합니다.

  • 문자열 자르기 :
    년도만 입력한 것이 아니라 "년도/월"의 형식으로 입력했다면 '/'문자를 기준으로 년도와 월을 잘라내야 합니다. 위의 예제에서는 strstr() 함수를 사용해서 "/" 문자의 위치를 찾습니다. "strstr(대상문자열, 찾을 문자열)"과 같이 호출하면 찾을 문자열이 대상 문자열에 없으면 NULL을 리턴하고 존재한다면 해당 위치의 포인터를 리턴합니다. 예제에서는 찾은 위치에 NULL 문자('\0')를 삽입해서 년도 문자열을 만들고 바로 다음 위치를 월단위 문자열로 활용하고 있습니다.

  • 문자열의 정수 변환 :
    atoi()는 "ASCII to Integer"의 의미로 지정한 문자열을 정수(int)로 변환해서 리턴합니다. 영문 단어등 변환 불가한 문자열이라면 0을 리턴합니다. 단, 숫자 앞뒤에 공백문자(White space)가 있다면 무시되고 숫자만 변환합니다. atoi()와 유사한 함수로 long형 값을 리턴하는 atol(), 부동소숫점 double 값을 리턴하는 atof()도 있습니다. 추가적으로 long 값을 리턴하는 strtol(), unsigned long 값을 리턴하는 strtoul(), 
    부동소숫점 double 값을 리턴하는 strtod()함수가 있는데 str...함수들은 인수로 "strtol(대상문자열, 변환이 멈춘 위치를 받을 포인터 변수의 주소, 기수)"처럼 사용해서 long, unsigned long, double 타입의 값을 리턴받을 수 있을 뿐만아니라 변환이 멈춘 곳의 위치도 받을 수 있고 문자열의 기수를 지정해서 10진수 문자열을 비롯하여 2~36진수의 문자열을 숫자로 변환할 수 있습니다.

  • 시작 요일 구하기 :
    윤년 검사 로직인 "year%4==0 && (year%100 != 0 || year%400 == 0)"를 활용합니다. 원리는 1년 365일을 7로 나누면 나머지가 1이라는 점에 착안하여 직년 년도에 윤년의 횟수를 감안하여 올해초의 시작 요일을 구하고 시작월이 1월이 아닌 경우 직전 월까지를 감안한 요일을 계산하면 됩니다.

  • 다국어 기반 :
    최근의 프로그램들은 시스템의 언어를 인식해서 메뉴나 메시지들을 각 언어에 맞도록 자동으로 전환하는 기능을 가지고 있습니다. 본 예제의 경우에는 사용자가 옵션을 지정한 경우에 옵션을 인식하는 과정과 옵션에 따라 가장 기본적인 방법으로 한국어를 적용해 보았으나 국제화 또는 다국어 환경을 적용하기 위한 체계적인 기술을 다양한 방법을 사용하고 있습니다. 이러한 과정을 internationalization 또는 localization이라고 하는데 줄여서 i18n 또는 l10n으로 부르기도 합니다. 관련 작업을 편리하게 하기 위한 다양한 모듈도 있으므로 프로젝트 초기에 이러한 점을 검토한다면 완성도 높은 프로그램을 개발하는 초석이 될 수 있습니다.



댓글
댓글쓰기 폼