오늘은 어디로 갈까...

한글(HangulMessageFormat) 0x00 본문

낙서

한글(HangulMessageFormat) 0x00

剛宇 2009. 2. 21. 09:31
오늘은 지난 시간에 만든 한글(Hangul) 클래스를 조금 이용하여 조사 처리하는 메시지 클래스를 만들어보자.
자바에는 java.util.MessageFormat 이라는 아주 유용한 메시지 포맷 클래스가 존재한다.
하지만 불행히도 한글 조사 처리를 해주지 않는다. 그리고 너무나 기능이 많다보니 약간의 오버헤드(overhead)가 있다.
그래서, 한글 조사 처리 기능만 추가한 아주 간단한 MessageFormat 클래스를 만들어보도록 하자.
너무 간단하여 메소드 안에 구현되어서, 복수의 쓰레드(Thead)가 동시에 접근해도 안전하다. (아마도..)

1. 간단해서 별 다른 설명이 필요없을거 같다. 소스를 보고 파악하자.

package kr.kangwoo.util.hangul;

import java.util.Date;

import kr.kangwoo.util.StringUtils;

public class HangulMessageFormat {

	private static final char QUOTE = '\'';
	private static final char OPEN_BRACES = '{';
	private static final char CLOSE_BRACES = '}';
	private static final char BLANK = ' ';
	
	private String pattern;
	private int patternLen;
	
	private boolean enableAux = true;	// 알맞은 조사 붙이기 여부
	
	/**
	 * HangulMessageFormat의 생성자
	 * 
	 * @param pattern 패턴
	 */
	public HangulMessageFormat(String pattern) {
		this(pattern, true);
	}
	
	/**
	 * HangulMessageFormat의 생성자
	 * 
	 * @param pattern 패턴
	 * @param enableAux 알맞은 조사 붙이기 사용여부
	 */
	public HangulMessageFormat(String pattern, boolean enableAux) {
		this.pattern = pattern;
		this.patternLen = StringUtils.isNotEmpty(pattern) ? pattern.length() : 0;
		this.enableAux = enableAux;
	}
	
	/**
	 * <p>패턴에 맞게 인자를 치환하여 메시지를 생성한다.</p>
	 * 
	 * @param arguments
	 * @return
	 */
	public String format(Object argument) {
		return format(new Object[] {argument});
	}
	
	/**
	 * <p>패턴에 맞게 인자를 치환하여 메시지를 생성한다.</p>
	 * 
	 * @param arguments
	 * @return
	 */
	public String format(Object[] arguments) {
		if (patternLen < 1 || arguments == null) {
			return pattern;
		}
		
		StringBuilder result = new StringBuilder();
		int beginIndex = 0, index = 0;
		boolean inQuote = false;
		for (index = 0; index < patternLen; index++) {
			char ch = pattern.charAt(index);
			if (inQuote) {
				if (ch == QUOTE) {
					// 따옴표 안에거는 그대로 표현한다.
					result.append(pattern.substring(beginIndex, index));
					beginIndex = ++index;
					inQuote = false;
				}
			} else {
				if (ch == QUOTE) {
					result.append(pattern.substring(beginIndex, index));
					beginIndex = ++index;
					if (index < patternLen && pattern.charAt(index) == QUOTE) {
						// 이후에 따옴표가 붙음
					} else {
						inQuote = true;	
					}
				} else if (ch == OPEN_BRACES){
					result.append(pattern.substring(beginIndex, index));
					beginIndex = ++index;

					for ( ; index < patternLen; index++) {
						if (pattern.charAt(index) == CLOSE_BRACES) {
							break;
						}
					}
					
					if (index == beginIndex) {
						// 인자 번호가 없음({})
						// 현재는 그대로 붙이게 처리
						beginIndex = beginIndex - 1;
					} else if (index == patternLen) {
						// 닫기 괄호가 없음.
						// 현재는 그대로 붙이게 처리
						beginIndex = beginIndex - 1;
					} else {
						String arg = pattern.substring(beginIndex, index);
						beginIndex = ++index;
						
						int argumentNumber = toInteger(arg, arguments.length);
						if (arguments == null || argumentNumber >= arguments.length) {
							result.append(OPEN_BRACES).append(arg).append(CLOSE_BRACES);
						} else {
							Object obj = arguments[argumentNumber];
							boolean needAux = false;	// 조사 붙이기 여부
							if (obj == null) {
								arg = "null";
							} else if (obj instanceof String) {
								arg = (String)obj;
								if (arg.length() > 0) {
									// 마지막 글자가 한글인지 판단하기
									needAux = Hangul.isHangulSyllables(arg.charAt(arg.length() - 1));	
								}
							} else if (obj instanceof Number) {
								arg = obj.toString();
							} else if (obj instanceof Date) {
								arg = obj.toString();
							} else {
			                    arg = obj.toString();
			                    if (arg == null) arg = "null";
							}
							result.append(arg);

							// 조사 붙이기
							if (enableAux && needAux) {
								String aux = null;
								
								// 조사를 붙이기 위해 공백까지 글자 가져오기
								for ( ; index < patternLen; index++) {
									if (pattern.charAt(index) == BLANK) {
										break;
									}
								}
								String auxPattern = pattern.substring(beginIndex, index);
								beginIndex = index;
								if (auxPattern.length() > 0 && arg.length() > 0) {
									char lastChar = arg.charAt(arg.length() - 1);
									if (Hangul.isHangulSyllables(lastChar)) {
										String aux1 = StringUtils.substringBefore(auxPattern, ";");
										String aux2 = StringUtils.substringAfter(auxPattern, ";");
										aux = Hangul.getJosa(lastChar, aux1, aux2);
									} else {
										// 한글이 아닐 경우 조사 처리
										aux = StringUtils.substringBefore(auxPattern, ";");
									}
								}
								
								if (aux != null) {
									result.append(aux);	
								} else {
									result.append(auxPattern);	
								}
							}
						}
					}
				} 
			}
		} // end for
		if (beginIndex < patternLen) {
			result.append(pattern.substring(beginIndex, patternLen));	
		}
		
		return result.toString();
	}
	
	/**
	 * <p>패턴에 맞게 인자를 치환하여 메시지를 생성한다.</p>
	 * 
	 * @param pattern
	 * @param arguments
	 * @return
	 */
	public static String format(String pattern, Object... arguments) {
		HangulMessageFormat temp = new HangulMessageFormat(pattern);
		return temp.format(arguments);
	}
	
	
	/**
	 * 문자열을 숫자(int)형으로 변환한다.
	 * 
	 * <pre>
	 * toInteger(null, 0)   = 0
	 * toInteger("NaN", 0)  = 0
	 * toInteger("1234", 0) = 1234
	 * </pre>
	 * 
	 * @param value
	 * @param defaultValue 기본값
	 * @return
	 */
	private int toInteger(String value, int defaultValue) {
		if (value == null) {
			return defaultValue;
		}
		int result = defaultValue;
		try {
			result = Integer.parseInt(value);
		} catch (NumberFormatException e) {
			// ignore
		}
		
		return result;
	}
}

2. 테스트 케이스

package kr.kangwoo.util.hangul;

import junit.framework.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class HangulMessageFormatTest {

	@Before
	public void setUp() throws Exception {
	}

	@After
	public void tearDown() throws Exception {
	}

	@Test
	public void testFormat() {
		HangulMessageFormat hmf = new HangulMessageFormat("{0}을;를 입력하세요");
		Assert.assertEquals(hmf.format("이름"), "이름을 입력하세요");
		Assert.assertEquals(hmf.format("나이"), "나이를 입력하세요");
		// 첨자가  null일 경우는 그대로 출력
		Assert.assertEquals(hmf.format(null), "{0}을;를 입력하세요");
		// 영어는 조사 처리 안하고 그대로 출력
		Assert.assertEquals(hmf.format("Age"), "Age을;를 입력하세요");
		// 첨자가 공백("")일 경우 그대로 출력
		Assert.assertEquals(hmf.format(""), "을;를 입력하세요");
		
		// 작은 따옴표 사용시 내용 그대로 출력
		Assert.assertEquals(HangulMessageFormat.format("'{0}'을;를 입력하세요", "이름"), "{0}을;를 입력하세요");
		
		// 작은 따옴표 2개 사용시 작은 따옴표 출력
		Assert.assertEquals(HangulMessageFormat.format("Blue''s {0}을;를 입력하세요", "이름"), "Blue's 이름을 입력하세요");	
	}

}

3. 뱀다리
  - 현재는 첨자 치환, 첨자에 맞는 조사 붙이기 기능 밖에 없는데, 여기에다 날짜 포맷, 숫자 포맷만 추가해 준다면 쓸만해지지 않을까....