오늘은 어디로 갈까...

날짜(Date) 0x01 본문

낙서

날짜(Date) 0x01

剛宇 2009. 2. 28. 13:17
지난 시간에 배운 지식을 기반으로 날짜유틸 클래스를 만들어보자.

1. Date를 Calendar로 변환하기
	/**
	 * <p><code>java.util.Date</code>를 <code>java.util.Calendar</code>로 변환한다.</p>
	 * 
	 * @param date
	 * @return
	 */
	public static Calendar toCalendar(Date date) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		return calendar;
	}
  - 지난 시간에 배운거라서 별로 어려운게 없다.

2. 년, 월, 일, 시, 분, 초를 입력받아 날짜형(Date)으로 만들기.
	/**
     * <p>년, 월, 일, 시, 분, 초를 입력받아 날짜형(Date)으로 변환한다.</p>
     * 
     * @param year 년
     * @param month 월(1-12)
     * @param day 일
     * @param hour 시
     * @param min 분
     * @param sec 초
     * @return
     */
	public static Date toDate(int year, int month, int day, int hour,
			int minute, int second) {
		Calendar calendar = Calendar.getInstance();
		calendar.set(year, month - 1, day, hour, minute, second);
		calendar.set(Calendar.MILLISECOND, 0);
		return calendar.getTime();
	}

	/**
	 * <p>년, 월, 일, 시, 분을 입력받아 날짜형(Date)으로 변환한다.(0초로 설정된다)</p>
     * 
	 * @param year 년
	 * @param month 월(1-12)
	 * @param day 일
	 * @param hour 시
	 * @param minute 분
	 * @return
	 */
	public static Date toDate(int year, int month, int day, int hour, int minute) {
		return toDate(year, month, day, hour, minute, 0);
	}

	/**
	 * <p>년, 월, 일, 시을 입력받아 날짜형(Date)으로 변환한다.(0시 0분 0초로 설정된다)</p>
     * 
	 * @param year 년
	 * @param month 월(1-12)
	 * @param day 일
	 * @return
	 */
	public static Date toDate(int year, int month, int day) {
		return toDate(year, month, day, 0, 0, 0);
	}
  - 지난 시간에 배운거라서 별로 어려운게 없다.

3. 문자열을 입력받아 날짜형(Date)으로 만들기.
	// 자동 변환용 패턴 설정
	protected static final String[] patterns = { "yyyy-MM-dd HH:mm:ss.SSS",
			"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH",
			"yyyy-MM-dd", "yyyyMMddHHmmssSSS", "yyyyMMddHHmmss",
			"yyyyMMddHHmm", "yyyyMMddHH", "yyyyMMdd", "yyMMdd" };

	private static DateFormat getDateFormat(String pattern) {
		return new SimpleDateFormat(pattern);
	}

	/**
	 * <p>문자열을 입력받아 패턴에 맞게 날짜형(Date)으로 변환하여 반환한다.</p>
	 * 
	 * <pre>
	 * DateUtils.toDate("2008-11-11 06:15:00", "yyyy-MM-dd HH:mm:ss") = Date@"2008-11-11 06:15:00"
	 * DateUtils.toDate("Time is Gold", "yyyy-MM-dd HH:mm:ss")        = ParseException
	 * DateUtils.toDate(null, *)                                      = ParseException
	 * </pre>
	 * 
	 * @param dateStr
	 * @param pattern
	 * @return
	 * @throws ParseException 변환을 실패할때 발생
	 */
	public static Date toDate(String dateStr, String pattern)
			throws ParseException {
		if (dateStr == null) {
			return null;
		}
		DateFormat dateFormat = getDateFormat(pattern);
		Date date = dateFormat.parse(dateStr);
		if (dateStr.equals(dateFormat.format(date))) {
			return date;
		} else {
			throw new ParseException(MessageUtils.format(
					"Out of bound date:\"{0}\" with format \"{1}\"", dateStr,
					pattern), 0);
		}
	}

	/**
	 * <p>문자열을 입력받아 패턴에 맞게 날짜형(Date)으로 변환하여 반환한다.</p>
	 * <p>(변환을 실패할때는 기본  날짜를 반환한다.<)/p>
	 * 
	 * <pre>
	 * DateUtils.toDate("2008-11-11 06:15:00", "yyyy-MM-dd HH:mm:ss", *)                      = Date@"2008-11-11 06:15:00"
	 * DateUtils.toDate("Time is Gold", "yyyy-MM-dd HH:mm:ss", toDate("2008-11-11 06:15:00")) = Date@"2008-11-11 06:15:00"
	 * DateUtils.toDate(null, *, toDate("2008-11-11 06:15:00"))                               = Date@"2008-11-11 06:15:00"
	 * </pre>
	 * 
	 * @param dateStr
	 * @param pattern
	 * @param defaultDate 기본 날짜
	 * @return
	 */
	public static Date toDate(String dateStr, String pattern, Date defaultDate) {
		if (dateStr == null) {
			return defaultDate;
		}
		try {
			DateFormat dateFormat = getDateFormat(pattern);
			Date date = getDateFormat(pattern).parse(dateStr);
			if (dateStr.equals(dateFormat.format(date))) {
				return date;
			}
		} catch (ParseException e) {
		}
		return defaultDate;
	}

	/**
	 * <p>문자열을 입력받아 날짜형(Date)으로 변환한다.</p>
	 * 
	 * <pre>
	 * DateUtils.toDate(null)                  = null
	 * DateUtils.toDate("2008-11-11")          = Date@"2008-11-11 00:00:00"
	 * DateUtils.toDate("2008-11-11 06:15:00") = Date@"2008-11-11 06:15:00"
	 * </pre>
	 * 
	 * <h4>지원하는형식</h4>
	 * <ul>
	 * 	<li>yyyy-MM-dd HH:mm:ss.SSS
	 * 	<li>yyyy-MM-dd HH:mm:ss
	 * 	<li>yyyy-MM-dd HH:mm
	 * 	<li>yyyy-MM-dd HH
	 * 	<li>yyyy-MM-dd
	 * 	<li>yyyyMMddHHmmssSSS
	 * 	<li>yyyyMMddHHmmss
	 * 	<li>yyyyMMddHHmm
	 * 	<li>yyyyMMddHH
	 * 	<li>yyyyMMdd
	 * </ul>
	 * @param dateStr
	 * @return
	 * @throws IllegalArgumentException 지원하지 않는 형식일 경우
	 */
	public static Date toDate(String dateStr) throws IllegalArgumentException {
		if (dateStr == null) {
			return null;
		}
		dateStr = StringUtils.trim(dateStr);
		int dateStrLen = dateStr.length();
		Date date = null;
		for (String pattern : patterns) {
			if (dateStrLen == pattern.length()) {
				DateFormat dateFormat = getDateFormat(pattern);
				try {
					date = dateFormat.parse(dateStr);
					if (dateStr.equals(dateFormat.format(date))) {
						return date;
					} else {
						date = null;
					}
				} catch (ParseException e) {
					date = null;
				}
			}
		}
		if (date == null) {
			throw new IllegalArgumentException(MessageUtils.format(
					"Illegal Argument Date String \"{0}\"", dateStr));
		}
		return date;
	}
  - 날짜형(Date)을 문자열로 출력할때 SimpleDateFormat 클래스를 사용한것처럼, 문자열을 날짜형으로 변환할때도 SimpleDateFormat 클래스를 사용하면 손쉽게 변환할 수 있다. parse(String) 메소드를 사용하면 날짜형으로 변환이 되는데, 경험상으로봐서는 별로 정확(?)하지만 않다. 그래서 데이터가 정확한지 확인하는 아래 코드를 추가한 것이다.
		Date date = dateFormat.parse(dateStr);
		if (dateStr.equals(dateFormat.format(date))) {
		}
  - 즉, 날짜형으로 변환한 값을 다시 해당 포맷으로 변환하면, 처음에 입력한 값과 같아야한다는것이다.
  - toDate(String) 메소드는 필자같은 게으름뱅이를 위해서 만듯것인데, 별로 추천할 메소드는 못된다.

4. 기준일의 다음주 월요일 가져오기.
	/**
	 * <p>해당 일을 기준으로 명시된 요일의 다음 날짜를 계산한다.</p>
	 * 
	 * <pre>
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.SUNDAY)   = "2008-07-20 06:15:00"
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.MONDAY)   = "2008-07-21 06:15:00"
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.SATURDAY) = "2008-07-26 06:15:00"
	 * </pre>
	 * 
	 * @param date
	 * @param dayOfWeek SUNDAY=1, MONDAY=2, ... SATURDAY=7
	 * @return
	 */
	public static Date getNextDate(Date date, int dayOfWeek) {
		Calendar cal = toCalendar(date);
		int amount = (7 - cal.get(Calendar.DAY_OF_WEEK)) + dayOfWeek;
		cal.add(Calendar.DAY_OF_MONTH, amount);
		return cal.getTime();
	}

	/**
	 * <p>해당 일을 기준으로 명시된 요일의 다음 날짜를 계산한다.</p>
	 * 
	 * <pre>
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.SUNDAY, false)   = "2008-07-20 06:15:00"
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.SUNDAY, true)    = "2008-07-20 00:00:00"
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.MONDAY, false)   = "2008-07-21 06:15:00"
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.MONDAY, true)    = "2008-07-21 00:00:00"
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.SATURDAY, false) = "2008-07-26 06:15:00"
	 * DateUtils.getNextDay(toDate("2008-07-19 06:15:00"), Calendar.SATURDAY, true)  = "2008-07-26 00:00:00"
	 * </pre>
	 * 
	 * @param date
	 * @param dayOfWeek SUNDAY=1, MONDAY=2, ... SATURDAY=7
	 * @param resetTime 시간 초기화 여부. <code>true</code>이면 시:분:초 0:0:0으로 설정한다.
	 * @return
	 */
	public static Date getNextDate(Date date, int dayOfWeek, boolean resetTime) {
		Calendar cal = toCalendar(date);
		if (resetTime) {
			cal.set(Calendar.HOUR_OF_DAY, 0);
			cal.set(Calendar.MINUTE, 0);
			cal.set(Calendar.SECOND, 0);
			cal.set(Calendar.MILLISECOND, 0);
		}
		int amount = (7 - cal.get(Calendar.DAY_OF_WEEK)) + dayOfWeek;
		cal.add(Calendar.DAY_OF_MONTH, amount);
		return cal.getTime();
	}
- cal.get(Calendar.DAY_OF_WEEK) 메소드를 이용해서 요일을 구한다음, 그 차이만큼 연산한면 된다.

5. 기준일이 속한 월의 마지막 날짜 가져오기.
	/**
	 * <p>해당 날짜가 속한 달의 마지막 날짜를 계산한다.</p>
	 * 
	 * <pre>
	 * DateUtils.getLastDay(toDate("2008-07-19 06:15:00"), Calendar.SUNDAY)   = "2008-07-31 06:15:00"
	 * </pre>
	 * 
	 * @param date
	 * @return
	 */
	public static Date getLastDate(Date date) {
		Calendar cal = toCalendar(date);
		int amount = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
				- cal.get(Calendar.DAY_OF_MONTH);
		cal.add(Calendar.DAY_OF_MONTH, amount);
		return cal.getTime();
	}

	/**
	 * <p>해당 날짜가 속한 달의 마지막 날짜를 계산한다.</p>
	 * 
	 * <pre>
	 * DateUtils.getLastDay(toDate("2008-07-19 06:15:00"), Calendar.SUNDAY, false)  = "2008-07-31 06:15:00"
	 * DateUtils.getLastDay(toDate("2008-07-19 06:15:00"), Calendar.SUNDAY, true)   = "2008-07-31 00:00:00"
	 * </pre>
	 * 
	 * @param date
	 * @param resetTime 시간 초기화 여부. <code>true</code>이면 시:분:초 0:0:0으로 설정한다.
	 * @return
	 */
	public static Date getLastDate(Date date, boolean resetTime) {
		Calendar cal = toCalendar(date);
		if (resetTime) {
			cal.set(Calendar.HOUR_OF_DAY, 0);
			cal.set(Calendar.MINUTE, 0);
			cal.set(Calendar.SECOND, 0);
			cal.set(Calendar.MILLISECOND, 0);
		}
		int amount = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
				- cal.get(Calendar.DAY_OF_MONTH);
		cal.add(Calendar.DAY_OF_MONTH, amount);
		return cal.getTime();
	}
- cal.getActualMaximum(int)을 이용해서 해당일의 마지막일을 구한다음 현재일과의 차이를 연산해주면 된다.

6. 두 날짜(년/월/일)가 동일한지 비교하기.
	/**
	 * <p>두 달력의 날짜(시간 무시)가 일치하는지 판단한다.</p>
	 * 
	 * @param cal1
	 * @param cal2
	 * @return
	 */
	public static boolean isSameDay(Calendar cal1, Calendar cal2) {
		if (cal1 == null) {
			return (cal2 == null);
		}
		if (cal1 == cal2) {
			return true;
		}
		return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA)
				&& cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1
				.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));
	}

	/**
	 * <p>두 날짜의 날짜(시간 무시)가 일치하는지 판단한다.</p
	 * 
	 * @param date1
	 * @param date2
	 * @return
	 */
	public static boolean isSameDay(Date date1, Date date2) {
		if (date1 == null) {
			return (date2 == null);
		}
		if (date1 == date2) {
			return true;
		}
		return isSameDay(toCalendar(date1), toCalendar(date2));
	}
- 시간 부분은 무시하고 년/월/일만 비교하는 메소드이다. 소스를 보면 Calendar.ERA의 값도 비교하게 되는데, Calendar.ERA는 율리우스력을 나타낸다. 즉 AD 또는 BC. 그리고 월과 일을 두번 비교하는게 아니라 DAY_OF_YEAR를 써서 한번에 끝내주는 센스~.

7. 두 날짜간의 기간 구하기.
	public static final long MILLIS_PER_SECOND = 1000;
	public static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
	public static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
	public static final long MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR;

	/**
	 * <p>시작일부터 종료일까지의 기간을 ms로 계산한다.</p>
	 * 
	 * <pre>
	 * DateUtils.getBetween(toDate("2008-11-11 23:59"), toDate("2008-11-12 23:58")) = 86340000
	 * DateUtils.getBetween(toDate("2008-11-11 23:59"), toDate("2008-11-12 23:59")) = 86400000
	 * DateUtils.getBetween(null, *)                                                = 0
	 * DateUtils.getBetween(*, null)                                                = 0
	 * </pre>
	 * 
	 * @param from 시작일
	 * @param to 종료일
	 * @return 기간의 1/1000초(ms)
	 */
	public static long getBetween(Date from, Date to) {
		if (from == null || to == null) {
			return 0;
		}
		return to.getTime() - from.getTime();
	}

	/**
	 * <p>시작일부터 종료일까지의 차이를 일(day)로 계산한다.
	 * (시간 단위로 계산하기 때문에 24시간이 지나지않으면 하루로 계산하지 않는다.)</p>
	 * 
	 * <pre>
	 * DateUtils.getDaysBetween(toDate("2008-11-11"), toDate("2008-11-13"))             = 2
	 * DateUtils.getDaysBetween(toDate("2008-11-11"), toDate("2008-11-11"))             = 0
	 * DateUtils.getDaysBetween(toDate("2008-11-12"), toDate("2008-11-11"))             = -1
	 * DateUtils.getDaysBetween(toDate("2008-11-11 23:59"), toDate("2008-11-12 23:58")) = 0
	 * DateUtils.getDaysBetween(toDate("2008-11-11 23:59"), toDate("2008-11-12 23:59")) = 1
	 * DateUtils.getDaysBetween(null, *)                                                = 0
	 * DateUtils.getDaysBetween(*, null)                                                = 0
	 * </pre>
	 * 
	 * @param from 시작일
	 * @param to 종료일
	 * @return 기간의 일(day)수
	 */
	public static int getDaysBetween(Date from, Date to) {
		return (int) (getBetween(from, to) / MILLIS_PER_DAY);
	}
- 두 날짜의 기간을 일/시/분/초/밀리초로 구하는것은 아주 싶다. 밀리초로 두 값의 차이를 구한다음 해당 단위로 나누면 끝이다. 그런데, 몇 월이 지났나 계산하는것은 머리를 아프게 한다. 한달은 28일/30일/31일 다르기에 우리를 괴롭힌다. 하지만 여기서 포기할 수는 없으니 한번 도전해보도록하자.

8. 두 날짜의 기간을 패턴에 의해서 년/월/일/시/분/초/밀리초로 출력하기.
 - 별다른 방법이 생각나지 않는다. 그래서 숫자를 셀때 손가락을 구부리듯이, 기간 사이의 월수를 하나씩 세어서 계산하는 방법을 사용하겠다. 혹시 좋은 알고리즘이 있으면 알려주길 바란다.
 - 일단 패턴을 분석을 하기 위한 토큰을 만들어 보겠다.
package kr.kangwoo.util.date;

import java.util.ArrayList;
import java.util.List;

import kr.kangwoo.util.StringUtils;

public class DatePatternToken {
	private Object value;
	private int count;
	
	public DatePatternToken(Object value) {
		this(value, 1);
	}
	
	public DatePatternToken(Object value, int count) {
		this.value = value;
		this.count = count;
	}
	
	public void increment() { 
		count++;
	}

	public Object getValue() {
		return value;
	}

	public int getCount() {
		return count;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof DatePatternToken) {
			DatePatternToken another = (DatePatternToken)obj;
			if (this.value.getClass() != another.value.getClass()) {
				return false;
			}
			if (this.count != another.count) {
				return false;
			}
			if (this.value instanceof StringBuilder || this.value instanceof StringBuffer) {
				return this.value.toString().equals(another.value.toString());
			} else {
				return this.value.equals(another.value);
			}
		}
		return false;
	}

	@Override
	public int hashCode() {
		return toString().hashCode();
	}

	@Override
	public String toString() {
		return StringUtils.repeat(value.toString(), count);
	}
	
	
	/**
	 * <p>해당 패턴을 분석하여 토큰을 만든다.</p>
	 * 
	 * @param pattern
	 * @return
	 */
	public static DatePatternToken[] getTokens(String pattern) {
		int patternLen = pattern.length();
		List<DatePatternToken> list = new ArrayList<DatePatternToken>(patternLen);
		
		DatePatternToken previous = null;
		StringBuilder buffer = null;
		boolean inQuote = false;
		for (int i = 0; i < patternLen; i++) {
			char ch = pattern.charAt(i);
			if(inQuote && ch != '\'') {
				buffer.append(ch);
				continue;
			}

			if (ch == '\'') {
				// espace 문자
				if(inQuote) {
					if (buffer.length() == 0) {
						// '' 일때 처리
						buffer.append(ch);
					}
					buffer = null;
					inQuote = false;
				} else {
					buffer = new StringBuilder();
					list.add(new DatePatternToken(buffer));
					inQuote = true;
				}
			} else if (ch == 'y' || ch == 'M' || ch == 'd' || ch == 'H' || ch == 'm' || ch == 's' || ch == 'S') {
				String value = String.valueOf(ch);
				if (previous != null && previous.getValue().equals(value)) {
					previous.increment();
				} else {
					DatePatternToken token = new DatePatternToken(value);
					list.add(token); 
					previous = token;
				}
				buffer = null;
			} else {
				if (buffer == null) {
					buffer = new StringBuilder();
					list.add(new DatePatternToken(buffer));
				}
				buffer.append(ch);
			}
		}
		return (DatePatternToken[])list.toArray(new DatePatternToken[list.size()]);
	}
	
	/**
	 * <p>패턴 존재유무를 파악하여 반환한다.</p>
	 * 
	 * @param tokens
	 * @return boolean[hasYears, hasMonths, hasDays, hasHours, hasMinutes, hasSeconds, hasMilliSeconds]
	 */
	public static boolean[] containsPattern(DatePatternToken[] tokens) {
		boolean[] result = new boolean[7];
		for (DatePatternToken token : tokens) {
			Object value = token.getValue();
			if ("y".equals(value)) {
				result[0] = true;
			} else if ("M".equals(value)) {
				result[1] = true;
			} else if ("d".equals(value)) {
				result[2] = true;
			} else if ("H".equals(value)) {
				result[3] = true;
			} else if ("m".equals(value)) {
				result[4] = true;
			} else if ("s".equals(value)) {
				result[5] = true;
			} else if ("S".equals(value)) {
				result[6] = true;
			}
		}
		return result;
	}
}

 - 이 날짜패턴토큰을 가지고 기간을 구하는 메소드를 만들어 보자.
	/**
	 * <p>시작일부터 종료일까지의 기간을 패턴에 맞게 출력한다.('0'으로 패딩한다.)</p>
	 * 
	 * <pre>
	 * DaetUtils.toString(toDate("1978-07-19 06:15:00.000"), toDate("2004-11-11 07:19:01.004"), "yyyy-MM-dd- HH:mm:ss.SSS") = "0026-03-23 01:04:01.004";
	 * DaetUtils.toString(toDate("1978-07-19 06:15:00.000"), toDate("2004-11-11 07:19:01.004"), "MM-dd- HH:mm:ss.SSS")      = "315-23 01:04:01.004";
	 * </pre>
	 * 
	 * @param startDate 시작일
	 * @param endDate 종료일
	 * @param pattern 패턴(yyyyMMddHHMmmssSSS)
	 * @return
	 * @since 1.1
	 */
	public static String toString(Date startDate, Date endDate, String pattern) {
		return toString(startDate, endDate, pattern, true);
	}

	/**
	 * <p>시작일부터 종료일까지의 기간을 패턴에 맞게 출력한다.</p>
	 *
	 * <pre>
	 * DaetUtils.toString(toDate("1978-07-19 06:15:00.000"), toDate("2004-11-11 07:19:01.004"), "yyyy-MM-dd- HH:mm:ss.SSS", true)  = "0026-03-23 01:04:01.004";
	 * DaetUtils.toString(toDate("1978-07-19 06:15:00.000"), toDate("2004-11-11 07:19:01.004"), "yyyy-MM-dd- HH:mm:ss.SSS", false) = "26-3-23 01:04:01.004";
	 * DaetUtils.toString(toDate("1978-07-19 06:15:00.000"), toDate("2004-11-11 07:19:01.004"), "MM-dd- HH:mm:ss.SSS", true)       = "315-23 01:04:01.004";
	 * </pre>
	 * 
	 * @param startDate 시작일
	 * @param endDate 종료일
	 * @param pattern 패턴(yyyyMMddHHMmmssSSS)
	 * @param padWithZeros '0'을 패딩할지 여부
	 * @return
	 * @since 1.1
	 */
	public static String toString(Date startDate, Date endDate, String pattern,
			boolean padWithZeros) {
		if (pattern == null) {
			return null;
		}
		DatePatternToken[] tokens = DatePatternToken.getTokens(pattern);

		boolean[] contains = DatePatternToken.containsPattern(tokens);
		boolean hasYears = contains[0], hasMonths = contains[1], hasDays = contains[2];
		boolean hasHours = contains[3], hasMinutes = contains[4], hasSeconds = contains[5];

		boolean needRevert = false;
		if (startDate.compareTo(endDate) > 0) {
			Date tempDate = startDate;
			startDate = endDate;
			endDate = tempDate;
			needRevert = true;
		}

		int years = 0;
		int months = 0;
		int days = 0;
		int hours = 0;
		int minutes = 0;
		int seconds = 0;
		int milliseconds = 0;

		if (hasMonths) {
			Calendar start = toCalendar(startDate);
			Calendar end = toCalendar(endDate);

			hours = end.get(Calendar.HOUR_OF_DAY)
					- start.get(Calendar.HOUR_OF_DAY);
			minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE);
			seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND);
			milliseconds = end.get(Calendar.MILLISECOND)
					- start.get(Calendar.MILLISECOND);

			while (milliseconds < 0) {
				milliseconds += 1000;
				seconds -= 1;
			}
			while (seconds < 0) {
				seconds += 60;
				minutes -= 1;
			}
			while (minutes < 0) {
				minutes += 60;
				hours -= 1;
			}
			while (hours < 0) {
				hours += 24;
				days -= 1;
			}

			int endDay = end.get(Calendar.DAY_OF_MONTH);
			if (end.get(Calendar.YEAR) > start.get(Calendar.YEAR)) {
				years += (end.get(Calendar.YEAR) - (start.get(Calendar.YEAR) + 1));
				months += end.get(Calendar.MONTH);
				days += endDay;

				end.set(start.get(Calendar.YEAR), 11, 1);
				end.set(Calendar.DAY_OF_MONTH, end
						.getActualMaximum(Calendar.DAY_OF_MONTH));
			}

			endDay = end.get(Calendar.DAY_OF_MONTH);
			if (end.get(Calendar.MONTH) > start.get(Calendar.MONTH)) {
				months += (end.get(Calendar.MONTH) - (start.get(Calendar.MONTH) + 1));
				days += endDay;

				if (days >= start.getActualMaximum(Calendar.DAY_OF_MONTH)) {
					months++;
					days -= start.getActualMaximum(Calendar.DAY_OF_MONTH);
				}

				end.set(end.get(Calendar.YEAR), start.get(Calendar.MONTH), 1);
				end.set(Calendar.DAY_OF_MONTH, end
						.getActualMaximum(Calendar.DAY_OF_MONTH));
			}

			days += (end.get(Calendar.DAY_OF_MONTH) - start
					.get(Calendar.DAY_OF_MONTH));

			if (days >= start.getActualMaximum(Calendar.DAY_OF_MONTH)) {
				months++;
				days -= start.getActualMaximum(Calendar.DAY_OF_MONTH);
			}

			if (hasYears) {
				if (months > 11) {
					years += (months / 12);
					months = (months % 12);
				}
			} else {
				if (years != 0) {
					months += (years * 12);
					years = 0;
				}
			}
		} else {
			long period = getBetween(startDate, endDate);
			days = (int) (period / MILLIS_PER_DAY);
			hours = (int) ((period % MILLIS_PER_DAY) / MILLIS_PER_HOUR);
			minutes = (int) ((period % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE);
			seconds = (int) ((period % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND);
			milliseconds = (int) (period % MILLIS_PER_SECOND);
		}

		if (!hasDays) {
			hours += 24 * days;
			days = 0;
		}
		if (!hasHours) {
			minutes += 60 * hours;
			hours = 0;
		}
		if (!hasMinutes) {
			seconds += 60 * minutes;
			minutes = 0;
		}
		if (!hasSeconds) {
			milliseconds += 1000 * seconds;
			seconds = 0;
		}
		String result = format(tokens, years, months, days, hours, minutes,
				seconds, milliseconds, padWithZeros);
		return needRevert ? "-" + result : result;
	}

	/**
	 * <p>패턴에 맞게 기간을 문자열로 출력한다.</p>
	 * 
	 * @param tokens
	 * @param years
	 * @param months
	 * @param days
	 * @param hours
	 * @param minutes
	 * @param seconds
	 * @param milliseconds
	 * @param padWithZeros
	 * @return
	 * @since 1.1
	 */
	protected static String format(DatePatternToken[] tokens, int years,
			int months, int days, int hours, int minutes, int seconds,
			int milliseconds, boolean padWithZeros) {
		StringBuilder result = new StringBuilder();
		for (DatePatternToken token : tokens) {
			Object value = token.getValue();
			int count = token.getCount();
			if (value instanceof StringBuilder) {
				result.append(value.toString());
			} else {
				if ("y".equals(value)) {
					result.append(padWithZeros ? StringUtils.leftPad(Integer
							.toString(years), count, "0") : Integer
							.toString(years));
				} else if ("M".equals(value)) {
					result.append(padWithZeros ? StringUtils.leftPad(Integer
							.toString(months), count, "0") : Integer
							.toString(months));
				} else if ("d".equals(value)) {
					result.append(padWithZeros ? StringUtils.leftPad(Integer
							.toString(days), count, "0") : Integer
							.toString(days));
				} else if ("H".equals(value)) {
					result.append(padWithZeros ? StringUtils.leftPad(Integer
							.toString(hours), count, "0") : Integer
							.toString(hours));
				} else if ("m".equals(value)) {
					result.append(padWithZeros ? StringUtils.leftPad(Integer
							.toString(minutes), count, "0") : Integer
							.toString(minutes));
				} else if ("s".equals(value)) {
					result.append(padWithZeros ? StringUtils.leftPad(Integer
							.toString(seconds), count, "0") : Integer
							.toString(seconds));
				} else if ("S".equals(value)) {
					result.append(padWithZeros ? StringUtils.leftPad(Integer
							.toString(milliseconds), count, "0") : Integer
							.toString(milliseconds));
				}
			}
		}
		return result.toString();
	}

 - 근데 잘보면, 버그가 존재한다. 귀찮아서 수정은 안하겠다. 한번 연구해보도록.