Post

Java 날짜와 시간 API

1. 날짜와 시간 API

Java의 기본 SDK에서 날짜와 시간을 다루는 java.util.Date 클래스와 java.util.Calendar 클래스는 사용하기 불편하기 때문에, 이 클래스를 대체하려고 Joda-Time 같은 오픈소스 라이브러리도 있다. 그러나 JDK 8에서는 개선된 날짜와 시간 API가 제공되고 있다.

2. JDK 기본 날짜 클래스의 문제점

  • 불변 객체가 아니다(not immutable)

  • int 상수 필드의 남용

  • 헷갈리는 월 지정

  • 일관성 없는 요일 상수

  • Date와 Calendar의 불편한 역할 분담

  • 오류에 둔감한 시간대 ID지정

  • java.util.Date 하위 클래스의 문제

3. 기본 JDK 클래스 날짜 연산 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import static org.fest.assertions.api.Assertions.*;

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

import org.junit.Test;

public class OldJdkDateTest {
    @Test
    public void shouldGetAfterOneDay() {
        TimeZone utc = TimeZone.getTimeZone("UTC");
        Calendar calendar = Calendar.getInstance(utc);
        calendar.set(1582, Calendar.OCTOBER , 4);
        String pattern = "yyyy.MM.dd";
        String theDay = toString(calendar, pattern, utc);
        assertThat(theDay).isEqualTo("1582.10.04");
        
        calendar.add(Calendar.DATE, 1);
        String nextDay = toString(calendar, pattern, utc);
        assertThat(nextDay).isEqualTo("1582.10.15");
    }
    
    @Test
    public void shouldGetAfterOneHour() {
        TimeZone seoul = TimeZone.getTimeZone("Asia/Seoul");
        Calendar calendar = Calendar.getInstance(seoul);
        calendar.set(1988, Calendar.MAY , 7, 23, 0);
        String pattern = "yyyy.MM.dd HH:mm";
        String theTime = toString(calendar, pattern, seoul);
        assertThat(seoul.inDaylightTime(calendar.getTime())).isFalse();
        assertThat(theTime).isEqualTo("1988.05.07 23:00");
        
        calendar.add(Calendar.HOUR_OF_DAY, 1);
        String after1Hour = toString(calendar, pattern, seoul);
        assertThat(seoul.inDaylightTime(calendar.getTime())).isTrue();
        assertThat(after1Hour).isEqualTo("1988.05.08 01:00");
    }
    
    @Test
    public void shouldGetAfterOneMinute() {
        TimeZone seoul = TimeZone.getTimeZone("Asia/Seoul");
        Calendar calendar = Calendar.getInstance(seoul);
        calendar.set(1961, Calendar.AUGUST, 9, 23, 59);
        String pattern = "yyyy.MM.dd HH:mm";
        String theTime = toString(calendar, pattern, seoul);
        assertThat(theTime).isEqualTo("1961.08.09 23:59");
        
        calendar.add(Calendar.MINUTE, 1);
        String after1Minute = toString(calendar, pattern, seoul);
        assertThat(after1Minute).isEqualTo("1961.08.10 00:30");
    }
    
    @Test
    public void shouldGetAfterTwoSecond() {
        TimeZone utc = TimeZone.getTimeZone("UTC");
        Calendar calendar = Calendar.getInstance(utc);
        calendar.set(2012, Calendar.JUNE, 30, 23, 59, 59);
        String pattern = "yyyy.MM.dd HH:mm:ss";
        String theTime = toString(calendar, pattern, utc);
        assertThat(theTime).isEqualTo("2012.06.30 23:59:59");
        
        calendar.add(Calendar.SECOND, 2);
        String afterTwoSeconds = toString(calendar, pattern, utc);
        assertThat(afterTwoSeconds).isEqualTo("2012.07.01 00:00:01");
    }
    
    @Test
    public void shouldSetGmtWhenWrongTimeZoneId(){
        TimeZone zone = TimeZone.getTimeZone("Seoul/Asia");
        assertThat(zone.getID()).isEqualTo("GMT");
    }
    
    private String toString(Calendar calendar, String pattern, TimeZone zone) {
        SimpleDateFormat format = new SimpleDateFormat(pattern);
        format.setTimeZone(zone);
        return format.format(calendar.getTime());
    }
    
    @Test
    public void shouldGetDate() {
        Calendar calendar = Calendar.getInstance();
        calendar.set(1999, 12, 31);
        assertThat(calendar.get(Calendar.YEAR)).isEqualTo(2000);
        assertThat(calendar.get(Calendar.MONTH)).isEqualTo(Calendar.JANUARY);
        assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(31);
    }
    
    @Test (expected = IllegalArgumentException.class)
    public void shouldNotAcceptWrongMonth() {
        Calendar calendar = Calendar.getInstance();
        calendar.setLenient(false);
        calendar.set(1999, 12, 31);
        calendar.get(Calendar.MONTH);
    }
    
    @Test
    @SuppressWarnings("deprecation")
    public void shouldGetDayOfWeek() {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2014, Calendar.JANUARY, 1);
        
        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        assertThat(dayOfWeek).isEqualTo(Calendar.WEDNESDAY);
        assertThat(dayOfWeek).isEqualTo(4);
        Date theDate = calendar.getTime();
        assertThat(theDate.getDay()).isEqualTo(3);
    }
}

4. Joda-Time 날짜 연산 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import static org.fest.assertions.api.Assertions.*;

import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.DateTimeZone;
import org.joda.time.IllegalFieldValueException;
import org.joda.time.LocalDate;
import org.joda.time.chrono.GJChronology;
import org.joda.time.chrono.GregorianChronology;
import org.junit.Test;

public class JodaTimeTest {

    @Test
    public void shouldGetAfterOneDay() {
        Chronology chrono = GregorianChronology.getInstance();
        LocalDate theDay = new LocalDate(1582, 10, 4, chrono);
        String pattern = "yyyy.MM.dd";
        assertThat(theDay.toString(pattern)).isEqualTo("1582.10.04");
        
        LocalDate nextDay = theDay.plusDays(1);
        assertThat(nextDay.toString(pattern)).isEqualTo("1582.10.05");
    }
    
    @Test
    public void shouldGetAfterOneDayWithGJChronology() {
        Chronology chrono = GJChronology.getInstance();
        LocalDate theDay = new LocalDate(1582, 10, 4, chrono);
        String pattern = "yyyy.MM.dd";
        assertThat(theDay.toString(pattern)).isEqualTo("1582.10.04");
        
        LocalDate nextDay = theDay.plusDays(1);
        assertThat(nextDay.toString(pattern)).isEqualTo("1582.10.15");
    }
    
    @Test
    public void shouldGetAfterOneHour() {
        DateTimeZone seoul = DateTimeZone.forID("Asia/Seoul");
        DateTime theTime = new DateTime(1988,5,7,23,0, seoul);
        String pattern = "yyyy.MM.dd HH:mm";
        assertThat(theTime.toString(pattern)).isEqualTo("1988.05.07 23:00");
        assertThat(seoul.isStandardOffset(theTime.getMillis())).isTrue();
        
        DateTime after1Hour = theTime.plusHours(1);
        assertThat(after1Hour.toString(pattern)).isEqualTo("1988.05.08 01:00");
        assertThat(seoul.isStandardOffset(after1Hour.getMillis())).isFalse();
    }
    
    @Test
    public void shouldGetAfterOneMinute() {
        DateTimeZone seoul = DateTimeZone.forID("Asia/Seoul");
        DateTime theTime = new DateTime(1961, 8, 9, 23, 59, seoul);
        String pattern = "yyyy.MM.dd HH:mm";
        assertThat(theTime.toString(pattern)).isEqualTo("1961.08.09 23:59");
        
        DateTime after1Minute = theTime.plusMinutes(1);
        assertThat(after1Minute.toString(pattern)).isEqualTo("1961.08.10 00:30");
    }
    
    @Test
    public void shouldGetAfterTwoSecond() {
        DateTimeZone utc = DateTimeZone.forID("UTC");
        DateTime theTime = new DateTime(2012, 6, 30, 23, 59, 59, utc);
        String pattern = "yyyy.MM.dd HH:mm:ss";
        assertThat(theTime.toString(pattern)).isEqualTo("2012.06.30 23:59:59");
        
        DateTime after2Seconds = theTime.plusSeconds(2);
        assertThat(after2Seconds.toString(pattern)).isEqualTo("2012.07.01 00:00:01");
    }
    
    @Test(expected=IllegalArgumentException.class)
    public void shouldThrowExceptionWhenWrongTimeZoneId(){
        DateTimeZone.forID("Seoul/Asia");
    }
    
    @Test
    public void shouldGetDate() {
        LocalDate theDay = new LocalDate(1999, 12, 31);
        
        assertThat(theDay.getYear()).isEqualTo(1999);
        assertThat(theDay.getMonthOfYear()).isEqualTo(12);
        assertThat(theDay.getDayOfMonth()).isEqualTo(31);
    }
    
    @Test (expected=IllegalFieldValueException.class)
    public void shouldNotAcceptWrongMonth() {
        new LocalDate(1999, 13, 31);
    }
    
    @Test
    public void shouldGetDayOfWeek() {
        LocalDate theDay = new LocalDate(2014, 1, 1);
        
        int dayOfWeek = theDay.getDayOfWeek();
        assertThat(dayOfWeek).isEqualTo(DateTimeConstants.WEDNESDAY);
        assertThat(dayOfWeek).isEqualTo(3);
    }
}

5. SR-310의 JDK 7 백포트 모듈 테스트

JDK 7 이상 설치

  • JSR-310의 백포트 모듈의 의존성 선언
1
2
3
4
5
<dependency>
    <groupId>org.threeten</groupId>
    <artifactId>threetenbp</artifactId>
    <version>0.8.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import static org.fest.assertions.api.Assertions.*;

import org.junit.Test;
import org.threeten.bp.DateTimeException;
import org.threeten.bp.DayOfWeek;
import org.threeten.bp.Instant;
import org.threeten.bp.LocalDate;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.chrono.IsoChronology;
import org.threeten.bp.format.DateTimeFormatter;
import org.threeten.bp.zone.ZoneRules;
import org.threeten.bp.zone.ZoneRulesException;

public class Jsr310BackportTest {
    @Test
    public void shouldGetAfterOneDay() {
        LocalDate theDay = IsoChronology.INSTANCE.date(1582, 10, 4);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd");
        assertThat(theDay.toString(formatter)).isEqualTo("1582.10.04");
        
        LocalDate nextDay = theDay.plusDays(1);
        assertThat(nextDay.toString(formatter)).isEqualTo("1582.10.05");
    }
    
    @Test
    public void shouldGetAfterOneHour() {
        ZoneId seoul = ZoneId.of("Asia/Seoul");
        ZonedDateTime theTime = ZonedDateTime.of(1988, 5, 7, 23, 0, 0, 0, seoul);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
        assertThat(theTime.toString(formatter)).isEqualTo("1988.05.07 23:00");
        ZoneRules seoulRules = seoul.getRules();
        assertThat(seoulRules.isDaylightSavings(Instant.from(theTime))).isFalse();
        
        ZonedDateTime after1Hour = theTime.plusHours(1);
        assertThat(after1Hour.toString(formatter)).isEqualTo("1988.05.08 01:00");
        assertThat(seoulRules.isDaylightSavings(Instant.from(after1Hour))).isTrue();
    }
    
    @Test
    public void shouldGetAfterOneMinute() {
        ZoneId seoul = ZoneId.of("Asia/Seoul");
        ZonedDateTime theTime = ZonedDateTime.of(1961, 8, 9, 23, 59, 59, 0, seoul);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
        assertThat(theTime.toString(formatter)).isEqualTo("1961.08.09 23:59");
        
        ZonedDateTime after1Minute = theTime.plusMinutes(1);
        assertThat(after1Minute.toString(formatter)).isEqualTo("1961.08.10 00:30");
    }
    
    @Test
    public void shouldGetAfterTwoSecond() {
        ZoneId utc = ZoneId.of("UTC");
        ZonedDateTime theTime = ZonedDateTime.of(2012, 6, 30, 23, 59, 59, 0, utc);
        DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss");
        assertThat(theTime.toString(format)).isEqualTo("2012.06.30 23:59:59");
        
        ZonedDateTime after2Seconds = theTime.plusSeconds(2);
        assertThat(after2Seconds.toString(format)).isEqualTo("2012.07.01 00:00:01");
    }
    
    @Test(expected=ZoneRulesException.class)
    public void shouldThrowExceptionWhenWrongTimeZoneId(){
        ZoneId.of("Seoul/Asia");
    }
    
    @Test
    public void shouldGetDate() {
        LocalDate theDay = LocalDate.of(1999, 12, 31);
        
        assertThat(theDay.getYear()).isEqualTo(1999);
        assertThat(theDay.getMonthValue()).isEqualTo(12);
        assertThat(theDay.getDayOfMonth()).isEqualTo(31);
    }
    
    @Test(expected=DateTimeException.class)
    public void shouldNotAcceptWrongDate() {
        LocalDate.of(1999, 13, 31);
    }
    
    @Test
    public void shouldGetDayOfWeek() {
        LocalDate theDay = LocalDate.of(2014, 1, 1);
        
        DayOfWeek dayOfWeek = theDay.getDayOfWeek();
        assertThat(dayOfWeek).isEqualTo(DayOfWeek.WEDNESDAY);
    }
}

6. JSR-310 + JDK 8 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import static org.fest.assertions.api.Assertions.*;

import org.junit.Test;

import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.zone.ZoneRules;
import java.time.zone.ZoneRulesException;
import java.time.DateTimeException;

public class Jsr310Test {
    @Test
    public void shouldGetAfterOneDay() {
        LocalDate theDay = IsoChronology.INSTANCE.date(1582, 10, 4);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd");
        assertThat(theDay.format(formatter)).isEqualTo("1582.10.04");
        
        LocalDate nextDay = theDay.plusDays(1);
        assertThat(nextDay.format(formatter)).isEqualTo("1582.10.05");
    }
    
    @Test
    public void shouldGetAfterOneHour() {
        ZoneId seoul = ZoneId.of("Asia/Seoul");
        ZonedDateTime theTime = ZonedDateTime.of(1988, 5, 7, 23, 0, 0, 0, seoul);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
        assertThat(theTime.format(formatter)).isEqualTo("1988.05.07 23:00");
        ZoneRules seoulRules = seoul.getRules();
        assertThat(seoulRules.isDaylightSavings(Instant.from(theTime))).isFalse();
        
        ZonedDateTime after1Hour = theTime.plusHours(1);
        assertThat(after1Hour.format(formatter)).isEqualTo("1988.05.08 01:00");
        assertThat(seoulRules.isDaylightSavings(Instant.from(after1Hour))).isTrue();
    }
    
    @Test
    public void shouldGetAfterOneMinute() {
        ZoneId seoul = ZoneId.of("Asia/Seoul");
        ZonedDateTime theTime = ZonedDateTime.of(1961, 8, 9, 23, 59, 59, 0, seoul);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
        assertThat(theTime.format(formatter)).isEqualTo("1961.08.09 23:59");
        
        ZonedDateTime after1Minute = theTime.plusMinutes(1);
        assertThat(after1Minute.format(formatter)).isEqualTo("1961.08.10 00:30");
    }
    
    @Test
    public void shouldGetAfterTwoSecond() {
        ZoneId utc = ZoneId.of("UTC");
        ZonedDateTime theTime = ZonedDateTime.of(2012, 6, 30, 23, 59, 59, 0, utc);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss");
        assertThat(theTime.format(formatter)).isEqualTo("2012.06.30 23:59:59");
        
        ZonedDateTime after2Seconds = theTime.plusSeconds(2);
        assertThat(after2Seconds.format(formatter)).isEqualTo("2012.07.01 00:00:01");
    }
    
    @Test(expected=ZoneRulesException.class)
    public void shouldThrowExceptionWhenWrongTimeZoneId(){
        ZoneId.of("Seoul/Asia");
    }
    
    @Test
    public void shouldGetDate() {
        LocalDate theDay = LocalDate.of(1999, 12, 31);
        
        assertThat(theDay.getYear()).isEqualTo(1999);
        assertThat(theDay.getMonthValue()).isEqualTo(12);
        assertThat(theDay.getDayOfMonth()).isEqualTo(31);
    }
    
    @Test(expected=DateTimeException.class)
    public void shouldNotAcceptWrongDate() {
        LocalDate.of(1999, 13, 31);
    }
    
    
    @Test
    public void shouldGetDayOfWeek() {
        LocalDate theDay = LocalDate.of(2014, 1, 1);
        
        DayOfWeek dayOfWeek = theDay.getDayOfWeek();
        assertThat(dayOfWeek).isEqualTo(DayOfWeek.WEDNESDAY);
    }
}

[출처 및 참고]

This post is licensed under CC BY 4.0 by the author.