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);
}
}
}
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객체로 변환한다.
ZonedDateTime → LocalDateTime → Timestamp
시차를 계산하려면 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시간)로 나누면 시간 단위의 값을 구할 수 있다.
'Java의 정석 : 3rd Edition' 카테고리의 다른 글
[Java의 정석 - 연습문제] Chapter12. 지네릭스, 열거형, 애너테이션 (0) | 2023.05.31 |
---|---|
[Java의 정석 - 연습문제] Chapter11. Collections Framework (0) | 2023.05.30 |
[Java의 정석 - 연습문제] Chapter09. java.lang패키지와 유용한 클래스 (0) | 2023.02.06 |
[Java의 정석 - 연습문제] Chapter08. 예외처리 (2) | 2023.01.10 |
[Java의 정석 - 연습문제] Chapter07. 객체지향 프로그래밍 II(OOP) (2) | 2023.01.03 |