You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2022/07/16 19:03:20 UTC

[freemarker] branch FREEMARKER-35 updated (192364cd -> 28545e14)

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

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


    from 192364cd [FREEMARKER-35] Added MissingTimeZoneParserPolicy to parse method, and implemented it for JavaTemplateTemporalFormat and ISOLikeTemplateTemporalTemporalFormat. Added more parsing tests. Changed TemporalUtils to internal class (now called _TemporalUtils).
     new e5cbaa39 [FREEMARKER-35] Added temporal format caching to Environment. TemplateTemporalFormat was adjusted for the needs of that.
     new 28545e14 [FREEMARKER-35] Code cleanup in Temporal related code

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/main/java/freemarker/core/Configurable.java    |  29 +-
 ...eTimeFormatterBasedTemplateTemporalFormat.java} | 115 ++++---
 src/main/java/freemarker/core/Environment.java     | 366 +++++++++++++++++++--
 .../ISOLikeTemplateTemporalTemporalFormat.java     |  44 +--
 .../core/ISOTemplateTemporalFormatFactory.java     |  10 +-
 .../core/JavaTemplateTemporalFormat.java           | 137 ++++----
 .../core/JavaTemplateTemporalFormatFactory.java    |   5 +-
 .../core/MissingTimeZoneParserPolicy.java          |   5 +-
 .../java/freemarker/core/TemplateDateFormat.java   |  48 +--
 .../java/freemarker/core/TemplateNumberFormat.java |  45 +--
 .../freemarker/core/TemplateTemporalFormat.java    |  72 ++--
 .../core/TemplateTemporalFormatFactory.java        |   2 +-
 .../java/freemarker/core/TemplateValueFormat.java  |   3 +-
 .../core/XSTemplateTemporalFormatFactory.java      |   7 +-
 src/main/java/freemarker/core/_MessageUtil.java    |   4 +-
 src/main/java/freemarker/core/_TemporalUtils.java  |  14 +-
 .../java/freemarker/template/Configuration.java    |   4 +
 .../freemarker/template/TemplateDateModel.java     |   9 +-
 .../freemarker/template/TemplateTemporalModel.java |   5 +-
 .../getTemplateTemporalFormatCaching.ftl           |  25 ++
 .../core/AbstractTemporalFormatTest.java           |   2 +-
 ...pochMillisDivTemplateTemporalFormatFactory.java |   4 +-
 .../EpochMillisTemplateTemporalFormatFactory.java  |   4 +-
 .../core/HTMLISOTemplateTemporalFormatFactory.java |   8 +-
 .../core/ISOLikeTemplateTemporalFormatTest.java    |   4 +-
 .../core/JavaTemplateTemporalFormatTest.java       |  41 ++-
 ...ndTZSensitiveTemplateTemporalFormatFactory.java |   4 +-
 ...lateTemporalFormatCachingInEnvironmentTest.java | 248 ++++++++++++++
 .../java/freemarker/core/_TemporalUtilsTest.java   |  12 +-
 29 files changed, 968 insertions(+), 308 deletions(-)
 rename src/main/java/freemarker/core/{DateTimeFormatBasedTemplateTemporalFormat.java => DateTimeFormatterBasedTemplateTemporalFormat.java} (50%)
 create mode 100644 src/main/misc/templateTemporalFormatCache/getTemplateTemporalFormatCaching.ftl
 create mode 100644 src/test/java/freemarker/core/TemplateTemporalFormatCachingInEnvironmentTest.java


[freemarker] 01/02: [FREEMARKER-35] Added temporal format caching to Environment. TemplateTemporalFormat was adjusted for the needs of that.

Posted by dd...@apache.org.
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

commit e5cbaa391625f566c6f33a597fe0efb013924421
Author: ddekany <dd...@apache.org>
AuthorDate: Sun Mar 6 23:51:02 2022 +0100

    [FREEMARKER-35] Added temporal format caching to Environment. TemplateTemporalFormat was adjusted for the needs of that.
---
 src/main/java/freemarker/core/Configurable.java    |   4 +-
 src/main/java/freemarker/core/Environment.java     | 336 ++++++++++++++++++++-
 .../ISOLikeTemplateTemporalTemporalFormat.java     |  25 +-
 ...va => JavaOrISOLikeTemplateTemporalFormat.java} |  26 +-
 .../core/JavaTemplateTemporalFormat.java           | 132 ++++----
 .../java/freemarker/core/TemplateDateFormat.java   |   4 +-
 .../freemarker/core/TemplateTemporalFormat.java    |  16 +-
 .../getTemplateTemporalFormatCaching.ftl           |  25 ++
 ...pochMillisDivTemplateTemporalFormatFactory.java |   4 +-
 .../EpochMillisTemplateTemporalFormatFactory.java  |   4 +-
 .../core/HTMLISOTemplateTemporalFormatFactory.java |   8 +-
 .../core/JavaTemplateTemporalFormatTest.java       |  41 +--
 ...ndTZSensitiveTemplateTemporalFormatFactory.java |   4 +-
 ...lateTemporalFormatCachingInEnvironmentTest.java | 248 +++++++++++++++
 14 files changed, 731 insertions(+), 146 deletions(-)

diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 525c25fe..57becfac 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -2950,9 +2950,9 @@ public class Configurable {
             } else if (DATETIME_FORMAT_KEY_SNAKE_CASE.equals(name) || DATETIME_FORMAT_KEY_CAMEL_CASE.equals(name)) {
                 setDateTimeFormat(value);
             } else if (YEAR_FORMAT_KEY_SNAKE_CASE.equals(name) || YEAR_FORMAT_KEY_CAMEL_CASE.equals(name)) {
-                this.yearFormat = value;
+                setYearFormat(value);
             } else if (YEAR_MONTH_FORMAT_KEY_SNAKE_CASE.equals(name) || YEAR_MONTH_FORMAT_KEY_CAMEL_CASE.equals(name)) {
-                this.yearMonthFormat = value;
+                setYearMonthFormat(value);
             } else if (CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE.equals(name)
                     || CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE.equals(name)) {
                 Map map = (Map) _ObjectBuilderSettingEvaluator.eval(
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index b7153d26..cd2ce83e 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -29,7 +29,16 @@ import java.text.Collator;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
+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.Year;
+import java.time.YearMonth;
 import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.time.temporal.Temporal;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -141,9 +150,9 @@ public final class Environment extends Configurable {
     private ZoneId cachedZoneId;
 
     /**
-     * Stores the date/time/date-time formatters that are used when no format is explicitly given at the place of
-     * formatting. That is, in situations like ${lastModified} or even ${lastModified?date}, but not in situations like
-     * ${lastModified?string.iso}.
+     * Stores the date/time/date-time formatters that are used when no format is explicitly given where the value is
+     * converted to text. That is, it's used in situations like ${lastModified} or even ${lastModified?date}, but not in
+     * situations like ${lastModified?string.iso}.
      * 
      * <p>
      * The index of the array is calculated from what kind of formatter we want (see
@@ -159,6 +168,85 @@ public final class Environment extends Configurable {
      * first needed.
      */
     private TemplateDateFormat[] cachedTempDateFormatArray;
+
+    /**
+     * Similar to {@link #cachedTempDateFormatArray}, but for {@link TemplateTemporalFormat}-s. It's not an array as
+     * {@code java.time} classes have no numerical value, unlike legacy FreeMarker date types.
+     */
+    private TemplateTemporalFormatCache cachedTemporalFormatCache;
+    private final class TemplateTemporalFormatCache {
+        // Notes:
+        // - "reusable" fields are set when the current cache field is set
+        // - non-reusable fields are cleared when any related setting is changed, but reusableXxx fields are only
+        //   if the format string changes
+        // - When there's a cache-miss, we check if the "reusable" field has compatible timeZone, and locale, and if
+        //   so, we copy it back into the non-reusable field, and use it.
+
+        private TemplateTemporalFormat localDateTimeFormat;
+        private TemplateTemporalFormat reusableLocalDateTimeFormat;
+        private TemplateTemporalFormat offsetDateTimeFormat;
+        private TemplateTemporalFormat reusableOffsetDateTimeFormat;
+        private TemplateTemporalFormat zonedDateTimeFormat;
+        private TemplateTemporalFormat reusableZonedDateTimeFormat;
+        private TemplateTemporalFormat localDateFormat;
+        private TemplateTemporalFormat reusableLocalDateFormat;
+        private TemplateTemporalFormat localTimeFormat;
+        private TemplateTemporalFormat reusableLocalTimeFormat;
+        private TemplateTemporalFormat offsetTimeFormat;
+        private TemplateTemporalFormat reusableOffsetTimeFormat;
+        private TemplateTemporalFormat yearMonthFormat;
+        private TemplateTemporalFormat reusableYearMonthFormat;
+        private TemplateTemporalFormat yearFormat;
+        private TemplateTemporalFormat reusableYearFormat;
+        private TemplateTemporalFormat instantFormat;
+        private TemplateTemporalFormat reusableInstantFormat;
+
+        private void evictAfterTimeZoneOrLocaleChange() {
+            localDateTimeFormat = null;
+            offsetDateTimeFormat = null;
+            zonedDateTimeFormat = null;
+            localDateFormat = null;
+            localTimeFormat = null;
+            offsetTimeFormat = null;
+            yearMonthFormat = null;
+            yearFormat = null;
+            instantFormat = null;
+        }
+
+        private void evictAfterDateTimeFormatChange() {
+            localDateTimeFormat = null;
+            reusableLocalDateTimeFormat = null;
+            offsetDateTimeFormat = null;
+            reusableOffsetDateTimeFormat = null;
+            zonedDateTimeFormat = null;
+            reusableZonedDateTimeFormat = null;
+            instantFormat = null;
+            reusableInstantFormat = null;
+        }
+
+        private void evictAfterDateFormatChange() {
+            localDateFormat = null;
+            reusableLocalDateFormat = null;
+        }
+
+        private void evictAfterTimeFormatChange() {
+            localTimeFormat = null;
+            reusableLocalTimeFormat = null;
+            offsetTimeFormat = null;
+            reusableOffsetTimeFormat = null;
+        }
+
+        private void evictAfterYearMonthFormatChange() {
+            yearMonthFormat = null;
+            reusableYearMonthFormat = null;
+        }
+
+        private void evictAfterYearFormatChange() {
+            yearFormat = null;
+            reusableYearFormat = null;
+        }
+    }
+
     /** Similar to {@link #cachedTempDateFormatArray}, but used when a formatting string was specified. */
     private HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray;
     private static final int CACHED_TDFS_ZONELESS_INPUT_OFFS = 4;
@@ -1270,6 +1358,10 @@ public final class Environment extends Configurable {
             cachedTempDateFormatsByFmtStrArray = null;
 
             cachedCollator = null;
+
+            if (cachedTemporalFormatCache != null) {
+                cachedTemporalFormatCache.evictAfterTimeZoneOrLocaleChange();
+            }
         }
     }
 
@@ -1294,6 +1386,10 @@ public final class Environment extends Configurable {
             }
 
             cachedSQLDateAndTimeTimeZoneSameAsNormal = null;
+
+            if (cachedTemporalFormatCache != null) {
+                cachedTemporalFormatCache.evictAfterTimeZoneOrLocaleChange();
+            }
         }
     }
 
@@ -1729,6 +1825,10 @@ public final class Environment extends Configurable {
                     cachedTempDateFormatArray[i + TemplateDateModel.TIME] = null;
                 }
             }
+
+            if (cachedTemporalFormatCache != null) {
+                cachedTemporalFormatCache.evictAfterTimeFormatChange();
+            }
         }
     }
 
@@ -1742,6 +1842,10 @@ public final class Environment extends Configurable {
                     cachedTempDateFormatArray[i + TemplateDateModel.DATE] = null;
                 }
             }
+
+            if (cachedTemporalFormatCache != null) {
+                cachedTemporalFormatCache.evictAfterDateFormatChange();
+            }
         }
     }
 
@@ -1755,6 +1859,36 @@ public final class Environment extends Configurable {
                     cachedTempDateFormatArray[i + TemplateDateModel.DATETIME] = null;
                 }
             }
+
+            if (cachedTemporalFormatCache != null) {
+                cachedTemporalFormatCache.evictAfterDateTimeFormatChange();
+            }
+        }
+    }
+
+    @Override
+    public void setYearFormat(String yearFormat) {
+        if (cachedTemporalFormatCache == null) {
+            super.setYearFormat(yearFormat);
+        } else {
+            String prevYearFormat = getYearFormat();
+            super.setYearFormat(yearFormat);
+            if (!yearFormat.equals(prevYearFormat)) {
+                cachedTemporalFormatCache.evictAfterYearFormatChange();
+            }
+        }
+    }
+
+    @Override
+    public void setYearMonthFormat(String yearMonthFormat) {
+        if (cachedTemporalFormatCache == null) {
+            super.setYearMonthFormat(yearMonthFormat);
+        } else {
+            String prevYearMonthFormat = getYearMonthFormat();
+            super.setYearMonthFormat(yearMonthFormat);
+            if (!yearMonthFormat.equals(prevYearMonthFormat)) {
+                cachedTemporalFormatCache.evictAfterYearMonthFormatChange();
+            }
         }
     }
 
@@ -2363,10 +2497,195 @@ public final class Environment extends Configurable {
         }
     }
 
-    TemplateTemporalFormat getTemplateTemporalFormat(Class<? extends Temporal> temporalClass)
+    /**
+     * Returns the current format for the given temporal class.
+     *
+     * @since 2.3.31
+     */
+    public TemplateTemporalFormat getTemplateTemporalFormat(Class<? extends Temporal> temporalClass)
             throws TemplateValueFormatException {
-        // TODO [FREEMARKER-35] Temporal class keyed cache, invalidated by temporalFormat (instantFormat, localDateFormat, etc.), locale, and timeZone change.
-        return getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+        temporalClass = _TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
+        if (cachedTemporalFormatCache == null) {
+            cachedTemporalFormatCache = new TemplateTemporalFormatCache();
+        }
+
+        TemplateTemporalFormat result;
+
+        // BEGIN Generated with getTemplateTemporalFormatCaching.ftl
+        if (temporalClass == LocalDateTime.class) {
+            result = cachedTemporalFormatCache.localDateTimeFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableLocalDateTimeFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.localDateTimeFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.localDateTimeFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableLocalDateTimeFormat = result;
+            return result;
+        }
+        if (temporalClass == Instant.class) {
+            result = cachedTemporalFormatCache.instantFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableInstantFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.instantFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.instantFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableInstantFormat = result;
+            return result;
+        }
+        if (temporalClass == LocalDate.class) {
+            result = cachedTemporalFormatCache.localDateFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableLocalDateFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.localDateFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.localDateFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableLocalDateFormat = result;
+            return result;
+        }
+        if (temporalClass == LocalTime.class) {
+            result = cachedTemporalFormatCache.localTimeFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableLocalTimeFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.localTimeFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.localTimeFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableLocalTimeFormat = result;
+            return result;
+        }
+        if (temporalClass == ZonedDateTime.class) {
+            result = cachedTemporalFormatCache.zonedDateTimeFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableZonedDateTimeFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.zonedDateTimeFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.zonedDateTimeFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableZonedDateTimeFormat = result;
+            return result;
+        }
+        if (temporalClass == OffsetDateTime.class) {
+            result = cachedTemporalFormatCache.offsetDateTimeFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableOffsetDateTimeFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.offsetDateTimeFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.offsetDateTimeFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableOffsetDateTimeFormat = result;
+            return result;
+        }
+        if (temporalClass == OffsetTime.class) {
+            result = cachedTemporalFormatCache.offsetTimeFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableOffsetTimeFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.offsetTimeFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.offsetTimeFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableOffsetTimeFormat = result;
+            return result;
+        }
+        if (temporalClass == YearMonth.class) {
+            result = cachedTemporalFormatCache.yearMonthFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableYearMonthFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.yearMonthFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.yearMonthFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableYearMonthFormat = result;
+            return result;
+        }
+        if (temporalClass == Year.class) {
+            result = cachedTemporalFormatCache.yearFormat;
+            if (result != null) {
+                return result;
+            }
+
+            result = cachedTemporalFormatCache.reusableYearFormat;
+            if (result != null
+                    && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+                cachedTemporalFormatCache.yearFormat = result;
+                return result;
+            }
+
+            result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+            cachedTemporalFormatCache.yearFormat = result;
+            // We do this ahead of time, to decrease the cost of evictions:
+            cachedTemporalFormatCache.reusableYearFormat = result;
+            return result;
+        }
+        // END Generated with getTemplateTemporalFormatCaching.ftl
+
+        throw new AssertionError("Unhandled case: " + temporalClass);
     }
 
     /**
@@ -2406,8 +2725,7 @@ public final class Environment extends Configurable {
 
     private TemplateTemporalFormat getTemplateTemporalFormat(String formatString, Class<? extends Temporal> temporalClass)
             throws TemplateValueFormatException {
-        // TODO [FREEMARKER-35] format keyed cache, invalidated by locale, and timeZone change.
-        return getTemplateTemporalFormatWithoutCache(formatString, temporalClass, getLocale(), getTimeZone());
+        return getTemplateTemporalFormat(formatString, temporalClass, getLocale(), getTimeZone());
     }
 
     /**
@@ -2422,7 +2740,7 @@ public final class Environment extends Configurable {
      * @param zonelessInput
      *            See the similar parameter of {@link TemplateTemporalFormatFactory#get}
      */
-    private TemplateTemporalFormat getTemplateTemporalFormatWithoutCache(
+    private TemplateTemporalFormat getTemplateTemporalFormat(
             String formatString, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone)
             throws TemplateValueFormatException {
         final int formatStringLen = formatString.length();
diff --git a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
index 07d6b8e4..680546dd 100644
--- a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
+++ b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
@@ -31,6 +31,7 @@ import java.time.YearMonth;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.Temporal;
+import java.util.Locale;
 import java.util.TimeZone;
 import java.util.regex.Pattern;
 
@@ -44,7 +45,7 @@ import freemarker.template.TemplateTemporalModel;
  *
  * @since 2.3.32
  */
-final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatBasedTemplateTemporalFormat {
+final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat {
     private final DateTimeFormatter dateTimeFormatter;
     private final boolean instantConversion;
     private final String description;
@@ -55,8 +56,8 @@ final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatBasedTem
             DateTimeFormatter dateTimeFormatter,
             DateTimeFormatter parserExtendedDateTimeFormatter,
             DateTimeFormatter parserBasicDateTimeFormatter,
-            Class<? extends Temporal> temporalClass, TimeZone zone, String formatString) {
-        super(temporalClass, zone.toZoneId());
+            Class<? extends Temporal> temporalClass, TimeZone timeZone, String formatString) {
+        super(temporalClass, timeZone);
         temporalClass = normalizeSupportedTemporalClass(temporalClass);
         this.dateTimeFormatter = dateTimeFormatter;
         this.parserExtendedDateTimeFormatter = parserExtendedDateTimeFormatter;
@@ -85,8 +86,6 @@ final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatBasedTem
 
     @Override
     public Object parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy) throws TemplateValueFormatException {
-        // TODO [FREEMARKER-35] Implement missingTimeZoneParserPolicy
-
         final boolean extendedFormat;
         final boolean add1Day;
         if (temporalClass == LocalDate.class || temporalClass == YearMonth.class) {
@@ -124,9 +123,11 @@ final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatBasedTem
             }
         }
 
-        DateTimeFormatter parserDateTimeFormatter = parserBasicDateTimeFormatter == null || extendedFormat
-                ? parserExtendedDateTimeFormatter : parserBasicDateTimeFormatter;
-        Temporal resultTemporal = parse(s, missingTimeZoneParserPolicy, parserDateTimeFormatter);
+        Temporal resultTemporal = parse(
+                s, missingTimeZoneParserPolicy,
+                parserBasicDateTimeFormatter == null || extendedFormat
+                        ? parserExtendedDateTimeFormatter : parserBasicDateTimeFormatter);
+
         if (add1Day) {
             resultTemporal = resultTemporal.plus(1, ChronoUnit.DAYS);
         }
@@ -162,13 +163,7 @@ final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatBasedTem
     }
 
     @Override
-    public boolean isLocaleBound() {
-        return false;
-    }
-
-    @Override
-    public boolean isTimeZoneBound() {
-        // TODO [FREEMARKER-35] Even for local temporals?
+    public boolean canBeUsedForLocale(Locale locale) {
         return true;
     }
 
diff --git a/src/main/java/freemarker/core/DateTimeFormatBasedTemplateTemporalFormat.java b/src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java
similarity index 87%
rename from src/main/java/freemarker/core/DateTimeFormatBasedTemplateTemporalFormat.java
rename to src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java
index eb8ee951..b83ec05d 100644
--- a/src/main/java/freemarker/core/DateTimeFormatBasedTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java
@@ -33,21 +33,29 @@ import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoField;
 import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAccessor;
+import java.util.Objects;
+import java.util.TimeZone;
 
 /**
  * Was created ad-hoc to contain whatever happens to be common between some of our {@link TemplateTemporalFormat}-s.
  */
-abstract class DateTimeFormatBasedTemplateTemporalFormat extends TemplateTemporalFormat {
+abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalFormat {
     protected final Class<? extends Temporal> temporalClass;
     protected final boolean isLocalTemporalClass;
+    protected final TimeZone timeZone;
     protected final ZoneId zoneId;
 
-    public DateTimeFormatBasedTemplateTemporalFormat(
-            Class<? extends Temporal> temporalClass, ZoneId zoneId) {
-        temporalClass = _TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
-        this.temporalClass = temporalClass;
-        this.isLocalTemporalClass = isLocalTemporalClass(temporalClass);
-        this.zoneId = zoneId;
+    public JavaOrISOLikeTemplateTemporalFormat(
+            Class<? extends Temporal> temporalClass, TimeZone timeZone) {
+        this.temporalClass = Objects.requireNonNull(_TemporalUtils.normalizeSupportedTemporalClass(temporalClass));
+        this.isLocalTemporalClass = isLocalTemporalClass(this.temporalClass);
+        if (isLocalTemporalClass) {
+            this.zoneId = null;
+            this.timeZone = null;
+        } else {
+            this.timeZone = Objects.requireNonNull(timeZone);
+            this.zoneId = timeZone.toZoneId();
+        }
     }
 
     protected Temporal parse(
@@ -140,4 +148,8 @@ abstract class DateTimeFormatBasedTemplateTemporalFormat extends TemplateTempora
                 e);
     }
 
+    @Override
+    public final boolean canBeUsedForTimeZone(TimeZone timeZone) {
+        return this.timeZone == null || this.timeZone.equals(timeZone);
+    }
 }
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
index 718f70bb..dc7845af 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
@@ -27,13 +27,13 @@ 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.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
 import java.time.temporal.Temporal;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.TimeZone;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -47,9 +47,10 @@ import freemarker.template.utility.ClassUtil;
  *
  * @since 2.3.32
  */
-class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalFormat {
+class JavaTemplateTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat {
 
     enum PreFormatValueConversion {
+        IDENTITY,
         INSTANT_TO_ZONED_DATE_TIME,
         AS_LOCAL_IN_CURRENT_ZONE,
         SET_ZONE_FROM_OFFSET,
@@ -70,15 +71,16 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
     private static final Pattern FORMAT_STYLE_PATTERN = Pattern.compile(
             "(?:" + ANY_FORMAT_STYLE + "(?:_" + ANY_FORMAT_STYLE + ")?)?");
 
+    private final Locale locale;
     private final DateTimeFormatter dateTimeFormatter;
-    private final ZoneId zoneId;
     private final String formatString;
     private final PreFormatValueConversion preFormatValueConversion;
 
     JavaTemplateTemporalFormat(String formatString, Class<? extends Temporal> temporalClass, Locale locale,
             TimeZone timeZone)
             throws InvalidFormatParametersException {
-        super(temporalClass, timeZone.toZoneId());
+        super(temporalClass, timeZone);
+        this.locale = Objects.requireNonNull(locale);
 
         final Matcher formatStylePatternMatcher = FORMAT_STYLE_PATTERN.matcher(formatString);
         final boolean isFormatStyleString = formatStylePatternMatcher.matches();
@@ -124,7 +126,7 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
 
         // Handling of time zone related edge cases
         if (isLocalTemporalClass) {
-            this.preFormatValueConversion = null;
+            this.preFormatValueConversion = PreFormatValueConversion.IDENTITY;
             if (isFormatStyleString && (temporalClass == LocalTime.class || temporalClass == LocalDateTime.class)) {
                 // The localized pattern possibly contains the time zone (for most locales, LONG and FULL does), so they
                 // fail with local temporals that have a time part. To work this issue around, we decrease the verbosity
@@ -163,7 +165,7 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
                         (temporalClass == OffsetDateTime.class || temporalClass == OffsetTime.class)) {
                     preFormatValueConversion = PreFormatValueConversion.SET_ZONE_FROM_OFFSET;
                 } else {
-                    preFormatValueConversion = null;
+                    preFormatValueConversion = PreFormatValueConversion.IDENTITY;
                 }
             } else { // Doesn't show zone
                 if (temporalClass == OffsetTime.class) {
@@ -172,8 +174,8 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
                     preFormatValueConversion =
                             PreFormatValueConversion.OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION;
                 } else {
-                    // As no zone is shown, but our temporal class is not local, we tell the formatter convert to
-                    // the current time zone. Also, when parsing, that same time zone will be assumed.
+                    // As no zone is shown, but our temporal class is not local, the formatter will convert to a local
+                    // in the current time zone.
                     preFormatValueConversion = PreFormatValueConversion.AS_LOCAL_IN_CURRENT_ZONE;
                 }
             }
@@ -183,7 +185,6 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
         dateTimeFormatter = dateTimeFormatter.withLocale(locale);
         this.dateTimeFormatter = dateTimeFormatter;
         this.formatString = formatString;
-        this.zoneId = timeZone.toZoneId();
     }
 
     @Override
@@ -192,46 +193,46 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
         DateTimeFormatter dateTimeFormatter = this.dateTimeFormatter;
         Temporal temporal = TemplateFormatUtil.getNonNullTemporal(tm);
 
-        if (preFormatValueConversion != null) {
-            switch (preFormatValueConversion) {
-                case INSTANT_TO_ZONED_DATE_TIME:
-                    // Typical date-time formats will fail with "UnsupportedTemporalTypeException: Unsupported field:
-                    // YearOfEra" if we leave the value as Instant. (But parse(String, Instant::from) has no similar
-                    // issue.)
-                    temporal = ((Instant) temporal).atZone(zoneId);
-                    break;
-                case 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) {
-                        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 or OffsetDateTime, but value was a "
-                                        + ClassUtil.getShortClassNameOfObject(temporal));
-                    }
-                    break;
-                case AS_LOCAL_IN_CURRENT_ZONE:
-                    // We could use dateTimeFormatter.withZone(zoneId) for these, but it's not obvious if that will
-                    // always behave as a straightforward conversion to the local temporal type.
-                    if (temporal instanceof OffsetDateTime) {
-                        temporal = ((OffsetDateTime) temporal).atZoneSameInstant(zoneId).toLocalDateTime();
-                    } else if (temporal instanceof ZonedDateTime) {
-                        temporal = ((ZonedDateTime) temporal).withZoneSameInstant(zoneId).toLocalDateTime();
-                    } else if (temporal instanceof Instant) {
-                        temporal = ((Instant) temporal).atZone(zoneId).toLocalDateTime();
-                    } else {
-                        throw new AssertionError("Unhandled case: " + temporal.getClass());
-                    }
-                    break;
-                case OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION:
-                    throw newOffsetTimeWithoutOffsetOnTheFormatException();
-                default:
-                    throw new BugException();
-            }
+        switch (preFormatValueConversion) {
+            case IDENTITY:
+                break;
+            case INSTANT_TO_ZONED_DATE_TIME:
+                // Typical date-time formats will fail with "UnsupportedTemporalTypeException: Unsupported field:
+                // YearOfEra" if we leave the value as Instant. (But parse(String, Instant::from) has no similar
+                // issue.)
+                temporal = ((Instant) temporal).atZone(zoneId);
+                break;
+            case 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) {
+                    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 or OffsetDateTime, but value was a "
+                                    + ClassUtil.getShortClassNameOfObject(temporal));
+                }
+                break;
+            case AS_LOCAL_IN_CURRENT_ZONE:
+                // We could use dateTimeFormatter.withZone(zoneId) for these, but it's not obvious that that will
+                // always behave as a straightforward conversion to the local temporal type.
+                if (temporal instanceof OffsetDateTime) {
+                    temporal = ((OffsetDateTime) temporal).atZoneSameInstant(zoneId).toLocalDateTime();
+                } else if (temporal instanceof ZonedDateTime) {
+                    temporal = ((ZonedDateTime) temporal).withZoneSameInstant(zoneId).toLocalDateTime();
+                } else if (temporal instanceof Instant) {
+                    temporal = ((Instant) temporal).atZone(zoneId).toLocalDateTime();
+                } else {
+                    throw new AssertionError("Unhandled case: " + temporal.getClass());
+                }
+                break;
+            case OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION:
+                throw newOffsetTimeWithoutOffsetOnTheFormatException();
+            default:
+                throw new BugException();
         }
 
         try {
@@ -255,25 +256,13 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
     }
 
     @Override
-    public String getDescription() {
-        return formatString;
-    }
-
-    /**
-     * Tells if this formatter should be re-created if the locale changes.
-     */
-    @Override
-    public boolean isLocaleBound() {
-        return true;
+    public boolean canBeUsedForLocale(Locale locale) {
+        return this.locale.equals(locale);
     }
 
-    /**
-     * Tells if this formatter should be re-created if the time zone changes.
-     */
     @Override
-    public boolean isTimeZoneBound() {
-        // TODO [FREEMARKER-35] Even for local temporals?
-        return true;
+    public String getDescription() {
+        return formatString;
     }
 
     private static final ZonedDateTime SHOWS_OFFSET_OR_ZONE_SAMPLE_TEMPORAL_1 = ZonedDateTime.of(
@@ -286,19 +275,6 @@ class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalForm
                 .equals(dateTimeFormatter.format(SHOWS_OFFSET_OR_ZONE_SAMPLE_TEMPORAL_2));
     }
 
-    private static FormatStyle getMoreVerboseStyle(FormatStyle style) {
-        switch (style) {
-            case SHORT:
-                return FormatStyle.MEDIUM;
-            case MEDIUM:
-                return FormatStyle.LONG;
-            case LONG:
-                return FormatStyle.FULL;
-            default:
-                return null;
-        }
-    }
-
     private static FormatStyle getLessVerboseStyle(FormatStyle style) {
         switch (style) {
             case FULL:
diff --git a/src/main/java/freemarker/core/TemplateDateFormat.java b/src/main/java/freemarker/core/TemplateDateFormat.java
index ec75627a..da2cfab3 100644
--- a/src/main/java/freemarker/core/TemplateDateFormat.java
+++ b/src/main/java/freemarker/core/TemplateDateFormat.java
@@ -28,12 +28,14 @@ import freemarker.template.TemplateModelException;
 /**
  * Represents a date/time/dateTime format; used in templates for formatting and parsing with that format. This is
  * similar to Java's {@link DateFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define
- * formats that can't be represented with Java's existing {@link DateFormat} implementations.
+ * formats that can't be described with Java's existing {@link DateFormat} implementations.
  * 
  * <p>
  * Implementations need not be thread-safe if the {@link TemplateDateFormatFactory} doesn't recycle them among
  * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single
  * {@link Environment}, and {@link Environment}-s are thread-local objects.
+ *
+ * @see TemplateTemporalFormat
  * 
  * @since 2.3.24
  */
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormat.java b/src/main/java/freemarker/core/TemplateTemporalFormat.java
index 2c1dc263..a282093d 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormat.java
@@ -20,6 +20,8 @@ package freemarker.core;
 
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.Temporal;
+import java.util.Locale;
+import java.util.TimeZone;
 
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateTemporalModel;
@@ -27,7 +29,7 @@ import freemarker.template.TemplateTemporalModel;
 /**
  * Represents a {@link Temporal} format; used in templates for formatting {@link Temporal}-s, and parsing strings to
  * {@link Temporal}-s. This is similar to Java's {@link DateTimeFormatter}, but made to fit the requirements of
- * FreeMarker. Also, it makes it possible to define formats that can't be represented with {@link DateTimeFormatter}.
+ * FreeMarker. Also, it makes it possible to define formats that can't be described with {@link DateTimeFormatter}.
  *
  * <p>{@link TemplateTemporalFormat} instances are usually created by a {@link TemplateTemporalFormatFactory}.
  *
@@ -37,6 +39,8 @@ import freemarker.template.TemplateTemporalModel;
  * {@link TemplateTemporalFormat} instances in multiple {@link Environment}-s, and an {@link Environment} is only used
  * in a single thread.
  *
+ * @see TemplateDateFormat
+ *
  * @since 2.3.32
  */
 public abstract class TemplateTemporalFormat extends TemplateValueFormat {
@@ -59,16 +63,14 @@ public abstract class TemplateTemporalFormat extends TemplateValueFormat {
     }
 
     /**
-     * Tells if the same formatter can be used regardless of the desired locale (so for example after a
-     * {@link Environment#getLocale()} change we can keep using the old instance).
+     * Tells if this formatter can be used for the given locale.
      */
-    public abstract boolean isLocaleBound();
+    public abstract boolean canBeUsedForLocale(Locale locale);
 
     /**
-     * Tells if the same formatter can be used regardless of the desired time zone (so for example after a
-     * {@link Environment#getTimeZone()} change we can keep using the old instance).
+     * Tells if this formatter can be used for the given {@link TimeZone}.
      */
-    public abstract boolean isTimeZoneBound();
+    public abstract boolean canBeUsedForTimeZone(TimeZone timeZone);
 
     /**
      * Parser a string to a {@link Temporal}, according to this format. Some format implementations may throw
diff --git a/src/main/misc/templateTemporalFormatCache/getTemplateTemporalFormatCaching.ftl b/src/main/misc/templateTemporalFormatCache/getTemplateTemporalFormatCaching.ftl
new file mode 100644
index 00000000..5fd50ed2
--- /dev/null
+++ b/src/main/misc/templateTemporalFormatCache/getTemplateTemporalFormatCaching.ftl
@@ -0,0 +1,25 @@
+// BEGIN Generated with getTemplateTemporalFormatCaching.ftl
+<#-- Classes are in order of frequency (guessed). -->
+<#list ['LocalDateTime', 'Instant', 'LocalDate', 'LocalTime', 'ZonedDateTime', 'OffsetDateTime', 'OffsetTime', 'YearMonth', 'Year'] as TemporalClass>
+  <#assign temporalClass = TemporalClass[0]?lowerCase + TemporalClass[1..]>
+  if (temporalClass == ${TemporalClass}.class) {
+      result = cachedTemporalFormatCache.${temporalClass}Format;
+      if (result != null) {
+          return result;
+      }
+
+      result = cachedTemporalFormatCache.reusable${TemporalClass}Format;
+      if (result != null
+              && result.canBeUsedForTimeZone(getTimeZone()) && result.canBeUsedForLocale(getLocale())) {
+          cachedTemporalFormatCache.${temporalClass}Format = result;
+          return result;
+      }
+
+      result = getTemplateTemporalFormat(getTemporalFormat(temporalClass), temporalClass);
+      cachedTemporalFormatCache.${temporalClass}Format = result;
+      // We do this ahead of time, to decrease the cost of evictions:
+      cachedTemporalFormatCache.reusable${TemporalClass}Format = result;
+      return result;
+  }
+</#list>
+// END Generated with getTemplateTemporalFormatCaching.ftl
diff --git a/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
index 4c5174d3..414ae114 100644
--- a/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
@@ -83,12 +83,12 @@ public class EpochMillisDivTemplateTemporalFormatFactory extends TemplateTempora
         }
 
         @Override
-        public boolean isLocaleBound() {
+        public boolean canBeUsedForLocale(Locale locale) {
             return false;
         }
 
         @Override
-        public boolean isTimeZoneBound() {
+        public boolean canBeUsedForTimeZone(TimeZone timeZone) {
             return false;
         }
 
diff --git a/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
index 4eaf4102..2094f1d6 100644
--- a/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
@@ -69,12 +69,12 @@ public class EpochMillisTemplateTemporalFormatFactory extends TemplateTemporalFo
         }
 
         @Override
-        public boolean isLocaleBound() {
+        public boolean canBeUsedForLocale(Locale locale) {
             return false;
         }
 
         @Override
-        public boolean isTimeZoneBound() {
+        public boolean canBeUsedForTimeZone(TimeZone timeZone) {
             return false;
         }
 
diff --git a/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
index 36c3c7e5..b8492039 100644
--- a/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
@@ -65,13 +65,13 @@ public class HTMLISOTemplateTemporalFormatFactory extends TemplateTemporalFormat
         }
 
         @Override
-        public boolean isLocaleBound() {
-            return false;
+        public boolean canBeUsedForLocale(Locale locale) {
+            return isoFormat.canBeUsedForLocale(locale);
         }
 
         @Override
-        public boolean isTimeZoneBound() {
-            return false;
+        public boolean canBeUsedForTimeZone(TimeZone timeZone) {
+            return isoFormat.canBeUsedForTimeZone(timeZone);
         }
 
         @Override
diff --git a/src/test/java/freemarker/core/JavaTemplateTemporalFormatTest.java b/src/test/java/freemarker/core/JavaTemplateTemporalFormatTest.java
index a81841f1..2c68309d 100644
--- a/src/test/java/freemarker/core/JavaTemplateTemporalFormatTest.java
+++ b/src/test/java/freemarker/core/JavaTemplateTemporalFormatTest.java
@@ -278,23 +278,30 @@ public class JavaTemplateTemporalFormatTest extends AbstractTemporalFormatTest {
 
     @Test
     public void testParseWrongFormat() throws TemplateException, TemplateValueFormatException {
-        try {
-            assertParsingResults(
-                    conf -> conf.setDateTimeFormat("y-MM-dd HH:mm"),
-                    "2020-12-10 01:14 PM", LocalDateTime.of(2020, 12, 10, 13, 14));
-            fail("Parsing should have failed");
-        } catch (UnparsableValueException e) {
-            assertThat(
-                    e.getMessage(),
-                    allOf(
-                            containsString("\"2020-12-10 01:14 PM\""),
-                            containsString("\"y-MM-dd HH:mm\""),
-                            containsString("\"en_US\""),
-                            containsString("\"UTC\""),
-                            containsString("LocalDateTime")
-                    )
-            );
-        }
+        assertParsingFails(
+                conf -> conf.setDateTimeFormat("y-MM-dd HH:mm"),
+                "2020-12-10 01:14 PM",
+                LocalDateTime.class,
+                e -> assertThat(
+                e.getMessage(),
+                allOf(
+                        containsString("\"2020-12-10 01:14 PM\""),
+                        containsString("\"y-MM-dd HH:mm\""),
+                        containsString("\"en_US\""),
+                        not(containsString("\"UTC\"")), // Because local formats don't depend on timeZone
+                        containsString(LocalDateTime.class.getSimpleName()))));
+        assertParsingFails(
+                conf -> conf.setDateTimeFormat("y-MM-dd HH:mm X"),
+                "2020-12-10 01:14 PM",
+                ZonedDateTime.class,
+                e -> assertThat(
+                        e.getMessage(),
+                        allOf(
+                                containsString("\"2020-12-10 01:14 PM\""),
+                                containsString("\"y-MM-dd HH:mm X\""),
+                                containsString("\"en_US\""),
+                                containsString("\"UTC\""), //
+                                containsString(ZonedDateTime.class.getSimpleName()))));
     }
 
     @Test
diff --git a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java
index b58ecd4d..7051b75d 100644
--- a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java
@@ -74,12 +74,12 @@ public class LocAndTZSensitiveTemplateTemporalFormatFactory extends TemplateTemp
         }
 
         @Override
-        public boolean isLocaleBound() {
+        public boolean canBeUsedForLocale(Locale locale) {
             return true;
         }
 
         @Override
-        public boolean isTimeZoneBound() {
+        public boolean canBeUsedForTimeZone(TimeZone timeZone) {
             return true;
         }
 
diff --git a/src/test/java/freemarker/core/TemplateTemporalFormatCachingInEnvironmentTest.java b/src/test/java/freemarker/core/TemplateTemporalFormatCachingInEnvironmentTest.java
new file mode 100644
index 00000000..0268f92a
--- /dev/null
+++ b/src/test/java/freemarker/core/TemplateTemporalFormatCachingInEnvironmentTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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 org.junit.Assert.*;
+
+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.Year;
+import java.time.YearMonth;
+import java.time.ZonedDateTime;
+import java.time.temporal.Temporal;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.utility.DateUtil;
+import freemarker.template.utility.NullWriter;
+
+public class TemplateTemporalFormatCachingInEnvironmentTest {
+
+    @Test
+    public void testTemporalClassSeparation() throws Exception {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
+        cfg.setLocale(Locale.US);
+        cfg.setTimeZone(DateUtil.UTC);
+
+        Environment env = new Template(null, "", cfg)
+                .createProcessingEnvironment(null, NullWriter.INSTANCE);
+
+        env.setDateTimeFormat("iso");
+        TemplateTemporalFormat lastLocalDateTimeFormat = env.getTemplateTemporalFormat(LocalDateTime.class);
+        TemplateTemporalFormat lastOffsetDateTimeFormat = env.getTemplateTemporalFormat(OffsetDateTime.class);
+        TemplateTemporalFormat lastZonedDateTimeFormat = env.getTemplateTemporalFormat(ZonedDateTime.class);
+        TemplateTemporalFormat lastInstantDateTimeFormat = env.getTemplateTemporalFormat(Instant.class);
+        TemplateTemporalFormat lastOffsetTimeFormat = env.getTemplateTemporalFormat(OffsetTime.class);
+        TemplateTemporalFormat lastLocalTimeFormat = env.getTemplateTemporalFormat(LocalTime.class);
+        TemplateTemporalFormat lastLocalDateFormat = env.getTemplateTemporalFormat(LocalDate.class);
+        TemplateTemporalFormat lastYearFormat = env.getTemplateTemporalFormat(Year.class);
+        TemplateTemporalFormat lastYearMonthFormat = env.getTemplateTemporalFormat(YearMonth.class);
+        env.setDateTimeFormat("long");
+        assertNotSame(lastLocalDateTimeFormat, env.getTemplateTemporalFormat(LocalDateTime.class));
+        assertNotSame(lastOffsetDateTimeFormat, env.getTemplateTemporalFormat(OffsetDateTime.class));
+        assertNotSame(lastZonedDateTimeFormat, env.getTemplateTemporalFormat(ZonedDateTime.class));
+        assertNotSame(lastInstantDateTimeFormat, env.getTemplateTemporalFormat(Instant.class));
+        assertSame(lastOffsetTimeFormat, env.getTemplateTemporalFormat(OffsetTime.class));
+        assertSame(lastLocalTimeFormat, env.getTemplateTemporalFormat(LocalTime.class));
+        assertSame(lastLocalDateFormat, env.getTemplateTemporalFormat(LocalDate.class));
+        assertSame(lastYearFormat, env.getTemplateTemporalFormat(Year.class));
+        assertSame(lastYearMonthFormat, env.getTemplateTemporalFormat(YearMonth.class));
+
+        lastLocalDateTimeFormat = env.getTemplateTemporalFormat(LocalDateTime.class);
+        lastOffsetDateTimeFormat = env.getTemplateTemporalFormat(OffsetDateTime.class);
+        lastZonedDateTimeFormat = env.getTemplateTemporalFormat(ZonedDateTime.class);
+        lastInstantDateTimeFormat = env.getTemplateTemporalFormat(Instant.class);
+        lastOffsetTimeFormat = env.getTemplateTemporalFormat(OffsetTime.class);
+        lastLocalTimeFormat = env.getTemplateTemporalFormat(LocalTime.class);
+        lastLocalDateFormat = env.getTemplateTemporalFormat(LocalDate.class);
+        lastYearFormat = env.getTemplateTemporalFormat(Year.class);
+        lastYearMonthFormat = env.getTemplateTemporalFormat(YearMonth.class);
+        env.setTimeFormat("short");
+        assertSame(lastLocalDateTimeFormat, env.getTemplateTemporalFormat(LocalDateTime.class));
+        assertSame(lastOffsetDateTimeFormat, env.getTemplateTemporalFormat(OffsetDateTime.class));
+        assertSame(lastZonedDateTimeFormat, env.getTemplateTemporalFormat(ZonedDateTime.class));
+        assertSame(lastInstantDateTimeFormat, env.getTemplateTemporalFormat(Instant.class));
+        assertNotSame(lastOffsetTimeFormat, env.getTemplateTemporalFormat(OffsetTime.class));
+        assertNotSame(lastLocalTimeFormat, env.getTemplateTemporalFormat(LocalTime.class));
+        assertSame(lastLocalDateFormat, env.getTemplateTemporalFormat(LocalDate.class));
+        assertSame(lastYearFormat, env.getTemplateTemporalFormat(Year.class));
+        assertSame(lastYearMonthFormat, env.getTemplateTemporalFormat(YearMonth.class));
+    }
+
+    @Test
+    public void testForDateTime() throws Exception {
+        // Locale dependent formatters:
+        genericTest(LocalDateTime.class,
+                (cfg, first) -> cfg.setDateTimeFormat(first ? "yyyy-MM-dd HH:mm" : "yyyyMMddHHmm"),
+                true, false);
+        genericTest(ZonedDateTime.class,
+                (cfg, first) -> cfg.setDateTimeFormat(first ? "yyyy-MM-dd HH:mm" : "yyyyMMddHHmm"),
+                true, true);
+        genericTest(OffsetDateTime.class,
+                (cfg, first) -> cfg.setDateTimeFormat(first ? "yyyy-MM-dd HH:mm" : "yyyyMMddHHmm"),
+                true, true);
+        genericTest(Instant.class,
+                (cfg, first) -> cfg.setDateTimeFormat(first ? "yyyy-MM-dd HH:mm" : "yyyyMMddHHmm"),
+                true, true);
+
+        // Locale independent formatters:
+        genericTest(LocalDateTime.class,
+                (cfg, first) -> cfg.setDateTimeFormat(first ? "iso" : "xs"),
+                false, false);
+        genericTest(ZonedDateTime.class,
+                (cfg, first) -> cfg.setDateTimeFormat(first ? "iso" : "xs"),
+                false, true);
+    }
+
+    @Test
+    public void testForDate() throws Exception {
+        // Locale dependent formatters:
+        genericTest(LocalDate.class,
+                (cfg, first) -> cfg.setDateFormat(first ? "yyyy-MM-dd" : "yyyyMM-dd"),
+                true, false);
+
+        // Locale independent formatters:
+        genericTest(LocalDate.class,
+                (cfg, first) -> cfg.setDateFormat(first ? "iso" : "xs"),
+                false, false);
+    }
+
+    @Test
+    public void testForTime() throws Exception {
+        // Locale dependent formatters:
+        genericTest(LocalTime.class,
+                (cfg, first) -> cfg.setTimeFormat(first ? "HH:mm" : "HHmm"),
+                true, false);
+        genericTest(OffsetTime.class,
+                (cfg, first) -> cfg.setTimeFormat(first ? "HH:mm" : "HHmm"),
+                true, true);
+
+        // Locale independent formatters:
+        genericTest(LocalTime.class,
+                (cfg, first) -> cfg.setTimeFormat(first ? "iso" : "xs"),
+                false, false);
+        genericTest(OffsetTime.class,
+                (cfg, first) -> cfg.setTimeFormat(first ? "iso" : "xs"),
+                false, true);
+    }
+
+    @Test
+    public void testForYearMonth() throws Exception {
+        // Locale dependent formatters:
+        genericTest(YearMonth.class,
+                (cfg, first) -> cfg.setYearMonthFormat(first ? "yyyy-MM" : "yyyyMM"),
+                true, false);
+
+        // Locale independent formatters:
+        genericTest(YearMonth.class,
+                (cfg, first) -> cfg.setYearMonthFormat(first ? "iso" : "xs"),
+                false, false);
+    }
+
+    @Test
+    public void testForYear() throws Exception {
+        // Locale dependent formatters:
+        genericTest(Year.class,
+                (cfg, first) -> cfg.setYearFormat(first ? "yyyy" : "yy"),
+                true, false);
+
+        // Locale independent formatters:
+        genericTest(Year.class,
+                (cfg, first) -> cfg.setYearFormat(first ? "iso" : "xs"),
+                false, false);
+    }
+
+    private void genericTest(
+            Class<? extends Temporal> temporalClass,
+            SettingSetter settingSetter,
+            boolean localeDependent, boolean timeZoneDependent)
+            throws Exception {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
+        cfg.setLocale(Locale.GERMANY);
+        cfg.setTimeZone(DateUtil.UTC);
+        settingSetter.setSetting(cfg, true);
+
+        Environment env = new Template(null, "", cfg)
+                .createProcessingEnvironment(null, NullWriter.INSTANCE);
+
+        TemplateTemporalFormat lastFormat;
+        TemplateTemporalFormat newFormat;
+
+        lastFormat = env.getTemplateTemporalFormat(temporalClass);
+        // Assert that it keeps returning the same instance from cache:
+        assertSame(lastFormat, env.getTemplateTemporalFormat(temporalClass));
+        assertSame(lastFormat, env.getTemplateTemporalFormat(temporalClass));
+
+        settingSetter.setSetting(env, true);
+        // Assert that the cache wasn't cleared when the setting was set to the same value again:
+        assertSame(lastFormat, env.getTemplateTemporalFormat(temporalClass));
+
+        env.setLocale(Locale.JAPAN); // Possibly clears non-reusable TemplateTemporalFormatCache field
+        newFormat = env.getTemplateTemporalFormat(temporalClass);
+        if (localeDependent) {
+            assertNotSame(lastFormat, newFormat);
+        } else {
+            assertSame(lastFormat, env.getTemplateTemporalFormat(temporalClass));
+        }
+        lastFormat = newFormat;
+
+        env.setLocale(Locale.JAPAN);
+        assertSame(lastFormat, env.getTemplateTemporalFormat(temporalClass));
+
+        env.setLocale(Locale.GERMANY); // Possibly clears non-reusable TemplateTemporalFormatCache field
+        env.setLocale(Locale.JAPAN);
+        // Assert that it restores the same instance from TemplateTemporalFormatCache.reusableXxx field:
+        assertSame(lastFormat, env.getTemplateTemporalFormat(temporalClass));
+
+        TimeZone otherTimeZone = TimeZone.getTimeZone("GMT+01");
+        env.setTimeZone(otherTimeZone); // Possibly clears non-reusable TemplateTemporalFormatCache field
+        newFormat = env.getTemplateTemporalFormat(temporalClass);
+        if (timeZoneDependent) {
+            assertNotSame(newFormat, lastFormat);
+            assertSame(newFormat, env.getTemplateTemporalFormat(temporalClass));
+        } else {
+            assertSame(newFormat, lastFormat);
+        }
+        lastFormat = newFormat;
+
+        env.setTimeZone(DateUtil.UTC); // Possibly clears non-reusable TemplateTemporalFormatCache field
+        env.setTimeZone(otherTimeZone);
+        // Assert that it restores the same instance from TemplateTemporalFormatCache.reusableXxx field:
+        assertSame(lastFormat, env.getTemplateTemporalFormat(temporalClass));
+
+        settingSetter.setSetting(env, false); // Clears even TemplateTemporalFormatCache.reusableXxx
+        newFormat = env.getTemplateTemporalFormat(temporalClass);
+        assertNotSame(lastFormat, newFormat);
+    }
+
+    @FunctionalInterface
+    interface SettingSetter {
+        void setSetting(Configurable configurable, boolean firstValue);
+    }
+
+}


[freemarker] 02/02: [FREEMARKER-35] Code cleanup in Temporal related code

Posted by dd...@apache.org.
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

commit 28545e14abab92d7497c73a00761d3e11559061e
Author: ddekany <dd...@apache.org>
AuthorDate: Sat Jul 16 20:59:36 2022 +0200

    [FREEMARKER-35] Code cleanup in Temporal related code
---
 src/main/java/freemarker/core/Configurable.java    | 25 +++---
 ...eTimeFormatterBasedTemplateTemporalFormat.java} | 93 ++++++++++++----------
 src/main/java/freemarker/core/Environment.java     | 32 ++++----
 .../ISOLikeTemplateTemporalTemporalFormat.java     | 23 ++++--
 .../core/ISOTemplateTemporalFormatFactory.java     | 10 ++-
 .../core/JavaTemplateTemporalFormat.java           |  7 +-
 .../core/JavaTemplateTemporalFormatFactory.java    |  5 +-
 .../core/MissingTimeZoneParserPolicy.java          |  5 +-
 .../java/freemarker/core/TemplateDateFormat.java   | 44 +++++-----
 .../java/freemarker/core/TemplateNumberFormat.java | 45 ++++++-----
 .../freemarker/core/TemplateTemporalFormat.java    | 62 +++++++++++----
 .../core/TemplateTemporalFormatFactory.java        |  2 +-
 .../java/freemarker/core/TemplateValueFormat.java  |  3 +-
 .../core/XSTemplateTemporalFormatFactory.java      |  7 +-
 src/main/java/freemarker/core/_MessageUtil.java    |  4 +-
 src/main/java/freemarker/core/_TemporalUtils.java  | 14 ++--
 .../java/freemarker/template/Configuration.java    |  4 +
 .../freemarker/template/TemplateDateModel.java     |  9 +--
 .../freemarker/template/TemplateTemporalModel.java |  5 +-
 .../core/AbstractTemporalFormatTest.java           |  2 +-
 .../core/ISOLikeTemplateTemporalFormatTest.java    |  4 +-
 .../java/freemarker/core/_TemporalUtilsTest.java   | 12 +--
 22 files changed, 246 insertions(+), 171 deletions(-)

diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 57becfac..cc3ea3c9 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -1440,7 +1440,8 @@ public class Configurable {
      */
     public String getTemporalFormat(Class<? extends Temporal> temporalClass) {
         Objects.requireNonNull(temporalClass);
-        // The temporal classes are final (for now at least), so we can use == operator instead of instanceof.
+        // We can use == operator instead of instanceof, as temporal classes are final in Java 8. Just in case that
+        // changes in some later Java version, we have "else" branch that retries with a normalized class.
         if (temporalClass == Instant.class
                 || temporalClass == LocalDateTime.class
                 || temporalClass == ZonedDateTime.class
@@ -1455,14 +1456,14 @@ public class Configurable {
         } else if (temporalClass == YearMonth.class) {
             return getYearMonthFormat();
         } else {
-            // Handle the unlikely situation that in some future Java version we can have subclasses.
-            Class<? extends Temporal> normTemporalClass =
+            // Branch to handle the unlikely situation that in some Java version we can have subclasses.
+            Class<? extends Temporal> normalizedTemporalClass =
                     _TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
-            if (normTemporalClass == temporalClass) {
+            if (normalizedTemporalClass == temporalClass) {
                 throw new IllegalArgumentException("There's no temporal format setting for this class: "
                         + temporalClass.getName());
             } else {
-                return getTemporalFormat(normTemporalClass);
+                return getTemporalFormat(normalizedTemporalClass);
             }
         }
     }
@@ -1503,7 +1504,7 @@ public class Configurable {
      * date_format}, {@link #setDateTimeFormat(String) time_format}, and {@link #setDateTimeFormat(String)
      * datetime_format} settings with values starting with <code>@<i>name</i></code>.
      *
-     * <p>It's important that the formats you set here will be only used when formatting {@link Date}-s, not when
+     * <p>It's important that the formats you set here will be only visible when formatting {@link Date}-s, not when
      * formatting {@link Temporal}-s. For the later, use {@link #setCustomTemporalFormats(Map)}. Ideally, you set the
      * same custom formatter names with both methods.
      *
@@ -1528,7 +1529,8 @@ public class Configurable {
     }
     
     /**
-     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     * Tells if this setting is set directly in this object, or its value is coming from the {@link #getParent()
+     * parent}.
      * 
      * @since 2.3.24
      */
@@ -1588,16 +1590,16 @@ public class Configurable {
     }
 
     /**
-     * Associates names with {@link Temporal} formatter factories, which then can be referred by the
+     * Associates names with {@link TemplateTemporalFormatFactory}-es, which then can be referred by the
      * {@link #setDateTimeFormat(String) date_time_format}, {@link #setDateFormat(String) date_format}, and
      * {@link #setTimeFormat(String) time_format} settings, with values starting with <code>@<i>name</i></code>.
      *
-     * <p>It's important that the formats you set here will be only used when formatting {@link Temporal}-s, not when
+     * <p>It's important that the formats you set here will be only visible when formatting {@link Temporal}-s, not when
      * formatting {@link Date}-s. For the later, use {@link #setCustomDateFormats(Map)}. Ideally, you set the same
      * custom formatter names with both methods.
      *
      * @param customTemporalFormats
-     *            Can't be {@code null}. The name must start with an UNICODE letter, and can only contain UNICODE
+     *            Can't be {@code null}. The name must start with a UNICODE letter, and can only contain UNICODE
      *            letters and digits.
      *            
      * @see #setCustomDateFormats(Map) 
@@ -1611,7 +1613,8 @@ public class Configurable {
     }
 
     /**
-     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     * Tells if this setting is set directly in this object, or its value is coming from the {@link #getParent()
+     * parent}.
      *
      * @since 2.3.32
      */
diff --git a/src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java b/src/main/java/freemarker/core/DateTimeFormatterBasedTemplateTemporalFormat.java
similarity index 60%
rename from src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java
rename to src/main/java/freemarker/core/DateTimeFormatterBasedTemplateTemporalFormat.java
index b83ec05d..46c4db0c 100644
--- a/src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/DateTimeFormatterBasedTemplateTemporalFormat.java
@@ -19,6 +19,7 @@
 
 package freemarker.core;
 
+import static freemarker.core.MissingTimeZoneParserPolicy.*;
 import static freemarker.core._TemporalUtils.*;
 import static freemarker.template.utility.StringUtil.*;
 
@@ -37,15 +38,15 @@ import java.util.Objects;
 import java.util.TimeZone;
 
 /**
- * Was created ad-hoc to contain whatever happens to be common between some of our {@link TemplateTemporalFormat}-s.
+ * Common logic among our {@link TemplateTemporalFormat}-s that are based on {@link TemplateTemporalFormat}.
  */
-abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalFormat {
+abstract class DateTimeFormatterBasedTemplateTemporalFormat extends TemplateTemporalFormat {
     protected final Class<? extends Temporal> temporalClass;
     protected final boolean isLocalTemporalClass;
     protected final TimeZone timeZone;
     protected final ZoneId zoneId;
 
-    public JavaOrISOLikeTemplateTemporalFormat(
+    public DateTimeFormatterBasedTemplateTemporalFormat(
             Class<? extends Temporal> temporalClass, TimeZone timeZone) {
         this.temporalClass = Objects.requireNonNull(_TemporalUtils.normalizeSupportedTemporalClass(temporalClass));
         this.isLocalTemporalClass = isLocalTemporalClass(this.temporalClass);
@@ -58,6 +59,11 @@ abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalForma
         }
     }
 
+    /**
+     * Called from {@link TemplateTemporalFormat#parse(String, MissingTimeZoneParserPolicy)}, when that has figured
+     * out the {@link DateTimeFormatter} to use, this method will deal with the time zone related matters, and some
+     * more (like converting parsing exceptions).
+     */
     protected Temporal parse(
             String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy,
             DateTimeFormatter parserDateTimeFormatter) throws UnparsableValueException {
@@ -70,39 +76,36 @@ abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalForma
                 return parseResult.query(_TemporalUtils.getTemporalQuery(temporalClass));
             }
 
-            switch (missingTimeZoneParserPolicy) {
-                case ASSUME_CURRENT_TIME_ZONE:
-                case FALL_BACK_TO_LOCAL_TEMPORAL:
-                    boolean fallbackToLocal = missingTimeZoneParserPolicy == MissingTimeZoneParserPolicy.FALL_BACK_TO_LOCAL_TEMPORAL;
-                    Class<? extends Temporal> localFallbackTemporalClass;
-                    if (temporalClass == Instant.class) {
-                        localFallbackTemporalClass = LocalDateTime.class;
-                    } else {
-                        localFallbackTemporalClass = getLocalTemporalClassForNonLocal(temporalClass);
-                        if (localFallbackTemporalClass == null) {
-                            throw newUnparsableValueException(
-                                    s, parserDateTimeFormatter,
-                                    "String contains no zone offset, and no local temporal type "
-                                            + "exists for target type " + temporalClass.getName(),
-                                    null);
-                        }
-                        if (!fallbackToLocal && temporalClass == OffsetTime.class) {
-                            throw newUnparsableValueException(
-                                    s, parserDateTimeFormatter,
-                                    "It's not possible to parse the string that contains no zone offset to OffsetTime, "
-                                            + "because we don't know the day, and hence can't account for "
-                                            + "Daylight Saving Time, and thus we can't apply the current time zone."
-                                            + temporalClass.getName(),
-                                    null);
-                        }
-                    }
+            if (missingTimeZoneParserPolicy == ASSUME_CURRENT_TIME_ZONE ||
+                    missingTimeZoneParserPolicy == FALL_BACK_TO_LOCAL_TEMPORAL) {
+                boolean fallbackToLocal = missingTimeZoneParserPolicy == FALL_BACK_TO_LOCAL_TEMPORAL;
+                Class<? extends Temporal> localFallbackTemporalClass;
+                localFallbackTemporalClass = tryGetLocalTemporalClassForNonLocal(temporalClass);
+                if (localFallbackTemporalClass == null) {
+                    throw newUnparsableValueException(
+                            s, parserDateTimeFormatter,
+                            "String contains no zone, nor offset, and no local variant exists for target type "
+                                    + temporalClass.getName(),
+                            null);
+                }
+                if (!fallbackToLocal && temporalClass == OffsetTime.class) {
+                    throw newUnparsableValueException(
+                            s, parserDateTimeFormatter,
+                            "It's not possible to parse a string that contains no zone, nor offset, to OffsetTime. "
+                                    + "We don't know the day, and hence can't account for Daylight Saving Time, "
+                                    + "and thus we can't use the current time zone."
+                                    + temporalClass.getName(),
+                            null);
+                }
 
-                    Temporal resultTemporal = parseResult.query(
-                            _TemporalUtils.getTemporalQuery(localFallbackTemporalClass));
-                    if (fallbackToLocal) {
-                        return resultTemporal;
-                    }
-                    ZonedDateTime zonedDateTime = ((LocalDateTime) resultTemporal).atZone(zoneId);
+                Temporal resultLocalTemporal = parseResult.query(
+                        getTemporalQuery(localFallbackTemporalClass));
+                if (fallbackToLocal) {
+                    return resultLocalTemporal;
+                }
+
+                if (resultLocalTemporal instanceof LocalDateTime) {
+                    ZonedDateTime zonedDateTime = ((LocalDateTime) resultLocalTemporal).atZone(zoneId);
                     if (temporalClass == ZonedDateTime.class) {
                         return zonedDateTime;
                     } else if (temporalClass == OffsetDateTime.class) {
@@ -110,22 +113,24 @@ abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalForma
                     } else if (temporalClass == Instant.class) {
                         return zonedDateTime.toInstant();
                     }
-                    throw new AssertionError("Unexpected case: " + temporalClass);
-                case FAIL:
-                    throw newUnparsableValueException(
-                            s, parserDateTimeFormatter,
-                            _MessageUtil.FAIL_MISSING_TIME_ZONE_PARSER_POLICY_ERROR_DETAIL, null);
-                default:
-                    throw new AssertionError();
+                }
+                throw new BugException("Unexpected case: "
+                        + "temporalClass=" + temporalClass + ", "
+                        + "missingTimeZoneParserPolicy=" + missingTimeZoneParserPolicy);
+            } else if (missingTimeZoneParserPolicy == FAIL) {
+                throw newUnparsableValueException(
+                        s, parserDateTimeFormatter,
+                        _MessageUtil.FAIL_MISSING_TIME_ZONE_PARSER_POLICY_ERROR_DETAIL, null);
             }
-        } catch (DateTimeException e) {
+            throw new AssertionError();
+        } catch (DateTimeException|ArithmeticException e) {
             throw newUnparsableValueException(s, parserDateTimeFormatter, e.getMessage(), e);
         }
     }
 
     protected UnparsableValueException newUnparsableValueException(
             String s, DateTimeFormatter dateTimeFormatter,
-            String cause, DateTimeException e) {
+            String cause, Exception e) {
         StringBuilder message = new StringBuilder();
 
         message.append("Failed to parse value ").append(jQuote(s))
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index cd2ce83e..c7baa2dd 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -176,7 +176,7 @@ public final class Environment extends Configurable {
     private TemplateTemporalFormatCache cachedTemporalFormatCache;
     private final class TemplateTemporalFormatCache {
         // Notes:
-        // - "reusable" fields are set when the current cache field is set
+        // - "reusable" fields are set together with related non-reusable fields
         // - non-reusable fields are cleared when any related setting is changed, but reusableXxx fields are only
         //   if the format string changes
         // - When there's a cache-miss, we check if the "reusable" field has compatible timeZone, and locale, and if
@@ -1760,7 +1760,7 @@ public final class Environment extends Configurable {
      */
     private TemplateNumberFormat getTemplateNumberFormatWithoutCache(String formatString, Locale locale)
             throws TemplateValueFormatException {
-        int formatStringLen = formatString.length();
+        final int formatStringLen = formatString.length();
         if (formatStringLen > 1
                 && formatString.charAt(0) == '@'
                 && (isIcI2324OrLater() || hasCustomFormats())
@@ -1768,7 +1768,7 @@ public final class Environment extends Configurable {
             final String name;
             final String params;
             {
-                int endIdx = getCustomFormatStringNameEnd(formatString, formatStringLen);
+                int endIdx = getCustomFormatStringNameEnd(formatString);
                 name = formatString.substring(1, endIdx);
                 params = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
             }
@@ -2344,7 +2344,7 @@ public final class Environment extends Configurable {
                 && Character.isLetter(formatString.charAt(1))) {
             final String name;
             {
-                int endIdx = getCustomFormatStringNameEnd(formatString, formatStringLen);
+                int endIdx = getCustomFormatStringNameEnd(formatString);
                 name = formatString.substring(1, endIdx);
                 formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
             }
@@ -2479,9 +2479,8 @@ public final class Environment extends Configurable {
                 settingName = _TemporalUtils.temporalClassToFormatSettingName(
                         temporalClass,
                         blamedTemporalSourceExp != null
-                                ? blamedTemporalSourceExp.getTemplate().getActualNamingConvention()
-                                        == Configuration.CAMEL_CASE_NAMING_CONVENTION
-                                : false);
+                                && blamedTemporalSourceExp.getTemplate().getActualNamingConvention()
+                                        == Configuration.CAMEL_CASE_NAMING_CONVENTION);
                 settingValue = getTemporalFormat(temporalClass);
             } catch (IllegalArgumentException e2) {
                 settingName = "???";
@@ -2730,14 +2729,16 @@ public final class Environment extends Configurable {
 
     /**
      * Returns the {@link TemplateTemporalFormat} for the given parameters without using the {@link Environment}-level
-     * cache. Of course, the {@link TemplateTemporalFormatFactory} involved might still uses its own cache, which can be
-     * global (class-loader-level) or {@link Environment}-level.
+     * cache. The {@link TemplateTemporalFormatFactory} involved might still uses its own internal cache, which can be
+     * global (class-loader-level), or {@link Environment}-level.
      *
      * @param formatString
      *            See the similar parameter of {@link TemplateTemporalFormatFactory#get}
-     * @param dateType
+     * @param temporalClass
      *            See the similar parameter of {@link TemplateTemporalFormatFactory#get}
-     * @param zonelessInput
+     * @param locale
+     *            See the similar parameter of {@link TemplateTemporalFormatFactory#get}
+     * @param timeZone
      *            See the similar parameter of {@link TemplateTemporalFormatFactory#get}
      */
     private TemplateTemporalFormat getTemplateTemporalFormat(
@@ -2765,7 +2766,7 @@ public final class Environment extends Configurable {
                 && Character.isLetter(formatString.charAt(1))) {
             final String name;
             {
-                int endIdx = getCustomFormatStringNameEnd(formatString, formatStringLen);
+                int endIdx = getCustomFormatStringNameEnd(formatString);
                 name = formatString.substring(1, endIdx);
                 formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
             }
@@ -2783,13 +2784,12 @@ public final class Environment extends Configurable {
         return formatFactory.get(formatParams, temporalClass, locale, timeZone, this);
     }
 
-    private static int getCustomFormatStringNameEnd(String formatString, int formatStringLen) {
+    private static int getCustomFormatStringNameEnd(String formatString) {
         int endIdx;
-        findParamsStart:
-        for (endIdx = 1; endIdx < formatStringLen; endIdx++) {
+        for (endIdx = 1; endIdx < formatString.length(); endIdx++) {
             char c = formatString.charAt(endIdx);
             if (c == ' ' || c == '_') {
-                break findParamsStart;
+                return endIdx;
             }
         }
         return endIdx;
diff --git a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
index 680546dd..6f1e1d27 100644
--- a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
+++ b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
@@ -45,7 +45,7 @@ import freemarker.template.TemplateTemporalModel;
  *
  * @since 2.3.32
  */
-final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat {
+final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatterBasedTemplateTemporalFormat {
     private final DateTimeFormatter dateTimeFormatter;
     private final boolean instantConversion;
     private final String description;
@@ -56,7 +56,9 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT
             DateTimeFormatter dateTimeFormatter,
             DateTimeFormatter parserExtendedDateTimeFormatter,
             DateTimeFormatter parserBasicDateTimeFormatter,
-            Class<? extends Temporal> temporalClass, TimeZone timeZone, String formatString) {
+            Class<? extends Temporal> temporalClass,
+            TimeZone timeZone,
+            String formatString) {
         super(temporalClass, timeZone);
         temporalClass = normalizeSupportedTemporalClass(temporalClass);
         this.dateTimeFormatter = dateTimeFormatter;
@@ -92,10 +94,10 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT
             extendedFormat = s.indexOf('-', 1) != -1;
             add1Day = false;
         } else if (temporalClass == LocalTime.class || temporalClass == OffsetTime.class) {
-            extendedFormat = s.indexOf(":") != -1;
+            extendedFormat = s.contains(":");
             add1Day = false;
             // ISO 8601 allows hour 24 if the rest of the time is 0:
-            if (isStartOf240000(s, 0)) {
+            if (isStartOf240000InISOFormat(s, 0)) {
                 s = "00" + s.substring(2);
             }
         } else if (temporalClass == Year.class) {
@@ -103,19 +105,21 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT
             add1Day = false;
         } else {
             int tIndex = s.indexOf('T');
-            if (tIndex < 1) {
+            if (tIndex < 1) { // tIndex 0 is deliberately not accepted
                 throw newUnparsableValueException(
                         s, null,
                         "Character \"T\" must be used to separate the date and time part.", null);
             }
             if (s.indexOf(":", tIndex + 1) != -1) {
+                // Time part has ":" => extendedFormat
                 extendedFormat = true;
             } else {
+                // Date part has "-" => extendedFormat
                 // Note: false for: -5000101T00, as there the last '-' has index 0
                 extendedFormat = s.lastIndexOf('-', tIndex - 1) > 0;
             }
             // ISO 8601 allows hour 24 if the rest of the time is 0:
-            if (isStartOf240000(s, tIndex + 1)) {
+            if (isStartOf240000InISOFormat(s, tIndex + 1)) {
                 s = s.substring(0, tIndex + 1) + "00" + s.substring(tIndex + 3);
                 add1Day = true;
             } else {
@@ -136,7 +140,12 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT
 
     private final static Pattern ZERO_TIME_AFTER_HH = Pattern.compile("(?::?+00(?::?+00(?:.?+0+)?)?)?");
 
-    private static boolean isStartOf240000(String s, int from) {
+    /**
+     * Checks if starting from the given index we have {@code "24:00:00"} or equivalent (like {@code "240000"},
+     * {@code "24:00:00.000"}, {@code "2400"}, {@code "24"}). This only accepts a format that is valid in ISO 8601,
+     * like for {@code "24:0"} this returns {@code false}, as ISO requires two 0-s.
+     */
+    private static boolean isStartOf240000InISOFormat(String s, int from) {
         if (from + 1 >= s.length() || s.charAt(from) != '2' || s.charAt(from + 1) != '4') {
             return false;
         }
diff --git a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
index 9a6ad0c5..4a532ce2 100644
--- a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
@@ -41,7 +41,7 @@ import java.util.Locale;
 import java.util.TimeZone;
 
 /**
- * Format factory related to {@link someJava8Temporal?string.iso}, {@link someJava8Temporal?string.iso_...}, etc.
+ * Format factory related to {@code someJava8Temporal?string.iso}, {@code someJava8Temporal?string.iso_...}, etc.
  */
 class ISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
 
@@ -195,8 +195,9 @@ class ISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
             .withResolverStyle(ResolverStyle.STRICT);
 
     @Override
-    public TemplateTemporalFormat get(String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env) throws
-            TemplateValueFormatException {
+    public TemplateTemporalFormat get(
+            String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env)
+            throws TemplateValueFormatException {
         if (!params.isEmpty()) {
             // TODO [FREEMARKER-35]
             throw new InvalidFormatParametersException("iso currently doesn't support parameters for Java 8 temporal types");
@@ -205,7 +206,8 @@ class ISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
         return getISOFormatter(temporalClass, timeZone);
     }
 
-    private static ISOLikeTemplateTemporalTemporalFormat getISOFormatter(Class<? extends Temporal> temporalClass, TimeZone timeZone) {
+    private static ISOLikeTemplateTemporalTemporalFormat getISOFormatter(
+            Class<? extends Temporal> temporalClass, TimeZone timeZone) {
         final DateTimeFormatter dateTimeFormatter;
         final DateTimeFormatter parserExtendedDateTimeFormatter;
         final DateTimeFormatter parserBasicDateTimeFormatter;
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
index dc7845af..47208c2f 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
@@ -47,7 +47,7 @@ import freemarker.template.utility.ClassUtil;
  *
  * @since 2.3.32
  */
-class JavaTemplateTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat {
+class JavaTemplateTemporalFormat extends DateTimeFormatterBasedTemplateTemporalFormat {
 
     enum PreFormatValueConversion {
         IDENTITY,
@@ -76,8 +76,8 @@ class JavaTemplateTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat {
     private final String formatString;
     private final PreFormatValueConversion preFormatValueConversion;
 
-    JavaTemplateTemporalFormat(String formatString, Class<? extends Temporal> temporalClass, Locale locale,
-            TimeZone timeZone)
+    JavaTemplateTemporalFormat(
+            String formatString, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone)
             throws InvalidFormatParametersException {
         super(temporalClass, timeZone);
         this.locale = Objects.requireNonNull(locale);
@@ -139,6 +139,7 @@ class JavaTemplateTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat {
                     } catch (DateTimeException e) {
                         timePartFormatStyle = getLessVerboseStyle(timePartFormatStyle);
                         if (timePartFormatStyle == null) {
+                            // Not even the least verbose style worked
                             throw e;
                         }
 
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java
index b1e049db..ac4228bf 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java
@@ -31,8 +31,9 @@ class JavaTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
     }
 
     @Override
-    public TemplateTemporalFormat get(String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone,
-            Environment env) throws TemplateValueFormatException {
+    public TemplateTemporalFormat get(
+            String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env)
+            throws TemplateValueFormatException {
         return new JavaTemplateTemporalFormat(params, temporalClass, locale, timeZone);
     }
 
diff --git a/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java b/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java
index c4d7b681..89e4ebf4 100644
--- a/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java
+++ b/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java
@@ -20,13 +20,14 @@
 package freemarker.core;
 
 import java.time.OffsetDateTime;
+import java.time.temporal.Temporal;
 
 import freemarker.template.Configuration;
 
 /**
  * Used as a parameter to {@link TemplateTemporalFormat#parse(String, MissingTimeZoneParserPolicy)}, specifies what to
- * do if we have to parse a string that contains no time zone or offset information to a non-local {@code java.time}
- * temporal (like to {@link OffsetDateTime}).
+ * do if we have to parse a string that contains no time zone, nor offset information to a non-local {@link Temporal}
+ * (like to {@link OffsetDateTime}).
  *
  * <p>There's no {@link Configuration} setting for this. Instead, the build-ins that parse to given non-local temporal
  * type have 3 variants, one for each policy. For example, in the case of parsing a string to {@link OffsetDateTime},
diff --git a/src/main/java/freemarker/core/TemplateDateFormat.java b/src/main/java/freemarker/core/TemplateDateFormat.java
index da2cfab3..a4f729f9 100644
--- a/src/main/java/freemarker/core/TemplateDateFormat.java
+++ b/src/main/java/freemarker/core/TemplateDateFormat.java
@@ -27,9 +27,9 @@ import freemarker.template.TemplateModelException;
 
 /**
  * Represents a date/time/dateTime format; used in templates for formatting and parsing with that format. This is
- * similar to Java's {@link DateFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define
+ * similar to Java's {@link DateFormat}, but made to fit the requirements of FreeMarker. Also, it allows defining
  * formats that can't be described with Java's existing {@link DateFormat} implementations.
- * 
+ *
  * <p>
  * Implementations need not be thread-safe if the {@link TemplateDateFormatFactory} doesn't recycle them among
  * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single
@@ -42,12 +42,15 @@ import freemarker.template.TemplateModelException;
 public abstract class TemplateDateFormat extends TemplateValueFormat {
     
     /**
+     * Formats the value to plain text (string that contains no markup or escaping).
+     *
      * @param dateModel
-     *            The date/time/dateTime to format; not {@code null}. Most implementations will just work with the return value of
-     *            {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of
-     *            a custom {@link TemplateDateModel} implementation.
-     * 
-     * @return The date/time/dateTime as text, with no escaping (like no HTML escaping); can't be {@code null}.
+     *            The date/time/dateTime to format; not {@code null}. Most implementations will just work with the
+     *            return value of {@link TemplateDateModel#getAsDate()}, but some may format differently depending on
+     *            the properties of a custom {@link TemplateDateModel} implementation.
+     *
+     * @return The date/time/dateTime value as plain text (not markup), with no escaping (like no HTML escaping);
+     *             can't be {@code null}.
      * 
      * @throws TemplateValueFormatException
      *             When a problem occurs during the formatting of the value. Notable subclass:
@@ -59,14 +62,15 @@ public abstract class TemplateDateFormat extends TemplateValueFormat {
             throws TemplateValueFormatException, TemplateModelException;
 
     /**
-     * Formats the model to markup instead of to plain text if the result markup will be more than just plain text
-     * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
-     * {@link #formatToPlainText(TemplateDateModel)} escaped, it must return the {@link String} that
+     * Formats the value to markup instead of to plain text, but only if the result markup will be more than just plain
+     * text escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
+     * {@link #formatToPlainText(TemplateDateModel)} escaped, then instead it must return the {@link String} that
      * {@link #formatToPlainText(TemplateDateModel)} does.
      * 
      * <p>The implementation in {@link TemplateDateFormat} simply calls {@link #formatToPlainText(TemplateDateModel)}.
-     * 
-     * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}.
+     *
+     * @return A {@link String} (assumed to be plain text, not markup), or a {@link TemplateMarkupOutputModel};
+     *             not {@code null}.
      */
     public Object format(TemplateDateModel dateModel) throws TemplateValueFormatException, TemplateModelException {
         return formatToPlainText(dateModel);
@@ -86,14 +90,14 @@ public abstract class TemplateDateFormat extends TemplateValueFormat {
      *            respectively. This parameter rarely if ever {@link TemplateDateModel#UNKNOWN}, but the implementation
      *            that cares about this parameter should be prepared for that. If nothing else, it should throw
      *            {@link UnknownDateTypeParsingUnsupportedException} then.
-     * 
-     * @return The interpretation of the text either as a {@link Date} or {@link TemplateDateModel}. Typically, a
-     *         {@link Date}. {@link TemplateDateModel} is used if you have to attach some application-specific
-     *         meta-information that's also extracted during {@link #formatToPlainText(TemplateDateModel)} (so if you format
-     *         something and then parse it, you get back an equivalent result). It can't be {@code null}. Known issue
-     *         (at least in FTL 2): {@code ?date}/{@code ?time}/{@code ?datetime}, when not invoked as a method, can't
-     *         return the {@link TemplateDateModel}, only the {@link Date} from inside it, hence the additional
-     *         application-specific meta-info will be lost.
+     *
+     * @return The text converted to either {@link Date}, or to {@link TemplateDateModel}; not {@code null}.
+     *         Typically, the result should be a {@link Date}. Converting to {@link TemplateDateModel} should only be
+     *         done if you need to store additional data next to the {@link Date}, which is then also used by
+     *         {@link #formatToPlainText(TemplateDateModel)} (so if you format something and then parse it, you get
+     *         back an equivalent result). Known issue (at least in 2.x): {@code ?date}/{@code ?time}/{@code ?datetime},
+     *         when not invoked as a method, can't return the {@link TemplateDateModel}, only the {@link Date} from
+     *         inside it, hence the additional application-specific meta-info will be lost.
      */
     public abstract Object parse(String s, int dateType) throws TemplateValueFormatException;
     
diff --git a/src/main/java/freemarker/core/TemplateNumberFormat.java b/src/main/java/freemarker/core/TemplateNumberFormat.java
index 23325755..1c89f917 100644
--- a/src/main/java/freemarker/core/TemplateNumberFormat.java
+++ b/src/main/java/freemarker/core/TemplateNumberFormat.java
@@ -20,57 +20,60 @@ package freemarker.core;
 
 import java.text.NumberFormat;
 
-import freemarker.template.TemplateDateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateNumberModel;
 
 /**
  * Represents a number format; used in templates for formatting and parsing with that format. This is similar to Java's
- * {@link NumberFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define formats that
- * can't be represented with Java's existing {@link NumberFormat} implementations.
- * 
+ * {@link NumberFormat}, but made to fit the requirements of FreeMarker. Also, it allows defining formats that can't be
+ * described with Java's existing {@link NumberFormat} implementations.
+ *
  * <p>
  * Implementations need not be thread-safe if the {@link TemplateNumberFormatFactory} doesn't recycle them among
  * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single
  * {@link Environment}, and {@link Environment}-s are thread-local objects.
- * 
+ *
  * @since 2.3.24
  */
 public abstract class TemplateNumberFormat extends TemplateValueFormat {
 
     /**
+     * Formats the value to plain text (string that contains no markup or escaping).
+     *
      * @param numberModel
      *            The number to format; not {@code null}. Most implementations will just work with the return value of
-     *            {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of
-     *            a custom {@link TemplateDateModel} implementation.
-     *            
-     * @return The number as text, with no escaping (like no HTML escaping); can't be {@code null}.
-     * 
+     *            {@link TemplateNumberModel#getAsNumber()}, but some may format differently depending on the properties
+     *            of a custom {@link TemplateNumberModel} implementation.
+     *
+     * @return The {@link Number} as plain text (not markup), with no escaping (like no HTML escaping);
+     *             can't be {@code null}.
+     *
      * @throws TemplateValueFormatException
      *             If any problem occurs while parsing/getting the format. Notable subclass:
      *             {@link UnformattableValueException}.
      * @throws TemplateModelException
-     *             Exception thrown by the {@code dateModel} object when calling its methods.
+     *             Exception thrown by the {@code numberModel} object when calling its methods.
      */
     public abstract String formatToPlainText(TemplateNumberModel numberModel)
             throws TemplateValueFormatException, TemplateModelException;
 
     /**
-     * Formats the model to markup instead of to plain text if the result markup will be more than just plain text
-     * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
-     * {@link #formatToPlainText(TemplateNumberModel)} escaped, it must return the {@link String} that
+     * Formats the value to markup instead of to plain text, but only if the result markup will be more than just plain
+     * text escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
+     * {@link #formatToPlainText(TemplateNumberModel)} escaped, then instead it must return the {@link String} that
      * {@link #formatToPlainText(TemplateNumberModel)} does.
-     * 
+     *
      * <p>
      * The implementation in {@link TemplateNumberFormat} simply calls {@link #formatToPlainText(TemplateNumberModel)}.
-     * 
-     * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}.
+     *
+     * @return A {@link String} (assumed to be plain text, not markup), or a {@link TemplateMarkupOutputModel};
+     *             not {@code null}.
      */
     public Object format(TemplateNumberModel numberModel)
             throws TemplateValueFormatException, TemplateModelException {
         return formatToPlainText(numberModel);
     }
-    
+
     /**
      * Tells if this formatter should be re-created if the locale changes.
      */
@@ -80,11 +83,11 @@ public abstract class TemplateNumberFormat extends TemplateValueFormat {
      * This method is reserved for future purposes; currently it always throws {@link ParsingNotSupportedException}. We
      * don't yet support number parsing with {@link TemplateNumberFormat}-s, because currently FTL parses strings to
      * number with the {@link ArithmeticEngine} ({@link TemplateNumberFormat} were only introduced in 2.3.24). If it
-     * will be support, it will be similar to {@link TemplateDateFormat#parse(String, int)}.
+     * will be supported, it will behave similarly to {@link TemplateDateFormat#parse(String, int)}.
      */
     public final Object parse(String s) throws TemplateValueFormatException {
         throw new ParsingNotSupportedException("Number formats currenly don't support parsing");
     }
-    
-    
+
+
 }
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormat.java b/src/main/java/freemarker/core/TemplateTemporalFormat.java
index a282093d..f4335bb2 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormat.java
@@ -23,13 +23,14 @@ import java.time.temporal.Temporal;
 import java.util.Locale;
 import java.util.TimeZone;
 
+import freemarker.template.TemplateDateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateTemporalModel;
 
 /**
  * Represents a {@link Temporal} format; used in templates for formatting {@link Temporal}-s, and parsing strings to
  * {@link Temporal}-s. This is similar to Java's {@link DateTimeFormatter}, but made to fit the requirements of
- * FreeMarker. Also, it makes it possible to define formats that can't be described with {@link DateTimeFormatter}.
+ * FreeMarker. Also, it allows defining formats that can't be described with Java's {@link DateTimeFormatter}.
  *
  * <p>{@link TemplateTemporalFormat} instances are usually created by a {@link TemplateTemporalFormatFactory}.
  *
@@ -45,44 +46,75 @@ import freemarker.template.TemplateTemporalModel;
  */
 public abstract class TemplateTemporalFormat extends TemplateValueFormat {
 
+    /**
+     * Formats the value to plain text (string that contains no markup or escaping).
+     *
+     * @param temporalModel
+     *            The temporal value to format; not {@code null}. Most implementations will just work with the return
+     *            value of {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the
+     *            properties of a custom {@link TemplateDateModel} implementation.
+     *
+     * @return The {@link Temporal} value as plain text (not markup), with no escaping (like no HTML escaping);
+     *             can't be {@code null}.
+     *
+     * @throws TemplateValueFormatException
+     *             If any problem occurs while parsing/getting the format. Notable subclass:
+     *             {@link UnformattableValueException}.
+     * @throws TemplateModelException
+     *             Exception thrown by the {@code temporalModel} object when calling its methods.
+     */
     public abstract String formatToPlainText(TemplateTemporalModel temporalModel)
             throws TemplateValueFormatException, TemplateModelException;
 
     /**
-     * Formats the model to markup instead of to plain text, if the result markup will be more than just plain text
-     * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
-     * {@link #formatToPlainText(TemplateTemporalModel)} escaped, it must return the {@link String} that
-     * {@link #formatToPlainText(TemplateTemporalModel)} does.
+     * Formats the value to markup instead of to plain text, but only if the result markup will be more than just plain
+     * text escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
+     * {@link #formatToPlainText(TemplateTemporalModel)} escaped, then it must return the {@link String} that
+     * {@link #formatToPlainText(TemplateTemporalModel)} would.
      *
-     * <p>The implementation in {@link TemplateTemporalFormat} simply calls {@link #formatToPlainText(TemplateTemporalModel)}.
+     * <p>The implementation in {@link TemplateTemporalFormat} simply calls
+     * {@link #formatToPlainText(TemplateTemporalModel)}.
      *
-     * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}.
+     * @return A {@link String} (assumed to be plain text, not markup), or a {@link TemplateMarkupOutputModel};
+     *             not {@code null}.
      */
     public Object format(TemplateTemporalModel temporalModel) throws TemplateValueFormatException, TemplateModelException {
         return formatToPlainText(temporalModel);
     }
 
     /**
-     * Tells if this formatter can be used for the given locale.
+     * Tells if this formatter can be used for the parameter {@link Locale}. Meant to be used for cache entry
+     * invalidation.
+     *
+     * @param locale Not {@code null}
      */
     public abstract boolean canBeUsedForLocale(Locale locale);
 
     /**
-     * Tells if this formatter can be used for the given {@link TimeZone}.
+     * Tells if this formatter can be used for the parameter {@link TimeZone}. Meant to be used for cache entry
+     * invalidation.
+     *
+     * @param timeZone Not {@code null}
      */
     public abstract boolean canBeUsedForTimeZone(TimeZone timeZone);
 
     /**
-     * Parser a string to a {@link Temporal}, according to this format. Some format implementations may throw
-     * {@link ParsingNotSupportedException} here.
+     * Parses a string to a {@link Temporal}, according to this format. This is optional functionality; some
+     * implementations may throw {@link ParsingNotSupportedException} here.
      *
      * @param s
      *            The string to parse
+     * @param missingTimeZoneParserPolicy
+     *            See {@link MissingTimeZoneParserPolicy}; shouldn't be {@code null}, unless you are sure
+     *            that the target type is a local temporal type, or that the input string contains zone offset,
+     *            time zone, or distance from the UTC epoch. The implementation must accept {@code null} if the
+     *            policy is not actually needed.
      *
-     * @return The interpretation of the text either as a {@link Temporal} or {@link TemplateTemporalModel}. Typically,
-     *         a {@link Temporal}. {@link TemplateTemporalModel} is used if you have to attach some application-specific
-     *         meta-information that's also extracted during {@link #formatToPlainText(TemplateTemporalModel)} (so if
-     *         you format something and then parse it, you get back an equivalent result). It can't be {@code null}.
+     * @return The text converted to either {@link Temporal}, or to {@link TemplateTemporalModel}; not {@code null}.
+     *         Typically, the result should be a {@link Temporal}. Converting to {@link TemplateTemporalModel} should
+     *         only be done if you need to store additional data next to the {@link Temporal}, which is then also used
+     *         by {@link #formatToPlainText(TemplateTemporalModel)} (so if you format something and then parse it, you
+     *         get back an equivalent object).
      *
      * @throws ParsingNotSupportedException If this format doesn't implement parsing.
      */
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
index 3e8c8c6d..16f5757a 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
@@ -23,7 +23,7 @@ import java.util.Locale;
 import java.util.TimeZone;
 
 /**
- * Factory for a certain kind of {@link Temporal} formatting ({@link TemplateTemporalFormat}).
+ * Factory for a certain kind of {@link TemplateTemporalFormat}.
  * See more at {@link TemplateValueFormatFactory}.
  *
  * @see Configurable#setCustomTemporalFormats(java.util.Map)
diff --git a/src/main/java/freemarker/core/TemplateValueFormat.java b/src/main/java/freemarker/core/TemplateValueFormat.java
index 488fc8e6..e8eb82d3 100644
--- a/src/main/java/freemarker/core/TemplateValueFormat.java
+++ b/src/main/java/freemarker/core/TemplateValueFormat.java
@@ -19,7 +19,8 @@
 package freemarker.core;
 
 /**
- * Superclass of all value format objects; objects that convert values to strings, or parse strings.
+ * Superclass of all value format objects; objects that convert values to strings in templates, or parse strings
+ * to an object of the given type in templates.
  * 
  * @since 2.3.24
  */
diff --git a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
index 32435728..31719ccc 100644
--- a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
@@ -36,7 +36,7 @@ import java.util.Locale;
 import java.util.TimeZone;
 
 /**
- * Format factory related to {@link someJava8Temporal?string.xs}, {@link someJava8Temporal?string.xs_...}, etc.
+ * Format factory related to {@code someJava8Temporal?string.xs}, {@code someJava8Temporal?string.xs_...}, etc.
  */
 // TODO [FREEMARKER-35] Historical date handling compared to ISO
 class XSTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
@@ -48,8 +48,9 @@ class XSTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
     }
 
     @Override
-    public TemplateTemporalFormat get(String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env) throws
-            TemplateValueFormatException {
+    public TemplateTemporalFormat get(
+            String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env)
+            throws TemplateValueFormatException {
         if (!params.isEmpty()) {
             // TODO [FREEMARKER-35]
             throw new InvalidFormatParametersException("xs currently doesn't support parameters for Java 8 temporal types");
diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java
index 112399f6..7eff6501 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -55,8 +55,8 @@ public class _MessageUtil {
     };
 
     static final String FAIL_MISSING_TIME_ZONE_PARSER_POLICY_ERROR_DETAIL
-            = "The parsed string doesn't contain time zone or offset, and the specified policy is "
-                    + "to fail in that case (see " + MissingTimeZoneParserPolicy.class.getName()
+            = "The parsed string doesn't contain time zone, nor offset, and that target type is non-local, and the "
+                    + "specified policy is to fail in that case (see " + MissingTimeZoneParserPolicy.class.getName()
                     + "." + MissingTimeZoneParserPolicy.FAIL + ").";
 
     static final String EMBEDDED_MESSAGE_BEGIN = "---begin-message---\n";
diff --git a/src/main/java/freemarker/core/_TemporalUtils.java b/src/main/java/freemarker/core/_TemporalUtils.java
index 8aa38f8c..311bf987 100644
--- a/src/main/java/freemarker/core/_TemporalUtils.java
+++ b/src/main/java/freemarker/core/_TemporalUtils.java
@@ -90,7 +90,7 @@ public final class _TemporalUtils {
 
     // Not private because of tests
     static final boolean SUPPORTED_TEMPORAL_CLASSES_ARE_FINAL = SUPPORTED_TEMPORAL_CLASSES.stream()
-            .allMatch(cl -> (cl.getModifiers() & Modifier.FINAL) == Modifier.FINAL);
+            .allMatch(cl -> (cl.getModifiers() & Modifier.FINAL) != 0);
 
     private _TemporalUtils() {
         throw new AssertionError();
@@ -439,6 +439,7 @@ public final class _TemporalUtils {
         if (SUPPORTED_TEMPORAL_CLASSES_ARE_FINAL) {
             return temporalClass;
         } else {
+            if (true) throw new AssertionError(); //!!T
             if (Instant.class.isAssignableFrom(temporalClass)) {
                 return Instant.class;
             } else if (LocalDate.class.isAssignableFrom(temporalClass)) {
@@ -458,13 +459,13 @@ public final class _TemporalUtils {
             } else if (Year.class.isAssignableFrom(temporalClass)) {
                 return Year.class;
             } else {
-                throw new IllegalArgumentException("Unsupprted temporal class: " + temporalClass.getName());
+                throw new IllegalArgumentException("Unsupported temporal class: " + temporalClass.getName());
             }
         }
     }
 
     /**
-     * Tells if the temporal class is one that doesn't store, nor have an implied time zone or offset.
+     * Tells if the temporal class is one that doesn't store, nor have an implied time zone, or offset.
      *
      * @throws IllegalArgumentException If the temporal class is not currently supported by FreeMarker.
      */
@@ -485,7 +486,7 @@ public final class _TemporalUtils {
      *
      * @throws IllegalArgumentException If the temporal class is not currently supported by FreeMarker.
      */
-    public static Class<? extends Temporal> getLocalTemporalClassForNonLocal(Class<? extends Temporal> temporalClass) {
+    public static Class<? extends Temporal> tryGetLocalTemporalClassForNonLocal(Class<? extends Temporal> temporalClass) {
         temporalClass = normalizeSupportedTemporalClass(temporalClass);
         if (temporalClass == OffsetDateTime.class) {
             return LocalDateTime.class;
@@ -496,13 +497,16 @@ public final class _TemporalUtils {
         if (temporalClass == OffsetTime.class) {
             return LocalTime.class;
         }
+        if (temporalClass == Instant.class) {
+            return LocalDateTime.class;
+        }
         return null;
     }
 
     /**
      * Returns the FreeMarker configuration format setting name for a temporal class.
      *
-     * @throws IllegalArgumentException If {@link temporalClass} is not a supported {@link Temporal} subclass.
+     * @throws IllegalArgumentException If {@code temporalClass} is not a supported {@link Temporal} subclass.
      */
     public static String temporalClassToFormatSettingName(Class<? extends Temporal> temporalClass, boolean camelCase) {
         temporalClass = normalizeSupportedTemporalClass(temporalClass);
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 45f2c78b..678e3ed7 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -122,10 +122,14 @@ import freemarker.template.utility.XmlEscape;
  *  cfg.set<i>SomeSetting</i>(...);
  *  cfg.set<i>OtherSetting</i>(...);
  *  ...
+ *  // Do not modify the settings later, when you have already started processing templates!
  *  
  *  // Later, whenever the application needs a template (so you may do this a lot, and from multiple threads):
  *  {@link Template Template} myTemplate = cfg.{@link #getTemplate(String) getTemplate}("myTemplate.ftlh");
  *  myTemplate.{@link Template#process(Object, java.io.Writer) process}(dataModel, out);</pre>
+ *
+ *  <p><b>Do not modify the {@link Configuration} settings after you started processing templates!</b> Doing so can
+ *  cause to undefined behavior, even if you only have a single thread!</p>
  * 
  * <p>A couple of settings that you should not leave on its default value are:
  * <ul>
diff --git a/src/main/java/freemarker/template/TemplateDateModel.java b/src/main/java/freemarker/template/TemplateDateModel.java
index d354fda3..24a551b8 100644
--- a/src/main/java/freemarker/template/TemplateDateModel.java
+++ b/src/main/java/freemarker/template/TemplateDateModel.java
@@ -25,12 +25,11 @@ import java.util.Date;
 import java.util.List;
 
 /**
- * "date", "time" and "date-time" template language data types: corresponds to {@link java.util.Date}. Contrary to Java,
- * FreeMarker distinguishes date (no time part), time and date-time values.
+ * "date", "time", and "date-time" template language data types: corresponds to {@link java.util.Date}. Contrary to
+ * Java, FreeMarker distinguishes date (no time part), time (no date part), and date-time values.
  * 
- * <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>Objects of this type should be immutable, that is, {@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}.
diff --git a/src/main/java/freemarker/template/TemplateTemporalModel.java b/src/main/java/freemarker/template/TemplateTemporalModel.java
index 7b642bfe..ea7c2419 100644
--- a/src/main/java/freemarker/template/TemplateTemporalModel.java
+++ b/src/main/java/freemarker/template/TemplateTemporalModel.java
@@ -35,6 +35,9 @@ import java.time.temporal.Temporal;
  * This does not deal with {@link java.time.Duration}, and {@link java.time.Period}, because those don't implement the
  * {@link Temporal} interface.
  *
+ * <p>Objects of this type should be immutable, that is, {@link #getAsTemporal()}} should always return the same value
+ * as for the first time.
+ *
  * <p>{@link java.util.Date} values (the way date/time values were represented prior Java 8) are handled by
  * {@link TemplateDateModel}.
  *
@@ -42,7 +45,7 @@ import java.time.temporal.Temporal;
  */
 public interface TemplateTemporalModel extends TemplateModel {
 	/**
-	 * Returns the date value. The return value must not be {@code null}.
+	 * Returns the temporal value; can't be {@code null}.
 	 */
 	Temporal getAsTemporal() throws TemplateModelException;
 }
diff --git a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
index 75518fa0..d7661e59 100644
--- a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
+++ b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
@@ -243,7 +243,7 @@ public abstract class AbstractTemporalFormatTest {
         assertThat(
                 e.getMessage(),
                 allOf(
-                        containsStringIgnoringCase("doesn't contain time zone or offset"),
+                        containsStringIgnoringCase("doesn't contain time zone, nor offset"),
                         containsString(MissingTimeZoneParserPolicy.class.getName() + "."
                                 + MissingTimeZoneParserPolicy.FAIL)));
     }
diff --git a/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java b/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java
index 7590d62e..7850b4bd 100644
--- a/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java
+++ b/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java
@@ -385,7 +385,7 @@ public class ISOLikeTemplateTemporalFormatTest extends AbstractTemporalFormatTes
                     temporalClass,
                     e -> assertThat(e.getMessage(), allOf(
                             containsString(jQuote(stringToParse)),
-                            containsString("time zone or offset"),
+                            containsString("time zone, nor offset"),
                             containsString(temporalClass.getSimpleName()))));
         }
 
@@ -482,7 +482,7 @@ public class ISOLikeTemplateTemporalFormatTest extends AbstractTemporalFormatTes
                     OffsetTime.class,
                     e -> assertThat(e.getMessage(), allOf(
                             containsString(jQuote(stringToParse)),
-                            containsString("time zone or offset"),
+                            containsString("time zone, nor offset"),
                             containsString(OffsetTime.class.getSimpleName()))));
         }
 
diff --git a/src/test/java/freemarker/core/_TemporalUtilsTest.java b/src/test/java/freemarker/core/_TemporalUtilsTest.java
index be258b24..bdfd755d 100644
--- a/src/test/java/freemarker/core/_TemporalUtilsTest.java
+++ b/src/test/java/freemarker/core/_TemporalUtilsTest.java
@@ -22,6 +22,7 @@ package freemarker.core;
 import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.*;
 
+import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.OffsetDateTime;
@@ -85,11 +86,12 @@ public class _TemporalUtilsTest {
     }
 
     @Test
-    public void testGetLocalTemporalClassForNonLocal() {
-        assertThat(_TemporalUtils.getLocalTemporalClassForNonLocal(OffsetDateTime.class), equalTo(LocalDateTime.class));
-        assertThat(_TemporalUtils.getLocalTemporalClassForNonLocal(ZonedDateTime.class), equalTo(LocalDateTime.class));
-        assertThat(_TemporalUtils.getLocalTemporalClassForNonLocal(OffsetTime.class), equalTo(LocalTime.class));
-        assertNull(_TemporalUtils.getLocalTemporalClassForNonLocal(LocalDateTime.class));
+    public void testTryGetLocalTemporalClassForNonLocal() {
+        assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(OffsetDateTime.class), equalTo(LocalDateTime.class));
+        assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(ZonedDateTime.class), equalTo(LocalDateTime.class));
+        assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(OffsetTime.class), equalTo(LocalTime.class));
+        assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(Instant.class), equalTo(LocalDateTime.class));
+        assertNull(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(LocalDateTime.class));
     }
 
 }
\ No newline at end of file