[Java] Zeller's Congruence를 이용해 달력 출력하기

Gerry·2022년 4월 28일
1
post-thumbnail

Zeller's Congruence

첼러의 공식 또는 첼러의 합동식이라고 불리는 아래의 공식은 그레고리력 또는 율리우스력에서 특정 날짜가 무슨 요일인지 계산하는 공식입니다.

그레고리력

율리우스력


  • h: 요일 ( 0 = 토, 1 = 일, 2 = 월, ..., 6 = 금 )
  • q: 일
  • m: 월
  • K: 연도 뒷2자리 ( 연도를 100으로 나눈 나머지 )
  • J: 세기 ( [연도 / 100] )
  • mod는 나머지 연산을 의미하며, [ ]는 가우스 기호로 "X 보다 크지 않은 최대 정수"를 의미합니다.

⚠️ 주의사항
주의해야될 점은 1월과 2월을 계산할때는 값을 집어넣기전에 부가적인 작업이 필요합니다.

  • 1월과 2월은 m 에 12를 더한 후 계산한다. ( 1월 = 13 , 2월 = 14 )
  • 1월과 2월은 연도에서 1을 빼고 KJ 를 설정한다.
  • 예시) 2000년 1월 1일 의 경우 q = 1 , m = 13 , K = 99 , J = 19

바로 위의 예시를 계산하면 (1+[(13×(13+1)÷5)]+99+[99÷4]+[19÷4]−2×19) mod7 = 0 즉, 2000년 1월 1일 은 토요일임을 확인할 수 있습니다.



달력 출력하기

👀 개요

아래의 모든 코드에서 Day 는 요일을 의미하며 Date 는 날짜를 의미합니다.

자바를 이용해 달력을 출력하는 프로그램을 설계할때 요일 정보를 가져오는 방법은 크게 세가지입니다.

  • 자바에 내장되어있는 클래스 java.util.Calendar 사용하기
  • 2022년 1월 1일은 토요일 이렇게 기본값을 설정해두고 계산을 통해 요일 알아내기
  • Zeller's Congruence 와 같이 공식을 사용해 요일 알아내기

첫번째, 두번째 방법을 이용하면 좀 더 손쉽게 요일을 알아낼 수 있지만 이 두 방식은 정답지를 베끼는 느낌이라("자, 숫자 2를 기억해놓고 시험을 볼때는 3을 더해서 정답 5를 써내면 됩니다." 요런 느낌이랄까...) 첼러의 공식을 이용해 코드를 설계하였습니다.


📚 코드 구조

  • Calendar.java
    • main()
    • detectMinusOne()
  • CalendarPrinter.java
    • getDay()
    • isLeapYear()
    • getEndDate()
    • printCalendar()

💡 코드 실행순서
1. main() 에서 연도와 월을 입력
2. getDay() 에서 첼러의 공식으로 해당 월의 1일이 무슨 요일인지 구함
3. getEndDate() 로 해당 월의 마지막 날짜를 구함
4. printCalendar() 에서 위의 정보를 조합해 달력을 출력


💻 Calendar 클래스 작성

main()

public static void main(String[] args) {
    int month, year = 0;
    Scanner sc = new Scanner(System.in);
    CalendarPrinter calendarPrinter = new CalendarPrinter();

    while (true) {
        System.out.println("종료하려면 -1을 입력하시오.");
        System.out.print("연도를 입력하시오 : ");
        year = sc.nextInt();
        if (detectMinusOne(year)) break;

        System.out.print("월을 입력하시오 : ");
        month = sc.nextInt();
        if (detectMinusOne(month)) break;

        calendarPrinter.printCalendar(year, month);
    }
    sc.close();
}
  • 메인함수에서 연도와 월을 입력받습니다.
  • detectMinusOne() 메소드를 호출해 -1 을 입력받았을 경우에는 프로그램을 종료할 수 있도록 합니다.

detectMinusOne()

private static boolean detectMinusOne(int year) {
    if (year == -1) {
        System.out.println("프로그램을 종료합니다.");
        return true;
    }
    return false;
}

💻 CalendarPrinter 클래스 작성

getDay()

static int getDay(int year, int month) {
    int h = 0, q = 1;               // h는 요일(0~6 == 토~일), q는 날짜
    double m, K, J;                 // m은 월, K는 연도 마지막 두자리, J는 세기

    if (month <= 2) {               // 1, 2월은 월에 12를 더하고 년도에 1을 빼서 계산
        m = month + 12;
        K = (int)(year - 1) % 100;
        J = (int)(year - 1) / 100;
    } else {
        m = month;
        K = (int)year % 100;        // 연도 마지막 두자리
        J = (int)year / 100;        // 세기
    }

    // Zeller's congruence
    h = (q +
            (int)floor((13 * (m + 1)) / 5) +
            (int)K +
            (int)floor(K / 4) +
            (int)floor(J / 4) -
            (int)(2 * J)
        ) % 7;

    if (h < 0) {                    // 나머지 값 결과가 음수일 경우 값을 보정
        h += 7;
    }

    //System.out.println("h = " + h);
    return h;
}
  • 첼러의 공식을 이용해 입력받은 월의 1일이 무슨 요일인지 알아냅니다.
  • 가우스 기호는 Math.floor() 를 통해 계산하였으며 반환형이 double 형이기 때문에 정수형 변수 h 에 저장하기 위해 (int) 형변환 과정을 거쳤습니다.
  • 자바에서 음수를 사용하여 나머지연산( % )을 할 경우 mod 연산과 값이 다르기때문에 값의 보정이 필요합니다. (자세한 내용은 Jhon Grib님의 블로그 참고)

isLeapYear()

static boolean isLeapYear(int year) {
    if (year % 4 == 0) {
        if (year % 400 != 0 && year % 100 == 0) {
            return false;
        } else {
            return true;
        }
    } else {
        return false;
    }
}
  • 입력된 연도가 윤년인지 알아내는 메소드입니다.

⚠️ 윤년
1. 서력 기원 연수가 4로 나누어 떨어지는 해는 윤년으로 한다.
2. 서력 기원 연수가 4, 100으로 나누어 떨어지는 해는 평년으로 한다.
3. 서력 기원 연수가 4, 100, 400으로 나누어 떨어지는 해는 윤년으로 둔다.
-출처: 위키백과


getEndDate()

static int getEndDate(int year, int month) {
    if (isLeapYear(year) && month == 2) {
        return  29;
    } else {
        switch (month) {
            case 1: case 3: case 5: case 7: case 8: case 10: case 12:
                return 31;
            case 4: case 6: case 9: case 11:
                return  30;
            case 2:
                return  28;
            default:
                System.out.println("월을 제대로 입력하시오!");
                System.exit(0);
                return 0;
        }
    }
}
  • 입력된 월의 마지막 날짜를 알아냅니다.

printCalendar()

void printCalendar(int year, int month) {
    int day = getDay(year, month);
    int date = 1, skipper = 0, endDay = getEndDate(year, month);

    if (day == 0) {
        skipper = 6;
    } else {
        skipper = day - 1;
    }

    System.out.printf("일\t월\t화\t수\t목\t금\t토\n" +
            "--------------------------\n");

    for (int i=0; i<6; i++) {
        for (int j=0; j<7; j++) {
            if (skipper > 0) {
                System.out.printf("\t");
                skipper -= 1;
            } else if (date > endDay) {
                System.out.printf("\t");
            } else {
                System.out.printf("%d\t", date);
                date++;
            }
        }
        System.out.println();
    }
}
  • getDay()getEndDate() 를 호출해 요일을 day 에 저장하고 해당 월의 마지막 날짜는 endDay 에 저장합니다.
  • 달력은 아래와 같이 ( 일 ~ 목 ) 빈 공간이 존재하기 때문에 반복문으로 날짜들을 출력하기 이전에 skipper 이라는 변수를 통해서 빈 공간을 출력하도록 설계하였습니다. ( 토요일은 6번 스킵, 월요일은 1번 스킵 )
  • 6X7 이중 for문을 통해서 날짜들을 출력하며 \t 를 이용해 칸을 조정하였습니다.

👉 달력 출력하기

프로젝트를 실행시켜보면 아래와 같이 2022년도의 달력이 출력됩니다.


윤년에 해당되는 2016년의 2월도 정상적으로 29일까지 출력되는 모습을 확인할 수 있습니다.


사용자로부터 연도와 월을 입력받는 코드는 while문을 통해 계속 입력받도록 설계하였기 때문에 아래와 같이 -1 을 입력하면 프로그램을 종료할 수 있습니다.

❗️해당 프로젝트의 전체 코드는 gerry-mandering 깃헙 에서 확인하실 수 있습니다.

0개의 댓글