ICU-22853 Support formatting types in java.time

This commit is contained in:
Mihai Nita 2024-08-21 17:03:05 +00:00 committed by Mihai Nita
parent 51e21af692
commit 01d755749c
9 changed files with 884 additions and 5 deletions

View file

@ -0,0 +1,309 @@
// © 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 java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
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;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;
/**
* 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 Instant}, {@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}.
*
* @deprecated This API is ICU internal only.
*/
@Deprecated
public class JavaTimeConverters {
// Milliseconds per day
private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1_000;
private JavaTimeConverters() {
// Prevent instantiation, making this an utility class
}
/**
* Converts the current instant from a {@link Clock} to a {@link Calendar}.
*
* <p>
* This method creates a {@link Calendar} instance that represents the current
* instant as provided by the specified {@link Clock}.
*
* @param clock The {@link Clock} providing the current instant.
* @return A {@link Calendar} instance representing the current instant as
* provided by the specified {@link Clock}.
*
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static Calendar temporalToCalendar(Clock clock) {
long epochMillis = clock.millis();
String timeZone = clock.getZone().getId();
TimeZone icuTimeZone = TimeZone.getTimeZone(timeZone);
return millisToCalendar(epochMillis, icuTimeZone);
}
/**
* Converts an {@link Instant} to a {@link Calendar}.
*
* <p>
* This method creates a {@link Calendar} instance that represents the same
* point in time as the specified {@link Instant}. The resulting
* {@link Calendar} will be in the default time zone of the JVM.
*
* @param instant The {@link Instant} to convert.
* @return A {@link Calendar} instance representing the same point in time as
* the specified {@link Instant}.
*
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static Calendar temporalToCalendar(Instant instant) {
long epochMillis = instant.toEpochMilli();
return millisToCalendar(epochMillis, TimeZone.GMT_ZONE);
}
/**
* 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.
*
* @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.
*
* @deprecated This API is ICU internal only.
*/
@Deprecated
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.
*
* @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.
*
* @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}.
*
* @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}.
*
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static Calendar temporalToCalendar(Temporal temp) {
if (temp instanceof Clock) {
return temporalToCalendar((Clock) temp);
} else if (temp instanceof Instant) {
return temporalToCalendar((Instant) temp);
} 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 {
System.out.println("WTF is " + temp.getClass());
return null;
}
}
/**
* 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}.
*
* @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}.
*
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static TimeZone zoneOffsetToTimeZone(ZoneOffset zoneOffset) {
return new SimpleTimeZone(zoneOffset.getTotalSeconds() * 1_000, zoneOffset.getId());
}
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;
}
}

View file

@ -3,6 +3,8 @@
package com.ibm.icu.message2;
import java.time.Clock;
import java.time.temporal.Temporal;
import java.util.Calendar;
import java.util.Locale;
import java.util.Map;
@ -10,6 +12,7 @@ import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ibm.icu.impl.JavaTimeConverters;
import com.ibm.icu.text.DateFormat;
/**
@ -349,7 +352,12 @@ class DateTimeFormatterFactory implements FormatterFactory {
return new FormattedPlaceholder(
toFormat, new PlainStringFormattedValue("{|" + toFormat + "|}"));
}
} else if (toFormat instanceof Clock) {
toFormat = JavaTimeConverters.temporalToCalendar((Clock) toFormat);
} else if (toFormat instanceof Temporal) {
toFormat = JavaTimeConverters.temporalToCalendar((Temporal) toFormat);
}
// Not an else-if here, because the `Clock` & `Temporal` conditions before make `toFormat` a `Calendar`
if (toFormat instanceof Calendar) {
TimeZone tz = ((Calendar) toFormat).getTimeZone();
long milis = ((Calendar) toFormat).getTimeInMillis();

View file

@ -3,6 +3,8 @@
package com.ibm.icu.message2;
import java.time.Clock;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -62,6 +64,8 @@ class MFDataModelFormatter {
.setDefaultFormatterNameForType(Date.class, "datetime")
.setDefaultFormatterNameForType(Calendar.class, "datetime")
.setDefaultFormatterNameForType(java.util.Calendar.class, "datetime")
.setDefaultFormatterNameForType(Clock.class, "datetime")
.setDefaultFormatterNameForType(Temporal.class, "datetime")
// Number formatting
.setFormatter("number", new NumberFormatterFactory("number"))

View file

@ -14,6 +14,8 @@ import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.Clock;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Date;
import java.util.EnumSet;
@ -24,6 +26,7 @@ import java.util.Map;
import java.util.MissingResourceException;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.JavaTimeConverters;
import com.ibm.icu.impl.RelativeDateFormat;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
@ -623,16 +626,22 @@ public abstract class DateFormat extends UFormat {
public final StringBuffer format(Object obj, StringBuffer toAppendTo,
FieldPosition fieldPosition)
{
if (obj instanceof Calendar)
if (obj instanceof Calendar) {
return format( (Calendar)obj, toAppendTo, fieldPosition );
else if (obj instanceof Date)
} else if (obj instanceof Date) {
return format( (Date)obj, toAppendTo, fieldPosition );
else if (obj instanceof Number)
} else if (obj instanceof Number) {
return format( new Date(((Number)obj).longValue()),
toAppendTo, fieldPosition );
else
} else if (obj instanceof Clock) {
return format(JavaTimeConverters.temporalToCalendar((Clock) obj),
toAppendTo, fieldPosition);
} else if (obj instanceof Temporal) {
return format( (Temporal)obj, toAppendTo, fieldPosition );
} else {
throw new IllegalArgumentException("Cannot format given Object (" +
obj.getClass().getName() + ") as a Date");
}
}
/**
@ -706,6 +715,46 @@ public abstract class DateFormat extends UFormat {
return format(date, new StringBuffer(64),new FieldPosition(0)).toString();
}
/**
* Formats a {@link Temporal} into a date/time string.
*
* @param date a {@link Temporal} to be formatted into a date/time string.
* @param toAppendTo the string buffer for the returning date/time string.
* @param fieldPosition keeps track of the position of the field within the returned string.<br>
* On input: an alignment field, if desired.<br>
* On output: the offsets of the alignment field.<br>
* For example, given a time text "1996.07.10 AD at 15:08:56 PDT",
* if the given {@code fieldPosition} is {@code DateFormat.YEAR_FIELD}, the begin index and end index
* of {@code fieldPosition} will be set to 0 and 4, respectively.<br>
* Notice that if the same time field appears more than once in a pattern, the fieldPosition will
* be set for the first occurrence of that time field. For instance, formatting a {@link Temporal}
* to the time string "1 PM PDT (Pacific Daylight Time)" using the pattern "h a z (zzzz)" and the
* alignment field {@code DateFormat.TIMEZONE_FIELD}, the begin index and end index
* of {@code fieldPosition} will be set to 5 and 8, respectively, for the first occurrence of the
* timezone pattern character 'z'.
*
* @return the formatted date/time string.
*
* @draft ICU 76
*/
public StringBuffer format(Temporal date, StringBuffer toAppendTo,
FieldPosition fieldPosition) {
return format(JavaTimeConverters.temporalToCalendar(date), toAppendTo, fieldPosition);
}
/**
* Formats a {@link Temporal} into a date/time string.
*
* @param date the time value to be formatted into a time string.
* @return the formatted time string.
*
* @draft ICU 76
*/
public final String format(Temporal date)
{
return format(date, new StringBuffer(64),new FieldPosition(0)).toString();
}
/**
* Parses a date/time string. For example, a time text "07/10/96 4:5 PM, PDT"
* will be parsed into a Date that is equivalent to Date(837039928046).

View file

@ -13,6 +13,7 @@ import java.io.ObjectInputStream;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -24,6 +25,7 @@ import com.ibm.icu.impl.FormattedValueFieldPositionIteratorImpl;
import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.JavaTimeConverters;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.Utility;
@ -889,6 +891,34 @@ public class DateIntervalFormat extends UFormat {
return formatImpl(fromCalendar, toCalendar, appendTo, pos, null, null);
}
/**
* Format two {@link Temporal}s to produce a string.
*
* @param fromTemporal temporal set to the start of the interval
* to be formatted into a string
* @param toTemporal temporal set to the end of the interval
* to be formatted into a string
* @param appendTo Output parameter to receive result.
* Result is appended to existing contents.
* @param pos On input: an alignment field, if desired.
* On output: the offsets of the alignment field.
* There may be multiple instances of a given field type
* in an interval format; in this case the fieldPosition
* offsets refer to the first instance.
* @return Reference to 'appendTo' parameter.
* @throws IllegalArgumentException if the two calendars are not equivalent.
*
* @draft ICU 76
*/
public final StringBuffer format(Temporal fromTemporal,
Temporal toTemporal,
StringBuffer appendTo,
FieldPosition pos) {
Calendar fromCalendar = JavaTimeConverters.temporalToCalendar(fromTemporal);
Calendar toCalendar = JavaTimeConverters.temporalToCalendar(toTemporal);
return formatImpl(fromCalendar, toCalendar, appendTo, pos, null, null);
}
/**
* Format 2 Calendars to produce a FormattedDateInterval.
*
@ -915,6 +945,25 @@ public class DateIntervalFormat extends UFormat {
return new FormattedDateInterval(sb, attributes);
}
/**
* Format two {@link Temporal}s to produce a FormattedDateInterval.
*
* The FormattedDateInterval exposes field information about the formatted string.
*
* @param fromTemporal temporal set to the start of the interval
* to be formatted into a string
* @param toTemporal temporal set to the end of the interval
* to be formatted into a string
* @return A FormattedDateInterval containing the format result.
*
* @draft ICU 76
*/
public FormattedDateInterval formatToValue(Temporal fromTemporal, Temporal toTemporal) {
Calendar fromCalendar = JavaTimeConverters.temporalToCalendar(fromTemporal);
Calendar toCalendar = JavaTimeConverters.temporalToCalendar(toTemporal);
return formatToValue(fromCalendar, toCalendar);
}
private synchronized StringBuffer formatImpl(Calendar fromCalendar,
Calendar toCalendar,
StringBuffer appendTo,

View file

@ -24,6 +24,8 @@ import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.Clock;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -41,6 +43,7 @@ import com.ibm.icu.text.MessagePattern.ArgType;
import com.ibm.icu.text.MessagePattern.Part;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
@ -1745,9 +1748,18 @@ public class MessageFormat extends UFormat {
if (arg instanceof Number) {
// format number if can
dest.formatAndAppend(getStockNumberFormatter(), arg);
} else if (arg instanceof Date) {
} else if (arg instanceof Date) {
// format a Date if can
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof Calendar) {
// format a Calendar if can
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof Clock) {
// format a Clock if can
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof Temporal) {
// format a Temporal if can
dest.formatAndAppend(getStockDateFormatter(), arg);
} else {
dest.append(arg.toString());
}

View file

@ -17,6 +17,8 @@ import java.text.AttributedString;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.time.Clock;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -30,6 +32,7 @@ import com.ibm.icu.impl.DayPeriodRules;
import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.JavaTimeConverters;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.SimpleFormatterImpl;
@ -3917,6 +3920,10 @@ public class SimpleDateFormat extends DateFormat {
calendar.setTime((Date)obj);
} else if (obj instanceof Number) {
calendar.setTimeInMillis(((Number)obj).longValue());
} else if (obj instanceof Clock) {
cal = JavaTimeConverters.temporalToCalendar((Clock) obj);
} else if (obj instanceof Temporal) {
cal = JavaTimeConverters.temporalToCalendar((Temporal) obj);
} else {
throw new IllegalArgumentException("Cannot format given Object as a Date");
}

View file

@ -0,0 +1,155 @@
// © 2024 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html
package com.ibm.icu.dev.test.format;
import java.time.Clock;
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.HijrahDate;
import java.time.chrono.JapaneseDate;
import java.time.chrono.JapaneseEra;
import java.time.chrono.MinguoDate;
import java.time.chrono.ThaiBuddhistDate;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import com.ibm.icu.dev.test.CoreTestFmwk;
import com.ibm.icu.impl.JavaTimeConverters;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.TimeZone;
/* This class tests the raw conversion, java.time classes to an ICU Calendar. */
@RunWith(JUnit4.class)
public class JavaTimeConvertersTest extends CoreTestFmwk {
/*
* Fields that we expect in the calendar when formatting dates.
*
* A LocalDate object will not have hour, minutes, seconds, etc.
* So when we convert it to a Calendar the result can't be directly compared
* to the expected Calendar because some fields are different.
*
* Think of this field list as a "mask" we use when we compare a calendar
* from conversion with the expected Calendar.
*/
private final static int[] DATE_ONLY_FIELDS = {
Calendar.DAY_OF_MONTH, Calendar.MONTH, Calendar.YEAR,
Calendar.DAY_OF_WEEK, Calendar.DAY_OF_YEAR, Calendar.ERA,
Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.DOW_LOCAL,
Calendar.WEEK_OF_MONTH, Calendar.WEEK_OF_YEAR, Calendar.EXTENDED_YEAR
};
// Fields that we expect in the calendar when formatting time
private final static int[] TIME_ONLY_FIELDS = {
Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND,
Calendar.AM_PM, Calendar.MILLISECONDS_IN_DAY
};
// Make it easier to build all kind of temporal objects
final static LocalDateTime LOCAL_DATE_TIME = LocalDateTime.of(2018, Month.SEPTEMBER, 23,
19, 42, 57, /*nanoseconds*/ 123_000_000);
final static String TIME_ZONE_ID = "Europe/Paris";
// Match the fields in the LOCAL_DATE_TIME above
final static Calendar EXPECTED_CALENDAR = new GregorianCalendar(2018, Calendar.SEPTEMBER,
23, 19, 42, 57);
static {
EXPECTED_CALENDAR.setTimeZone(TimeZone.getTimeZone(TIME_ZONE_ID));
EXPECTED_CALENDAR.setTimeInMillis(EXPECTED_CALENDAR.getTimeInMillis() + 123);
}
@Test
public void testDateOnly() {
LocalDate ld = LOCAL_DATE_TIME.toLocalDate();
Calendar calendar = JavaTimeConverters.temporalToCalendar(ld);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
HijrahDate hd = HijrahDate.of(1440, 1, 13);
calendar = JavaTimeConverters.temporalToCalendar(hd);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
JapaneseDate jd = JapaneseDate.of(JapaneseEra.HEISEI, 30, Month.SEPTEMBER.getValue(), 23);
calendar = JavaTimeConverters.temporalToCalendar(jd);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
MinguoDate md = MinguoDate.of(107, Month.SEPTEMBER.getValue(), 23);
calendar = JavaTimeConverters.temporalToCalendar(md);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
ThaiBuddhistDate td = ThaiBuddhistDate.of(2561, Month.SEPTEMBER.getValue(), 23);
calendar = JavaTimeConverters.temporalToCalendar(td);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
}
@Test
public void testTimesOnly() {
LocalTime lt = LOCAL_DATE_TIME.toLocalTime();
Calendar calendar = JavaTimeConverters.temporalToCalendar(lt);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, TIME_ONLY_FIELDS);
OffsetTime ot = OffsetTime.of(lt, ZoneOffset.ofHours(1));
calendar = JavaTimeConverters.temporalToCalendar(ot);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, TIME_ONLY_FIELDS);
}
@Test
public void testDateAndTimes() {
Calendar calendar = JavaTimeConverters.temporalToCalendar(LOCAL_DATE_TIME);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, TIME_ONLY_FIELDS);
ZonedDateTime zdt = ZonedDateTime.of(LOCAL_DATE_TIME, ZoneId.of(TIME_ZONE_ID)); // Date + Time + TimeZone
calendar = JavaTimeConverters.temporalToCalendar(zdt);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, TIME_ONLY_FIELDS);
assertEquals("", EXPECTED_CALENDAR.getTimeZone().getID(), calendar.getTimeZone().getID());
OffsetDateTime odt = OffsetDateTime.of(LOCAL_DATE_TIME, ZoneOffset.ofHours(1)); // Date + Time + TimeZone
calendar = JavaTimeConverters.temporalToCalendar(odt);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, TIME_ONLY_FIELDS);
assertEquals("", EXPECTED_CALENDAR.getTimeZone().getRawOffset(), calendar.getTimeZone().getRawOffset());
}
@Test
public void testInstantAndClock() {
// Instant has no time zone, assumes GMT.
EXPECTED_CALENDAR.setTimeZone(TimeZone.GMT_ZONE);
Instant instant = Instant.ofEpochMilli(EXPECTED_CALENDAR.getTimeInMillis());
Calendar calendar = JavaTimeConverters.temporalToCalendar(instant);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, TIME_ONLY_FIELDS);
assertEquals("", EXPECTED_CALENDAR.getTimeZone().getID(), calendar.getTimeZone().getID());
assertEquals("", EXPECTED_CALENDAR.getTimeZone().getRawOffset(), calendar.getTimeZone().getRawOffset());
// Restore the time zone on the expected calendar
EXPECTED_CALENDAR.setTimeZone(TimeZone.getTimeZone(TIME_ZONE_ID));
Clock clock = Clock.fixed(instant, ZoneId.of(TIME_ZONE_ID));
calendar = JavaTimeConverters.temporalToCalendar(clock);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, DATE_ONLY_FIELDS);
assertCalendarsEquals(EXPECTED_CALENDAR, calendar, TIME_ONLY_FIELDS);
assertEquals("", EXPECTED_CALENDAR.getTimeZone().getID(), calendar.getTimeZone().getID());
assertEquals("", EXPECTED_CALENDAR.getTimeZone().getRawOffset(), calendar.getTimeZone().getRawOffset());
}
// Compare the expected / actual calendar, but using an allowlist
private static void assertCalendarsEquals(Calendar exected, Calendar actual, int[] fieldsToCheck) {
for (int field : fieldsToCheck) {
assertEquals("Bad conversion", exected.get(field), actual.get(field));
}
}
}

View file

@ -0,0 +1,286 @@
// © 2024 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html
package com.ibm.icu.dev.test.format;
import java.text.FieldPosition;
import java.time.Clock;
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.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.HijrahDate;
import java.time.chrono.JapaneseDate;
import java.time.chrono.MinguoDate;
import java.time.chrono.ThaiBuddhistDate;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import com.ibm.icu.dev.test.CoreTestFmwk;
import com.ibm.icu.message2.MessageFormatter;
import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.DateIntervalFormat;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.util.TimeZone;
@RunWith(JUnit4.class)
public class JavaTimeFormatTest extends CoreTestFmwk {
final static LocalDateTime LDT =
LocalDateTime.of(/*year*/ 2013, Month.SEPTEMBER, 27,
/*hour*/ 19, /*min*/43, /*sec*/ 56, /*nanosec*/ 123_456_789);
@Test
public void testLocalDateFormatting() {
LocalDate ld = LDT.toLocalDate();
// Formatting with skeleton
DateFormat formatFromSkeleton = DateFormat.getInstanceForSkeleton("EEEEyMMMMd", Locale.US);
assertEquals("", "Friday, September 27, 2013", formatFromSkeleton.format(ld));
// Format with style
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, Locale.US);
assertEquals("", "September 27, 2013", dateFormat.format(ld));
}
@Test
public void testLocalDateTimeFormatting() {
// Formatting with skeleton
DateFormat formatFromSkeleton = DateFormat.getInstanceForSkeleton("EEEEyMMMMd jmsSSS", Locale.US);
assertEquals("", "Friday, September 27, 2013 at 7:43:56.123\u202FPM", formatFromSkeleton.format(LDT));
// Format with style
DateFormat dateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, Locale.US);
assertEquals("", "September 27, 2013 at 7:43\u202FPM", dateTimeFormat.format(LDT));
DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.US);
assertEquals("", "7:43\u202FPM", timeFormat.format(LDT));
}
@Test
public void testThatConvertedCalendarUsesDefaultTimeZone() {
// Save the default time zones
TimeZone savedTimeZone = TimeZone.getDefault();
java.util.TimeZone jdkSavedTimeZone = java.util.TimeZone.getDefault();
// Set to one that we control
String timeZoneId = "America/New_York";
TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId));
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(timeZoneId));
// We check that the calendar from conversion uses the default time zone.
DateFormat icuDateFormat = DateFormat.getInstanceForSkeleton("EEEEdMMMMyjmszzzz", Locale.US);
String result = icuDateFormat.format(LDT);
// Restore the default time zones
TimeZone.setDefault(savedTimeZone);
java.util.TimeZone.setDefault(jdkSavedTimeZone);
assertEquals("", "Friday, September 27, 2013 at 7:43:56PM Eastern Daylight Time", result);
}
@Test
public void testDateTimeWithTimeZoneFormatting() {
// Formatting with skeleton
ZonedDateTime zdt = ZonedDateTime.of(LDT, ZoneId.of("Europe/Paris"));
OffsetDateTime odt = OffsetDateTime.of(LDT, ZoneOffset.ofHoursMinutes(5, 30));
DateFormat formatFromSkeleton = DateFormat.getInstanceForSkeleton("EEEEyMMMMd jmsSSS vvvv", Locale.US);
assertEquals("", "Friday, September 27, 2013 at 7:43:56.123\u202FPM Central European Time", formatFromSkeleton.format(zdt));
assertEquals("", "Friday, September 27, 2013 at 7:43:56.123\u202FPM GMT+05:30", formatFromSkeleton.format(odt));
// Format with style
DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.FULL, Locale.US);
assertEquals("", "7:43:56\u202FPM Central European Summer Time",timeFormat.format(zdt));
assertEquals("", "7:43:56\u202FPM GMT+05:30", timeFormat.format(odt));
DateFormat dateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.US);
assertEquals("", "Friday, September 27, 2013 at 7:43:56\u202FPM Central European Summer Time", dateTimeFormat.format(zdt));
assertEquals("", "Friday, September 27, 2013 at 7:43:56\u202FPM GMT+05:30", dateTimeFormat.format(odt));
}
@Test
public void testNonGregorianDateFormatting() {
// Non-Gregorian as input
LocalDate ld = LDT.toLocalDate();
HijrahDate hd = HijrahDate.from(ld);
JapaneseDate jd = JapaneseDate.from(ld);
MinguoDate md = MinguoDate.from(ld);
ThaiBuddhistDate td = ThaiBuddhistDate.from(ld);
DateFormat formatFromSkeleton = DateFormat.getInstanceForSkeleton("EEEEGGGyMMMMd", Locale.US);
String expected = "Friday, September 27, 2013 AD";
assertEquals("", expected, formatFromSkeleton.format(hd));
assertEquals("", expected, formatFromSkeleton.format(jd));
assertEquals("", expected, formatFromSkeleton.format(md));
assertEquals("", expected, formatFromSkeleton.format(td));
// Non-Gregorian as formatting calendar
String[] expectedPerCalendar = {
"buddhist", "September 27, 2556 BE",
"chinese", "Eighth Month 23, 2013(gui-si)",
"hebrew", "23 Tishri 5774 AM",
"indian", "Asvina 5, 1935 Saka",
"islamic", "Dhuʻl-Qiʻdah 22, 1434 AH",
"japanese", "September 27, 25 Heisei",
"persian", "Mehr 5, 1392 AP",
"roc", "September 27, 102 Minguo",
};
String skeleton = "GGGGyMMMMd";
for (int i = 0; i < expectedPerCalendar.length; i++) {
Locale locale = Locale.forLanguageTag("en-u-ca-" + expectedPerCalendar[i++]);
formatFromSkeleton = DateFormat.getInstanceForSkeleton(skeleton, locale);
assertEquals("", expectedPerCalendar[i], formatFromSkeleton.format(LDT));
}
}
@Test
public void testInstantAndClockFormatting() {
DateFormat formatFromSkeleton = DateFormat.getInstanceForSkeleton("yMMMMd jmsSSSvvvv", Locale.US);
Instant instant = LDT.toInstant(ZoneOffset.UTC);
assertEquals("", "September 27, 2013 at 7:43:56.123PM Greenwich Mean Time", formatFromSkeleton.format(instant));
Clock clock = Clock.fixed(instant, ZoneId.of("America/Los_Angeles"));
assertEquals("", "September 27, 2013 at 12:43:56.123PM Pacific Time", formatFromSkeleton.format(clock));
}
@Test
public void testMessageFormat() {
Locale locale = Locale.FRENCH;
Map<String, Object> arguments = new HashMap<>();
arguments.put("expDate", LDT);
// Make sure that the type detection works, we don't pass a type for the formatter
MessageFormat mf = new MessageFormat("Your card expires on {expDate}", locale);
assertEquals("", "Your card expires on 27/09/2013 19:43", mf.format(arguments));
// Now we specify that the placeholder is a date, make sure that the style & skeleton are honored.
mf = new MessageFormat("Your card expires on {expDate, date}", locale);
assertEquals("", "Your card expires on 27 sept. 2013", mf.format(arguments));
mf = new MessageFormat("Your card expires on {expDate, date, FULL}", locale);
assertEquals("", "Your card expires on vendredi 27 septembre 2013", mf.format(arguments));
mf = new MessageFormat("Your card expires on {expDate, date, ::EEEyMMMd}", locale);
assertEquals("", "Your card expires on ven. 27 sept. 2013", mf.format(arguments));
// MessageFormatter (MF2)
MessageFormatter.Builder mf2Builder = MessageFormatter.builder()
.setLocale(locale);
MessageFormatter mf2 = mf2Builder.setPattern("(mf2) Your card expires on {$expDate}").build();
assertEquals("", "(mf2) Your card expires on 27/09/2013 19:43", mf2.formatToString(arguments));
mf2 = mf2Builder.setPattern("(mf2) Your card expires on {$expDate :date}").build();
assertEquals("", "(mf2) Your card expires on 27/09/2013", mf2.formatToString(arguments));
mf2 = mf2Builder.setPattern("(mf2) Your card expires on {$expDate :datetime dateStyle=long}").build();
assertEquals("", "(mf2) Your card expires on 27 septembre 2013", mf2.formatToString(arguments));
mf2 = mf2Builder.setPattern("(mf2) Your card expires on {$expDate :date icu:skeleton=EEEyMMMd}").build();
assertEquals("", "(mf2) Your card expires on ven. 27 sept. 2013", mf2.formatToString(arguments));
// Test several java.time types
// We don't care much about the string result, as we test that somewhere else.
// We only want to make sure that MessageFormat(ter) recognizes the types.
String expectedMf1Result = "Your card expires on ven. 27 sept. 2013";
String expectedMf2Result = "(mf2) " + expectedMf1Result;
// LocalDate
arguments.put("expDate", LDT.toLocalDate());
assertEquals("", expectedMf1Result, mf.format(arguments));
assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
// ZonedDateTime
arguments.put("expDate", LDT.atZone(ZoneId.of("Europe/Paris")));
assertEquals("", expectedMf1Result, mf.format(arguments));
assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
// OffsetDateTime
arguments.put("expDate", LDT.atOffset(ZoneOffset.ofHours(2)));
assertEquals("", expectedMf1Result, mf.format(arguments));
assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
// Instant
Instant instant = LDT.toInstant(ZoneOffset.UTC);
arguments.put("expDate", instant);
assertEquals("", expectedMf1Result, mf.format(arguments));
assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
// Clock
arguments.put("expDate", Clock.fixed(instant, ZoneId.of("Europe/Paris")));
assertEquals("", expectedMf1Result, mf.format(arguments));
assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
// Test that both JDK and ICU Calendar are recognized as types.
arguments.put("expDate", new java.util.GregorianCalendar(2013, 8, 27));
// We don't test MessageFormat (MF1) with a java.util.Calendar
// because it throws. The ICU DateFormat does not support it.
// I filed https://unicode-org.atlassian.net/browse/ICU-22852
// MF2 converts the JDK Calendar to an ICU Calendar, so it works.
assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
}
@Test
public void testDateIntervalFormat() {
Locale locale = Locale.FRENCH;
String intervalSkeleton = "dMMMMy";
LocalDate from = LocalDate.of(2024, Month.SEPTEMBER, 17);
LocalDate to = LocalDate.of(2024, Month.SEPTEMBER, 23);
StringBuffer result = new StringBuffer();
result.setLength(0);
DateIntervalFormat di = DateIntervalFormat.getInstance(intervalSkeleton, locale);
assertEquals("", "1723 septembre 2024",
di.format(from, to, result, new FieldPosition(0)).toString());
to = LocalDate.of(2024, Month.OCTOBER, 3);
result.setLength(0);
di = DateIntervalFormat.getInstance(intervalSkeleton, locale);
assertEquals("", "17 septembre3 octobre 2024",
di.format(from, to, result, new FieldPosition(0)).toString());
// LocalDateTime. Date + time difference, same day, different times
LocalDateTime fromDt = LocalDateTime.of(2024, Month.SEPTEMBER, 17, 9, 30, 0);
LocalDateTime toDt = LocalDateTime.of(2024, Month.SEPTEMBER, 17, 18, 0, 0);
result.setLength(0);
di = DateIntervalFormat.getInstance("dMMMMy jm", locale);
assertEquals("", "17 septembre 2024, 09:3018:00",
di.format(fromDt, toDt, result, new FieldPosition(0)).toString());
// LocalDateTime. Time difference, same day
LocalTime fromT = LocalTime.of(9, 30, 0);
LocalTime toT = LocalTime.of(18, 0, 0);
result.setLength(0);
di = DateIntervalFormat.getInstance("jm", locale);
assertEquals("", "09:3018:00",
di.format(fromT, toT, result, new FieldPosition(0)).toString());
// Non-Gregorian output
di = DateIntervalFormat.getInstance(intervalSkeleton, Locale.forLanguageTag("fr-u-ca-hebrew"));
result.setLength(0);
assertEquals("", "14 éloul1 tichri 5785 A. M.",
di.format(from, to, result, new FieldPosition(0)).toString());
di = DateIntervalFormat.getInstance(intervalSkeleton, Locale.forLanguageTag("fr-u-ca-coptic"));
result.setLength(0);
assertEquals("", "7 tout23 tout 1741 ap. D.",
di.format(from, to, result, new FieldPosition(0)).toString());
di = DateIntervalFormat.getInstance(intervalSkeleton, Locale.forLanguageTag("fr-u-ca-japanese"));
result.setLength(0);
assertEquals("", "17 septembre3 octobre 6 Reiwa",
di.format(from, to, result, new FieldPosition(0)).toString());
}
}