본문 바로가기

Java의 정석 : 3rd Edition

[Java의 정석 - 연습문제] Chapter10. 날짜와 시간 & 형식화

Java의 정석 : 3rd Edition, 2016을 개인 학습용으로 정리한 내용입니다.
"ppt 파일 자료, 연습문제"를 학습 용도로 배포할 수 있음을 고지하셨습니다.
저자님께 감사드립니다.

 

 

 

 

[10-1] Calendar클래스와 SimpleDateFormat클래스를 이용해서 2010년의 매월 두 번째 일요일의 날짜를 출력하시오.

// 실행 결과
2010-01-10은 2번째 일요일입니다.
2010-02-14은 2번째 일요일입니다.
2010-03-14은 2번째 일요일입니다.
2010-04-11은 2번째 일요일입니다.
2010-05-09은 2번째 일요일입니다.
2010-06-13은 2번째 일요일입니다.
2010-07-11은 2번째 일요일입니다.
2010-08-08은 2번째 일요일입니다.
2010-09-12은 2번째 일요일입니다.
2010-10-10은 2번째 일요일입니다.
2010-11-14은 2번째 일요일입니다.
2010-12-12은 2번째 일요일입니다.

 

나의 답 :

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

class Exercise10_1 {
    public static void main(String[] args) {
        SimpleDateFormat second, sdf;
        second = new SimpleDateFormat("F");                            // 월의 몇 번째 요일인지 확인
        sdf = new SimpleDateFormat("yyyy-MM-dd은 F번째 E요일입니다.");  // 출력 형식

        int[] endOfMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};  // 1월~12월의 말일

        Calendar cal = Calendar.getInstance();

        for(int i = 0; i < 12; i++) {                     // 월(0~11) : 0(1월), 11(12월)
            for(int j = 1; j <= endOfMonth[i]; j++) {     // 일(1~endOfMonth)
                cal.set(2010, i, j);

                // 일요일인지 검사하는 조건문
                if(cal.get(Calendar.DAY_OF_WEEK) == 1) {  // DAY_OF_WEEK : 요일(1~7)
                    Date date = cal.getTime();            // format() : Calendar → Date
                    String sunday = second.format(date);

                    // 두 번째 일요일인지 검사하는 조건문
                    if(sunday.equals("2")) {
                        System.out.println(sdf.format(date));
                        break;  // 해당 날짜 출력 후 이중 반복문(일) 종료
                    }
                }
            }
        }
    }
}

 

SimpleDateFormat
* Date, Calendar로 날짜 데이터를 원하는 형태로 출력하는 건 복잡하고 불편하다.
* SimpleDateFormat를 이용하면 간단하게 날짜와 시간을 다양한 형식으로 출력할 수 있다.
* SimpleDateFormat인스턴스를 생성할 때 생성자에 원하는 출력 형식의 패턴을 작성해야 한다.
  +) DateFormat.format(Date d) : 지정한 출력 형식에 맞게 변환된 문자열을 얻는다.
  +) DateFormat Date parse(String source)
      : 기호와 문자가 포함된 문자열을 날짜/시간으로 변환한다.
      : 반환 타입이 Date여서 Calendar인스턴스는 인자로 들어올 수 없다(변환 필요)
  +) DateFormat : SimpleDateFormat의 조상 클래스이다.

1) endOfMonth[] : 1월~12월의 말일을 담은 배열. 이중 반복문(일)에 사용한다.

2) 현재가 아닌 특정 날짜를 구해야 하므로 Calendar객체를 생성한 후 Calendar.set()로 날짜를 설정한다.

3) 이중 반복문으로 월과 일의 값을 증가시켜 조건문을 검사한다.

  3-1) if(cal.get(Calendar.DAY_OF_WEEK) == 1) : 해당 월, 일(i월 j일)의 요일이 일요일(1)인지 확인한다.

    3-1-1) second.format(date); : 일요일이면, 해당 날짜를 second("F" : 월의 몇 번째 요일) 형태로 변환

              * DateFormat.format(Date d) : 날짜를 원하는 출력 형식으로 변환하며 인자로 Date타입을 받는다.

              * cal.getTime()를 이용하여 Calendar객체를 Date객체로 변환해야 한다.

              * DAY_OF_WEEK : 요일(1(일) ~ 7(토))

    3-1-2) sunday : 첫 번째 일요일이면 "1", 두 번째 일요일이면 "2", 세 번째 일요일이면 "3"가 저장된다.

              * 변환한 값을 조건식에 사용하기 위해 문자열에 저장한다.

              * DateFormat.format(Date d)의 반환 타입이 String(문자열)이여서 문자열에 저장해야 한다.

  3-2) if(sunday.equals("2")) : sunday에 저장된 값이 "2"(두 번째 일요일)인지 확인한다.

         * 문자열(String)이므로 값의 같음을 확인할 ==가 아닌 equals()를 사용해야 한다.

         * "2"(두 번째 일요일)라면 해당 날짜를 sdf(출력 형식)을 이용하여 출력한다.

         * 두 번째 일요일(1월)의 날짜를 구했으므로 이중 반복문(일)을 종료하여 다음 반복문(2, ...12월)을 수행.

 

더보기

DAY_OF_WEEK, WEEK_OF_MONTH을 사용하여 두 번째 일요일을 구했을 때

* WEEK_OF_MONTH를 활용하면, 달의 두 번째 일요일이 아닌 둘째 주 일요일을 구할 수 있다.

* 2010년 1월 3일은 둘째 주 일요일이라, 조건식을 만족하게 된다.

DAY_OF_WEEK       : 요일(1(일) ~ 7(토))

WEEK_OF_MONTH : 그 달의 몇 번째 주(2 = 2번째)

// if (cal.get(Calendar.DAY_OF_WEEK) == 1) {
if (cal.get(Calendar.DAY_OF_WEEK) == 1 && cal.get(Calendar.WEEK_OF_MONTH) == 2)) {
    Date date = cal.getTime();
    System.out.println(sdf.format(date));
    break;
}

 

해설 :

import java.util.*;
import java.text.*;

class Exercise10_1 {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        cal.set(2010, 0, 1);  // cal의 날짜를 2010년 1월 1일로 설정

        for (int i = 0; i < 12; i++) {
            int weekday = cal.get(Calendar.DAY_OF_WEEK);  // 1일의 요일

            // 두 번째 일요일은 1일의 요일에 따라 달라진다.
            // 1) 1일이 일요일이라면 두 번째 일요일은 8일이다.
            // 2) 1일이 다른 요일이라면 16에서 1일의 요일(weekday)을 빼면 알 수 있다.
            int secondSunday = (weekday == 1) ? 8 : 16 - weekday;

            // 두 번째 일요일(secondSunday)로 cal의 날짜(DAY_OF_MONTH)를 변경한다.
            cal.set(Calendar.DAY_OF_MONTH, secondSunday);
 
            Date d = cal.getTime();  // Calendar → Date
            System.out.println(new SimpleDateFormat("yyyy-MM-dd은 F번째 E요일입니다.").format(d));

            // 날짜를 다음달 1일로 변경한다.
            cal.add(Calendar.MONTH, 1);
            cal.set(Calendar.DAY_OF_MONTH, 1);
        }
    }
}

[10-1] 두 번째 일요일을 구하는 과정

1) 매월 두 번째 일요일(secondSunday)을 구하려면, 매월 1일이 무슨 요일인지 알아내야 한다.

    * 1일의 요일(weekday)과 2번째 일요일의 날짜를 더하면 일정한 값(16)을 얻을 수 있다.

    * 일정한 값(16)에서 1일의 요일(weekday)을 빼면 2번째 일요일이 몇일인지 알 수 있다.

      * (1일) 일요일이라면 2번째 일요일은 9 - 1 = 8일이 된다.

      * (1일) 월요일이라면 2번째 일요일은 14일이 된다.

int secondSunday = (weekday == 1) ? 8 : 16 - weekday;

2) Calendar.set() : cal의 날짜(DAY_OF_MONTH)를 두 번째 일요일의 날짜로 변경한다.

    * DAY_OF_MONTH : 그 달의 몇 번째 일

3) SimpleDateFormat을 이용하여 결과를 출력 형식에 맞게 출력한다.

    * 날짜를 원하는 출력 형식으로 변환하는 SimpleDateFormat.format()는 인자로 Date타입을 받는다.

    * cal.getTime()을 호출하여 Calendar를 Date로 변환해야 한다.

4) 날짜를 다음달 1일로 변경하여 2월, 3월, ... 12월의 두 번째 일요일을 구한다.

 

 

 

 

[10-2] 어떤 회사의 월급날이 매월 21일이다. 두 날짜 사이에 월급날이 몇 번있는지 계산해서 반환하는 메서드를 작성하고 테스트 하시오.

import java.util.*;
import java.text.*;

class Exercise10_2 {
    static int paycheckCount(Calendar from, Calendar to) {
        /*
            (1) 아래의 로직에 맞게 코드를 작성하시오.
            1. from 또는 to가 null이면 0을 반환한다.
            2. from와 to가 같고 날짜가 21일이면 1을 반환한다.
            3. to와 from이 몇 개월 차이인지 계산해서 변수 monDiff에 담는다.
            4. monDiff가 음수이면 0을 반환한다.
            5. 만일 from의 일(DAY_OF_MONTH)이 21일이거나 이전이고,
               to의 일(DAY_OF_MONTH)이 21일이거나 이후이면 monDiff의 값을 1 증가시킨다.
            6. 만일 from의 일(DAY_OF_MONTH)이 21일 이후고,
               to의 일(DAY_OF_MONTH)이 21일 이전이면 monDiff의 값을 1 감소시킨다.
        */

        return monDiff;
    }

    static void printResult(Calendar from, Calendar to) {
        Date fromDate = from.getTime();
        Date toDate = to.getTime();

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        System.out.print(sdf.format(fromDate) + " ~ " + sdf.format(toDate) + " : ");
        System.out.println(paycheckCount(from, to));
    }

    public static void main(String[] args) {
        Calendar fromCal = Calendar.getInstance();
        Calendar toCal = Calendar.getInstance();

        fromCal.set(2010, 0, 1);
        toCal.set(2010, 0, 1);
        printResult(fromCal, toCal);
    
        fromCal.set(2010, 0, 21);
        toCal.set(2010, 0, 21);
        printResult(fromCal, toCal);

        fromCal.set(2010, 0, 1);
        toCal.set(2010, 2, 1);
        printResult(fromCal, toCal);

        fromCal.set(2010, 0, 1);
        toCal.set(2010, 2, 23);
        printResult(fromCal, toCal);

        fromCal.set(2010, 0, 23);
        toCal.set(2010, 2, 21);
        printResult(fromCal, toCal);

        fromCal.set(2011, 0, 22);
        toCal.set(2010, 2, 21);
        printResult(fromCal, toCal);
    }
}
// 2010-01-01 ~ 2010-01-01 : 0
// 2010-01-21 ~ 2010-01-21 : 1
// 2010-01-01 ~ 2010-03-01 : 2
// 2010-01-01 ~ 2010-03-23 : 3
// 2010-01-23 ~ 2010-03-21 : 2
// 2011-01-22 ~ 2010-03-21 : 0

 

나의 답 :

static int paycheckCount(Calendar from, Calendar to) {
    // from 또는 to가 null이면 0을 반환한다.
    if (from == null || to == null) return 0;

    // 1. from와 to가 같고 날짜가 21일이면 1을 반환한다.
    if (from.equals(to) && from.get(Calendar.DAY_OF_MONTH) == 21) return 1;

    int fromYear = from.get(Calendar.YEAR);             // 년도
    int fromMonth = from.get(Calendar.MONTH);           // 월
    int fromDay = from.get(Calendar.DAY_OF_MONTH);      // 일
    
    int toYear = to.get(Calendar.YEAR);                 // 년도
    int toMonth = to.get(Calendar.MONTH);               // 월
    int toDay = to.get(Calendar.DAY_OF_MONTH);          // 일

    int fromYearToMonth = (fromYear * 12) + fromMonth;  // 년도 → 월 : 년도 * 12
    int toYearToMonth = (toYear * 12) + toMonth;        // 년도 → 월 : 년도 * 12

    // 2. to와 from이 몇 개월 차이인지 계산해서 변수 monDiff에 담는다.
    int monDiff = toYearToMonth - fromYearToMonth;

    // 3. monDiff가 음수이면 0을 반환한다.
    if (monDiff < 0) return 0;

    /* 
       4. 만일 from의 일(DAY_OF_MONTH)이 21일이거나 이전이고,
          to의 일(DAY_OF_MONTH)이 21일이거나 이후이면 monDiff의 값을 1 증가시킨다.
    */
    if (fromDay <= 21 && toDay >= 21) monDiff++;
    
    /* 
       5. 만일 from의 일(DAY_OF_MONTH)이 21일 이후고,
          to의 일(DAY_OF_MONTH)이 21일 이전이면 monDiff의 값을 1 감소시킨다.
    */
    if (fromDay > 21 && toDay < 21) monDiff--;

    return monDiff;
}

지정된 날짜 범위에 특정 날짜(21일)가 몇 번 포함되는지 계산하려면

① 시작일과 마지막일 간의 개월 수 차이를 구한다.

② 시작일 또는 마지막일이 특정 날짜(21일)인지 아닌지 확인하면 된다.

 

1) Calendar.get()로 두 날짜(from, to)의 년도, 월, 일을 얻는다.

2) 두 날짜간의 개월 수 차이를 구하기 위해 년도를 월로 변환한 두 값을 뺀다.

     * 2010 x 12 : 년도(2010)에 12를 곱하면 월이 된다(년도 → 월)

     * to의 값이 더 크므로 to가 앞에 위치해야 올바른 값을 얻는다.

3) monDiff가 음수라면 from의 날짜가 더 큰 것이므로 0을 반환한다(올바르지 않은 시작 값)

4) from의 일이 21일이거나 이전이고, to의 일이 21일이거나 이후이면 monDiff의 값을 1 증가시킨다.

    * 2010-01-21(24121) ~ 2010-03-21(24123) : 24123 - 24121(년 → 월) = 2

    * 2010-01-21, 2010-02-21, 2010-03-21 : 월급날이 3번이므로 1을 더해야 함.

5) from의 일이 21일 이후이고, to의 일이 21일 이전이면 monDiff의 값을 1 감소시킨다.

    * 2010-01-22(24121) ~ 2010-03-19(24123) : 24123 - 24121(년 → 월) = 2

    * 2010-02-21 : 월급날이 1번이므로 1을 빼야 함.

 

 

 

 

[10-3] 문자열 “123,456,789.5”를 소수점 첫 번째 자리에서 반올림하고, 그 값을 만 단위마다 콤마(,)로 구분해서 출력하시오.

// 실행 결과
data : 123,456,789.5
반올림 : 123456790
만단위 : 1,2345,6790

 

나의 답 :

import java.text.DecimalFormat;

class Exercise10_3 {
    public static void main(String[] args) {
        String data = "123,456,789.5";

        DecimalFormat df1 = new DecimalFormat("#,###.#");
        // number를 출력했을 때
        // "###,###,###" : 1.234567895E8
        // "#,###.##"    : 1.234567895E8(해설 답)

        DecimalFormat df2 = new DecimalFormat("#,####");  // 만 단위로 출력하기 위한 패턴

        try {
            Number number = df1.parse(data);
            // System.out.println(number);    // 1.234567895E8
            double d = number.doubleValue();  // Number -> double

            System.out.println("data  : " + data);
            System.out.println("반올림 : " + Math.round(d));
            System.out.println("만단위 : " + df2.format(d));
            // System.out.println("만단위 : " + df2.format(number));  // format(Object obj)
        } catch (Exception e) { }
    }
}

 

DecimalFormat
* 자바에서는 숫자 or 날짜 데이터를 일정한 형식에 맞게 출력할 수 있도록 형식화 클래스를 제공한다.
* DecimalFormat(숫자 형식화)를 이용하면 형식 문자열 ↔ 숫자로 변환 가능하다.

* 형식 문자열 : 특정 형식이 적용된 문자열(예 : "1,000")
                      : 숫자 데이터를 일정한 형식(정수, 부동 소수점, 금액 등)에 맞게 표현할 때 사용한다.
* java.text패키지 : DecimalFormat(숫자 형식화), SimpleDateFormat(날짜 형식화)

1) DecimalFormat를 이용하여 숫자를 원하는 형식으로 표현(변환)하려면 패턴을 정의해야 한다.

  1-1) DecimalFormat인스턴스를 생성할 때 생성자 안에 원하는 출력 형식의 패턴을 작성하면 된다.

         * DecimalFormat df = new DecimalFormat("형식 문자열");

    1-1-1) #,###.# : 형식 문자열을 숫자로 변환하기 위한 패턴("123,456,789.5" → 123,456,789.5)

               * 패턴에 사용되는 기호 : #(10진수, 반올림, 빈 자리 출력X) ,(단위 구분자) .(소수점)

+) 실수의 저장 방식(부동 소수점) 때문인지, 패턴을 다르게 지정해봐도 동일한 값(1.234567895E8)을 얻는다.

     - 부동 소수점(floating point) 방식 : 하나의 실수를 가수부와 지수부로 나누어 표현하는 방식(오차 발생)

     - 컴퓨터는 정수와 실수를 2진수로만 표현하며, floating point 방식으로 실수를 저장한다.

     - 컴퓨터에서 실수를 표현하는 방법은 정확한 표현이 아닌 언제나 근사치를 표현할 뿐임을 명심해야 한다.

       출처 : tcpschool

+) "123,456,789.5"가 1.234567895E8로 저장되는 이유가 궁금하다.

    실제로는 2진수의 부동 소수점으로 저장되며, 표현을 10진수로 한 것이라는데 추후에 알아봐야겠다.

    1-1-2) #,#### : 만 단위로 출력하기 위한 패턴(ex. 1,0000)

2) df1.parse(data) : 반올림을 수행하기 위해 형식 문자열(df1)을 숫자(Number 타입의 객체)로 변환한다.

    * parse(String source) : 반환 타입이 Number객체이므로, 반환 결과를 Number객체에 담아야 한다.

       - Number number = df1.parse(data);

       Number : 숫자를 저장하는 래퍼 클래스(Integer, Double)의 조상

    * 변환하는 과정 도중에 예외가 발생할 수 있으므로 예외 처리(try-catch)가 필요하다.

3) Math.round()는 인자로 double, int를 받으므로 Number에 저장된 값을 double타입으로 변환해야 한다.

    * Number.doubleValue() : Number객체에 저장된 값을 double타입으로 변환한다.

4) Math.round()를 이용하여 소수점 첫째 자리에서 반올림을 수행한다.

5) format(Object obj)를 이용하여 df2(만 단위) 형식을 적용한다. (숫자 → 형식 문자열)

    * format(Object obj) : 인자로 전달한 값을 정의된 형식에 맞게 변환한 다음 문자열(String)로 반환한다.

    * 모든 객체를 인자로 받으므로 number(Number객체)를 넣어도 무방하다.

형변환 하지 않고 패턴(#,###.#)을 사용한 이유?
data를 double로 형변환(Double.parseDouble)하면 단위 구분자(,)로 인해 예외가 발생한다.
* 형식 문자열을 숫자로 변환하려면 DecimalFormat.parse(String source)를 이용해야 한다.
* 단위 구분자(,)는 문자여서 숫자 리터럴로 저장할 수 없다.
String data = "123,456,789.5";
// System.out.println(Double.parseDouble(data));  // NumberFormatException 발생​​
형식 문자열(#)을 이용하여 반올림을 수행한 답
import java.text.DecimalFormat;

public class Exercise10_3_1 {
    public static void main(String[] args) {
        String data = "123,456,789.5";

        DecimalFormat df  = new DecimalFormat("#,###.#");
        DecimalFormat df2 = new DecimalFormat("#");
        DecimalFormat df3 = new DecimalFormat("#,####");

        try {
            Number number = df.parse(data);  // 1.234567895E8

            System.out.println("data  : " + data);
            System.out.println("반올림 : " + df2.format(number));  // # : 반올림
            System.out.println("만단위 : " + df3.format(number));
        } catch (Exception e) { }
    }
}

왜 들여쓰기가 안될까?

 

 

 

 

[10-4] 화면으로부터 날짜를 “2007/05/11”의 형태로 입력받아서 무슨 요일인지 출력하는 프로그램을 작성하시오.

단, 입력된 날짜의 형식이 잘못된 경우 메세지를 보여주고 다시 입력받아야 한다.

// 실행 결과
날짜를 yyyy/MM/dd의 형태로 입력해주세요. (입력예:2007/05/11)
>>2009-12-12
날짜를 yyyy/MM/dd의 형태로 입력해주세요. (입력예:2007/05/11)
>>2009/12/12
입력하신 날짜는 토요일입니다.

 

나의 답 :

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

class Exercise10_4 {
    public static void main(String[] args) {
        DateFormat sdf    = new SimpleDateFormat("yyyy/MM/dd");  // 입력 받아야 할 형식
        DateFormat result = new SimpleDateFormat("입력하신 날짜는 E요일입니다.");  // 출력 형식
        
        Scanner s = new Scanner(System.in);

        Date date = null;
        
        do {
            System.out.println("날짜를 yyyy/MM/dd의 형태로 입력해주세요.(입력예 : 2023/01/01)");
            System.out.print(">>");

            try {
                date = sdf.parse(s.nextLine());
                break;
            } catch(Exception e) { }
        } while (true);

        System.out.println(result.format(date));
    }
}

1) SimpleDateFormat인스턴스에 입력 받아야 할 형식과 출력 형식 패턴을 지정한다.

    * yyyy/MM/dd : 입력된 날짜가 지정된 형식에 맞는지 검사할 때 사용

    * 입력하신 날짜는 E요일입니다. : 날짜를 지정된 형식으로 출력할 때 사용

    * E : 요일(일~토)

2) 입력된 날짜가 지정된 형식에 맞게 입력될 때까지 입력을 반복하기 위해 do-while문을 사용한다.

    * 입력받은 날짜(문자열)가 지정된 형식과 다르면 ParseException 예외가 발생한다(예외 처리 필요)

       - parse()에서 예외가 발생하면 break문이 수행되지 않고, 실행 흐름이 반복문의 첫 문장으로 이동된다.

       - parse()에서 예외가 발생하지 않으면 break문이 수행되어 반복문을 벗어 난다.

3) format(Date d)를 통해 date(입력받은 날짜)에 출력 형식(result)을 적용하여 출력한다.

 

해설 : 출력 형식 패턴을 문자열로 저장하여 사용한다.

import java.util.*;
import java.text.*;

class Exercise10_4 {
    public static void main(String[] args) {
        String pattern  = "yyyy/MM/dd";
        String pattern2 = "입력하신 날짜는 E요일입니다.";  // 'E'는 일~토 중의 하나가 된다.

        DateFormat df  = new SimpleDateFormat(pattern);
        DateFormat df2 = new SimpleDateFormat(pattern2);

        Scanner s = new Scanner(System.in);

        Date inDate = null;

        do {
            System.out.println("날짜를 " + pattern + "의 형태로 입력해주세요.(입력예 : 2007/05/11)");

            try {
                System.out.print(">>");
                inDate = df.parse(s.nextLine());  // 입력받은 날짜를 Date로 변환(String -> Date)
                break;                            // parse()에서 예외가 발생하면 이 문장은 수행되지 않는다.
            } catch(Exception e) {}
        } while(true);

        System.out.println(df2.format(inDate));
    }
}

 

 

 

 

[10-5] 다음과 같이 정의된 메서드를 작성하고 테스트하시오.

메서드명 : getDayDiff

기       능 : yyyymmdd형식의 두 문자열을 넘겨받으면 두 날짜의 차이를 일(day) 단위로 반환한다.

                 단, 첫 번째 날짜 빼기 두 번째 날짜의 결과를 반환한다.

                 만일 주어진 문자열이 유효하지 않으면 0을 반환한다.

반환타입 : int

매개변수 : String yyyymmdd1 - 시작 날짜

                 String yyyymmdd2 - 끝 날짜

import java.util.*;

class Exercise10_5 {
    // (1) getDayDiff메서드를 작성하시오.

    public static void main(String[] args) {
        System.out.println(getDayDiff("20010103", "20010101"));
        System.out.println(getDayDiff("20010103", "20010103"));
        System.out.println(getDayDiff("20010103", "200103"));
    }
}
// 2
// 0
// 0

 

나의 답 : 넘겨받은 문자열에 SimpleDateFormat을 적용한다.

public static int getDayDiff(String yyyymmdd1, String yyyymmdd2) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
    int diff = 0;  // 결과 값 저장

    try {
        Date date1 = sdf.parse(yyyymmdd1);  // Date parse(String source)
        Date date2 = sdf.parse(yyyymmdd2);

        Calendar cal1 = Calendar.getInstance();
        Calendar cal2 = Calendar.getInstance();

        cal1.setTime(date1);  // Date -> Calendar
        cal2.setTime(date2);

        diff = (int) (cal1.getTimeInMillis() - cal2.getTimeInMillis()) / (24 * 60 * 60 * 1000);
    } catch (Exception e) {
        diff = 0;
    }
    return diff;
}

1) 넘겨받은 문자열을 날짜 형식("yyyyMMdd")으로 저장하기 위해 SimpleDateFormat인스턴스 생성

2) parse(String source) : 문자열에 날짜 형식(sdf)를 적용하여 날짜(Date) 인스턴스로 변환(String → Date)

3) Calendar.getTimeInMillis()를 이용하여 두 날짜의 차이를 구하기 위해 Calendar인스턴스를 얻는다.

  3-1) Calendar는 추상 클래스라 getInstance()를 통해 구현된 클래스의 인스턴스를 얻어야 한다.

         * Calendar.getInstance()

           - Calendar클래스를 구현한 클래스의 인스턴스를 반환한다(직접 객체 생성 불가)

           - 사용자 컴퓨터 설정에 따라 달력 객체(서양력(한국), 불교력, 일본력 등)를 생성한다.

  3-2) setTime(Date d) : 날짜 형식으로 변환한 값(Date)을 Calendar로 변환한다(Date → Calendar)

         * Date → Calendar : Calendar.setTime(Date d)

         * Calendar → Date : Calendar.getTime()

4) Calendar.getTimeInMillis()를 이용하여 두 날짜의 차이를 계산한다.

5) 두 날짜의 차이를 구하는 과정 도중 예외가 발생하면 0을 반환하고, 발생하지 않으면 결과 값을 반환한다.

두 날짜의 차이(일)를 계산하는 방법
Calendar.getTimeInMillis()를 이용하여 두 날짜의 년월일을 ms(1000/1초) 단위로 변환한다.
② 변환한 두 값을 뺀다.
③ 결과 값(두 날짜의 차이)을 일 단위로 변환한다.
     * 초 단위 : (결과 값) / 1000(ms → s)
     * 일 단위 : (결과 값) / 24(시간) * 60(분) * 60(초) * 1000(ms → s)
시간 계산
* 1분     : 60초
* 1시간 : 60초(1분) x 60분
* 1일     : 60초(1분) x 60분(1시간) x 24시간
* 1개월 : 60초(1분) x 60분(1시간) x 24시간 x 30일
* 1년     : 60초(1분) x 60분(1시간) x 24시간 x 365일
* 10년   : 60초(1분) x 60분(1시간) x 24시간 x 365일 x 10년

 

해설 : 넘겨받은 문자열을 substring()으로 잘라 사용한다.

static int getDayDiff(String yyyymmdd1, String yyyymmdd2) {
    int diff = 0;

    try {
        // 년, 월, 일
        int year1  = Integer.parseInt(yyyymmdd1.substring(0, 4));
        int month1 = Integer.parseInt(yyyymmdd1.substring(4, 6)) - 1;
        int day1   = Integer.parseInt(yyyymmdd1.substring(6, 8));

        int year2  = Integer.parseInt(yyyymmdd2.substring(0, 4));
        int month2 = Integer.parseInt(yyyymmdd2.substring(4, 6)) - 1;
        int day2   = Integer.parseInt(yyyymmdd2.substring(6, 8));

        Calendar date1 = Calendar.getInstance();
        Calendar date2 = Calendar.getInstance();
        
        // 초기화
        date1.clear();
        date2.clear();

        date1.set(year1, month1, day1);
        date2.set(year2, month2, day2);

        // 두 날짜의 차이
        diff = (int) (date1.getTimeInMillis() - date2.getTimeInMillis()) / (24 * 60 * 60 * 1000);
    } catch (Exception e) {
        diff = 0;  // substring(), parseInt()에서 예외가 발생하면 0을 반환한다.
    }
    return diff;
}

1) 넘겨받은 문자열을 substring()으로 잘라 년, 월, 일을 각각 구한다.

     * month1, month2 : Calendar에서는 월(month)가 0부터 시작하기 때문에 1을 빼야 한다.

2) Calendar.clear() : Calendar인스턴스를 얻은 후 초기화를 수행한다.
     * 두 객체가 완벽히 동시에 생성되는 게 아니라 초기화를 수행하지 않으면 ms(밀리초) 차이가 발생한다.

        예) 2023/5/1 00:00:00.012초에 실행

              - date1객체가 2023/5/1 00:00:00.145초, date2객체가 2023/5/1 00:00:00.256초에 만들어졌을 때

              - 145, 256를 계산하면 1.9999...라는 값이 나오는데 int는 소수점을 버리므로 1이 된다.
     * 현재 시간으로 작업하는 게 아니라면 정확한 계산을 위해 Calendar를 생성한 후 초기화를 수행해야 한다.

     * clear(), clear(int field) : 모든 or 지정된 필드의 값을 기본값(EPOCH Time)으로 초기화한다.

     * EPOCH Time : 날짜 계산 기준. 1970.1.1 00:00:00(컴퓨터가 만들어진 시기)

3) 초기화를 수행한 후 앞에서 구한 값들로 Calendar의 년월일을 설정한다.

3) Calendar.getTimeInMillis()를 이용하여 두 날짜의 차이를 구한 다음 일(day) 단위로 변환한다.

    * Calendar.getTimeInMillis() : 날짜를 천분의 일초 단위(ms)로 변환해서 반환한다.

    * 천분의 일초 단위(ms)를 일 단위로 바꾸려면 24(시간) * 60(분) * 60(초) * 1000(ms)으로 나눠주면 된다.

 

 

 

 

[10-6] 자신이 태어난 날부터 지금까지 며칠이 지났는지 계산해서 출력하시오.

// 실행 결과
birthday : 2000-01-01
today : 2016-01-29
5872 days

 

나의 답 : 예제와 동일한 결과를 얻기 위해 날짜를 예제와 동일하게 설정했다.

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

class Exercise10_6 {
    public static void main(String[] args) {
        LocalDate birthday = LocalDate.of(2000, 1, 1);      // 생일 지정
        LocalDate today = LocalDate.of(2016, 1, 29);        // 현재 날짜(예제)
        // LocalDate today = LocalDate.now();               // 현재 날짜

        long day = birthday.until(today, ChronoUnit.DAYS);

        System.out.println("birthday : " + birthday);
        System.out.println("today : " + today);
        System.out.println(day + " days");
    }
}
// birthday : 2000-01-01
// today : 2016-01-29
// 5872 days

1) LocalDate객체를 생성하여 생일과 현재 날짜(예제)를 지정한다.

    * LocalDate.now() : 컴퓨터의 현재 날짜 정보를 저장하는 LocalDate객체를 생성하여 반환
    * LocalDate.of()    : 지정된 날짜 정보를 저장하는 LocalDate객체를 생성하여 반환

2) LocalDate.until()를 이용하면 D-day를 쉽게 구할 수 있다.

    * LocalDate long until(Temporal endExclusive, TemporalUnit unit) (O) until(a, b)

    * LocalDate Period until(ChronoLocalDate endDateExclusive)       (X) until(a)

      - until(a)는 날짜의 차이를 뺄셈(단순 차이)으로 반환하고,

      - until(a, b)는 날짜의 차이를 전체 시간을 기준으로 반환한다.

      - 인자를 하나만 넘기는 until(a)는 Period를 반환하고, 두 개를 넘기는 until(a, b)는 long를 반환한다.

LocalDate long until(Temporal endExclusive, TemporalUnit unit)
ChronoUnit.YEARS     : 날짜의 차이를 년도 기준으로 반환(전체 시간 기준)
* ChronoUnit.MONTHS : 날짜의 차이를 개월 기준으로 반환(전체 시간 기준)
ChronoUnit.DAYS       : 날짜의 차이를 일 기준으로 반환(전체 시간 기준)

LocalDate birthday = LocalDate.of(2000, 1, 1);
LocalDate today = LocalDate.of(2016, 1, 29);

// LocalDate long until(Temporal endExclusive, TemporalUnit unit)
long year = birthday.until(today, ChronoUnit.YEARS);
long month = birthday.until(today, ChronoUnit.MONTHS);
long day = birthday.until(today, ChronoUnit.DAYS);

System.out.println(year + " year");    // 16 year
System.out.println(month + " month");  // 192 month
System.out.println(day + " days");     // 5872 days​


ChronoUnit.between(Temporal temporal1Inclusive, Temporal temporal2Exclusive)
ChronoUnit.between()도 두 날짜의 차이를 전체 시간을 기준으로 반환한다.
*
 ChronoUnit.YEARS.between()    : 날짜의 차이를 년도 기준으로 반환(전체 시간 기준)
* ChronoUnit.MONTHS.between() : 날짜의 차이를 개월 기준으로 반환(전체 시간 기준)
* ChronoUnit.DAYS.between()
       : 날짜의 차이를 일 기준으로 반환(전체 시간 기준)

LocalDate birthday = LocalDate.of(2000, 1, 1);
LocalDate today = LocalDate.of(2016, 1, 29);

// ChronoUnit.between(Temporal temporal1Inclusive, Temporal temporal2Exclusive)
long day = ChronoUnit.DAYS.between(birthday, today);

System.out.println("birthday : " + birthday);
System.out.println("today : " + today);
System.out.println(day + " days");
LocalDate Period until(ChronoLocalDate endDateExclusive)
날짜의 차이를 단순 차이(뺄셈)으로 반환한다.
Period는 년, 월, 일을 분리하여 저장한다.
Period    : 날짜의 차이(단위 : Years, Months, Days)

Duration : 시간의 차이(단위 : Seconds, Nanos)
LocalDate birthday = LocalDate.of(2000, 1, 1);
LocalDate today = LocalDate.of(2016, 1, 29);

// LocalDate Period until(ChronoLocalDate endDateExclusive)
Period pe = birthday.until(today);

System.out.println("pe : " + pe);               // pe : P16Y28D
System.out.println(pe.getYears() + " year");    // 16 year
System.out.println(pe.getMonths() + " month");  // 0 month
System.out.println(pe.getDays() + " days");     // 28 days

Period Period between(LocalDate startDateInclusive, LocalDate endDateExclusive)
Period.between()로도 날짜 or 시간의 차이를 구할 수 있다.
단, LocalDate Period until()처럼 년, 월, 일, 초의 단순 차이(뺄셈)를 반환한다.

LocalDate birthday = LocalDate.of(2000, 1, 1);
LocalDate today = LocalDate.of(2016, 1, 29);

Period pe = Period.between(birthday, today);

System.out.println(pe.getYears() + " year");    // 16 year
System.out.println(pe.getMonths() + " month");  // 0 month
System.out.println(pe.getDays() + " days");     // 28 days

 

 

 

 

[10-7] 2016년 12월 네번째 화요일의 날짜를 아래의 실행결과와 같은 형식으로 출력하시오.

2016-12-27

 

나의 답 : LocalDate.of()로 날짜를 설정한 후 일을 반복문으로 증가시켜 조건식을 검사한다.

import java.time.LocalDate;
import java.time.temporal.ChronoField;

import static java.time.temporal.ChronoField.ALIGNED_WEEK_OF_MONTH;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;

class Exercise10_7 {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2016, 12, 1);

        for (int i = date.get(ChronoField.DAY_OF_MONTH); i < 30; i++) {  // DAY_OF_MONTH : 일
            date = date.withDayOfMonth(i);        // 날짜 재설정 : withDayOfMonth(int dayOfMonth)
            // date = LocalDate.of(2016, 12, i);  // 날짜 재설정 : of(int year, int month, int dayOfMonth)

            // ALIGNED_WEEK_OF_MONTH : 그 달의 n번째 주
            // DAY_OF_WEEK           : 요일
            if (date.get(ALIGNED_WEEK_OF_MONTH) == 4 && date.get(DAY_OF_WEEK) == 2) {
                System.out.println(date);
                break;
            }
        }
    }
}

1) LocalDate.of()를 통해 날짜를 2016.12.1로 설정한다.

2) LocalDate.get(TemporalField field)를 통해 LocalDate객체의 일(DAY_OF_MONTH)을 얻는다.

    * LocalDate.get(TemporalField field) : LocalDate / LocalTime에서 특정 필드의 값을 가져올 때 사용

    * LocalDate long getLong(TemporalField field) : int타입의 범위를 넘는 필드는 getLong()를 사용

3) 반복문으로 일(DAY_OF_MONTH)을 말일이 될때까지 증가시키기 위해 LocalDate객체의 날짜를 재설정

    * withDayOfMonth(int dayOfMonth) : LocalDate의 일(DAY_OF_MONTH) 필드를 지정한 값으로 변경

                                                                                (예 : 2023년 5월의 몇 일로 설정)

    * LocalDate는 불변 객체라 속성(년, 월, 일)을 변경하려면 새 객체를 생성하거나 기존 객체에 재할당해야 함.

    * 필드를 변경하는 메서드들은 항상 새로운 객체를 생성하여 반환하므로 대입 연산자를 같이 사용해야 한다.    

4) 조건식으로 date가 달의 4번째 주이면서 화요일인지 검사한다.

    * 조건식을 만족하면 date을 출력한 후 반복문을 종료한다.

    * yyyy-mm-dd 형식으로 출력되므로 출력 형식을 따로 지정하지 않아도 된다.

withDayOfMonth(int dayOfMonth)를 이용하여 해당 월의 1일과 말일을 구하는 방법
LocalDate firstDate = date.withDayOfMonth(1);                    // 해당 월의 1일
LocalDate lastDate = date.withDayOfMonth(date.lengthOfMonth());  // 해당 월의 말일

System.out.println(firstDate);  // 2016-12-01
System.out.println(lastDate);   // 2016-12-31

 

해설 :

import java.time.LocalDate;
import static java.time.DayOfWeek.TUESDAY;
import static java.time.temporal.TemporalAdjusters.dayOfWeekInMonth;

class Exercise10_7 {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2016, 12, 1);
        System.out.println(date.with(dayOfWeekInMonth(4, TUESDAY)));
    }
}

?째주 ?요일은 TemporalAdjusters클래스의 dayOfWeekInMonth()를 이용하면 된다.

* TemporalAdjusters.dayOfWeekInMonth() : 해당 월의 n번째 요일에 해당하는 날짜를 반환한다.

* with(TemporalAdjuster a) : LocalDate or LocalTime객체에서 특정 필드 값을 변경할 때 사용한다.

TemporalAdjusters와 LocalDate.with()
1) TemporalAdjusters : 자주 쓰일만한 복잡한 날짜 계산들을 수행하는 메서드를 정의해놓은 클래스
    * 예 : 지난 주 토요일이 며칠인지, 이번 달의 3번째 금요일은 며칠인지
    * TemporalAdjuster 인터페이스를 구현한 클래스
      * adjustInto()만 구현하면 되는데, 이는 내부적으로만 사용할 의도로 작성되었다.
      * 구현된 클래스를 사용할 때는 with()를 같이 사용해야 한다.
        * adjustInto()TemporalAdjuster인터페이스에 정의되어 있는 하나뿐인 추상 메서드
2) LocalDate.with() : TemporalAdjuster인터페이스를 구현한 클래스의 객체를 인자로 제공해야 함.
    * 내부적으로 TemporalAdjuster.adjustInto()를 호출한다.

 

 

 

 

[10-8] 서울과 뉴욕간의 시차가 얼마인지 계산하여 출력하시오.

계절에 따라 변하는 시차(DST)로 인해 시차가 pdf파일과 다른 값을 얻게 된다(14 → 13)

// 실행 결과
2023-05-07T11:55:27.680+09:00[Asia/Seoul]
2023-05-06T22:55:27.691-04:00[America/New_York]
sec1 = 32400
sec2 = -14400
diff = 13hrs

 

나의 답 : Timestamp.getTime()를 활용하여 밀리초 차이를 구한 후 각각 시/초 단위로 변환하여 출력한다.

import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.ZonedDateTime;

class Exercise10_8 {
    public static void main(String[] args) {
        // 서울의 현재 시간
        ZonedDateTime seoul = ZonedDateTime.now();

        // 뉴욕의 현재 시간
        ZoneId nyId = ZoneId.of("America/New_York");
        ZonedDateTime ny = ZonedDateTime.now(nyId);    // now(ZoneId zone)

        // utc의 현재 시간
        ZoneId utcId = ZoneId.of("UTC");
        ZonedDateTime utc = ZonedDateTime.now(utcId);  // now(ZoneId zone)

        // 시차를 구하기 위해 Timestamp 활용(ZonedDateTime -> LocalDateTime -> Timestamp)
        // toLocalDateTime() : ZonedDateTime -> LocalDateTime
        // valueOf() : LocalDateTime -> Timestamp
        Timestamp seoulTS = Timestamp.valueOf(seoul.toLocalDateTime());
        Timestamp nyTS = Timestamp.valueOf(ny.toLocalDateTime());
        Timestamp utcTs = Timestamp.valueOf(utc.toLocalDateTime());

        // Timestamp.getTime() : 밀리초(ms)로 변환하여 반환
        double diffSeoulNy = seoulTS.getTime() - nyTS.getTime();    // 서울과 뉴욕의 ms 차이
        double diffSeoulUtc = seoulTS.getTime() - utcTs.getTime();  // 서울과 utc의 ms 차이
        double diffNyUtc = nyTS.getTime() - utcTs.getTime();        // 뉴욕과 utc의 ms 차이

        // 반환 결과(ms)를 시/초 단위로 변환하여 출력
        long sec1 = Math.round(diffSeoulUtc / 1000);           // 1초 : 1000(1초)
        long sec2 = Math.round(diffNyUtc / 1000);              // 1초 : 1000(1초)
        long diff = Math.round(diffSeoulNy / (1000 * 60 * 60));  // 1시간 : 1000(1초) * 60(초) * 60(분)

        System.out.println(seoul);
        System.out.println(ny);

        System.out.println("sec1 = " + sec1);
        System.out.println("sec2 = " + sec2);
        System.out.println("diff = " + diff + "hrs");
    }
}
// 2023-05-10T23:23:25.422+09:00[Asia/Seoul]
// 2023-05-10T10:23:25.458-04:00[America/New_York]
// sec1 = 32400
// sec2 = -14400
// diff = 13hrs
ZonedDateTime
* ISO-8601 달력 시스템에서 정의하는 Time Zone에 따라 날짜와 시간을 저장하는 클래스
* LocalDateTime(날짜+시간)에 시간대(time zone) 정보를 표현한다.
ZonedDateTime객체를 만들 때는 ZoneId를 사용하는게 유리하다. 계절에 따라 변하는 시차(DST)를 처리한다.
  - ZoneId : 타임존. 시간의 차이를 타임존 코드로 표현한 것(ex. "Asia/Seoul")
  - ZoneOffset : 시차. UTC와의 시간 차이를 +(양수) or -(음수)로 표현한 것(ex. "+09:00")

1) ZonedDateTime.now()를 이용하여 서울의 현재 시간을 구한다.

    * ZonedDateTime를 생성할 때는 정적 메소드 now()of()를 사용해야 한다(생성자가 non-public)

      - now() : 컴퓨터의 현재 날짜/시간/시간대 정보를 저장하는 ZonedDateTime를 생성
      - of() : 지정된 날짜/시간/시간대 정보를 저장하는 ZonedDateTime를 생성

2) 다른 도시(뉴욕, utc)의 현재 시간을 구하려면 ZonedDateTime.now(ZoneId zone)를 이용해야 한다.

     * now()는 컴퓨터(로컬)의 타임존 정보를 이용하여 ZonedDateTime객체를 생성한다.

     * now(ZoneId zone)의 인자로 다른 도시의 타임존 정보를 넘기면 해당 지역의 시간을 얻을 수 있다.

     * withZoneSameInstant(ZoneId zone)로도 다른 도시의 현재 시간을 얻을 수 있다.

       - withZoneSameInstant(ZoneId) : 다른 시간대의 시간으로 변경할 때 사용
       - withZoneSameLocal(ZoneId)   : 시간은 그대로 두고 시간대만 변경할 때 사용

ZonedDateTime seoul = ZonedDateTime.now();

ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime ny1 = ZonedDateTime.now().withZoneSameInstant(nyId);  // withZoneSameInstant(ZoneId zone)
ZonedDateTime ny2 = ZonedDateTime.now(nyId);                        // ZonedDateTime.now(ZoneId zone)

System.out.println(seoul);
System.out.println(ny1);  // 2023-05-08T00:12:59.250-04:00[America/New_York]
System.out.println(ny2);  // 2023-05-08T00:12:59.278-04:00[America/New_York]

3) 시차를 구하기 위해 ZonedDateTime객체Timestamp객체로 변환한다.

     ZonedDateTimeLocalDateTimeTimestamp

     시차를 계산하려면 ZonedDateTime객체를 정수 or 실수로 변환해서 구해야 되는 줄 알았다.

     * valueOf(LocalDateTime ldt) : LocalDateTime객체를 인자로 받아 Timestamp객체로 변환한다.

       ZonedDateTime객체는 LocalDateTime객체로의 변환 필요(ZonedDateTime → LocalDateTime)

     * Timestamp : 날짜와 시간을 초 단위로 표현(계산 및 순서 비교에 유리하여 데이터베이스에 주로 사용)

                          : SQL 데이터베이스에 TIMESTAMP(날짜+시간) 값을 넣을 때 사용한다.

4) Timestamp.getTime()를 이용하여 시차를 계산한다.

    * getTime() : Timestamp객체에 저장된 값을 밀리초(millisecond)로 변환하여 반환한다.

                         : (EPOCH TIME(1970-01-01 00:00:00 UTC)부터 경과한 시간을 초 단위로 계산한 값)

    * 반환 결과(ms)를 각각 시/초 단위로 변환해야 하므로 오차를 줄이기 위해 double타입에 저장한다.

    * 정수 타입(int, long)에 저장하면 소수점 이하의 값이 버려지므로 12.999..일 경우 12가 저장된다.

5) 밀리초(ms) 차이를 각각 시/초 단위로 변환하여 출력한다.

     * 1시간 : 1000(1초) * 60(초) * 60(분)

     * 1초 : 1000(1초)

 

해설 :

class Exercise10_8 {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.now();
        ZoneId nyId = ZoneId.of("America/New_York");
        ZonedDateTime zdtNY = ZonedDateTime.now().withZoneSameInstant(nyId);

        System.out.println(zdt);
        System.out.println(zdtNY);

        long sec1 = zdt.getOffset().getTotalSeconds();    // 날짜+시간 -> 초
        long sec2 = zdtNY.getOffset().getTotalSeconds();  // 날짜+시간 -> 초
        long diff = (sec1 - sec2) / 3600;                 // 초       -> 시간

        System.out.println("sec1 : " + sec1);
        System.out.println("sec2 : " + sec2);
        System.out.printf("diff : %dhrs%n", diff);
    }
}

1) ZonedDateTime객체를 생성하여 서울, 뉴욕의 현재 시간을 구한다.

2) ZonedDateTime.getOffset()를 호출하여 ZoneOffset를 얻는다.

3) ZoneOffset.getTotalSeconds()를 이용하여 현재 서울과 뉴욕의 날짜+시간을 초단위로 변환(UTC 기준)

4) 시차를 초단위로 구한 값(sec1 - sec2)을 3600초(1시간)로 나누면 시간 단위의 값을 구할 수 있다.