OceanusDateFormatter.java

/*******************************************************************************
 * Oceanus: Java Utilities
 * Copyright 2012,2025 Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package net.sourceforge.joceanus.oceanus.date;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

import net.sourceforge.joceanus.oceanus.convert.OceanusDataConverter;
import net.sourceforge.joceanus.oceanus.event.OceanusEventManager;
import net.sourceforge.joceanus.oceanus.event.OceanusEventRegistrar;
import net.sourceforge.joceanus.oceanus.event.OceanusEventRegistrar.OceanusEventProvider;

/**
 * Formatter for Date objects.
 * @author Tony Washer
 */
public class OceanusDateFormatter
        implements OceanusEventProvider<OceanusDateEvent> {
    /**
     * Date Byte length.
     */
    public static final int BYTE_LEN = Long.BYTES;

    /**
     * As of Java 16.0.2 the short format for September is Sept instead of Sep.
     */
    private static final int PATCH_JAVA_VER = 16;

    /**
     * As of Java 16.0.2 the short format for September is Sept instead of Sep.
     */
    private static final String PATCH_SEPT_NEW = "-Sept-";

    /**
     * As of Java 16.0.2 the short format for September is Sept instead of Sep.
     */
    private static final String PATCH_SEPT_OLD = "-Sep-";

    /**
     * The default format.
     */
    private static final String DEFAULT_FORMAT = "dd-MMM-yyyy";

    /**
     * One hundred years.
     */
    private static final int YEARS_CENTURY = 100;

    /**
     * The Event Manager.
     */
    private final OceanusEventManager<OceanusDateEvent> theEventManager;

    /**
     * The locale.
     */
    private Locale theLocale;

    /**
     * The Simple Date format for the locale and format string.
     */
    private String theFormat;

    /**
     * The Simple Date format for the locale and format string.
     */
    private SimpleDateFormat theDateFormat;

    /**
     * The DateTime format for the locale and format string.
     */
    private DateTimeFormatter theLocalDateFormat;

    /**
     * Constructor.
     */
    public OceanusDateFormatter() {
        /* Use default locale */
        this(Locale.getDefault());
    }

    /**
     * Constructor.
     * @param pLocale the locale
     */
    public OceanusDateFormatter(final Locale pLocale) {
        /* Store locale */
        theLocale = pLocale;
        theEventManager = new OceanusEventManager<>();
        setFormat(DEFAULT_FORMAT);
    }

    @Override
    public OceanusEventRegistrar<OceanusDateEvent> getEventRegistrar() {
        return theEventManager.getEventRegistrar();
    }

    /**
     * Set the date format.
     * @param pFormat the format string
     */
    public final void setFormat(final String pFormat) {
        /* If the format is the same */
        if (pFormat.equals(theFormat)) {
            /* Ignore */
            return;
        }

        /* Create the simple date format */
        theFormat = pFormat;
        theDateFormat = new SimpleDateFormat(theFormat, theLocale);
        theLocalDateFormat = DateTimeFormatter.ofPattern(theFormat, theLocale);

        /* Notify of the change */
        theEventManager.fireEvent(OceanusDateEvent.FORMATCHANGED);
    }

    /**
     * Set the locale.
     * @param pLocale the locale
     */
    public final void setLocale(final Locale pLocale) {
        /* If the locale is the same */
        if (theLocale.equals(pLocale)) {
            /* Ignore */
            return;
        }

        /* Store the locale */
        theLocale = pLocale;
        final String pFormat = theFormat;
        theFormat = null;
        setFormat(pFormat);
    }

    /**
     * Format a calendar Date.
     * @param pDate the date to format
     * @return the formatted date
     */
    public String formatCalendarDay(final Calendar pDate) {
        /* Handle null */
        if (pDate == null) {
            return null;
        }

        /* Format the date */
        return formatJavaDate(pDate.getTime());
    }

    /**
     * Format a local Date.
     * @param pDate the date to format
     * @return the formatted date
     */
    public String formatLocalDate(final LocalDate pDate) {
        /* Handle null */
        if (pDate == null) {
            return null;
        }

        /* Format the date */
        return pDate.format(theLocalDateFormat);
    }

    /**
     * Format a java Date.
     * @param pDate the date to format
     * @return the formatted date
     */
    public String formatJavaDate(final Date pDate) {
        /* Handle null */
        if (pDate == null) {
            return null;
        }

        /* Format the date */
        return theDateFormat.format(pDate);
    }

    /**
     * Format a Date.
     * @param pDate the date to format
     * @return the formatted date
     */
    public String formatDate(final OceanusDate pDate) {
        /* Handle null */
        if (pDate == null) {
            return null;
        }

        /* Format the date */
        return formatLocalDate(pDate.getDate());
    }

    /**
     * Format a DateRange.
     * @param pRange the range to format
     * @return the formatted date
     */
    public String formatDateRange(final OceanusDateRange pRange) {
        /* Handle null */
        if (pRange == null) {
            return null;
        }

        /* Access components */
        final OceanusDate myStart = pRange.getStart();
        final OceanusDate myEnd = pRange.getEnd();

        /* Build range description */
        return ((myStart == null)
                ? OceanusDateRange.DESC_UNBOUNDED
                : formatDate(myStart))
                    + OceanusDateRange.CHAR_BLANK
                    + OceanusDateRange.DESC_LINK
                    + OceanusDateRange.CHAR_BLANK
                    + ((myEnd == null)
                        ? OceanusDateRange.DESC_UNBOUNDED
                        : formatDate(myEnd));
    }

    /**
     * Parse Java Date.
     * @param pValue Formatted Date
     * @return the Date
     * @throws IllegalArgumentException on error
     */
    public Date parseJavaDate(final String pValue) {
        /* Parse the date */
        try {
            return theDateFormat.parse(pValue);
        } catch (ParseException e) {
            throw new IllegalArgumentException("Invalid date: "
                                               + pValue, e);
        }
    }

    /**
     * Parse LocalDate.
     * @param pValue Formatted Date
     * @return the Date
     * @throws IllegalArgumentException on error
     */
    public LocalDate parseLocalDate(final String pValue) {
        /* Parse the date */
        try {
            return LocalDate.parse(pValue, theLocalDateFormat);
        } catch (DateTimeParseException e) {
            /* Handle Patch for Java 16.0.2 change for September */
            final int myVersion = Runtime.version().feature();
            if (pValue.contains(PATCH_SEPT_OLD)
                    && myVersion >= PATCH_JAVA_VER) {
                return parsePatchedLocalDate(pValue.replace(PATCH_SEPT_OLD, PATCH_SEPT_NEW));
            }
            if (pValue.contains(PATCH_SEPT_NEW)
                    && Runtime.version().feature() < PATCH_JAVA_VER) {
                return parsePatchedLocalDate(pValue.replace(PATCH_SEPT_NEW, PATCH_SEPT_OLD));
            }

            /* throw exception */
            throw new IllegalArgumentException("Invalid date: "
                                               + pValue, e);
        }
    }

    /**
     * Parse LocalDate.
     * @param pValue Formatted Date
     * @return the Date
     * @throws IllegalArgumentException on error
     */
    private LocalDate parsePatchedLocalDate(final String pValue) {
        /* Parse the date */
        try {
            return LocalDate.parse(pValue, theLocalDateFormat);
        } catch (DateTimeParseException e) {
            /* throw exception */
            throw new IllegalArgumentException("Invalid date: "
                    + pValue, e);
        }
    }

    /**
     * Parse CalendarDay.
     * @param pValue Formatted CalendarDay
     * @return the CalendarDay
     * @throws IllegalArgumentException on error
     */
    public Calendar parseCalendarDay(final String pValue) {
        final Date myDate = parseJavaDate(pValue);
        final Calendar myCalendar = Calendar.getInstance(theLocale);
        myCalendar.setTime(myDate);
        return myCalendar;
    }

    /**
     * Parse Date.
     * @param pValue Formatted Date
     * @return the DateDay
     * @throws IllegalArgumentException on error
     */
    public OceanusDate parseDate(final String pValue) {
        final LocalDate myDate = parseLocalDate(pValue);
        return new OceanusDate(myDate);
    }

    /**
     * Parse Date using the passed year as base date.
     * <p>
     * This is used when a two digit year is utilised
     * @param pValue Formatted DateDay
     * @param pBaseYear the base year
     * @return the DateDay
     * @throws IllegalArgumentException on error
     */
    public OceanusDate parseDateBase(final String pValue,
                                     final int pBaseYear) {
        LocalDate myDate = parseLocalDate(pValue);
        if (myDate.getYear() >= pBaseYear + YEARS_CENTURY) {
            myDate = myDate.minusYears(YEARS_CENTURY);
        }
        return new OceanusDate(myDate);
    }

    /**
     * Obtain the locale.
     * @return the locale
     */
    public Locale getLocale() {
        return theLocale;
    }

    /**
     * Create a byte array representation of a date.
     * @param pDate the date to process
     * @return the processed date
     */
    public byte[] toBytes(final OceanusDate pDate) {
        final long myEpoch = pDate.getDate().toEpochDay();
        return OceanusDataConverter.longToByteArray(myEpoch);
    }

    /**
     * Parse a byte array representation of a date.
     * @param pBuffer the byte representation
     * @return the date
     */
    public OceanusDate fromBytes(final byte[] pBuffer) {
        if (pBuffer == null || pBuffer.length < Long.BYTES) {
            throw new IllegalArgumentException();
        }
        final long myEpoch = OceanusDataConverter.byteArrayToLong(pBuffer);
        final LocalDate myDate = LocalDate.ofEpochDay(myEpoch);
        return new OceanusDate(myDate);
    }
}