JavaTimeConverters.java

// © 2024 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html

package com.ibm.icu.impl;

import static java.time.temporal.ChronoField.MILLI_OF_SECOND;

import com.ibm.icu.util.BuddhistCalendar;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.JapaneseCalendar;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.time.temporal.Temporal;
import java.util.Date;

/**
 * This class provides utility methods for converting between Java 8's {@code java.time} classes and
 * the {@link com.ibm.icu.util.Calendar} and related classes from the {@code com.ibm.icu.util}
 * package.
 *
 * <p>The class includes methods for converting various temporal types, such as {@link
 * ZonedDateTime}, {@link OffsetTime}, {@link OffsetDateTime}, {@link LocalTime}, {@link
 * ChronoLocalDate}, and {@link ChronoLocalDateTime}, to {@link Calendar} instances.
 *
 * <p>Additionally, it provides methods to convert between {@link ZoneId} and {@link TimeZone}, and
 * {@link ZoneOffset} and {@link TimeZone}.
 *
 * @internal
 * @deprecated This API is ICU internal only.
 */
@Deprecated
public class JavaTimeConverters {
    // Milliseconds per hour
    private static final long MILLIS_PER_HOUR = 60 * 60 * 1_000;
    // Milliseconds per day
    private static final long MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR;

    private JavaTimeConverters() {
        // Prevent instantiation, making this an utility class
    }

    /**
     * Converts a {@link ZonedDateTime} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents the same date and time as
     * the specified {@link ZonedDateTime}, taking into account the time zone information associated
     * with the {@link ZonedDateTime}.
     *
     * @param dateTime The {@link ZonedDateTime} to convert.
     * @return A {@link Calendar} instance representing the same date and time as the specified
     *     {@link ZonedDateTime}, with the time zone set accordingly.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static Calendar temporalToCalendar(ZonedDateTime dateTime) {
        long epochMillis = dateTime.toEpochSecond() * 1_000 + dateTime.get(MILLI_OF_SECOND);
        TimeZone icuTimeZone = zoneIdToTimeZone(dateTime.getZone());
        return millisToCalendar(epochMillis, icuTimeZone);
    }

    /**
     * Converts an {@link OffsetTime} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents the same time of day as
     * the specified {@link OffsetTime}, taking into account the offset from UTC associated with the
     * {@link OffsetTime}. The resulting {@link Calendar} will have its date components (year,
     * month, day) set to the current date in the time zone represented by the offset.
     *
     * @param time The {@link OffsetTime} to convert.
     * @return A {@link Calendar} instance representing the same time of day as the specified {@link
     *     OffsetTime}, with the time zone set accordingly and date components set to the current
     *     date in that time zone.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    @SuppressWarnings("JavaTimeDefaultTimeZone")
    public static Calendar temporalToCalendar(OffsetTime time) {
        return temporalToCalendar(time.atDate(LocalDate.now()));
    }

    /**
     * Converts an {@link OffsetDateTime} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents the same date and time as
     * the specified {@link OffsetDateTime}, taking into account the offset from UTC associated with
     * the {@link OffsetDateTime}.
     *
     * @param dateTime The {@link OffsetDateTime} to convert.
     * @return A {@link Calendar} instance representing the same date and time as the specified
     *     {@link OffsetDateTime}, with the time zone set accordingly.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static Calendar temporalToCalendar(OffsetDateTime dateTime) {
        long epochMillis = dateTime.toEpochSecond() * 1_000 + dateTime.get(MILLI_OF_SECOND);
        TimeZone icuTimeZone = zoneOffsetToTimeZone(dateTime.getOffset());
        return millisToCalendar(epochMillis, icuTimeZone);
    }

    /**
     * Converts a {@link ChronoLocalDate} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents the same date as the
     * specified {@link ChronoLocalDate}. The resulting {@link Calendar} will be in the default time
     * zone of the JVM and will have its time components (hour, minute, second, millisecond) set to
     * zero.
     *
     * @param date The {@link ChronoLocalDate} to convert.
     * @return A {@link Calendar} instance representing the same date as the specified {@link
     *     ChronoLocalDate}, with time components set to zero.
     */
    @Deprecated
    static Calendar temporalToCalendar(ChronoLocalDate date) {
        long epochMillis = date.toEpochDay() * MILLIS_PER_DAY;
        return millisToCalendar(epochMillis);
    }

    /**
     * Converts a {@link LocalTime} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents the same time of day as
     * the specified {@link LocalTime}. The resulting {@link Calendar} will be in the default time
     * zone of the JVM and will have its date components (year, month, day) set to the current date
     * in the default time zone.
     *
     * @param time The {@link LocalTime} to convert.
     * @return A {@link Calendar} instance representing the same time of day as the specified {@link
     *     LocalTime}, with date components set to the current date in the default time zone.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static Calendar temporalToCalendar(LocalTime time) {
        long epochMillis = time.toNanoOfDay() / 1_000_000;
        return millisToCalendar(epochMillis);
    }

    /**
     * Converts a {@link ChronoLocalDateTime} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents the same date and time as
     * the specified {@link ChronoLocalDateTime}. The resulting {@link Calendar} will be in the
     * default time zone of the JVM.
     *
     * @param dateTime The {@link ChronoLocalDateTime} to convert.
     * @return A {@link Calendar} instance representing the same date and time as the specified
     *     {@link ChronoLocalDateTime}.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static Calendar temporalToCalendar(LocalDateTime dateTime) {
        ZoneOffset zoneOffset = ZoneId.systemDefault().getRules().getOffset(dateTime);
        long epochMillis =
                dateTime.toEpochSecond(zoneOffset) * 1_000 + dateTime.get(MILLI_OF_SECOND);
        return millisToCalendar(epochMillis, TimeZone.getDefault());
    }

    /**
     * Converts a {@link Temporal} to a {@link Calendar}.
     *
     * @param temp The {@link Temporal} to convert.
     * @return A {@link Calendar} instance representing the same date and time as the specified
     *     {@link Temporal}.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static Calendar temporalToCalendar(Temporal temp) {
        if (temp instanceof Instant) {
            throw new IllegalArgumentException(
                    "java.time.Instant cannot be formatted,"
                            + " it does not have enough information");
        } else if (temp instanceof ZonedDateTime) {
            return temporalToCalendar((ZonedDateTime) temp);
        } else if (temp instanceof OffsetDateTime) {
            return temporalToCalendar((OffsetDateTime) temp);
        } else if (temp instanceof OffsetTime) {
            return temporalToCalendar((OffsetTime) temp);
        } else if (temp instanceof LocalDate) {
            return temporalToCalendar((LocalDate) temp);
        } else if (temp instanceof LocalDateTime) {
            return temporalToCalendar((LocalDateTime) temp);
        } else if (temp instanceof LocalTime) {
            return temporalToCalendar((LocalTime) temp);
        } else if (temp instanceof ChronoLocalDate) {
            return temporalToCalendar((ChronoLocalDate) temp);
        } else if (temp instanceof ChronoLocalDateTime) {
            return temporalToCalendar((ChronoLocalDateTime<?>) temp);
        } else {
            throw new IllegalArgumentException(
                    "This type cannot be formatted: " + temp.getClass().getName());
        }
    }

    /**
     * Converts a {@link ZoneId} to a {@link TimeZone}.
     *
     * <p>This method creates a {@link TimeZone} from the specified {@link ZoneId}. The resulting
     * {@link TimeZone} will represent the time zone rules associated with the given {@link ZoneId}.
     *
     * @param zoneId The zone ID to convert.
     * @return A {@link TimeZone} representing the time zone rules associated with the given {@link
     *     ZoneId}.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static TimeZone zoneIdToTimeZone(ZoneId zoneId) {
        return TimeZone.getTimeZone(zoneId.getId());
    }

    /**
     * Converts a {@link ZoneOffset} to a {@link TimeZone}.
     *
     * <p>This method creates a {@link TimeZone} that has a fixed offset from UTC, represented by
     * the given {@link ZoneOffset}.
     *
     * @param zoneOffset The zone offset to convert.
     * @return A {@link TimeZone} that has a fixed offset from UTC, represented by the given {@link
     *     ZoneOffset}.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static TimeZone zoneOffsetToTimeZone(ZoneOffset zoneOffset) {
        return new SimpleTimeZone(zoneOffset.getTotalSeconds() * 1_000, zoneOffset.getId());
    }

    /**
     * Converts a {@link DayOfWeek} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents a day that is the same day
     * of week as specified by {@link DayOfWeek}. It is set somewhere close to epoch time.
     *
     * <p><b>Note:</b> this should only be used to format if using a pattern or skeleton with a day
     * of week field only. That means that {@code c}-{@code cccccc} patterns are recommended, {@code
     * E}-{@code EEEEEE} and {@code e}-{@code eeeeee} are likely wrong (because they are not
     * stand-alone). Anything else is clearly wrong. It does not make sense to format a {@code
     * DayOfWeek} as {@code "MMMM d, y"}. See {@link
     * https://unicode.org/reports/tr35/tr35-dates.html#dfst-weekday}.
     *
     * @param dow The {@link DayOfWeek} to convert.
     * @return A {@link Calendar} instance representing the same day of week as the one specified by
     *     the input.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    @SuppressWarnings("JavaTimeDefaultTimeZone")
    public static Calendar dayOfWeekToCalendar(DayOfWeek dow) {
        return millisToCalendar(dayOfWeekToMillis(dow));
    }

    /**
     * Converts a {@link Month} to a {@link Calendar}.
     *
     * <p>This method creates a {@link Calendar} instance that represents the same month as
     * specified by {@link Month}. It is set somewhere close to epoch time.
     *
     * <p><b>Note:</b> this should only be used to format if using a pattern or skeleton with a day
     * of month field only. That means that {@code L}-{@code LLLLL} patterns are recommended, {@code
     * E}-{@code MMMMM} is likely wrong (because it is not stand-alone). Anything else is clearly
     * wrong. It does not make sense to format a {@code Month} as {@code "MMMM d, y"}. See {@link
     * https://unicode.org/reports/tr35/tr35-dates.html#dfst-month}.
     *
     * <p><b>Note:</b> only use this method for the Gregorian calendar and related calendars, given
     * that the {@link Month} documentation, states that the {@link Month} enum "... may be used by
     * any calendar system that has the month-of-year concept defined equivalent to the ISO-8601
     * calendar system".</i>
     *
     * @param month The {@link Month} to convert.
     * @return A {@link Calendar} instance representing the same month as the one specified by the
     *     input.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    @SuppressWarnings("JavaTimeDefaultTimeZone")
    public static Calendar monthToCalendar(Month month) {
        return millisToCalendar(monthToMilli(month));
    }

    private static Calendar millisToCalendar(long epochMillis) {
        return millisToCalendar(epochMillis, TimeZone.GMT_ZONE);
    }

    private static Calendar millisToCalendar(long epochMillis, TimeZone timeZone) {
        GregorianCalendar calendar = new GregorianCalendar(timeZone, ULocale.US);
        // java.time doesn't switch to Julian calendar
        calendar.setGregorianChange(new Date(Long.MIN_VALUE));
        calendar.setTimeInMillis(epochMillis);
        return calendar;
    }

    private static long dayOfWeekToMillis(DayOfWeek dow) {
        // Epoch time was 1970-01-01 00:00:00, and was a Thursday.
        // Add 12 hours, so we are in the middle of the day and have no surprises.
        // Then add 3 days to get a Monday (in fact 4, but DayOfWeek value is 1 based).
        return MILLIS_PER_HOUR * 12 + (3 + dow.getValue()) * MILLIS_PER_DAY;
    }

    /* Fails for non-Gregoran calendars. */
    private static long monthToMilli(Month month) {
        // Epoch time was 1970-01-01 00:00:00, and was a Thursday.
        // Add 12 hours, so we are in the middle of the day and have no surprises.
        // Then add 31 for each month. 31 days is safe, even if some months are shorter.
        // We start from Jan 1, Feb 1, Mar 4, Apr 4, May 5, ..., Dec 8.
        return MILLIS_PER_HOUR * 12 + (month.getValue() - 1) * MILLIS_PER_DAY * 31;
    }

    /**
     * Converts a {@link java.util.Calendar} to a {@link com.ibm.icu.util.Calendar}.
     *
     * @param inputCalendar The JDK Calendar to convert.
     * @return An ICU Calendar that has the same properties as the Java one.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static com.ibm.icu.util.Calendar convertCalendar(java.util.Calendar inputCalendar) {

        java.util.TimeZone tz = inputCalendar.getTimeZone();
        TimeZone zone = TimeZone.getTimeZone(tz.getID());

        /*
         * It would be even better to create these calendars with TimeZone and Locale.
         * But although the java.util.Calendar can be constructed with a Locale
         * or uses getDefaultLocale(), it stores it into a private field and there is no getter.
         * The documentation says (and the code seems to confirm) that the locale is used for
         * 2 things: "Calendar defines a locale-specific seven day week using two parameters:
         * the first day of the week and the minimal days in first week (from 1 to 7). These
         * numbers are taken from the locale resource data when a Calendar is constructed".
         *
         * So after we create the calendar we will copy this info from the original calendar.
         */
        Calendar result;
        switch (inputCalendar.getCalendarType()) {
            case "iso8601":
                result = new GregorianCalendar(zone);
                // make gcal a proleptic Gregorian
                ((GregorianCalendar) result).setGregorianChange(new Date(Long.MIN_VALUE));
                break;
            case "buddhist":
                result = new BuddhistCalendar(zone);
                break;
            case "japanese":
                result = new JapaneseCalendar(zone);
                break;
            case "gregory": // Fallthrough
            default:
                // Fallback to Gregorian
                result = new GregorianCalendar(zone);
        }

        result.setLenient(inputCalendar.isLenient());
        result.setFirstDayOfWeek(inputCalendar.getFirstDayOfWeek());
        result.setMinimalDaysInFirstWeek(inputCalendar.getMinimalDaysInFirstWeek());
        result.setTimeInMillis(inputCalendar.getTimeInMillis());

        return result;
    }
}