This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch FREEMARKER-35
in repository https://gitbox.apache.org/repos/asf/freemarker.git


The following commit(s) were added to refs/heads/FREEMARKER-35 by this push:
     new 9bd0817  [FREEMARKER-35] - When no time zone (or offset) is shown, 
convert temporals to the FreeMarker time zone. - Changed temporal format 
defaults to give useful output (at least on Java 8 and English locale) - 
Improved error message in case of formatting exception (mostly handy when 
people try to format local temporals with a format that wants to show a time 
zone or offset) - Improved javadocs
9bd0817 is described below

commit 9bd081750e567dd41be15b88819cdae80afafcd0
Author: ddekany <[email protected]>
AuthorDate: Sun Oct 31 17:47:22 2021 +0100

    [FREEMARKER-35]
    - When no time zone (or offset) is shown, convert temporals to the 
FreeMarker time zone.
    - Changed temporal format defaults to give useful output (at least on Java 
8 and English locale)
    - Improved error message in case of formatting exception (mostly handy when 
people try to format local temporals with a format that wants to show a time 
zone or offset)
    - Improved javadocs
---
 .../freemarker/core/BuiltInsForMultipleTypes.java  |   2 +-
 src/main/java/freemarker/core/Configurable.java    | 158 +++++++++++++-
 src/main/java/freemarker/core/Environment.java     |  28 ++-
 src/main/java/freemarker/core/EvalUtil.java        |   8 +-
 .../core/JavaTemplateTemporalFormat.java           |  92 ++++++--
 src/main/java/freemarker/core/_MessageUtil.java    |  25 ++-
 .../freemarker/template/TemplateDateModel.java     |   3 +
 .../freemarker/template/TemplateTemporalModel.java |  14 ++
 .../java/freemarker/core/TemporalFormatTest.java   | 243 +++++++++++++++++++++
 .../test/templatesuite/templates/temporal.ftl      |  42 ++--
 src/test/resources/logback-test.xml                |   1 +
 11 files changed, 548 insertions(+), 68 deletions(-)

diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java 
b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index aebbd57..8f15548 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -649,7 +649,7 @@ class BuiltInsForMultipleTypes {
                         cachedValue = 
EvalUtil.assertFormatResultNotNull(defaultFormat.format(temporalModel));
                     } catch (TemplateValueFormatException e) {
                         try {
-                            throw 
_MessageUtil.newCantFormatTemporalException(defaultFormat, target, e, true);
+                            throw 
_MessageUtil.newCantFormatTemporalException(defaultFormat, temporalModel, 
target, e, true);
                         } catch (TemplateException e2) {
                             // `e` should always be a TemplateModelException 
here, but to be sure:
                             throw 
_CoreAPI.ensureIsTemplateModelException("Failed to format date/time/datetime", 
e2);
diff --git a/src/main/java/freemarker/core/Configurable.java 
b/src/main/java/freemarker/core/Configurable.java
index c1ffce9..bce76e8 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -33,6 +33,8 @@ import java.time.OffsetTime;
 import java.time.Year;
 import java.time.YearMonth;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
 import java.time.temporal.Temporal;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -517,31 +519,31 @@ public class Configurable {
         dateTimeFormat = "";
         properties.setProperty(DATETIME_FORMAT_KEY, dateTimeFormat);
         
-        instantFormat = "";
+        instantFormat = JavaTemplateTemporalFormat.MEDIUM;
         properties.setProperty(INSTANT_FORMAT_KEY, instantFormat);
 
-        localDateFormat = "";
+        localDateFormat = JavaTemplateTemporalFormat.MEDIUM;
         properties.setProperty(LOCAL_DATE_FORMAT_KEY, localDateFormat);
 
-        localDateTimeFormat = "";
+        localDateTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
         properties.setProperty(LOCAL_DATE_TIME_FORMAT_KEY, 
localDateTimeFormat);
 
-        localTimeFormat = "";
+        localTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
         properties.setProperty(LOCAL_TIME_FORMAT_KEY, localTimeFormat);
 
-        offsetDateTimeFormat = "";
+        offsetDateTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
         properties.setProperty(OFFSET_DATE_TIME_FORMAT_KEY, 
offsetDateTimeFormat);
 
-        offsetTimeFormat = "";
+        offsetTimeFormat = JavaTemplateTemporalFormat.LONG;
         properties.setProperty(OFFSET_TIME_FORMAT_KEY, offsetTimeFormat);
 
-        zonedDateTimeFormat = "";
+        zonedDateTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
         properties.setProperty(ZONED_DATE_TIME_FORMAT_KEY, 
zonedDateTimeFormat);
 
-        yearFormat = "";
+        yearFormat = "iso";
         properties.setProperty(YEAR_FORMAT_KEY, yearFormat);
 
-        yearMonthFormat = "";
+        yearMonthFormat = "iso";
         properties.setProperty(YEAR_MONTH_FORMAT_KEY, yearMonthFormat);
 
         classicCompatible = Integer.valueOf(0);
@@ -1370,9 +1372,15 @@ public class Configurable {
     }
 
     /**
-     * Sets the format used to convert {@link java.time.Instant}-s to 
string-s, also the format that
+     * Sets the format used to convert {@link java.time.Instant}-s to strings, 
also the format that
      * {@code someString?instant} will use to parse strings.
-     * <p>Defaults to TODO [FREEMARKER-35].
+     *
+     * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)};
+     *     {@code iso}/{@code xs} will show the time offset.
+     *
      * @since 2.3.32
      */
     public void setInstantFormat(String instantFormat) {
@@ -1381,6 +1389,7 @@ public class Configurable {
 
     /**
      * Getter pair of {@link #setInstantFormat(String)}.
+     *
      * @since 2.3.32
      */
     public String getInstantFormat() {
@@ -1396,12 +1405,25 @@ public class Configurable {
         return instantFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.LocalDate}-s to 
strings, also the format that
+     * {@code someString?local_date} will use to parse strings.
+     *
+     * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)};
+     *     {@code iso}/{@code xs} will not show the time part.
+     *
+     * @since 2.3.32
+     */
     public void setLocalDateFormat(String localDateFormat) {
         this.localDateFormat = localDateFormat;
     }
 
     /**
      * Getter pair of {@link #setLocalDateFormat(String)}.
+     *
      * @since 2.3.32
      */
     public String getLocalDateFormat() {
@@ -1417,12 +1439,25 @@ public class Configurable {
         return localDateFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.LocalDateTime}-s to 
strings, also the format that
+     * {@code someString?local_date_time} will use to parse strings.
+     *
+     * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)};
+     *     {@code iso}/{@code xs} will not show an offset.
+     *
+     * @since 2.3.32
+     */
     public void setLocalDateTimeFormat(String localDateTimeFormat) {
         this.localDateTimeFormat = localDateTimeFormat;
     }
 
     /**
      * Getter pair of {@link #setLocalDateTimeFormat(String)}.
+     *
      * @since 2.3.32
      */
     public String getLocalDateTimeFormat() {
@@ -1438,12 +1473,25 @@ public class Configurable {
         return localDateTimeFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.LocalTime}-s to 
strings, also the format that
+     * {@code someString?local_time} will use to parse strings.
+     *
+     * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)};
+     *     {@code iso}/{@code xs} will not show the time offset.
+     *
+     * @since 2.3.32
+     */
     public void setLocalTimeFormat(String localTimeFormat) {
         this.localTimeFormat = localTimeFormat;
     }
 
     /**
      * Getter pair of {@link #setLocalTimeFormat(String)}.
+     *
      * @since 2.3.32
      */
     public String getLocalTimeFormat() {
@@ -1459,6 +1507,20 @@ public class Configurable {
         return localTimeFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.OffsetDateTime}-s to 
strings, also the format that
+     * {@code someString?offset_date_time} will use to parse strings. 
FreeMarker will detect if the format doesn't
+     * show the offset (as is typically the case for the {@code "medium"} 
format), and then it will convert the value to
+     * the time zone specified in the {@link #setTimeZone(TimeZone) timeZone} 
setting of FreeMarker.
+     *
+     * <p>Defaults to {@code "medium"}, which means {@link 
FormatStyle#MEDIUM}, which usually doesn't show the time
+     * offset; see the parameter JavaDoc for more.
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)}.
+     *
+     * @since 2.3.32
+     */
     public void setOffsetDateTimeFormat(String offsetDateTimeFormat) {
         this.offsetDateTimeFormat = offsetDateTimeFormat;
     }
@@ -1480,6 +1542,23 @@ public class Configurable {
         return offsetDateTimeFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.OffsetTime}-s to 
strings, also the format that
+     * {@code someString?offset_time} will use to parse strings. The format 
<b>should show the offset</b>, unless you
+     * are sure that {@link #setTimeZone(TimeZone) timeZone} setting will be a 
zone that has no daylight saving.
+     * This is because if the offset is not shown, FreeMarker has to convert 
the value to the time zone specified in the
+     * {@link #setTimeZone(TimeZone) timeZone} setting, but we don't know the 
day, so we can't account for daylight
+     * saving changes, and thus we can't do zone conversion reliably if a 
daylight saving is possible.
+     *
+     * <p>Defaults to {@code "long"}, which means {@link FormatStyle#LONG}, 
which usually show the time offset; see the
+     * parameter JavaDoc for more.
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)}, but it <b>must show the offset</b>
+     *     (see earlier why).
+     *
+     * @since 2.3.32
+     */
     public void setOffsetTimeFormat(String offsetTimeFormat) {
         this.offsetTimeFormat = offsetTimeFormat;
     }
@@ -1501,6 +1580,34 @@ public class Configurable {
         return offsetTimeFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.ZonedDateTime}-s to 
strings, also the format that
+     * {@code someString?offset_date_time} will use to parse strings. 
FreeMarker will detect if the format doesn't
+     * show the zone or offset (as is typically the case for the {@code 
"medium"} format), and then it will convert the
+     * value to the time zone specified in the {@link #setTimeZone(TimeZone) 
timeZone} setting of FreeMarker.
+     *
+     * <p>Defaults to {@code "medium"}, which means {@link 
FormatStyle#MEDIUM}, which usually doesn't show the time
+     * zone; see the parameter JavaDoc for more.
+     *
+     * @param localDateTimeFormat
+     *     One of:
+     *     <ul>
+     *         <li>{@code "iso"}: ISO-8601 format (like {@code 
2021-09-29T13:00:05.2})
+     *         <li>{@code "xs"}: XSD format (same as ISO-8601, but parsing is 
more restrictive)
+     *         <li>{@code "short"}, {@code "medium"}, {@code "long"}, {@code 
"full"}, or two of these connected with
+     *             an {@code "_"}: Refers to the {@link FormatStyle} 
constants. When in a pair, as in
+     *             {@code "medium_long"}, the 1st style refers to the date 
part, and the 2nd style to the time part.
+     *             Java doesn't specify what these styles actually mean. 
However, experience with Java 8 shows
+     *             that "short" and "medium" will not show the time zone or 
time offset (which then triggers the zone
+     *             conversion mentioned earlier), and will show months with 
numbers, while "long" and "full" will show
+     *             the zone and/or offset, and shows months with their names. 
(Also "long" and "full" before Java 9
+     *             fails for {@link LocalDateTime} and {@link LocalTime}, 
because of bug JDK-8085887.)
+     *         <li>Other: Interpreted as pattern via {@link 
DateTimeFormatter#ofPattern}. Example:
+     *             {@code "yyyy-MM-dd HH:mm:ss X"}.
+     *     </ul>
+     *
+     * @since 2.3.32
+     */
     public void setZonedDateTimeFormat(String zonedDateTimeFormat) {
         this.zonedDateTimeFormat = zonedDateTimeFormat;
     }
@@ -1522,6 +1629,19 @@ public class Configurable {
         return zonedDateTimeFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.Year}-s to strings, 
also the format that
+     * {@code someString?local_time} will use to parse strings.
+     *
+     * <p>Defaults to {@code "iso"}, which will simply show the year like 
{@code "2021"} (without the quotation marks).
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)},
+     *     {@code iso}/{@code xs} only the year is shown.
+     *     Java (as of version 8) doesn't support "styles" (like "short", 
"medium", etc.) for this.
+     *
+     * @since 2.3.32
+     */
     public void setYearFormat(String yearFormat) {
         this.yearFormat = yearFormat;
     }
@@ -1543,12 +1663,26 @@ public class Configurable {
         return yearFormat != null;
     }
 
+    /**
+     * Sets the format used to convert {@link java.time.YearMonth}-s to 
strings, also the format that
+     * {@code someString?local_time} will use to parse strings.
+     *
+     * <p>Defaults to {@code "iso"}, which will show the value like {@code 
"2021-12"} (without the quotation marks).
+     *
+     * @param localDateTimeFormat
+     *     See the similar parameter of {@link 
#setZonedDateTimeFormat(String)};
+     *     {@code iso}/{@code xs} will look like {@code 2021-12}.
+     *     Java (as of version 8) doesn't support "styles" (like "short", 
"medium", etc.) for this.
+     *
+     * @since 2.3.32
+     */
     public void setYearMonthFormat(String yearMonthFormat) {
         this.yearMonthFormat = yearMonthFormat;
     }
 
     /**
      * Getter pair of {@link #setYearMonthFormat(String)}.
+     *
      * @since 2.3.32
      */
     public String getYearMonthFormat() {
@@ -1571,7 +1705,7 @@ public class Configurable {
      *      are these: {@link Instant}, {@link LocalDate}, {@link 
LocalDateTime}, {@link LocalTime},
      *      {@link OffsetDateTime}, {@link OffsetTime}, {@link Year}, {@link 
YearMonth}, {@link ZonedDateTime}.
      *
-     * @return Never {@code null}, maybe {@code ""} though.
+     * @return Never {@code null}.
      *
      * @throws NullPointerException If {@link temporalClass} was {@code null}
      * @throws IllegalArgumentException If {@link temporalClass} is not a 
supported {@link Temporal} subclass.
diff --git a/src/main/java/freemarker/core/Environment.java 
b/src/main/java/freemarker/core/Environment.java
index 283e062..4f0826c 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -2217,7 +2217,8 @@ public final class Environment extends Configurable {
      * @param blamedTtmSourceExp
      *            The blamed expression if an error occurs; only used for 
error messages.
      */
-    String formatTemporalToPlainText(TemplateTemporalModel ttm, String 
formatString,
+    String formatTemporalToPlainText(
+            TemplateTemporalModel ttm, String formatString,
             Expression blamedTtmSourceExp, Expression blamedFormatterSourceExp,
             boolean useTempModelExc)
             throws TemplateException {
@@ -2225,21 +2226,26 @@ public final class Environment extends Configurable {
                 formatString, ttm,
                 blamedTtmSourceExp, blamedFormatterSourceExp,
                 useTempModelExc);
-        try {
-            return EvalUtil.assertFormatResultNotNull(ttf.format(ttm));
-        } catch (TemplateValueFormatException e) {
-            throw _MessageUtil.newCantFormatTemporalException(ttf, 
blamedTtmSourceExp, e, true);
-        }
+        return Environment.this.formatTemporalToPlainText(ttm, 
blamedTtmSourceExp, ttf, true);
     }
 
-    String formatTemporalToPlainText(TemplateTemporalModel ttm, Expression 
blamedTtmSourceExp,
-            boolean useTempModelExc) throws TemplateException {
+    String formatTemporalToPlainText(
+            TemplateTemporalModel ttm, Expression blamedTtmSourceExp,
+            boolean useTempModelExc)
+            throws TemplateException {
         TemplateTemporalFormat ttf = getTemplateTemporalFormat(
                 ttm, blamedTtmSourceExp, useTempModelExc);
+        return formatTemporalToPlainText(ttm, blamedTtmSourceExp, ttf, false);
+    }
+
+    String formatTemporalToPlainText(
+            TemplateTemporalModel ttm, Expression blamedTtmSourceExp, 
TemplateTemporalFormat ttf,
+            boolean useTempModelExc)
+            throws TemplateException {
         try {
             return EvalUtil.assertFormatResultNotNull(ttf.format(ttm));
         } catch (TemplateValueFormatException e) {
-            throw _MessageUtil.newCantFormatTemporalException(ttf, 
blamedTtmSourceExp, e, false);
+            throw _MessageUtil.newCantFormatTemporalException(ttf, ttm, 
blamedTtmSourceExp, e, useTempModelExc);
         }
     }
 
@@ -2275,8 +2281,8 @@ public final class Environment extends Configurable {
             }
 
             _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
-                    "The value of the \"", settingName,
-                    "\" FreeMarker configuration setting is a malformed 
temporal format string: ",
+                    "Problem with using the \"", settingName,
+                    "\" FreeMarker configuration setting value, ",
                     new _DelayedJQuote(settingValue), ". Reason given: ",
                     e.getMessage());
             throw useTempModelExc ? new _TemplateModelException(e, desc) : new 
_MiscTemplateException(e, desc);
diff --git a/src/main/java/freemarker/core/EvalUtil.java 
b/src/main/java/freemarker/core/EvalUtil.java
index 78d626a..2dbf57e 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -413,7 +413,7 @@ class EvalUtil {
             try {
                 return assertFormatResultNotNull(format.format(ttm));
             } catch (TemplateValueFormatException e) {
-                throw _MessageUtil.newCantFormatTemporalException(format, exp, 
e, false);
+                throw _MessageUtil.newCantFormatTemporalException(format, ttm, 
exp, e, false);
             }
         } else if (tm instanceof TemplateMarkupOutputModel) {
             return tm;
@@ -424,10 +424,10 @@ class EvalUtil {
 
     /**
      * Like {@link #coerceModelToStringOrMarkup(TemplateModel, Expression, 
String, Environment)}, but gives error
-     * if the result is markup. This is what you normally used where markup 
results can't be used.
+     * if the result is markup. This is what you normally use where markup 
results can't be used.
      *
      * @param seqTip
-     *            Tip to display if the value type is not coercable, but it's 
sequence or collection.
+     *            Tip to display if the value type is not coercable, and it's 
sequence or collection.
      * 
      * @return Never {@code null}
      */
@@ -456,7 +456,7 @@ class EvalUtil {
             try {
                 return ensureFormatResultString(format.format(ttm), exp, env);
             } catch (TemplateValueFormatException e) {
-                throw _MessageUtil.newCantFormatTemporalException(format, exp, 
e, false);
+                throw _MessageUtil.newCantFormatTemporalException(format, ttm, 
exp, e, false);
             }
         } else {
             return coerceModelToTextualCommon(tm, exp, seqTip, false, false, 
env);
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java 
b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
index 5482826..fb5f32d 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
@@ -25,7 +25,10 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.OffsetDateTime;
 import java.time.OffsetTime;
+import java.time.Year;
+import java.time.YearMonth;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
@@ -49,7 +52,8 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
 
     enum FormatTimeConversion {
         INSTANT_TO_ZONED_DATE_TIME,
-        SET_ZONE_FROM_OFFSET
+        SET_ZONE_FROM_OFFSET,
+        CONVERT_TO_CURRENT_ZONE
     }
 
     static final String SHORT = "short";
@@ -71,20 +75,13 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
 
         temporalClass = 
_CoreTemporalUtils.normalizeSupportedTemporalClass(temporalClass);
 
-        Matcher localizedPatternMatcher = 
FORMAT_STYLE_PATTERN.matcher(formatString);
-        boolean isLocalizedPattern = localizedPatternMatcher.matches();
-        if (temporalClass == Instant.class) {
-            this.formatTimeConversion = 
FormatTimeConversion.INSTANT_TO_ZONED_DATE_TIME;
-        } else if (isLocalizedPattern && (temporalClass == 
OffsetDateTime.class || temporalClass == OffsetTime.class)) {
-            this.formatTimeConversion = 
FormatTimeConversion.SET_ZONE_FROM_OFFSET;
-        } else {
-            this.formatTimeConversion = null;
-        }
+        Matcher formatStylePatternMatcher = 
FORMAT_STYLE_PATTERN.matcher(formatString);
+        boolean isFormatStyleString = formatStylePatternMatcher.matches();
 
         DateTimeFormatter dateTimeFormatter;
-        if (isLocalizedPattern) {
-            FormatStyle datePartFormatStyle = 
FormatStyle.valueOf(localizedPatternMatcher.group(1).toUpperCase(Locale.ROOT));
-            String group2 = localizedPatternMatcher.group(2);
+        if (isFormatStyleString) {
+            FormatStyle datePartFormatStyle = 
FormatStyle.valueOf(formatStylePatternMatcher.group(1).toUpperCase(Locale.ROOT));
+            String group2 = formatStylePatternMatcher.group(2);
             FormatStyle timePartFormatStyle = group2 != null
                     ? FormatStyle.valueOf(group2.toUpperCase(Locale.ROOT))
                     : datePartFormatStyle;
@@ -97,8 +94,8 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
                 dateTimeFormatter = 
DateTimeFormatter.ofLocalizedDate(datePartFormatStyle);
             } else {
                 throw new InvalidFormatParametersException(
-                        "Format " + StringUtil.jQuote(formatString) + " is not 
supported for "
-                        + temporalClass.getName());
+                        "Format styles (like " + 
StringUtil.jQuote(formatString) + ") is not supported for "
+                        + temporalClass.getName() + " values.");
             }
         } else {
             try {
@@ -109,6 +106,30 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
         }
         this.dateTimeFormatter = dateTimeFormatter.withLocale(locale);
 
+        if (isLocalTemporalClass(temporalClass)) {
+            this.formatTimeConversion = null;
+        } else {
+            if (showsZone(dateTimeFormatter)) {
+                if (temporalClass == Instant.class) {
+                    this.formatTimeConversion = 
FormatTimeConversion.INSTANT_TO_ZONED_DATE_TIME;
+                } else if (isFormatStyleString &&
+                        (temporalClass == OffsetDateTime.class || 
temporalClass == OffsetTime.class)) {
+                    this.formatTimeConversion = 
FormatTimeConversion.SET_ZONE_FROM_OFFSET;
+                } else {
+                    this.formatTimeConversion = null;
+                }
+            } else {
+                if (temporalClass == OffsetTime.class && 
timeZone.useDaylightTime()) {
+                    throw new InvalidFormatParametersException(
+                            "The format must show the time offset, as the 
current FreeMarker time zone, "
+                                    + StringUtil.jQuote(timeZone.getID()) + ", 
may uses Daylight Saving Time, and thus "
+                                    + "it's not possible to convert the value 
to the local time in that zone, "
+                                    + "since we don't know the day.");
+                }
+                this.formatTimeConversion = 
FormatTimeConversion.CONVERT_TO_CURRENT_ZONE;
+            }
+        }
+
         this.zoneId = timeZone.toZoneId();
     }
 
@@ -119,14 +140,33 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
 
         if (formatTimeConversion == 
FormatTimeConversion.INSTANT_TO_ZONED_DATE_TIME) {
             temporal = ((Instant) temporal).atZone(zoneId);
+        } else if (formatTimeConversion == 
FormatTimeConversion.CONVERT_TO_CURRENT_ZONE) {
+            if (temporal instanceof Instant) {
+                temporal = ((Instant) temporal).atZone(zoneId);
+            } else if (temporal instanceof OffsetDateTime) {
+                temporal = ((OffsetDateTime) 
temporal).atZoneSameInstant(zoneId);
+            } else if (temporal instanceof ZonedDateTime) {
+                temporal = ((ZonedDateTime) 
temporal).withZoneSameInstant(zoneId);
+            } else if (temporal instanceof OffsetTime) {
+                // Because of logic in the constructor, this is only reached 
if the zone never uses Daylight Saving.
+                temporal = ((OffsetTime) 
temporal).withOffsetSameInstant(zoneId.getRules().getOffset(Instant.EPOCH));
+            } else {
+                throw new InvalidFormatParametersException(
+                        "Don't know how to convert value of type " + 
temporal.getClass().getName() + " to the current "
+                                + "FreeMarker time zone, " + 
StringUtil.jQuote(zoneId.getId()) + ", which is "
+                                + "needed to format with " + 
StringUtil.jQuote(formatString) + ".");
+            }
         } else if (formatTimeConversion == 
FormatTimeConversion.SET_ZONE_FROM_OFFSET) {
+            // Formats like "long" want a time zone field, but oddly, they 
don't treat the zoneOffset as such.
             if (temporal instanceof OffsetDateTime) {
-                dateTimeFormatter = 
dateTimeFormatter.withZone(((OffsetDateTime) temporal).getOffset());
+                OffsetDateTime offsetDateTime = (OffsetDateTime) temporal;
+                temporal = ZonedDateTime.of(offsetDateTime.toLocalDateTime(), 
offsetDateTime.getOffset());
             } else if (temporal instanceof OffsetTime) {
+                // There's no ZonedTime class, so we must manipulate the 
format.
                 dateTimeFormatter = dateTimeFormatter.withZone(((OffsetTime) 
temporal).getOffset());
             } else {
                 throw new IllegalArgumentException(
-                        "Formatter was created for OffsetTime and 
OffsetDateTime, but value was a "
+                        "Formatter was created for OffsetTime or 
OffsetDateTime, but value was a "
                                 + 
ClassUtil.getShortClassNameOfObject(temporal));
             }
         }
@@ -159,4 +199,22 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
         return true;
     }
 
+    private static final ZonedDateTime SHOWS_ZONE_SAMPLE_TEMPORAL_1 = 
ZonedDateTime.of(
+            LocalDateTime.of(2011, 1, 1, 1, 1), ZoneOffset.ofHours(0));
+    private static final ZonedDateTime SHOWS_ZONE_SAMPLE_TEMPORAL_2 = 
ZonedDateTime.of(
+            LocalDateTime.of(2011, 1, 1, 1, 1), ZoneOffset.ofHours(1));
+
+    private boolean showsZone(DateTimeFormatter dateTimeFormatter) {
+        return !dateTimeFormatter.format(SHOWS_ZONE_SAMPLE_TEMPORAL_1)
+                
.equals(dateTimeFormatter.format(SHOWS_ZONE_SAMPLE_TEMPORAL_2));
+    }
+
+    private static boolean isLocalTemporalClass(Class<? extends Temporal> 
normalizedTemporalClass) {
+        return normalizedTemporalClass == LocalDateTime.class
+                || normalizedTemporalClass == LocalTime.class
+                || normalizedTemporalClass == LocalDate.class
+                || normalizedTemporalClass == Year.class
+                || normalizedTemporalClass == YearMonth.class;
+    }
+
 }
diff --git a/src/main/java/freemarker/core/_MessageUtil.java 
b/src/main/java/freemarker/core/_MessageUtil.java
index 034ec0f..74b4664 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -19,6 +19,7 @@
 
 package freemarker.core;
 
+import java.time.temporal.Temporal;
 import java.util.ArrayList;
 
 import freemarker.template.Template;
@@ -27,6 +28,7 @@ import freemarker.template.TemplateHashModelEx;
 import freemarker.template.TemplateHashModelEx2;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTemporalModel;
 import freemarker.template.utility.StringUtil;
 
 /**
@@ -322,10 +324,12 @@ public class _MessageUtil {
                 : new _MiscTemplateException(e, null, desc);
     }
     
-    public static TemplateException 
newCantFormatTemporalException(TemplateTemporalFormat format, Expression 
dataSrcExp,
+    public static TemplateException 
newCantFormatTemporalException(TemplateTemporalFormat format, 
TemplateTemporalModel ttm, Expression dataSrcExp,
             TemplateValueFormatException e, boolean useTempModelExc) {
         _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
-                "Failed to format temporal value with format ", new 
_DelayedJQuote(format.getDescription()), ": ",
+                "Failed to format temporal value of class ", 
safeGetTemporalClass(ttm),
+                        ", value ", new _DelayedJQuote(new 
_DelayedToString(safeGetTemporalValue(ttm))),
+                        ", with format ", new 
_DelayedJQuote(format.getDescription()), ": ",
                 e.getMessage())
                 .blame(dataSrcExp);
         return useTempModelExc
@@ -333,6 +337,23 @@ public class _MessageUtil {
                 : new _MiscTemplateException(e, null, desc);
     }
 
+    private static String safeGetTemporalClass(TemplateTemporalModel ttm) {
+        try {
+            return ttm.getAsTemporal().getClass().getName();
+        } catch (TemplateModelException e) {
+            return "[failed to get]";
+        }
+    }
+
+    private static Object safeGetTemporalValue(TemplateTemporalModel ttm) {
+        try {
+            Temporal value = ttm.getAsTemporal();
+            return value != null ? value : "null";
+        } catch (TemplateModelException e) {
+            return "[failed to get]";
+        }
+    }
+
     public static TemplateException 
newCantFormatNumberException(TemplateNumberFormat format, Expression dataSrcExp,
             TemplateValueFormatException e, boolean useTempModelExc) {
         _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
diff --git a/src/main/java/freemarker/template/TemplateDateModel.java 
b/src/main/java/freemarker/template/TemplateDateModel.java
index a00cbcf..d354fda 100644
--- a/src/main/java/freemarker/template/TemplateDateModel.java
+++ b/src/main/java/freemarker/template/TemplateDateModel.java
@@ -31,6 +31,9 @@ import java.util.List;
  * <p>
  * Objects of this type should be immutable, that is, calling {@link 
#getAsDate()} and {@link #getDateType()} should
  * always return the same value as for the first time.
+ *
+ * <p>{@link java.time.temporal.Temporal} values (the date/time classes 
introduced with Java 8) are handled by
+ * {@link TemplateTemporalModel}.
  */
 public interface TemplateDateModel extends TemplateModel {
     
diff --git a/src/main/java/freemarker/template/TemplateTemporalModel.java 
b/src/main/java/freemarker/template/TemplateTemporalModel.java
index c02e3e5..1f2d6d6 100644
--- a/src/main/java/freemarker/template/TemplateTemporalModel.java
+++ b/src/main/java/freemarker/template/TemplateTemporalModel.java
@@ -18,8 +18,22 @@
  */
 package freemarker.template;
 
+import java.time.LocalDateTime;
+import java.time.YearMonth;
 import java.time.temporal.Temporal;
 
+/**
+ * Any {@link Temporal} value that's included in Java; in Java 8 these are: 
{@link LocalDateTime}, {@link LocalDate},
+ * {@link LocalTime}, {@link OffsetDateTime}, {@link OffsetTime}, {@link 
ZonedDateTime}, {@link ZonedTime},
+ * {@link YearMonth}, {@link Year}.
+ * This does not deal with {@link java.time.Duration}, and {@link 
java.time.Period}, because those don't implement the
+ * {@link Temporal} interface.
+ *
+ * <p>{@link java.util.Date} values (the way date/time values were represented 
prior Java 8) are handled by
+ * {@link TemplateDateModel}.
+ *
+ * @since 2.3.32
+ */
 public interface TemplateTemporalModel extends TemplateModel {
        /**
         * Returns the date value. The return value must not be {@code null}.
diff --git a/src/test/java/freemarker/core/TemporalFormatTest.java 
b/src/test/java/freemarker/core/TemporalFormatTest.java
new file mode 100644
index 0000000..3e0e3ed
--- /dev/null
+++ b/src/test/java/freemarker/core/TemporalFormatTest.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 freemarker.core;
+
+import static freemarker.test.hamcerst.Matchers.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Year;
+import java.time.YearMonth;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.Temporal;
+import java.util.TimeZone;
+import java.util.function.Consumer;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.SimpleTemporal;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.utility.DateUtil;
+
+public class TemporalFormatTest {
+
+    @Test
+    public void testOffsetTimeAndZones() throws TemplateException, IOException 
{
+        OffsetTime offsetTime = OffsetTime.of(LocalTime.of(10, 0, 0), 
ZoneOffset.ofHours(1));
+
+        TimeZone zoneWithoutDST = TimeZone.getTimeZone("GMT+2");
+        assertFalse(zoneWithoutDST.useDaylightTime());
+
+        TimeZone zoneWithDST = TimeZone.getTimeZone("America/New_York");
+        assertTrue(zoneWithDST.useDaylightTime());
+
+        assertEquals(
+                "11:00",
+                formatTemporal(
+                        conf -> {
+                            conf.setOffsetTimeFormat("HH:mm");
+                            conf.setTimeZone(zoneWithoutDST);
+                        },
+                        offsetTime));
+
+        try {
+            assertEquals(
+                    "11:00",
+                    formatTemporal(
+                            conf -> {
+                                conf.setOffsetTimeFormat("HH:mm");
+                                conf.setTimeZone(zoneWithDST);
+                            },
+                            offsetTime));
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("daylight 
saving"));
+        }
+
+        assertEquals(
+                "10:00+01",
+                formatTemporal(
+                        conf -> {
+                            conf.setOffsetTimeFormat("HH:mmX");
+                            conf.setTimeZone(zoneWithDST);
+                        },
+                        offsetTime));
+
+        assertEquals(
+                "10:00+01",
+                formatTemporal(
+                        conf -> {
+                            conf.setOffsetTimeFormat("HH:mmX");
+                            conf.setTimeZone(zoneWithoutDST);
+                        },
+                        offsetTime));
+    }
+
+    @Test
+    public void testZoneConvertedWhenOffsetOrZoneNotShown() throws 
TemplateException, IOException {
+        TimeZone gbZone = TimeZone.getTimeZone("GB");
+        assertTrue(gbZone.useDaylightTime());
+        // Summer: GMT+1
+        // Winter: GMT+0
+
+        TimeZone nyZone = TimeZone.getTimeZone("America/New_York");
+        assertTrue(nyZone.useDaylightTime());
+        // Summer: GMT-4
+        // Winter: GMT-5
+
+        LocalTime localTime = LocalTime.of(10, 30, 0);
+        LocalDate winterLocalDate = LocalDate.of(2021, 12, 30);
+        LocalDate summerLocalDate = LocalDate.of(2021, 6, 30);
+        LocalDateTime winterLocalDateTime = LocalDateTime.of(winterLocalDate, 
localTime);
+        OffsetDateTime winterOffsetDateTime = 
OffsetDateTime.of(winterLocalDateTime, ZoneOffset.ofHours(2));
+        ZonedDateTime winterZonedDateTime = 
ZonedDateTime.of(winterLocalDateTime, nyZone.toZoneId());
+        LocalDateTime summerLocalDateTime = LocalDateTime.of(summerLocalDate, 
localTime);
+        OffsetDateTime summerOffsetDateTime = 
OffsetDateTime.of(summerLocalDateTime, ZoneOffset.ofHours(2));
+        ZonedDateTime summerZonedDateTime = 
ZonedDateTime.of(summerLocalDateTime, nyZone.toZoneId());
+
+        // If time zone (or offset) is not shown, the value is converted to 
the FreeMarker time zone:
+        assertEquals(
+                "2021-06-30 10:30, 2021-06-30 09:30, 2021-06-30 15:30, "
+                        + "2021-12-30 10:30, 2021-12-30 08:30, 2021-12-30 
15:30",
+                formatTemporal(
+                        conf -> {
+                            conf.setLocalDateTimeFormat("yyyy-MM-dd HH:mm");
+                            conf.setOffsetDateTimeFormat("yyyy-MM-dd HH:mm");
+                            conf.setZonedDateTimeFormat("yyyy-MM-dd HH:mm");
+                            conf.setTimeZone(gbZone);
+                        },
+                        summerLocalDateTime, summerOffsetDateTime, 
summerZonedDateTime,
+                        winterLocalDateTime, winterOffsetDateTime, 
winterZonedDateTime));
+        assertEquals(
+                "2021-06-30 10:30, 2021-06-30 08:30, 2021-06-30 14:30, "
+                        + "2021-12-30 10:30, 2021-12-30 08:30, 2021-12-30 
15:30",
+                formatTemporal(
+                        conf -> {
+                            conf.setLocalDateTimeFormat("yyyy-MM-dd HH:mm");
+                            conf.setOffsetDateTimeFormat("yyyy-MM-dd HH:mm");
+                            conf.setZonedDateTimeFormat("yyyy-MM-dd HH:mm");
+                            conf.setTimeZone(DateUtil.UTC);
+                        },
+                        summerLocalDateTime, summerOffsetDateTime, 
summerZonedDateTime,
+                        winterLocalDateTime, winterOffsetDateTime, 
winterZonedDateTime));
+
+        // If the time zone (or offset) is shown, the value is not converted 
from its original time zone:
+        assertEquals(
+                "2021-06-30 10:30+02, 2021-06-30 10:30-04, "
+                        + "2021-12-30 10:30+02, 2021-12-30 10:30-05",
+                formatTemporal(
+                        conf -> {
+                            conf.setOffsetDateTimeFormat("yyyy-MM-dd HH:mmX");
+                            conf.setZonedDateTimeFormat("yyyy-MM-dd HH:mmX");
+                            conf.setTimeZone(gbZone);
+                        },
+                        summerOffsetDateTime, summerZonedDateTime,
+                        winterOffsetDateTime, winterZonedDateTime));
+    }
+
+    @Test
+    public void testCanNotFormatLocalIfTimeZoneIsShown() {
+        try {
+            formatTemporal(
+                    conf -> {
+                        conf.setLocalDateTimeFormat("yyyy-MM-dd HH:mmX");
+                    },
+                    LocalDateTime.of(2021, 10, 30, 1, 2));
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(),
+                    allOf(
+                            containsString("LocalDateTime"),
+                            containsString("2021-10-30T01:02"),
+                            containsString("yyyy-MM-dd HH:mmX"),
+                            anyOf(containsStringIgnoringCase("offset"), 
containsStringIgnoringCase("zone"))));
+        }
+    }
+
+    @Test
+    public void testStylesAreNotSupportedForYear() {
+        try {
+            formatTemporal(
+                    conf -> {
+                        conf.setYearFormat("medium");
+                    },
+                    Year.of(2021));
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(),
+                    allOf(
+                            containsString("\"medium\""),
+                            containsString(Year.class.getName()),
+                            containsStringIgnoringCase("style")));
+        }
+    }
+
+    @Test
+    public void testStylesAreNotSupportedForYearMonth() {
+        try {
+            formatTemporal(
+                    conf -> {
+                        conf.setYearMonthFormat("medium");
+                    },
+                    YearMonth.of(2021, 10));
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(),
+                    allOf(
+                            containsString("\"medium\""),
+                            containsString(YearMonth.class.getName()),
+                            containsStringIgnoringCase("style")));
+        }
+    }
+
+    static private String formatTemporal(Consumer<Configurable> configurer, 
Temporal... values) throws
+            TemplateException {
+        Configuration conf = new Configuration(Configuration.VERSION_2_3_32);
+
+        configurer.accept(conf);
+
+        Environment env = null;
+        try {
+            env = new Template(null, "", 
conf).createProcessingEnvironment(null, null);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Temporal value : values) {
+            if (sb.length() != 0) {
+                sb.append(", ");
+            }
+            sb.append(env.formatTemporalToPlainText(new SimpleTemporal(value), 
null, false));
+        }
+
+        return sb.toString();
+    }
+}
diff --git 
a/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl 
b/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
index 5c42c29..cdadba8 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
@@ -16,15 +16,15 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<@assertEquals expected="2003-04-05T07:07:08+01:00[GMT+01:00]" 
actual=instant?string />
-<@assertEquals expected="2003-04-05T06:07:08" actual=localDateTime?string />
-<@assertEquals expected="2003-04-05" actual=localDate?string />
-<@assertEquals expected="06:07:08" actual=localTime?string />
-<@assertEquals expected="2003-04-05T06:07:08Z" actual=offsetDateTime?string />
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="Apr 5, 2003 7:07:08 AM" actual=instant?string />
+<@assertEquals expected="Apr 5, 2003 6:07:08 AM" actual=localDateTime?string />
+<@assertEquals expected="Apr 5, 2003" actual=localDate?string />
+<@assertEquals expected="6:07:08 AM" actual=localTime?string />
+<@assertEquals expected="Apr 5, 2003 7:07:08 AM" actual=offsetDateTime?string 
/>
+<@assertEquals expected="6:07:08 AM Z" actual=offsetTime?string />
 <@assertEquals expected="2003" actual=year?string />
 <@assertEquals expected="2003-04" actual=yearMonth?string />
-<@assertEquals expected="2003-04-05T06:07:08Z[UTC]" 
actual=zonedDateTime?string />
+<@assertEquals expected="Apr 5, 2003 7:07:08 AM" actual=zonedDateTime?string />
 
 <#setting timeZone="America/New_York">
 <@assertEquals expected="2003-04-05T01:07:08-05:00" actual=instant?string.iso 
/>
@@ -32,7 +32,7 @@
 <@assertEquals expected="2003-04-05" actual=localDate?string.iso />
 <@assertEquals expected="06:07:08" actual=localTime?string.iso />
 <@assertEquals expected="2003-04-05T06:07:08Z" 
actual=offsetDateTime?string.iso />
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="06:07:08Z" actual=offsetTime?string.iso />
 <@assertEquals expected="2003" actual=year?string.iso />
 <@assertEquals expected="2003-04" actual=yearMonth?string.iso />
 <@assertEquals expected="2003-04-05T06:07:08Z" actual=zonedDateTime?string.iso 
/>
@@ -43,7 +43,7 @@
 <@assertEquals expected="2003-04-05" actual=localDate?string.iso />
 <@assertEquals expected="06:07:08" actual=localTime?string.iso />
 <@assertEquals expected="2003-04-05T06:07:08Z" 
actual=offsetDateTime?string.iso />
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="06:07:08Z" actual=offsetTime?string.iso />
 <@assertEquals expected="2003" actual=year?string.iso />
 <@assertEquals expected="2003-04" actual=yearMonth?string.iso />
 <@assertEquals expected="2003-04-05T06:07:08Z" actual=zonedDateTime?string.iso 
/>
@@ -53,7 +53,7 @@
 <@assertEquals expected="2003-04-05" actual=localDate?string.xs />
 <@assertEquals expected="06:07:08" actual=localTime?string.xs />
 <@assertEquals expected="2003-04-05T06:07:08Z" actual=offsetDateTime?string.xs 
/>
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="06:07:08Z" actual=offsetTime?string.xs />
 <@assertEquals expected="2003" actual=year?string.xs />
 <@assertEquals expected="2003-04" actual=yearMonth?string.xs />
 <@assertEquals expected="2003-04-05T06:07:08Z" actual=zonedDateTime?string.xs 
/>
@@ -65,14 +65,14 @@
 <@assertEquals expected="5 avril 2003 01:07:08 EST" actual=instant?string.long 
/>
 <@assertEquals expected="samedi 5 avril 2003 01 h 07 EST" 
actual=instant?string.full />
 
-<@assertEquals expected="05/04/03 06:07" actual=offsetDateTime?string.short />
-<@assertEquals expected="5 avr. 2003 06:07:08" 
actual=offsetDateTime?string.medium />
+<@assertEquals expected="05/04/03 01:07" actual=offsetDateTime?string.short />
+<@assertEquals expected="5 avr. 2003 01:07:08" 
actual=offsetDateTime?string.medium />
 <@assertEquals expected="5 avril 2003 06:07:08 Z" 
actual=offsetDateTime?string.long />
 <@assertEquals expected="samedi 5 avril 2003 06 h 07 Z" 
actual=offsetDateTime?string.full />
 
 <@assertEquals expected="05/04/03 06:07" actual=localDateTime?string.short />
 <@assertEquals expected="5 avr. 2003 06:07:08" 
actual=localDateTime?string.medium />
-<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
+<#-- These fail on Java 8 because of JDK-8085887
 <@assertEquals expected="5 avril 2003 06:07:08 ET" 
actual=localDateTime?string.long />
 <@assertEquals expected="samedi 5 avril 2003 06 h 07 ET" 
actual=localDateTime?string.full />
 -->
@@ -87,14 +87,14 @@
 <@assertFails message="not supported for 
java.time.YearMonth">${yearMonth?string.long}</@>
 <@assertFails message="not supported for 
java.time.YearMonth">${yearMonth?string.full}</@>
 
-<@assertEquals expected="05/04/03 06:07" actual=zonedDateTime?string.short />
-<@assertEquals expected="5 avr. 2003 06:07:08" 
actual=zonedDateTime?string.medium />
+<@assertEquals expected="05/04/03 01:07" actual=zonedDateTime?string.short />
+<@assertEquals expected="5 avr. 2003 01:07:08" 
actual=zonedDateTime?string.medium />
 <@assertEquals expected="5 avril 2003 06:07:08 UTC" 
actual=zonedDateTime?string.long />
 <@assertEquals expected="samedi 5 avril 2003 06 h 07 UTC" 
actual=zonedDateTime?string.full />
 
 <@assertEquals expected="05/04/03 06:07" 
actual=localDateTime?string.short_short />
 <@assertEquals expected="05/04/03 06:07:08" 
actual=localDateTime?string.short_medium />
-<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
+<#-- These fail on Java 8 because of JDK-8085887
 <@assertEquals expected="05/04/03 06:07:08 ET" 
actual=localDateTime?string.short_long />
 <@assertEquals expected="05/04/03 06 h 07 ET" 
actual=localDateTime?string.short_full />
 -->
@@ -103,12 +103,12 @@
 <@assertEquals expected="5 avril 2003 06:07:08" 
actual=localDateTime?string.long_medium />
 <@assertEquals expected="samedi 5 avril 2003 06:07:08" 
actual=localDateTime?string.full_medium />
 
-<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
+<#-- These fail on Java 8 because of JDK-8085887
 <@assertEquals expected="5 avril 2003 06:07:08 ET" 
actual=localDateTime?string.long_long />
 <@assertEquals expected="samedi 5 avril 2003 06:07:08 ET" 
actual=localDateTime?string.full_long />
 -->
 
-<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
+<#-- These fail on Java 8 because of JDK-8085887
 <@assertEquals expected="samedi 5 avril 2003 06 h 07 ET" 
actual=localDateTime?string.full_full />
 -->
 
@@ -124,12 +124,12 @@
 <#setting localDateFormat="yyyy MMM dd">
 <@assertEquals expected="2003 Apr 05" actual=localDate?string />
 <#setting localDateTimeFormat="HH:mm:ss">
-<@assertEquals expected="06:07:08" actual=localTime?string />
+<@assertEquals expected="6:07:08 AM" actual=localTime?string />
 <#setting offsetDateTimeFormat="yyyy MMM dd HH:mm:ss">
-<@assertEquals expected="2003 Apr 05 06:07:08" actual=offsetDateTime?string />
+<@assertEquals expected="2003 Apr 05 01:07:08" actual=offsetDateTime?string />
 <#setting yearFormat="yyyy">
 <@assertEquals expected="2003" actual=year?string />
 <#setting yearMonthFormat="yyyy MMM">
 <@assertEquals expected="2003 Apr" actual=yearMonth?string />
 <#setting zonedDateTimeFormat="yyyy MMM dd HH:mm:ss">
-<@assertEquals expected="2003 Apr 05 06:07:08" actual=zonedDateTime?string />
+<@assertEquals expected="2003 Apr 05 01:07:08" actual=zonedDateTime?string />
diff --git a/src/test/resources/logback-test.xml 
b/src/test/resources/logback-test.xml
index e3876ec..0aefe85 100644
--- a/src/test/resources/logback-test.xml
+++ b/src/test/resources/logback-test.xml
@@ -26,6 +26,7 @@
        </appender>
        
        <logger name="org.eclipse.jetty" level="INFO" />
+       <logger name="freemarker.runtime.attempt" level="INFO" />
 
        <root level="debug">
                <appender-ref ref="STDOUT" />

Reply via email to