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

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


The following commit(s) were added to refs/heads/FREEMARKER-35 by this push:
     new 6f03463  [FREEMARKER-35] Improved ISO (and XS) formatters, added more 
tests for them. Some code cleanup.
6f03463 is described below

commit 6f03463696e604fda66dab80d7900d10eb59f4fa
Author: ddekany <[email protected]>
AuthorDate: Sun Feb 13 23:12:31 2022 +0100

    [FREEMARKER-35] Improved ISO (and XS) formatters, added more tests for 
them. Some code cleanup.
---
 .../core/AliasTemplateDateFormatFactory.java       |  11 +-
 .../ISOLikeTemplateTemporalTemporalFormat.java     | 154 ++++++++--
 .../core/JavaTemplateTemporalFormat.java           |  12 +-
 .../freemarker/core/TemplateDateFormatFactory.java |   2 +-
 .../freemarker/core/TemplateTemporalFormat.java    |  19 +-
 .../core/TemplateTemporalFormatFactory.java        |  11 +-
 .../core/TemplateValueFormatFactory.java           |   6 +-
 .../freemarker/template/utility/TemporalUtils.java |  23 +-
 .../core/AbstractTemporalFormatTest.java           |  38 ++-
 .../core/TemporalFormatWithIsoFormatTest.java      | 320 ++++++++++++++++-----
 .../core/TemporalFormatWithJavaFormatTest.java     |  13 +-
 11 files changed, 476 insertions(+), 133 deletions(-)

diff --git a/src/main/java/freemarker/core/AliasTemplateDateFormatFactory.java 
b/src/main/java/freemarker/core/AliasTemplateDateFormatFactory.java
index 4dec545..4bb788f 100644
--- a/src/main/java/freemarker/core/AliasTemplateDateFormatFactory.java
+++ b/src/main/java/freemarker/core/AliasTemplateDateFormatFactory.java
@@ -22,11 +22,16 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.TimeZone;
 
+import freemarker.template.Configuration;
 import freemarker.template.utility.StringUtil;
 
 /**
- * Creates an alias to another format, so that the format can be referred to 
with a simple name in the template, rather
- * than as a concrete pattern or other kind of format string.
+ * Creates an alias to another format that's given with a {@link String}, so 
that the format can be referred with a
+ * simple name in the template, rather than with a concrete pattern or other 
kind of format string. Internally, this
+ * will call {@link Environment#getTemplateDateFormat(String, int, Locale, 
TimeZone, boolean)} to resolve the other
+ * format.
+ *
+ * @see Configuration#customDateFormats
  * 
  * @since 2.3.24
  */
@@ -51,7 +56,7 @@ public final class AliasTemplateDateFormatFactory extends 
TemplateDateFormatFact
      * @param localizedTargetFormatStrings
      *            Maps {@link Locale}-s to format strings. If the desired 
locale doesn't occur in the map, a less
      *            specific locale is tried, repeatedly until only the language 
part remains. For example, if locale is
-     *            {@code new Locale("en", "US", "Linux")}, then these keys 
will be attempted untol a match is found, in
+     *            {@code new Locale("en", "US", "Linux")}, then these keys 
will be attempted until a match is found, in
      *            this order: {@code new Locale("en", "US", "Linux")}, {@code 
new Locale("en", "US")},
      *            {@code new Locale("en")}. If there's still no matching key, 
the value of the
      *            {@code targetFormatString} will be used.
diff --git 
a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java 
b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
index 482b257..2923940 100644
--- a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
+++ b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
@@ -20,6 +20,7 @@
 package freemarker.core;
 
 import static freemarker.template.utility.StringUtil.*;
+import static freemarker.template.utility.TemporalUtils.*;
 
 import java.time.DateTimeException;
 import java.time.Instant;
@@ -30,10 +31,13 @@ import java.time.Year;
 import java.time.YearMonth;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
 import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalQuery;
 import java.util.TimeZone;
+import java.util.regex.Pattern;
 
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateTemporalModel;
@@ -51,23 +55,26 @@ final class ISOLikeTemplateTemporalTemporalFormat extends 
TemplateTemporalFormat
     private final boolean instantConversion;
     private final ZoneId zoneId;
     private final String description;
-    private final TemporalQuery temporalQuery;
+    private final TemporalQuery<? extends Temporal> temporalQuery;
     private final Class<? extends Temporal> temporalClass;
     private final DateTimeFormatter parserExtendedDateTimeFormatter;
     private final DateTimeFormatter parserBasicDateTimeFormatter;
+    private final boolean localTemporalClass;
 
     ISOLikeTemplateTemporalTemporalFormat(
             DateTimeFormatter dateTimeFormatter,
             DateTimeFormatter parserExtendedDateTimeFormatter,
             DateTimeFormatter parserBasicDateTimeFormatter,
             Class<? extends Temporal> temporalClass, TimeZone zone, String 
formatString) {
+        temporalClass = normalizeSupportedTemporalClass(temporalClass);
         this.dateTimeFormatter = dateTimeFormatter;
         this.parserExtendedDateTimeFormatter = parserExtendedDateTimeFormatter;
         this.parserBasicDateTimeFormatter = parserBasicDateTimeFormatter;
         this.temporalQuery = TemporalUtils.getTemporalQuery(temporalClass);
-        this.instantConversion = Instant.class.isAssignableFrom(temporalClass);
+        this.instantConversion = temporalClass == Instant.class;
         this.temporalClass = temporalClass;
-        this.zoneId = zone.toZoneId();
+        this.localTemporalClass = isLocalTemporalClass(temporalClass);
+        this.zoneId = temporalClass == Instant.class ? zone.toZoneId() : null;
         this.description = formatString;
     }
 
@@ -83,17 +90,75 @@ final class ISOLikeTemplateTemporalTemporalFormat extends 
TemplateTemporalFormat
         try {
             return dateTimeFormatter.format(temporal);
         } catch (DateTimeException e) {
-            throw new UnformattableValueException(e.getMessage(), e);
+            throw new UnformattableValueException(
+                    "Failed to format temporal " + temporal + ". Reason: " + 
e.getMessage(),
+                    e);
         }
     }
 
     @Override
     public Object parse(String s) throws TemplateValueFormatException {
-        DateTimeFormatter parserDateTimeFormatter = 
parserBasicDateTimeFormatter == null || isExtendedFormatString(s)
+        final boolean extendedFormat;
+        final boolean add1Day;
+        if (temporalClass == LocalDate.class || temporalClass == 
YearMonth.class) {
+            extendedFormat = s.indexOf('-', 1) != -1;
+            add1Day = false;
+        } else if (temporalClass == LocalTime.class || temporalClass == 
OffsetTime.class) {
+            extendedFormat = s.indexOf(":") != -1;
+            add1Day = false;
+            // ISO 8601 allows hour 24 if the rest of the time is 0:
+            if (isStartOf240000(s, 0)) {
+                s = "00" + s.substring(2);
+            }
+        } else if (temporalClass == Year.class) {
+            extendedFormat = false;
+            add1Day = false;
+        } else {
+            int tIndex = s.indexOf('T');
+            if (tIndex < 1) {
+                throw new UnparsableValueException(
+                        "Failed to parse value " + jQuote(s) + " with format " 
+ jQuote(description)
+                                + ", and target class " + 
temporalClass.getSimpleName() + ": "
+                                + "Character \"T\" must be used to separate 
the date and time part.");
+            }
+            if (s.indexOf(":", tIndex + 1) != -1) {
+                extendedFormat = true;
+            } else {
+                // 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)) {
+                s = s.substring(0, tIndex + 1) + "00" + s.substring(tIndex + 
3);
+                add1Day = true;
+            } else {
+                add1Day = false;
+            }
+        }
+
+        DateTimeFormatter parserDateTimeFormatter = 
parserBasicDateTimeFormatter == null || extendedFormat
                 ? parserExtendedDateTimeFormatter : 
parserBasicDateTimeFormatter;
         try {
-            return parserDateTimeFormatter.parse(s, temporalQuery);
-        } catch (DateTimeParseException e) {
+            TemporalAccessor parseResult = parserDateTimeFormatter.parse(s);
+            if (!localTemporalClass && 
!parseResult.isSupported(ChronoField.OFFSET_SECONDS)) {
+                // Unlike for the Java format, for ISO we require the string 
to contain the offset for a non-local
+                // target type. We could use the default time zone, but that's 
really just guessing, also DST creates
+                // ambiguous cases. For the Java formatter we are lenient, as 
the shared date-time format typically
+                // misses the offset, and because we don't want a 
format-and-then-parse cycle to fail. But in ISO
+                // format, the offset is always shown for a non-local temporal.
+                throw new UnparsableValueException(
+                        "Failed to parse value " + jQuote(s) + " with format " 
+ jQuote(description)
+                                + ", and target class " + 
temporalClass.getSimpleName() + ": "
+                                + "The string must contain the time zone 
offset for this target class. "
+                                + "(Defaulting to the current time zone is not 
allowed for ISO-style formats.)");
+
+            }
+            Temporal resultTemporal = parseResult.query(temporalQuery);
+            if (add1Day) {
+                resultTemporal = resultTemporal.plus(1, ChronoUnit.DAYS);
+            }
+            return resultTemporal;
+        } catch (DateTimeException e) {
             throw new UnparsableValueException(
                     "Failed to parse value " + jQuote(s) + " with format " + 
jQuote(description)
                             + ", and target class " + 
temporalClass.getSimpleName() + ", "
@@ -104,31 +169,66 @@ final class ISOLikeTemplateTemporalTemporalFormat extends 
TemplateTemporalFormat
         }
     }
 
-    private boolean isExtendedFormatString(String s) throws 
UnparsableValueException {
-        if (temporalClass == LocalDate.class || temporalClass == 
YearMonth.class) {
-            return !s.isEmpty() && s.indexOf('-', 1) != -1;
-        } else if (temporalClass == LocalTime.class || temporalClass == 
OffsetTime.class) {
-            return s.indexOf(":") != -1;
-        } else if (temporalClass == Year.class) {
+    private final static Pattern ZERO_TIME_AFTER_HH = 
Pattern.compile("(?::?+00(?::?+00(?:.?+0+)?)?)?");
+
+    private static boolean isStartOf240000(String s, int from) {
+        if (from + 1 >= s.length() || s.charAt(from) != '2' || s.charAt(from + 
1) != '4') {
             return false;
-        } else {
-            int tIndex = s.indexOf('T');
-            if (tIndex < 1) {
-                throw new UnparsableValueException(
-                        "Failed to parse value " + jQuote(s) + " with format " 
+ jQuote(description)
-                                + ", and target class " + 
temporalClass.getSimpleName() + ": "
-                                + "Character \"T\" must be used to separate 
the date and time part.");
+        }
+
+        int index = from + 2;
+
+        int indexAfterHH = index;
+        // Seek for time zone start or end of string
+        while (index < s.length()) {
+            char c = s.charAt(index);
+            boolean cIsDigit = c >= '0' && c <= '9';
+            if (!(cIsDigit || c == ':' || c == '.')) {
+                break;
             }
-            if (s.indexOf(":", tIndex + 1) != -1) {
-                return true;
+            if (cIsDigit && c != '0') {
+                return false;
             }
-            // Note: false for: -5000101T00, as there the last '-' has index 0
-            return s.lastIndexOf('-', tIndex - 1) > 0;
+
+            index++;
         }
+
+        String timeAfterHH = s.substring(indexAfterHH, index);
+        return ZERO_TIME_AFTER_HH.matcher(timeAfterHH).matches();
     }
 
-    private boolean temporalClassHasNoTimePart() {
-        return temporalClass == LocalDate.class || temporalClass == Year.class 
|| temporalClass == YearMonth.class;
+    //!!T
+    public static void main(String[] args) {
+        for (String original : new String[] {"24", "24:00", "24:00:00", 
"24:00:00.0"}) {
+            for (boolean basic : new boolean[] {false, true}) {
+                for (String prefix : new String[] {"", "T"}) {
+                    for (String suffix : new String[] {"", "Z", "-01", "+01"}) 
{
+                        String s = prefix + (basic ? original.replace(":", "") 
: original)+ suffix;
+
+                        int startIndex = s.indexOf("24");
+                        if (!isStartOf240000(s, startIndex)) {
+                            throw new AssertionError("Couldn't find end of 
time part in: " + s);
+                        }
+                    }
+                }
+            }
+        }
+
+        for (String original : new String[] {
+                "24:", "24:01", "24:00:01", "24:00:00.1", "24:0", "24:00:x",
+                "2401", "240001", "240000.1", "240"}) {
+            for (String prefix : new String[] {"", "T"}) {
+                for (String suffix : new String[] {"", "Z", "-01", "+01"}) {
+                    String s = prefix + original + suffix;
+
+                    int startIndex = s.indexOf("24");
+                    if (isStartOf240000(s, startIndex)) {
+                        throw new AssertionError("Shouldn't match: " + s);
+                    }
+                }
+            }
+        }
+
     }
 
     @Override
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java 
b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
index 64e2cfd..85311f6 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
@@ -27,8 +27,6 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.OffsetDateTime;
 import java.time.OffsetTime;
-import java.time.Year;
-import java.time.YearMonth;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
@@ -136,7 +134,7 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
         }
 
         // Handling of time zone related edge cases
-        if (isLocalTemporalClass(temporalClass)) {
+        if (TemporalUtils.isLocalTemporalClass(temporalClass)) {
             this.preFormatValueConversion = null;
             this.specialParsing = null;
             formatWithZone = false;
@@ -316,14 +314,6 @@ class JavaTemplateTemporalFormat extends 
TemplateTemporalFormat {
                 
.equals(dateTimeFormatter.format(SHOWS_ZONE_SAMPLE_TEMPORAL_2));
     }
 
-    private static boolean isLocalTemporalClass(Class<? extends Temporal> 
normalizedTemporalClass) {
-        return normalizedTemporalClass == LocalDateTime.class
-                || normalizedTemporalClass == LocalTime.class
-                || normalizedTemporalClass == LocalDate.class
-                || normalizedTemporalClass == Year.class
-                || normalizedTemporalClass == YearMonth.class;
-    }
-
     private static FormatStyle getMoreVerboseStyle(FormatStyle style) {
         switch (style) {
             case SHORT:
diff --git a/src/main/java/freemarker/core/TemplateDateFormatFactory.java 
b/src/main/java/freemarker/core/TemplateDateFormatFactory.java
index 81faa19..63af11b 100644
--- a/src/main/java/freemarker/core/TemplateDateFormatFactory.java
+++ b/src/main/java/freemarker/core/TemplateDateFormatFactory.java
@@ -70,7 +70,7 @@ public abstract class TemplateDateFormatFactory extends 
TemplateValueFormatFacto
      *            <p>
      *            As of FreeMarker 2.3.21, this is {@code true} exactly when 
the date is an SQL "date without time of
      *            the day" (i.e., a {@link java.sql.Date java.sql.Date}) or an 
SQL "time of the day" value (i.e., a
-     *            {@link java.sql.Time java.sql.Time}, although this rule can 
change in future, depending on
+     *            {@link java.sql.Time java.sql.Time}, although this rule can 
change in the future, depending on
      *            configuration settings and such, so you shouldn't rely on 
this rule, just accept what this parameter
      *            says.
      * @param env
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormat.java 
b/src/main/java/freemarker/core/TemplateTemporalFormat.java
index aeaffd9..75eede4 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormat.java
@@ -25,24 +25,27 @@ import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateTemporalModel;
 
 /**
- * Represents a {@link Temporal} format; used in templates for formatting and 
parsing with that format. This is
- * similar to Java's {@link DateTimeFormatter}, but made to fit the 
requirements of FreeMarker. Also, it makes easier to
- * define formats that can't be represented with {@link DateTimeFormatter}.
+ * 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}.
+ *
+ * <p>{@link TemplateTemporalFormat} instances are usually created by a {@link 
TemplateTemporalFormatFactory}.
  *
  * <p>
  * Implementations need not be thread-safe if the {@link 
TemplateTemporalFormatFactory} doesn't recycle them among
  * different {@link Environment}-s. The code outside the {@link 
TemplateTemporalFormatFactory} will not try to reuse
- * {@link TemplateTemporalFormat} instances in multiple {@link Environment}-s, 
and {@link Environment}-s are
- * thread-local objects.
+ * {@link TemplateTemporalFormat} instances in multiple {@link Environment}-s, 
and an {@link Environment} is only used
+ * in a single thread.
  *
  * @since 2.3.32
  */
 public abstract class TemplateTemporalFormat extends TemplateValueFormat {
 
-    public abstract String formatToPlainText(TemplateTemporalModel 
temporalModel) throws TemplateValueFormatException, TemplateModelException;
+    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
+     * 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.
@@ -68,7 +71,7 @@ public abstract class TemplateTemporalFormat extends 
TemplateValueFormat {
     public abstract boolean isTimeZoneBound();
 
     /**
-     * Parsers a string to a {@link Temporal}, according to this format. Some 
format implementations may throw
+     * Parser a string to a {@link Temporal}, according to this format. Some 
format implementations may throw
      * {@link ParsingNotSupportedException} here.
      *
      * @param s
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java 
b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
index e6cc58f..3e8c8c6 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
@@ -22,11 +22,9 @@ import java.time.temporal.Temporal;
 import java.util.Locale;
 import java.util.TimeZone;
 
-import freemarker.template.Configuration;
-
 /**
- * Factory for a certain kind of {@link Temporal} formatting ({@link 
TemplateTemporalFormat}). Usually a singleton
- * (one-per-VM, or one-per-{@link Configuration}), and so must be thread-safe.
+ * Factory for a certain kind of {@link Temporal} formatting ({@link 
TemplateTemporalFormat}).
+ * See more at {@link TemplateValueFormatFactory}.
  *
  * @see Configurable#setCustomTemporalFormats(java.util.Map)
  * 
@@ -62,7 +60,7 @@ public abstract class TemplateTemporalFormatFactory extends 
TemplateValueFormatF
      *            The runtime environment from which the formatting was 
called. This is mostly meant to be used for
      *            {@link Environment#setCustomState(Object, Object)}/{@link 
Environment#getCustomState(Object)}. The
      *            result shouldn't depend on setting values in the {@link 
Environment}, because changing settings
-     *            will not necessarily invalidate the result.
+     *            will not necessarily invalidate the returned {@link 
TemplateTemporalFormat}.
      * 
      * @throws TemplateValueFormatException
      *             If any problem occurs while parsing/getting the format. 
Notable subclasses:
@@ -71,8 +69,7 @@ public abstract class TemplateTemporalFormatFactory extends 
TemplateValueFormatF
      *             not supported by this factory.
      */
     public abstract TemplateTemporalFormat get(
-            String params,
-            Class<? extends Temporal> temporalClass, Locale locale, TimeZone 
timeZone, Environment env)
+            String params, Class<? extends Temporal> temporalClass, Locale 
locale, TimeZone timeZone, Environment env)
                     throws TemplateValueFormatException;
 
 }
diff --git a/src/main/java/freemarker/core/TemplateValueFormatFactory.java 
b/src/main/java/freemarker/core/TemplateValueFormatFactory.java
index 78a2ae7..a454e77 100644
--- a/src/main/java/freemarker/core/TemplateValueFormatFactory.java
+++ b/src/main/java/freemarker/core/TemplateValueFormatFactory.java
@@ -18,8 +18,12 @@
  */
 package freemarker.core;
 
+import freemarker.template.Configuration;
+
 /**
- * Superclass of all format factories.
+ * Superclass of all format factories. A format factory is an object that 
creates instances of a certain kind of
+ * {@link TemplateValueFormat}. Generally, they are singletons (one per JVM, 
or one per {@link Configuration}). They
+ * should be thread safe. They may encapsulate a cache and return cached 
{@link TemplateValueFormat} instances.
  * 
  * @since 2.3.24
  */
diff --git a/src/main/java/freemarker/template/utility/TemporalUtils.java 
b/src/main/java/freemarker/template/utility/TemporalUtils.java
index fb82e85..3498ff5 100644
--- a/src/main/java/freemarker/template/utility/TemporalUtils.java
+++ b/src/main/java/freemarker/template/utility/TemporalUtils.java
@@ -431,6 +431,8 @@ public final class TemporalUtils {
      * that FreeMarker directly supports. At least in Java 8 they are all 
final anyway, but just in case this changes in
      * a future Java version, use this method before using {@code ==}.
      *
+     * @throws IllegalArgumentException If the temporal class is not currently 
supported by FreeMarker.
+     *
      * @since 2.3.32
      */
     public static Class<? extends Temporal> 
normalizeSupportedTemporalClass(Class<? extends Temporal> temporalClass) {
@@ -456,12 +458,30 @@ public final class TemporalUtils {
             } else if (Year.class.isAssignableFrom(temporalClass)) {
                 return Year.class;
             } else {
-                return temporalClass;
+                throw new IllegalArgumentException("Unsupprted temporal class: 
" + temporalClass.getName());
             }
         }
     }
 
     /**
+     * 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.
+     *
+     * @since 2.3.32
+     */
+    public static boolean isLocalTemporalClass(Class<? extends Temporal> 
temporalClass) {
+        temporalClass = normalizeSupportedTemporalClass(temporalClass);
+        if (temporalClass == Instant.class
+                || temporalClass == OffsetDateTime.class
+                || temporalClass == ZonedDateTime.class
+                || temporalClass == OffsetTime.class) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Returns the FreeMarker configuration format setting name for a temporal 
class.
      *
      * @throws IllegalArgumentException If {@link temporalClass} is not a 
supported {@link Temporal} subclass.
@@ -495,5 +515,4 @@ public final class TemporalUtils {
             throw new IllegalArgumentException("Unsupported temporal class: " 
+ temporalClass.getName());
         }
     }
-
 }
diff --git a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java 
b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
index 5f30bb3..4fcb177 100644
--- a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
+++ b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
@@ -20,6 +20,7 @@
 package freemarker.core;
 
 import static freemarker.template.utility.StringUtil.*;
+import static org.junit.Assert.*;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -40,11 +41,11 @@ import freemarker.template.utility.DateUtil;
  */
 public abstract class AbstractTemporalFormatTest {
 
-    static protected String formatTemporal(Consumer<Configurable> configurer, 
Temporal... values) throws
+    static protected String formatTemporal(Consumer<Configurable> 
configurator, Temporal... values) throws
             TemplateException {
         Configuration conf = new Configuration(Configuration.VERSION_2_3_32);
 
-        configurer.accept(conf);
+        configurator.accept(conf);
 
         Environment env = null;
         try {
@@ -65,13 +66,13 @@ public abstract class AbstractTemporalFormatTest {
     }
 
     static protected void assertParsingResults(
-            Consumer<Configurable> configurer,
+            Consumer<Configurable> configurator,
             Object... stringsAndExpectedResults) throws TemplateException, 
TemplateValueFormatException {
         Configuration conf = new Configuration(Configuration.VERSION_2_3_32);
         conf.setTimeZone(DateUtil.UTC);
         conf.setLocale(Locale.US);
 
-        configurer.accept(conf);
+        configurator.accept(conf);
 
         Environment env = null;
         try {
@@ -128,4 +129,33 @@ public abstract class AbstractTemporalFormatTest {
         }
     }
 
+    static protected void assertParsingFails(
+            Consumer<Configurable> configurator,
+            String parsed,
+            Class<? extends Temporal> temporalClass,
+            Consumer<TemplateValueFormatException> exceptionAssertions) throws 
TemplateException,
+            TemplateValueFormatException {
+        Configuration conf = new Configuration(Configuration.VERSION_2_3_32);
+        conf.setTimeZone(DateUtil.UTC);
+        conf.setLocale(Locale.US);
+
+        configurator.accept(conf);
+
+        Environment env = null;
+        try {
+            env = new Template(null, "", 
conf).createProcessingEnvironment(null, null);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+
+        TemplateTemporalFormat templateTemporalFormat = 
env.getTemplateTemporalFormat(temporalClass);
+
+        try {
+            templateTemporalFormat.parse(parsed);
+            fail("Parsing " + jQuote(parsed) + " with " + 
templateTemporalFormat + " should have failed.");
+        } catch (TemplateValueFormatException e) {
+            exceptionAssertions.accept(e);
+        }
+    }
+
 }
diff --git a/src/test/java/freemarker/core/TemporalFormatWithIsoFormatTest.java 
b/src/test/java/freemarker/core/TemporalFormatWithIsoFormatTest.java
index f096970..27a4bf3 100644
--- a/src/test/java/freemarker/core/TemporalFormatWithIsoFormatTest.java
+++ b/src/test/java/freemarker/core/TemporalFormatWithIsoFormatTest.java
@@ -19,59 +19,72 @@
 
 package freemarker.core;
 
+import static freemarker.template.utility.StringUtil.*;
 import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.*;
 
 import java.io.IOException;
+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.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatterBuilder;
+import java.time.temporal.Temporal;
 import java.util.Locale;
+import java.util.TimeZone;
 import java.util.function.Consumer;
 
 import org.junit.Test;
 
 import freemarker.template.TemplateException;
+import freemarker.template.utility.DateUtil;
 
 public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest {
 
-    private static final Consumer<Configurable> ISO_DATE_TIME_CONFIGURER = 
conf -> conf.setDateTimeFormat("iso");
-    private static final Consumer<Configurable> ISO_DATE_CONFIGURER = conf -> 
conf.setDateFormat("iso");
-    private static final Consumer<Configurable> ISO_TIME_CONFIGURER = conf -> 
conf.setTimeFormat("iso");
+    private static final Consumer<Configurable> ISO_DATE_CONFIGURATOR = conf 
-> conf.setDateFormat("iso");
+    private static final Consumer<Configurable> ISO_TIME_CONFIGURATOR = conf 
-> conf.setTimeFormat("iso");
+    private static final Consumer<Configurable> ISO_DATE_TIME_CONFIGURATOR = 
conf -> conf.setDateTimeFormat("iso");
+
+    private static Consumer<Configurable> isoDateTimeConfigurator(TimeZone 
timeZone) {
+        return conf -> { ISO_DATE_TIME_CONFIGURATOR.accept(conf); 
conf.setTimeZone(timeZone); };
+    }
+    private static final Consumer<Configurable> ISO_YEAR_MONTH_CONFIGURATOR = 
conf -> conf.setYearMonthFormat("iso");
+    private static final Consumer<Configurable> ISO_YEAR_CONFIGURATOR = conf 
-> conf.setYearFormat("iso");
 
     @Test
     public void testFormatOffsetTime() throws TemplateException, IOException {
         assertEquals(
                 "13:01:02Z",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
+                        ISO_TIME_CONFIGURATOR,
                         OffsetTime.of(LocalTime.of(13, 1, 2), 
ZoneOffset.UTC)));
         assertEquals(
                 "13:01:02+01:00",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
+                        ISO_TIME_CONFIGURATOR,
                         OffsetTime.of(LocalTime.of(13, 1, 2), 
ZoneOffset.ofHours(1))));
         assertEquals(
                 "13:00:00-02:30",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
+                        ISO_TIME_CONFIGURATOR,
                         OffsetTime.of(LocalTime.of(13, 0, 0), 
ZoneOffset.ofHoursMinutesSeconds(-2, -30, 0))));
         assertEquals(
                 "13:00:00.0123Z",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
+                        ISO_TIME_CONFIGURATOR,
                         OffsetTime.of(LocalTime.of(13, 0, 0, 12_300_000), 
ZoneOffset.UTC)));
         assertEquals(
-                "13:00:00.3Z",
+                "04:51:52.3Z",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
-                        OffsetTime.of(LocalTime.of(13, 0, 0, 300_000_000), 
ZoneOffset.UTC)));
+                        ISO_TIME_CONFIGURATOR,
+                        OffsetTime.of(LocalTime.of(4, 51, 52, 300_000_000), 
ZoneOffset.UTC)));
     }
 
     @Test
@@ -79,18 +92,18 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
         assertEquals(
                 "13:01:02",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
+                        ISO_TIME_CONFIGURATOR,
                         LocalTime.of(13, 1, 2)));
         assertEquals(
                 "13:00:00.0123",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
+                        ISO_TIME_CONFIGURATOR,
                         LocalTime.of(13, 0, 0, 12_300_000)));
         assertEquals(
-                "13:00:00.3",
+                "04:51:52.3",
                 formatTemporal(
-                        ISO_TIME_CONFIGURER,
-                        LocalTime.of(13, 0, 0, 300_000_000)));
+                        ISO_TIME_CONFIGURATOR,
+                        LocalTime.of(4, 51, 52, 300_000_000)));
     }
 
     @Test
@@ -98,18 +111,18 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
         assertEquals(
                 "2021-12-11T13:01:02",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         LocalDateTime.of(2021, 12, 11, 13, 1, 2, 0)));
         assertEquals(
                 "2021-12-11T13:01:02.0123",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         LocalDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000)));
         assertEquals(
-                "2021-12-11T13:01:02.3",
+                "2021-02-03T04:51:52.3",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
-                        LocalDateTime.of(2021, 12, 11, 13, 1, 2, 
300_000_000)));
+                        ISO_DATE_TIME_CONFIGURATOR,
+                        LocalDateTime.of(2021, 2, 3, 4, 51, 52, 300_000_000)));
     }
 
     @Test
@@ -117,28 +130,28 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
         assertEquals(
                 "2021-12-11T13:01:02Z",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.UTC)));
         assertEquals(
                 "2021-12-11T13:01:02+01:00",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.ofHours(1))));
         assertEquals(
                 "2021-12-11T13:01:02-02:30",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.ofHoursMinutesSeconds(-2, -30, 0))));
         assertEquals(
                 "2021-12-11T13:01:02.0123Z",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000, 
ZoneOffset.UTC)));
         assertEquals(
-                "2021-12-11T13:01:02.3Z",
+                "2021-02-03T04:51:52.3Z",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
-                        OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 300_000_000, 
ZoneOffset.UTC)));
+                        ISO_DATE_TIME_CONFIGURATOR,
+                        OffsetDateTime.of(2021, 2, 3, 4, 51, 52, 300_000_000, 
ZoneOffset.UTC)));
     }
 
     @Test
@@ -146,34 +159,68 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
         assertEquals(
                 "2021-12-11T13:01:02Z",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         ZonedDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.UTC)));
         ZoneId zoneId = ZoneId.of("America/New_York");
         assertEquals(
                 "2021-12-11T13:01:02-05:00",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         ZonedDateTime.of(2021, 12, 11, 13, 1, 2, 0, zoneId)));
         assertEquals(
                 "2021-07-11T13:01:02-04:00",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         ZonedDateTime.of(2021, 7, 11, 13, 1, 2, 0, zoneId)));
         assertEquals(
                 "2021-12-11T13:01:02-02:30",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.ofHoursMinutesSeconds(-2, -30, 0))));
         assertEquals(
                 "2021-12-11T13:01:02.0123Z",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
+                        ISO_DATE_TIME_CONFIGURATOR,
                         OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000, 
ZoneOffset.UTC)));
         assertEquals(
-                "2021-12-11T13:01:02.3Z",
+                "2021-02-03T04:51:52.3Z",
+                formatTemporal(
+                        ISO_DATE_TIME_CONFIGURATOR,
+                        OffsetDateTime.of(2021, 2, 3, 4, 51, 52, 300_000_000, 
ZoneOffset.UTC)));
+    }
+
+    @Test
+    public void testFormatInstant() throws TemplateException, IOException {
+        assertEquals(
+                "2021-12-11T13:01:02Z",
+                formatTemporal(
+                        isoDateTimeConfigurator(DateUtil.UTC),
+                        OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.UTC)
+                                .toInstant()));
+        assertEquals(
+                "2021-12-11T13:01:02+01:00",
+                formatTemporal(
+                        
isoDateTimeConfigurator(TimeZone.getTimeZone("GMT+01")),
+                        OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.ofHours(1))
+                                .toInstant()));
+        assertEquals(
+                "2021-12-11T13:01:02-02:30",
+                formatTemporal(
+                        
isoDateTimeConfigurator(TimeZone.getTimeZone("GMT-02:30")),
+                        OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 0, 
ZoneOffset.ofHoursMinutesSeconds(-2, -30, 0))
+                                .toInstant()));
+        assertEquals(
+                "2021-12-11T13:01:02.0123Z",
+                formatTemporal(
+                        isoDateTimeConfigurator(DateUtil.UTC),
+                        OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000, 
ZoneOffset.UTC)
+                                .toInstant()));
+        assertEquals(
+                "2021-02-03T04:51:52.3Z",
                 formatTemporal(
-                        ISO_DATE_TIME_CONFIGURER,
-                        OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 300_000_000, 
ZoneOffset.UTC)));
+                        isoDateTimeConfigurator(DateUtil.UTC),
+                        OffsetDateTime.of(2021, 2, 3, 4, 51, 52, 300_000_000, 
ZoneOffset.UTC)
+                                .toInstant()));
     }
 
     @Test
@@ -181,22 +228,78 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
         assertEquals(
                 "2021-12-11",
                 formatTemporal(
-                        ISO_DATE_CONFIGURER,
+                        ISO_DATE_CONFIGURATOR,
                         LocalDate.of(2021, 12, 11)));
     }
 
     @Test
+    public void testFormatYearMonth() throws TemplateException, IOException {
+        assertEquals(
+                "2021-12",
+                formatTemporal(
+                        ISO_YEAR_MONTH_CONFIGURATOR,
+                        YearMonth.of(2021, 12)));
+        assertEquals(
+                "1995-01",
+                formatTemporal(
+                        ISO_YEAR_MONTH_CONFIGURATOR,
+                        YearMonth.of(1995, 1)));
+    }
+
+    @Test
+    public void testFormatYear() throws TemplateException, IOException {
+        assertEquals(
+                "2021",
+                formatTemporal(
+                        ISO_YEAR_CONFIGURATOR,
+                        Year.of(2021)));
+        assertEquals(
+                "1995",
+                formatTemporal(
+                        ISO_YEAR_CONFIGURATOR,
+                        Year.of(1995)));
+    }
+
+    @Test
     public void testParseOffsetDateTime() throws TemplateException, 
TemplateValueFormatException {
+        testParseOffsetDateTimeAndInstant(OffsetDateTime.class);
+    }
+
+    @Test
+    public void testParseInstant() throws TemplateException, 
TemplateValueFormatException {
+        testParseOffsetDateTimeAndInstant(Instant.class);
+    }
+
+    @Test
+    public void testParseZonedDateTime() throws TemplateException, 
TemplateValueFormatException {
+        testParseOffsetDateTimeAndInstant(ZonedDateTime.class);
+    }
+
+    private Temporal convertToClass(OffsetDateTime offsetDateTime, Class<? 
extends Temporal> temporalClass) {
+        if (temporalClass == OffsetDateTime.class) {
+            return offsetDateTime;
+        }
+        if (temporalClass == Instant.class) {
+            return offsetDateTime.toInstant();
+        }
+        if (temporalClass == ZonedDateTime.class) {
+            return offsetDateTime.toZonedDateTime();
+        }
+        throw new IllegalArgumentException();
+    }
+
+    private void testParseOffsetDateTimeAndInstant(Class<? extends Temporal> 
temporalClass)
+            throws TemplateException, TemplateValueFormatException {
         // ISO extended and ISO basic format:
-        for (String s : new String[]{"2021-12-11T13:01:02.0123Z", 
"20211211T130102.0123Z"}) {
+        for (String parsedString : new String[]{"2021-12-11T13:01:02.0123Z", 
"20211211T130102.0123Z"}) {
             assertParsingResults(
-                    ISO_DATE_TIME_CONFIGURER,
-                    s,
+                    ISO_DATE_TIME_CONFIGURATOR,
+                    parsedString,
                     OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000, 
ZoneOffset.UTC)) ;
         }
 
         // Optional parts:
-        for (String s : new String[] {
+        for (String parsedString : new String[] {
                 "2021-12-11T13:00:00.0+02:00",
                 "2021-12-11T13:00:00+02:00",
                 "2021-12-11T13:00+02",
@@ -207,30 +310,83 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
                 "20211211T13+02",
         }) {
             assertParsingResults(
-                    ISO_DATE_TIME_CONFIGURER,
-                    s,
-                    OffsetDateTime.of(2021, 12, 11, 13, 0, 0, 0, 
ZoneOffset.ofHours(2)));
+                    ISO_DATE_TIME_CONFIGURATOR,
+                    parsedString,
+                    convertToClass(
+                            OffsetDateTime.of(2021, 12, 11, 13, 0, 0, 0, 
ZoneOffset.ofHours(2)),
+                            temporalClass));
         }
 
-        // TODO Zone default
+        // Negative year:
+        for (String parsedString : new String[] {
+                "-1000-02-03T04Z",
+                "-10000203T04Z"
+        }) {
+            assertParsingResults(
+                    ISO_DATE_TIME_CONFIGURATOR,
+                    parsedString,
+                    convertToClass(
+                            OffsetDateTime.of(-1000, 2, 3, 4, 0, 0, 0, 
ZoneOffset.UTC),
+                            temporalClass));
+        }
 
-        try {
+        // Hour 24:
+        for (String parsedString : new String[] {
+                "2020-01-02T24Z",
+                "2020-01-02T24:00Z",
+                "2020-01-02T24:00:00Z",
+                "2020-01-02T24:00:00.0Z",
+                "2020-01-02T24:00:00.0+00",
+                // For local temporals only: "2020-01-02T24:00:00",
+                "20200102T24Z",
+                "20200102T2400Z",
+                "20200102T240000Z",
+                "20200102T240000.0Z",
+                "20200102T240000.0+00",
+                // For local temporals only: "20200102T240000"
+        }) {
             assertParsingResults(
-                    ISO_DATE_TIME_CONFIGURER,
-                    "2021-12-11",
-                    OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000, 
ZoneOffset.UTC));
-            fail("OffsetDateTime parsing should have failed");
-        } catch (UnparsableValueException e) {
-            assertThat(e.getMessage(), allOf(
-                    containsString("\"2021-12-11\""),
-                    containsString("OffsetDateTime"),
-                    containsString("\"T\"")
-            ));
+                    ISO_DATE_TIME_CONFIGURATOR,
+                    parsedString,
+                    convertToClass(
+                            OffsetDateTime.of(2020, 1, 3, 0, 0, 0, 0, 
ZoneOffset.UTC),
+                            temporalClass));
+        }
+
+        // Unlike for the Java format, for ISO we require the string to 
contain the offset for a non-local target type.
+        for (String parsedString : new String[] {
+                "2020-01-02T03", "2020-01-02T03:00", "2020-01-02T03:00:00",
+                "20200102T03", "20200102T0300", "20200102T030000"}) {
+            assertParsingFails(
+                    ISO_DATE_TIME_CONFIGURATOR,
+                    parsedString,
+                    temporalClass,
+                    e -> assertThat(e.getMessage(), allOf(
+                                containsString(jQuote(parsedString)),
+                                containsString("time zone offset"),
+                                
containsString(temporalClass.getSimpleName()))));
+        }
+
+        for (String parsedString : new String[] {
+                "2021-12-11", "20211211", "2021-12-11T", "2021-12-11T0Z",
+                "2021-12-11T25Z", "2022-02-29T23Z", "2021-13-11T23Z"}) {
+            assertParsingFails(
+                    ISO_DATE_TIME_CONFIGURATOR,
+                    parsedString,
+                    temporalClass,
+                    e -> {
+                        assertThat(e.getMessage(), allOf(
+                                containsString(jQuote(parsedString)),
+                                
containsString(temporalClass.getSimpleName())));
+                        if (!parsedString.contains("T")) {
+                            assertThat(e.getMessage(), 
containsString("\"T\""));
+                        }
+                    });
         }
     }
 
     @Test
-    public void testParseZonedDateTime() throws TemplateException, 
TemplateValueFormatException {
+    public void testParseOffsetTime() throws TemplateException, 
TemplateValueFormatException {
         // TODO [FREEMARKER-35]
     }
 
@@ -240,23 +396,57 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
     }
 
     @Test
-    public void testParseInstance() throws TemplateException, 
TemplateValueFormatException {
+    public void testParseLocalDate() throws TemplateException, 
TemplateValueFormatException {
         // TODO [FREEMARKER-35]
     }
 
     @Test
-    public void testParseLocalDate() throws TemplateException, 
TemplateValueFormatException {
+    public void testParseLocalTime() throws TemplateException, 
TemplateValueFormatException {
         // TODO [FREEMARKER-35]
     }
 
     @Test
-    public void testParseOffsetTime() throws TemplateException, 
TemplateValueFormatException {
-        // TODO [FREEMARKER-35]
+    public void testParseYear() throws TemplateException, 
TemplateValueFormatException {
+        assertParsingResults(ISO_YEAR_CONFIGURATOR, "2021", Year.of(2021));
+        assertParsingResults(ISO_YEAR_CONFIGURATOR, "1995", Year.of(1995));
+        assertParsingResults(ISO_YEAR_CONFIGURATOR, "95", Year.of(95));
+        assertParsingResults(ISO_YEAR_CONFIGURATOR, "-1000", Year.of(-1000));
+
+        assertParsingFails(
+                ISO_DATE_TIME_CONFIGURATOR,
+                "2021-01",
+                Year.class,
+                e -> {
+                    assertThat(e.getMessage(), allOf(
+                            containsString(jQuote("2021-01")),
+                            containsString("Year")));
+                }
+        );
     }
 
     @Test
-    public void testParseLocalTime() throws TemplateException, 
TemplateValueFormatException {
-        // TODO [FREEMARKER-35]
+    public void testParseYearMonth() throws TemplateException, 
TemplateValueFormatException {
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "2021-01", 
YearMonth.of(2021, 1));
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "202101", 
YearMonth.of(2021, 1));
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "1995-12", 
YearMonth.of(1995, 12));
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "199512", 
YearMonth.of(1995, 12));
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "95-12", 
YearMonth.of(95, 12));
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "9512", 
YearMonth.of(95, 12));
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "-1000-01", 
YearMonth.of(-1000, 1));
+        assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "-100001", 
YearMonth.of(-1000, 1));
+
+        for (String parsedString : new String[] {"2021", "2021-12-11", 
"2021-13", "202113"}) {
+            assertParsingFails(
+                    ISO_YEAR_MONTH_CONFIGURATOR,
+                    parsedString,
+                    YearMonth.class,
+                    e -> {
+                        assertThat(e.getMessage(), allOf(
+                                containsString(jQuote(parsedString)),
+                                containsString("YearMonth")));
+                    }
+            );
+        }
     }
 
     @Test
@@ -282,14 +472,14 @@ public class TemporalFormatWithIsoFormatTest extends 
AbstractTemporalFormatTest
                                 .parse(stringWithYearOfEra, LocalDate::from));
             }
 
-            String output = formatTemporal(ISO_DATE_CONFIGURER, localDate);
+            String output = formatTemporal(ISO_DATE_CONFIGURATOR, localDate);
             assertEquals(iso8601String, output);
-            assertParsingResults(ISO_DATE_CONFIGURER, iso8601String, 
localDate);
+            assertParsingResults(ISO_DATE_CONFIGURATOR, iso8601String, 
localDate);
         }
     }
 
     @Test
-    public void testParseLocaleHasNoEffect() throws TemplateException, 
TemplateValueFormatException {
+    public void testLocaleHasNoEffect() throws TemplateException, 
TemplateValueFormatException {
         for (Locale locale : new Locale[] {
                 Locale.CHINA,
                 Locale.FRANCE,
diff --git 
a/src/test/java/freemarker/core/TemporalFormatWithJavaFormatTest.java 
b/src/test/java/freemarker/core/TemporalFormatWithJavaFormatTest.java
index 0107341..56fe2ab 100644
--- a/src/test/java/freemarker/core/TemporalFormatWithJavaFormatTest.java
+++ b/src/test/java/freemarker/core/TemporalFormatWithJavaFormatTest.java
@@ -195,10 +195,15 @@ public class TemporalFormatWithJavaFormatTest extends 
AbstractTemporalFormatTest
         TimeZone cfgTimeZone = TimeZone.getTimeZone(cfgZoneId);
 
         for (boolean winter : new boolean[] {true, false}) {
-            String stringToParse = winter ? "2020-12-10 13:14" : "2020-07-10 
13:14";
-            LocalDateTime localDateTime = winter
-                    ? LocalDateTime.of(2020, 12, 10, 13, 14)
-                    : LocalDateTime.of(2020, 07, 10, 13, 14);
+            final String stringToParse;
+            final LocalDateTime localDateTime;
+            if (winter) {
+                stringToParse = "2020-12-10 13:14";
+                localDateTime = LocalDateTime.of(2020, 12, 10, 13, 14);
+            } else {
+                stringToParse = "2020-07-10 13:14";
+                localDateTime = LocalDateTime.of(2020, 07, 10, 13, 14);
+            }
 
             {
                 ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, 
cfgZoneId);

Reply via email to