본문 바로가기

Programing/JVM(Java, Kotlin)

[java] Java 8의 시간은 10000년을 파싱 못한다?

10000년까지 살아 있을지는 모르겠지만 테스트 케이스를 만들었는데

LocalDate.parse("10000-01-01")

위와 같은 코드는 아래와 같은 파싱 예외가 발생한다.

java.time.format.DateTimeParseException: Text '10000-01-01' could not be parsed at index 0

	at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
	at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
	at java.time.LocalDate.parse(LocalDate.java:400)
	at java.time.LocalDate.parse(LocalDate.java:385)

사실 원인은 포매터가 아래와 같이 구현되어 있기 때문이다.

public final class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
    // ..
    public static LocalDate parse(CharSequence text) {
        return parse(text, DateTimeFormatter.ISO_LOCAL_DATE);
    }
    
    public static final DateTimeFormatter ISO_LOCAL_DATE;
    static {
        ISO_LOCAL_DATE = new DateTimeFormatterBuilder()
                .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
                .appendLiteral('-')
                .appendValue(MONTH_OF_YEAR, 2)
                .appendLiteral('-')
                .appendValue(DAY_OF_MONTH, 2)
                .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
    }

분명 java.time.temporal.ChronoField#YEAR 를 추가할 때 최소 4자리 최대 10자리라고 되어 있다.

근데 왜 고작 5자리가 파싱이 안되는 걸까?

 

DateTimeFormatterBuilder에 있는 NumberPinterParser는 아래와 같이 필드 및 생성자가 구현되어 있다.

static class NumberPrinterParser implements DateTimePrinterParser {

    final TemporalField field;
    final int minWidth;
    final int maxWidth;
    private final SignStyle signStyle;
    final int subsequentWidth;

    /**
     * Constructor.
     *
     * @param field  the field to format, not null
     * @param minWidth  the minimum field width, from 1 to 19
     * @param maxWidth  the maximum field width, from minWidth to 19
     * @param signStyle  the positive/negative sign style, not null
     */
    NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) {
        // validated by caller
        this.field = field;
        this.minWidth = minWidth;
        this.maxWidth = maxWidth;
        this.signStyle = signStyle;
        this.subsequentWidth = 0;
    }

DateTimeFormatterBuilder의 파싱하는 부분을 살펴보면 아래부분이 초과시에 기호를 붙이는지 여부를 검사를 하는 것을 알 수 있다.

if (signStyle == SignStyle.EXCEEDS_PAD && context.isStrict()) 안쪽이다.

주석도 해당 내용을 설명하고 있다.

// static class NumberPrinterParser implements DateTimePrinterParser {
@Override
public int parse(DateTimeParseContext context, CharSequence text, int position) {
    // ..
    for (int pass = 0; pass < 2; pass++) {
        int maxEndPos = Math.min(pos + effMaxWidth, length);
        while (pos < maxEndPos) {
            // .. 길이마다 처리하는 부분
    }
    if (negative) {
        if (totalBig != null) {
            if (totalBig.equals(BigInteger.ZERO) && context.isStrict()) {
                return ~(position - 1);  // minus zero not allowed
            }
            totalBig = totalBig.negate();
        } else {
            if (total == 0 && context.isStrict()) {
                return ~(position - 1);  // minus zero not allowed
            }
            total = -total;
        }
    } else if (signStyle == SignStyle.EXCEEDS_PAD && context.isStrict()) {
        int parseLen = pos - position;
        if (positive) {
            if (parseLen <= minWidth) {
                return ~(position - 1);  // '+' only parsed if minWidth exceeded
            }
        } else {
            if (parseLen > minWidth) {
                return ~position;  // '+' must be parsed if minWidth exceeded
            }
        }
    }
    // ..

결국 + 가 없는 채로 파싱이 되면 parseLen이 최소 길이보다 커지게 되어 응답(position)이  -1로 반환이 되어 에러로 빠지게 된다.

정상이라면 postion이 0보다 크거나 같아야 하기 때문이다.

static final class CompositePrinterParser implements DateTimePrinterParser {
    // ..
    @Override
    public int parse(DateTimeParseContext context, CharSequence text, int position) {
        if (optional) {
            context.startOptional();
            int pos = position;
            for (DateTimePrinterParser pp : printerParsers) {
                pos = pp.parse(context, text, pos);
                if (pos < 0) {
                    context.endOptional(false);
                    return position;  // return original position
                }
            }
            context.endOptional(true);
            return pos;
        } else {
            for (DateTimePrinterParser pp : printerParsers) {
                position = pp.parse(context, text, position);
                if (position < 0) {
                    break;
                }
            }
            return position;
        }
    }

레퍼런스 체크!

그런데 Java8의 Year 클래스의 parse 메서드의 JavaDoc 문서를 보면 0000 ~ 9999 밖의 년도에는 + 나 - 기호를 붙여야 한다고 되어 있다.

올바른 방법

따라서 해결책은 간단하다.

LocalDate.parse("+10000-01-01")

근데 살아 생전에 3000년도 겪지 않을 텐데 테스트 코드를 만들다 좀 머~언 미래에 갔다왔다.

 

Y10K 문제

참고로 2000년이 될 때 많은 사람들이 걱정했던 밀레니엄 버그의 다른 버전이 Y10K 문제라고 부른다는 것을 알게되었다.

자세한 것은 아래 위키 백과 참고.

https://en.wikipedia.org/wiki/Year_10,000_problem