All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.pwall.util.ISO8601Date Maven / Gradle / Ivy

There is a newer version: 2.4
Show newest version
/*
 * @(#) ISO8601Date.java
 *
 * javautil Java Utility Library
 * Copyright (c) 2013, 2014 Peter Wall
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package net.pwall.util;

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

/**
 * ISO 8601 Date.
 *
 * @author Peter Wall
 */
public class ISO8601Date extends Date {

    private static final long serialVersionUID = 7662874565511305042L;

    private static final int[] parseDays = { -1, Calendar.MONDAY, Calendar.TUESDAY,
            Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY,
            Calendar.SUNDAY, -1, -1 }; // corresponding to input day numbers 0 - 9 (-1 == error)

    private static final char dateSeparator = '-';
    private static final char weekNumberSeparator = 'W';
    private static final char dateTimeSeparator = 'T';
    private static final char zeroTimeZoneIndicator = 'Z';
    private static final char timeSeparator = ':';
    private static final char commaDecimalSeparator = ',';
    private static final char dotDecimalSeparator = '.';
    private static final char defaultDecimalSeparator =
            (new DecimalFormatSymbols()).getDecimalSeparator();
    private static final char[] decimalSeparators =
            { commaDecimalSeparator, dotDecimalSeparator };
    private static final char plusSign = '+';
    private static final char minusSign = '-';
    private static final char[] plusOrMinus = { plusSign, minusSign };

    // Mask values for calendar fields - these are the same values used in the Calendar class,
    // but for some reason that class does not make them public
    public final static int YEAR_MASK          = (1 << Calendar.YEAR);
    public final static int MONTH_MASK         = (1 << Calendar.MONTH);
    public final static int WEEK_OF_YEAR_MASK  = (1 << Calendar.WEEK_OF_YEAR);
    public final static int DAY_OF_MONTH_MASK  = (1 << Calendar.DAY_OF_MONTH);
    public final static int DAY_OF_YEAR_MASK   = (1 << Calendar.DAY_OF_YEAR);
    public final static int DAY_OF_WEEK_MASK   = (1 << Calendar.DAY_OF_WEEK);
    public final static int HOUR_OF_DAY_MASK   = (1 << Calendar.HOUR_OF_DAY);
    public final static int MINUTE_MASK        = (1 << Calendar.MINUTE);
    public final static int SECOND_MASK        = (1 << Calendar.SECOND);
    public final static int MILLISECOND_MASK   = (1 << Calendar.MILLISECOND);
    public final static int ZONE_OFFSET_MASK   = (1 << Calendar.ZONE_OFFSET);

    private TimeZone timeZone;

    public ISO8601Date() {
        timeZone = null;
    }

    public ISO8601Date(long date) {
        super(date);
        timeZone = null;
    }

    public ISO8601Date(String date) {
        this(decode(date));
    }

    public ISO8601Date(String date, TimeZone timeZone) {
        super(decode(date).getTimeInMillis());
        setTimeZone(timeZone);
    }

    public ISO8601Date(Date date, TimeZone timeZone) {
        super(date.getTime());
        setTimeZone(timeZone);
    }

    public ISO8601Date(Calendar cal) {
        super(cal.getTimeInMillis());
        setTimeZone(cal.getTimeZone());
    }

    public ISO8601Date(ISO8601Date date) {
        this(date, date.getTimeZone());
    }

    public TimeZone getTimeZone() {
        return timeZone;
    }

    public void setTimeZone(TimeZone timeZone) {
        this.timeZone = timeZone;
    }

    @Override
    public void setTime(long time) {
        super.setTime(time);
        timeZone = null;
    }

    @Override
    public String toString() {
        return toString(this, this.timeZone);
    }

    public static String toString(Date date, TimeZone timeZone) {
        StringBuilder sb = new StringBuilder(10);
        Calendar cal =
                Calendar.getInstance(timeZone != null ? timeZone : TimeZone.getDefault());
        cal.setTime(date);
        sb.append(cal.get(Calendar.YEAR));
        sb.append(dateSeparator);
        append2Digit(sb, cal.get(Calendar.MONTH) + 1);
        sb.append(dateSeparator);
        append2Digit(sb, cal.get(Calendar.DAY_OF_MONTH));
        return sb.toString();
    }

    public static String toString(Date date) {
        return toString(date, null);
    }

    /**
     * Get a {@link Calendar} object in a suitable form for ISO 8601 dates.
     *
     * @return  {@link Calendar} object
     */
    public static Calendar getCalendar() {
        Calendar cal = Calendar.getInstance();
        cal.setLenient(false);
        cal.setMinimalDaysInFirstWeek(4);
        cal.setFirstDayOfWeek(Calendar.MONDAY);
        return cal;
    }

    /**
     * Parse an ISO 8601 date into a {@link Calendar} object.
     *
     * @param   str     the input date in {@link CharSequence} (e.g. {@link String}) format
     * @return          a {@link Calendar} object containing the date details from the input
     * @throws          IllegalArgumentException if the input date is invalid
     */
    public static Calendar decode(CharSequence str) {
        // note - the method name "parse" was already taken (and deprecated)
        if (str == null)
            throw new IllegalArgumentException("ISO8601 string must not be null");
        parse:
        {
            Calendar cal = getCalendar();
            cal.clear();
            boolean complete = false;
            ParseText text = new ParseText(str);
            if (!text.matchDecFixed(4))
                break parse;
            cal.set(Calendar.YEAR, text.getResultInt());
            if (!text.isExhausted()) {
                if (text.match(dateSeparator)) {
                    if (text.match(weekNumberSeparator)) {
                        if (!text.matchDecFixed(2))
                            break parse;
                        cal.set(Calendar.WEEK_OF_YEAR, text.getResultInt());
                        if (text.match(dateSeparator)) {
                            if (!text.matchDecFixed(1))
                                break parse;
                            cal.set(Calendar.DAY_OF_WEEK, parseDays[text.getResultInt()]);
                            complete = true;
                        }
                    }
                    else if (text.matchDecFixed(3)) {
                        cal.set(Calendar.DAY_OF_YEAR, text.getResultInt());
                        complete = true;
                    }
                    else {
                        if (!text.matchDecFixed(2))
                            break parse;
                        cal.set(Calendar.MONTH, text.getResultInt() - 1);
                        if (text.match(dateSeparator)) {
                            if (!text.matchDecFixed(2))
                                break parse;
                            cal.set(Calendar.DAY_OF_MONTH, text.getResultInt());
                            complete = true;
                        }
                    }
                    if (complete && text.match(dateTimeSeparator)) {
                        if (!text.matchDecFixed(2))
                            break parse;
                        cal.set(Calendar.HOUR_OF_DAY, text.getResultInt());
                        if (text.match(timeSeparator)) {
                            if (!text.matchDecFixed(2))
                                break parse;
                            cal.set(Calendar.MINUTE, text.getResultInt());
                            if (text.match(timeSeparator)) {
                                if (!text.matchDecFixed(2))
                                    break parse;
                                cal.set(Calendar.SECOND, text.getResultInt());
                                if (text.matchAnyOf(decimalSeparators))
                                    fractionSeconds(cal, text);
                            }
                            else if (text.matchAnyOf(decimalSeparators))
                                fractionMinutes(cal, text);
                        }
                        else if (text.matchAnyOf(decimalSeparators))
                            fractionHours(cal, text);
                    }
                    if (text.match(zeroTimeZoneIndicator))
                        cal.set(Calendar.ZONE_OFFSET, 0);
                    else if (text.matchAnyOf(plusOrMinus)) {
                        char sign = text.getResultChar();
                        if (!text.matchDecFixed(2))
                            break parse;
                        int mins = text.getResultInt() * 60;
                        if (text.match(timeSeparator)) {
                            if (!text.matchDecFixed(2))
                                break parse;
                            mins += text.getResultInt();
                        }
                        if (sign == minusSign)
                            mins = -mins;
                        cal.set(Calendar.ZONE_OFFSET, mins * 60 * 1_000);
                    }
                }
                else {
                    if (text.match(weekNumberSeparator)) {
                        if (!text.matchDecFixed(2))
                            break parse;
                        cal.set(Calendar.WEEK_OF_YEAR, text.getResultInt());
                        if (text.matchDecFixed(1)) {
                            cal.set(Calendar.DAY_OF_WEEK, parseDays[text.getResultInt()]);
                            complete = true;
                        }
                    }
                    else if (text.matchDecFixed(4)) {
                        int i = text.getResultInt();
                        cal.set(Calendar.MONTH, i / 100 - 1);
                        cal.set(Calendar.DAY_OF_MONTH, i % 100);
                        complete = true;
                    }
                    else if (text.matchDecFixed(3)) {
                        cal.set(Calendar.DAY_OF_YEAR, text.getResultInt());
                        complete = true;
                    }
                    else if (text.matchDecFixed(2)) {
                        cal.set(Calendar.MONTH, text.getResultInt() - 1);
                    }
                    if (complete && text.match(dateTimeSeparator)) {
                        if (!text.matchDecFixed(2))
                            break parse;
                        cal.set(Calendar.HOUR_OF_DAY, text.getResultInt());
                        if (text.matchDecFixed(2)) {
                            cal.set(Calendar.MINUTE, text.getResultInt());
                            if (text.matchDecFixed(2)) {
                                cal.set(Calendar.SECOND, text.getResultInt());
                                if (text.matchAnyOf(decimalSeparators))
                                    fractionSeconds(cal, text);
                            }
                            else if (text.matchAnyOf(decimalSeparators))
                                fractionMinutes(cal, text);
                        }
                        else if (text.matchAnyOf(decimalSeparators))
                            fractionHours(cal, text);
                    }
                    if (text.match(zeroTimeZoneIndicator))
                        cal.set(Calendar.ZONE_OFFSET, 0);
                    else if (text.matchAnyOf(plusOrMinus)) {
                        char sign = text.getResultChar();
                        if (!text.matchDecFixed(2))
                            break parse;
                        int mins = text.getResultInt() * 60;
                        if (text.matchDecFixed(2))
                            mins += text.getResultInt();
                        if (sign == minusSign)
                            mins = -mins;
                        cal.set(Calendar.ZONE_OFFSET, mins * 60 * 1_000);
                    }
                }
                if (!text.isExhausted())
                    break parse;
            }
            return cal;
        }
        throw new IllegalArgumentException("Illegal ISO8601 date string");
    }

    private static void fractionHours(Calendar cal, ParseText text) {
        checkFraction(text);
        long millis = adjustFraction(text.getResultLong() * 3_600, text.getResultLength());
        cal.set(Calendar.MINUTE, (int)(millis / 60_000));
        millis %= 60_000;
        if (millis != 0) {
            cal.set(Calendar.SECOND, (int)(millis / 1_000));
            millis %= 1_000;
            if (millis != 0)
                cal.set(Calendar.MILLISECOND, (int)millis);
        }
    }

    private static void fractionMinutes(Calendar cal, ParseText text) {
        checkFraction(text);
        long millis = adjustFraction(text.getResultLong() * 60, text.getResultLength());
        cal.set(Calendar.SECOND, (int)(millis / 1_000));
        millis %= 1_000;
        if (millis != 0)
            cal.set(Calendar.MILLISECOND, (int)millis);
    }

    private static void fractionSeconds(Calendar cal, ParseText text) {
        checkFraction(text);
        cal.set(Calendar.MILLISECOND,
                (int)adjustFraction(text.getResultLong(), text.getResultLength()));
    }

    private static void checkFraction(ParseText text) {
        if (!text.matchDec(9))
            throw new IllegalArgumentException("Illegal fraction in ISO8601 date string");
    }

    private static long adjustFraction(long value, int len) {
        if (len > 3) {
            for (; len > 4; len--)
                value /= 10;
            value = (value + 5) / 10;
        }
        else {
            for (; len < 3; len++)
                value *= 10;
        }
        return value;
    }

    /**
     * Convert a {@link Calendar} object to an ISO 8601 string.  This function outputs to the
     * string only those fields that are marked as having been set explicitly.
     *
     * 

The "extended" format will be used, that is, date and time separators will be * included.

* * @param cal a {@link Calendar} object * @return the date represented by the {@link Calendar} in ISO 8601 format */ public static String toString(Calendar cal) { return toString(cal, true); } /** * Convert a {@link Calendar} object to an ISO 8601 string. This function outputs to the * string only those fields that are marked as having been set explicitly. * * @param cal a {@link Calendar} object * @param extended if {@code true}, use "extended" format (include date and time * separators) * @return the date represented by the {@link Calendar} in ISO 8601 format */ public static String toString(Calendar cal, boolean extended) { int fields = 0; if (cal.isSet(Calendar.YEAR)) fields |= YEAR_MASK; if (cal.isSet(Calendar.MONTH)) fields |= MONTH_MASK; if (cal.isSet(Calendar.DAY_OF_MONTH)) fields |= DAY_OF_MONTH_MASK; if (cal.isSet(Calendar.WEEK_OF_YEAR)) fields |= WEEK_OF_YEAR_MASK; if (cal.isSet(Calendar.DAY_OF_WEEK)) fields |= DAY_OF_WEEK_MASK; if (cal.isSet(Calendar.DAY_OF_YEAR)) fields |= DAY_OF_YEAR_MASK; if (cal.isSet(Calendar.HOUR_OF_DAY)) fields |= HOUR_OF_DAY_MASK; if (cal.isSet(Calendar.MINUTE)) fields |= MINUTE_MASK; if (cal.isSet(Calendar.SECOND)) fields |= SECOND_MASK; if (cal.isSet(Calendar.MILLISECOND)) fields |= MILLISECOND_MASK; if (cal.isSet(Calendar.ZONE_OFFSET)) fields |= ZONE_OFFSET_MASK; return toString(cal, extended, fields); } /** * Convert a {@link Calendar} object to an ISO 8601 string. This function outputs to the * string only those fields selected in a bit mask of field designators. Not all field * combinations are valid. The bit mask values are: * *
*
YEAR_MASK
*
Output the year
*
MONTH_MASK
*
Output the month
*
DAY_OF_MONTH_MASK
*
Output the day of month
*
WEEK_OF_YEAR_MASK
*
Output the week of year
*
DAY_OF_WEEK_MASK
*
Output the day of week
*
DAY_OF_YEAR_MASK
*
Output the day of year
*
HOUR_OF_DAY_MASK
*
Output the hour of day
*
MINUTE_MASK
*
Output the minutes
*
SECOND_MASK
*
Output the seconds
*
MILLISECOND_MASK
*
Output the milliseconds
*
* * @param cal a {@link Calendar} object * @param extended if {@code true}, use "extended" format (include date and time * separators) * @param fields the selected fields * @return the date represented by the {@link Calendar} in ISO 8601 format */ public static String toString(Calendar cal, boolean extended, int fields) { StringBuilder sb = new StringBuilder(); if (fieldSet(fields, YEAR_MASK)) { sb.append(cal.get(Calendar.YEAR)); if (fieldSet(fields, MONTH_MASK)) { if (extended) sb.append(dateSeparator); append2Digit(sb, cal.get(Calendar.MONTH) + 1); if (fieldSet(fields, DAY_OF_MONTH_MASK)) { if (extended) sb.append(dateSeparator); append2Digit(sb, cal.get(Calendar.DAY_OF_MONTH)); if (fieldSet(fields, HOUR_OF_DAY_MASK | MINUTE_MASK | SECOND_MASK | MILLISECOND_MASK)) { sb.append(dateTimeSeparator); appendTime(sb, cal, extended, fields); } } } else if (fieldSet(fields, WEEK_OF_YEAR_MASK)) { if (extended) sb.append(dateSeparator); sb.append(weekNumberSeparator); append2Digit(sb, cal.get(Calendar.WEEK_OF_YEAR)); if (fieldSet(fields, DAY_OF_WEEK_MASK)) { int d = cal.get(Calendar.DAY_OF_WEEK); for (int i = 1; i < 8; i++) { if (parseDays[i] == d) { if (extended) sb.append(dateSeparator); sb.append(i); break; } } if (fieldSet(fields, HOUR_OF_DAY_MASK | MINUTE_MASK | SECOND_MASK | MILLISECOND_MASK)) { sb.append(dateTimeSeparator); appendTime(sb, cal, extended, fields); } } } else if (fieldSet(fields, DAY_OF_YEAR_MASK)) { if (extended) sb.append(dateSeparator); append3Digit(sb, cal.get(Calendar.DAY_OF_YEAR)); if (fieldSet(fields, HOUR_OF_DAY_MASK | MINUTE_MASK | SECOND_MASK | MILLISECOND_MASK)) { sb.append(dateTimeSeparator); appendTime(sb, cal, extended, fields); } } } else appendTime(sb, cal, extended, fields); if (fieldSet(fields, ZONE_OFFSET_MASK)) { int mins = cal.get(Calendar.ZONE_OFFSET) / 60_000; if (mins == 0) sb.append(zeroTimeZoneIndicator); else { sb.append(mins < 0 ? minusSign : plusSign); mins = Math.abs(mins); append2Digit(sb, mins / 60); mins %= 60; if (extended) sb.append(timeSeparator); append2Digit(sb, mins); } } return sb.toString(); } private static boolean fieldSet(int fields, int mask) { return (fields & mask) != 0; } private static void appendTime(StringBuilder sb, Calendar cal, boolean extended, int fields) { append2Digit(sb, cal.get(Calendar.HOUR_OF_DAY)); if ((fields & (MINUTE_MASK | SECOND_MASK | MILLISECOND_MASK)) != 0) { if (extended) sb.append(timeSeparator); append2Digit(sb, cal.get(Calendar.MINUTE)); if ((fields & (SECOND_MASK | MILLISECOND_MASK)) != 0) { if (extended) sb.append(timeSeparator); append2Digit(sb, cal.get(Calendar.SECOND)); if ((fields & MILLISECOND_MASK) != 0) { sb.append(defaultDecimalSeparator); append3Digit(sb, cal.get(Calendar.MILLISECOND)); } } } } private static void append2Digit(StringBuilder sb, int n) { n = Math.abs(n) % 100; if (n < 10) sb.append('0'); sb.append(n); } private static void append3Digit(StringBuilder sb, int n) { n = Math.abs(n) % 1_000; if (n < 100) { sb.append('0'); if (n < 10) sb.append('0'); } sb.append(n); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy