http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java
new file mode 100644
index 0000000..091d55c
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Used when creating {@link TemplateDateFormat}-s and {@link 
TemplateNumberFormat}-s to indicate that the format
+ * string (like the value of the {@code dateFormat} setting) is malformed.
+ * 
+ * @since 2.3.24
+ */
+public abstract class InvalidFormatStringException extends 
TemplateValueFormatException {
+    
+    public InvalidFormatStringException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidFormatStringException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java
new file mode 100644
index 0000000..8700c88
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java
@@ -0,0 +1,40 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core.valueformat.TemplateValueFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Thrown when the {@link TemplateValueFormat} doesn't support parsing, and 
parsing was invoked.
+ * 
+ * @since 2.3.24
+ */
+public class ParsingNotSupportedException extends TemplateValueFormatException 
{
+
+    public ParsingNotSupportedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ParsingNotSupportedException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java
new file mode 100644
index 0000000..0154b67
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java
@@ -0,0 +1,110 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.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.
+ * 
+ * <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 TemplateDateFormat extends TemplateValueFormat {
+    
+    /**
+     * @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}.
+     * 
+     * @throws TemplateValueFormatException
+     *             When a problem occurs during the formatting of the value. 
Notable subclass:
+     *             {@link UnknownDateTypeFormattingUnsupportedException}
+     * @throws TemplateModelException
+     *             Exception thrown by the {@code dateModel} object when 
calling its methods.
+     */
+    public abstract String formatToPlainText(TemplateDateModel dateModel)
+            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
+     * {@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}.
+     */
+    public Object format(TemplateDateModel dateModel) throws 
TemplateValueFormatException, TemplateModelException {
+        return formatToPlainText(dateModel);
+    }
+
+    /**
+     * Parsers a string to date/time/datetime, according to this format. Some 
format implementations may throw
+     * {@link ParsingNotSupportedException} here.
+     * 
+     * @param s
+     *            The string to parse
+     * @param dateType
+     *            The expected date type of the result. Not all {@link 
TemplateDateFormat}-s will care about this;
+     *            though those who return a {@link TemplateDateModel} instead 
of {@link Date} often will. When strings
+     *            are parsed via {@code ?date}, {@code ?time}, or {@code 
?datetime}, then this parameter is
+     *            {@link TemplateDateModel#DATE}, {@link 
TemplateDateModel#TIME}, or {@link TemplateDateModel#DATETIME},
+     *            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 thats 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.
+     */
+    public abstract Object parse(String s, int dateType) throws 
TemplateValueFormatException;
+    
+    /**
+     * Tells if this formatter should be re-created if the locale changes.
+     */
+    public abstract boolean isLocaleBound();
+
+    /**
+     * Tells if this formatter should be re-created if the time zone changes. 
Currently always {@code true}.
+     */
+    public abstract boolean isTimeZoneBound();
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java
new file mode 100644
index 0000000..2abbaa9
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java
@@ -0,0 +1,92 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.*;
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Factory for a certain kind of date/time/dateTime formatting ({@link 
TemplateDateFormat}). Usually a singleton
+ * (one-per-VM or one-per-{@link Configuration}), and so must be thread-safe.
+ * 
+ * @see Configurable#setCustomDateFormats(java.util.Map)
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateDateFormatFactory extends 
TemplateValueFormatFactory {
+    
+    /**
+     * Returns a formatter for the given parameters.
+     * 
+     * <p>
+     * The returned formatter can be a new instance or a reused (cached) 
instance. Note that {@link Environment} itself
+     * caches the returned instances, though that cache is lost with the 
{@link Environment} (i.e., when the top-level
+     * template execution ends), also it might flushes lot of entries if the 
locale or time zone is changed during
+     * template execution. So caching on the factory level is still useful, 
unless creating the formatters is
+     * sufficiently cheap.
+     * 
+     * @param params
+     *            The string that further describes how the format should 
look. For example, when the
+     *            {@link Configurable#getDateFormat() dateFormat} is {@code 
"@fooBar 1, 2"}, then it will be
+     *            {@code "1, 2"} (and {@code "@fooBar"} selects the factory). 
The format of this string is up to the
+     *            {@link TemplateDateFormatFactory} implementation. Not {@code 
null}, often an empty string.
+     * @param dateType
+     *            {@link TemplateDateModel#DATE}, {@link 
TemplateDateModel#TIME}, {@link TemplateDateModel#DATETIME} or
+     *            {@link TemplateDateModel#UNKNOWN}. Supporting {@link 
TemplateDateModel#UNKNOWN} is not necessary, in
+     *            which case the method should throw an {@link 
UnknownDateTypeFormattingUnsupportedException} exception.
+     * @param locale
+     *            The locale to format for. Not {@code null}. The resulting 
format should be bound to this locale
+     *            forever (i.e. locale changes in the {@link Environment} must 
not be followed).
+     * @param timeZone
+     *            The time zone to format for. Not {@code null}. The resulting 
format must be bound to this time zone
+     *            forever (i.e. time zone changes in the {@link Environment} 
must not be followed).
+     * @param zonelessInput
+     *            Indicates that the input Java {@link Date} is not from a 
time zone aware source. When this is
+     *            {@code true}, the formatters shouldn't override the time 
zone provided to its constructor (most
+     *            formatters don't do that anyway), and it shouldn't show the 
time zone, if it can hide it (like a
+     *            {@link SimpleDateFormat} pattern-based formatter may can't 
do that, as the pattern prescribes what to
+     *            show).
+     *            <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
+     *            configuration settings and such, so you shouldn't rely on 
this rule, just accept what this parameter
+     *            says.
+     * @param env
+     *            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)}.
+     * 
+     * @throws TemplateValueFormatException
+     *             If any problem occurs while parsing/getting the format. 
Notable subclasses:
+     *             {@link InvalidFormatParametersException} if {@code params} 
is malformed;
+     *             {@link UnknownDateTypeFormattingUnsupportedException} if 
{@code dateType} is
+     *             {@link TemplateDateModel#UNKNOWN} and that's unsupported by 
this factory.
+     */
+    public abstract TemplateDateFormat get(
+            String params,
+            int dateType, Locale locale, TimeZone timeZone, boolean 
zonelessInput,
+            Environment env)
+                    throws TemplateValueFormatException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
new file mode 100644
index 0000000..0cff2fe
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core._EvalUtil;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+import java.util.Date;
+
+/**
+ * Utility classes for implementing {@link TemplateValueFormat}-s.
+ * 
+ * @since 2.3.24 
+ */
+public final class TemplateFormatUtil {
+    
+    private TemplateFormatUtil() {
+        // Not meant to be instantiated
+    }
+
+    public static void checkHasNoParameters(String params) throws 
InvalidFormatParametersException
+             {
+        if (params.length() != 0) {
+            throw new InvalidFormatParametersException(
+                    "This number format doesn't support any parameters.");
+        }
+    }
+
+    /**
+     * Utility method to extract the {@link Number} from an {@link 
TemplateNumberModel}, and throws
+     * {@link TemplateModelException} with a standard error message if that's 
{@code null}. {@link TemplateNumberModel}
+     * that store {@code null} are in principle not allowed, and so are 
considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
+     */
+    public static Number getNonNullNumber(TemplateNumberModel numberModel)
+            throws TemplateModelException, UnformattableValueException {
+        Number number = numberModel.getAsNumber();
+        if (number == null) {
+            throw _EvalUtil.newModelHasStoredNullException(Number.class, 
numberModel, null);
+        }
+        return number;
+    }
+
+    /**
+     * Utility method to extract the {@link Date} from an {@link 
TemplateDateModel}, and throw
+     * {@link TemplateModelException} with a standard error message if that's 
{@code null}. {@link TemplateDateModel}
+     * that store {@code null} are in principle not allowed, and so are 
considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
+     */
+    public static Date getNonNullDate(TemplateDateModel dateModel) throws 
TemplateModelException {
+        Date date = dateModel.getAsDate();
+        if (date == null) {
+            throw _EvalUtil.newModelHasStoredNullException(Date.class, 
dateModel, null);
+        }
+        return date;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
new file mode 100644
index 0000000..44068e4
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
@@ -0,0 +1,93 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import java.text.NumberFormat;
+
+import org.apache.freemarker.core.ArithmeticEngine;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.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.
+ * 
+ * <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 {
+
+    /**
+     * @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}.
+     * 
+     * @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.
+     */
+    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
+     * {@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}.
+     */
+    public Object format(TemplateNumberModel numberModel)
+            throws TemplateValueFormatException, TemplateModelException {
+        return formatToPlainText(numberModel);
+    }
+    
+    /**
+     * Tells if this formatter should be re-created if the locale changes.
+     */
+    public abstract boolean isLocaleBound();
+
+    /**
+     * 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)}.
+     */
+    public final Object parse(String s) throws TemplateValueFormatException {
+        throw new ParsingNotSupportedException("Number formats currenly don't 
support parsing");
+    }
+    
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
new file mode 100644
index 0000000..e2ae8c2
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
@@ -0,0 +1,66 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core.Configurable;
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+
+import java.util.Locale;
+
+/**
+ * Factory for a certain kind of number formatting ({@link 
TemplateNumberFormat}). Usually a singleton (one-per-VM or
+ * one-per-{@link Configuration}), and so must be thread-safe.
+ * 
+ * @see Configurable#setCustomNumberFormats(java.util.Map)
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateNumberFormatFactory extends 
TemplateValueFormatFactory {
+
+    /**
+     * Returns a formatter for the given parameters.
+     * 
+     * <p>
+     * The returned formatter can be a new instance or a reused (cached) 
instance. Note that {@link Environment} itself
+     * caches the returned instances, though that cache is lost with the 
{@link Environment} (i.e., when the top-level
+     * template execution ends), also it might flushes lot of entries if the 
locale or time zone is changed during
+     * template execution. So caching on the factory level is still useful, 
unless creating the formatters is
+     * sufficiently cheap.
+     * 
+     * @param params
+     *            The string that further describes how the format should 
look. For example, when the
+     *            {@link Configurable#getNumberFormat() numberFormat} is 
{@code "@fooBar 1, 2"}, then it will be
+     *            {@code "1, 2"} (and {@code "@fooBar"} selects the factory). 
The format of this string is up to the
+     *            {@link TemplateNumberFormatFactory} implementation. Not 
{@code null}, often an empty string.
+     * @param locale
+     *            The locale to format for. Not {@code null}. The resulting 
format must be bound to this locale
+     *            forever (i.e. locale changes in the {@link Environment} must 
not be followed).
+     * @param env
+     *            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)}.
+     *            
+     * @throws TemplateValueFormatException
+     *             if any problem occurs while parsing/getting the format. 
Notable subclasses:
+     *             {@link InvalidFormatParametersException} if the {@code 
params} is malformed.
+     */
+    public abstract TemplateNumberFormat get(String params, Locale locale, 
Environment env)
+            throws TemplateValueFormatException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
new file mode 100644
index 0000000..ffc61b2
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
@@ -0,0 +1,33 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+/**
+ * Superclass of all value format objects; objects that convert values to 
strings, or parse strings.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormat {
+
+    /**
+     * Meant to be used in error messages to tell what format the parsed 
string didn't fit.
+     */
+    public abstract String getDescription();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
new file mode 100644
index 0000000..a51abdc
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+/**
+ * Error while getting, creating or applying {@link TemplateValueFormat}-s 
(including its subclasses, like
+ * {@link TemplateNumberFormat}).
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormatException extends Exception {
+
+    public TemplateValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public TemplateValueFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
new file mode 100644
index 0000000..c6b77b8
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
@@ -0,0 +1,28 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+/**
+ * Superclass of all format factories.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormatFactory {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
new file mode 100644
index 0000000..7f380b4
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
@@ -0,0 +1,34 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+/**
+ * @since 2.3.24
+ */
+public class UndefinedCustomFormatException extends 
InvalidFormatStringException {
+
+    public UndefinedCustomFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UndefinedCustomFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
new file mode 100644
index 0000000..ebece36
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Thrown when a {@link TemplateModel} can't be formatted because of the 
value/properties of it are outside of that the
+ * {@link TemplateValueFormat} supports. For example, a formatter may not 
support dates before year 1, or can't format
+ * NaN. The most often used subclass is {@link 
UnknownDateTypeFormattingUnsupportedException}.
+ * 
+ * @since 2.3.24
+ */
+public class UnformattableValueException extends TemplateValueFormatException {
+
+    public UnformattableValueException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnformattableValueException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
new file mode 100644
index 0000000..e9c35b9
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
@@ -0,0 +1,36 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Thrown when a {@link TemplateDateModel} can't be formatted because its type 
is {@link TemplateDateModel#UNKNOWN}.
+ * 
+ * @since 2.3.24
+ */
+public final class UnknownDateTypeFormattingUnsupportedException extends 
UnformattableValueException {
+
+    public UnknownDateTypeFormattingUnsupportedException() {
+        super("Can't format the date-like value because it isn't "
+                + "known if it's desired result should be a date (no time 
part), a time, or a date-time value.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
new file mode 100644
index 0000000..802e2d1
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Thrown when a string can't be parsed to {@link TemplateDateModel}, because 
the provided target type is
+ * {@link TemplateDateModel#UNKNOWN}.
+ * 
+ * @since 2.3.24
+ */
+public final class UnknownDateTypeParsingUnsupportedException extends 
UnformattableValueException {
+
+    public UnknownDateTypeParsingUnsupportedException() {
+        super("Can't parse the string to date-like value because it isn't "
+                + "known if it's desired result should be a date (no time 
part), a time, or a date-time value.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
new file mode 100644
index 0000000..8dc6ab1
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat;
+
+import org.apache.freemarker.core.valueformat.TemplateValueFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Thrown when the content of the string that should be parsed by the {@link 
TemplateValueFormat} doesn't match what the
+ * format expects.
+ * 
+ * @since 2.3.24
+ */
+public class UnparsableValueException extends TemplateValueFormatException {
+
+    public UnparsableValueException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnparsableValueException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
new file mode 100644
index 0000000..631af5d
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
@@ -0,0 +1,38 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat.impl;
+
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Can't create a template format that the template format refers to 
(typically thrown by alias template formats).
+ * 
+ * @since 2.3.24
+ */
+class AliasTargetTemplateValueFormatException extends 
TemplateValueFormatException {
+
+    public AliasTargetTemplateValueFormatException(String message, Throwable 
cause) {
+        super(message, cause);
+    }
+
+    public AliasTargetTemplateValueFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
new file mode 100644
index 0000000..aefb252
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat.impl;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.util._LocaleUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * 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.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateDateFormatFactory extends 
TemplateDateFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to.
+     */
+    public AliasTemplateDateFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's 
no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @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
+     *            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.
+     */
+    public AliasTemplateDateFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> 
localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, 
TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws 
TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = 
localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = 
_LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = 
localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateDateFormat(targetFormatString, dateType, 
locale, timeZone, zonelessInput);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to 
create format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + 
e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..6e72a61
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
@@ -0,0 +1,96 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat.impl;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.util._LocaleUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * 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.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateNumberFormatFactory extends 
TemplateNumberFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to
+     */
+    public AliasTemplateNumberFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's 
no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @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
+     *            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.
+     */
+    public AliasTemplateNumberFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> 
localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment 
env)
+            throws TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = 
localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = 
_LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = 
localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateNumberFormat(targetFormatString, locale);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to 
create format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + 
e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
new file mode 100644
index 0000000..2a51ef8
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
@@ -0,0 +1,530 @@
+/*
+ * 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 org.apache.freemarker.core.valueformat.impl;
+
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Parses a {@link DecimalFormat} pattern string to a {@link DecimalFormat} 
instance, with the pattern string extensions
+ * described in the Manual (see "Extended Java decimal format"). The result is 
a standard {@link DecimalFormat} object,
+ * but further configured according the extension part.
+ */
+class ExtendedDecimalFormatParser {
+    
+    private static final String PARAM_ROUNDING_MODE = "roundingMode";
+    private static final String PARAM_MULTIPIER = "multipier";
+    private static final String PARAM_DECIMAL_SEPARATOR = "decimalSeparator";
+    private static final String PARAM_MONETARY_DECIMAL_SEPARATOR = 
"monetaryDecimalSeparator";
+    private static final String PARAM_GROUP_SEPARATOR = "groupingSeparator";
+    private static final String PARAM_EXPONENT_SEPARATOR = "exponentSeparator";
+    private static final String PARAM_MINUS_SIGN = "minusSign";
+    private static final String PARAM_INFINITY = "infinity";
+    private static final String PARAM_NAN = "nan";
+    private static final String PARAM_PERCENT = "percent";
+    private static final String PARAM_PER_MILL = "perMill";
+    private static final String PARAM_ZERO_DIGIT = "zeroDigit";
+    private static final String PARAM_CURRENCY_CODE = "currencyCode";
+    private static final String PARAM_CURRENCY_SYMBOL = "currencySymbol";
+
+    private static final String PARAM_VALUE_RND_UP = "up";
+    private static final String PARAM_VALUE_RND_DOWN = "down";
+    private static final String PARAM_VALUE_RND_CEILING = "ceiling";
+    private static final String PARAM_VALUE_RND_FLOOR = "floor";
+    private static final String PARAM_VALUE_RND_HALF_DOWN = "halfDown";
+    private static final String PARAM_VALUE_RND_HALF_EVEN = "halfEven";
+    private static final String PARAM_VALUE_RND_HALF_UP = "halfUp";
+    private static final String PARAM_VALUE_RND_UNNECESSARY = "unnecessary";
+    
+    private static final HashMap<String, ? extends ParameterHandler> 
PARAM_HANDLERS;
+    static {
+        HashMap<String, ParameterHandler> m = new HashMap<>();
+        m.put(PARAM_ROUNDING_MODE, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                RoundingMode parsedValue;
+                if (value.equals(PARAM_VALUE_RND_UP)) {
+                    parsedValue = RoundingMode.UP;
+                } else if (value.equals(PARAM_VALUE_RND_DOWN)) {
+                    parsedValue = RoundingMode.DOWN;
+                } else if (value.equals(PARAM_VALUE_RND_CEILING)) {
+                    parsedValue = RoundingMode.CEILING;
+                } else if (value.equals(PARAM_VALUE_RND_FLOOR)) {
+                    parsedValue = RoundingMode.FLOOR;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_DOWN)) {
+                    parsedValue = RoundingMode.HALF_DOWN;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_EVEN)) {
+                    parsedValue = RoundingMode.HALF_EVEN;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_UP)) {
+                    parsedValue = RoundingMode.HALF_UP;
+                } else if (value.equals(PARAM_VALUE_RND_UNNECESSARY)) {
+                    parsedValue = RoundingMode.UNNECESSARY;
+                } else {
+                    throw new InvalidParameterValueException("Should be one 
of: u, d, c, f, hd, he, hu, un");
+                }
+
+                parser.roundingMode = parsedValue;
+            }
+        });
+        m.put(PARAM_MULTIPIER, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                try {
+                    parser.multipier = Integer.valueOf(value);
+                } catch (NumberFormatException e) {
+                    throw new InvalidParameterValueException("Malformed 
integer.");
+                }
+            }
+        });
+        m.put(PARAM_DECIMAL_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain 
exactly 1 character.");
+                }
+                parser.symbols.setDecimalSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_MONETARY_DECIMAL_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain 
exactly 1 character.");
+                }
+                parser.symbols.setMonetaryDecimalSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_GROUP_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain 
exactly 1 character.");
+                }
+                parser.symbols.setGroupingSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setExponentSeparator(value);
+            }
+        });
+        m.put(PARAM_MINUS_SIGN, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain 
exactly 1 character.");
+                }
+                parser.symbols.setMinusSign(value.charAt(0));
+            }
+        });
+        m.put(PARAM_INFINITY, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setInfinity(value);
+            }
+        });
+        m.put(PARAM_NAN, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setNaN(value);
+            }
+        });
+        m.put(PARAM_PERCENT, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain 
exactly 1 character.");
+                }
+                parser.symbols.setPercent(value.charAt(0));
+            }
+        });
+        m.put(PARAM_PER_MILL, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain 
exactly 1 character.");
+                }
+                parser.symbols.setPerMill(value.charAt(0));
+            }
+        });
+        m.put(PARAM_ZERO_DIGIT, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain 
exactly 1 character.");
+                }
+                parser.symbols.setZeroDigit(value.charAt(0));
+            }
+        });
+        m.put(PARAM_CURRENCY_CODE, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String 
value)
+                    throws InvalidParameterValueException {
+                Currency currency;
+                try {
+                    currency = Currency.getInstance(value);
+                } catch (IllegalArgumentException e) {
+                    throw new InvalidParameterValueException("Not a known ISO 
4217 code.");
+                }
+                parser.symbols.setCurrency(currency);
+            }
+        });
+        PARAM_HANDLERS = m;
+    }
+
+    private static final String SNIP_MARK = "[...]";
+    private static final int MAX_QUOTATION_LENGTH = 10; // Must be more than 
SNIP_MARK.length!
+
+    private final String src;
+    private int pos = 0;
+
+    private final DecimalFormatSymbols symbols;
+    private RoundingMode roundingMode;
+    private Integer multipier;
+
+    static DecimalFormat parse(String formatString, Locale locale) throws 
ParseException {
+        return new ExtendedDecimalFormatParser(formatString, locale).parse();
+    }
+
+    private DecimalFormat parse() throws ParseException {
+        String stdPattern = fetchStandardPattern();
+        skipWS();
+        parseFormatStringExtension();
+
+        DecimalFormat decimalFormat;
+        try {
+            decimalFormat = new DecimalFormat(stdPattern, symbols);
+        } catch (IllegalArgumentException e) {
+            ParseException pe = new ParseException(e.getMessage(), 0);
+            if (e.getCause() != null) {
+                try {
+                    e.initCause(e.getCause());
+                } catch (Exception e2) {
+                    // Supress
+                }
+            }
+            throw pe;
+        }
+
+        if (roundingMode != null) {
+            decimalFormat.setRoundingMode(roundingMode);
+        }
+
+        if (multipier != null) {
+            decimalFormat.setMultiplier(multipier.intValue());
+        }
+
+        return decimalFormat;
+    }
+
+    private void parseFormatStringExtension() throws ParseException {
+        int ln = src.length();
+
+        if (pos == ln) {
+            return;
+        }
+
+        String currencySymbol = null;  // Exceptional, as must be applied 
after "currency code"
+        fetchParamters: do {
+            int namePos = pos;
+            String name = fetchName();
+            if (name == null) {
+                throw newExpectedSgParseException("name");
+            }
+
+            skipWS();
+
+            if (!fetchChar('=')) {
+                throw newExpectedSgParseException("\"=\"");
+            }
+
+            skipWS();
+
+            int valuePos = pos;
+            String value = fetchValue();
+            if (value == null) {
+                throw newExpectedSgParseException("value");
+            }
+            int paramEndPos = pos;
+
+            ParameterHandler handler = PARAM_HANDLERS.get(name);
+            if (handler == null) {
+                if (name.equals(PARAM_CURRENCY_SYMBOL)) {
+                    currencySymbol = value;
+                } else {
+                    throw newUnknownParameterException(name, namePos);
+                }
+            } else {
+                try {
+                    handler.handle(this, value);
+                } catch (InvalidParameterValueException e) {
+                    throw newInvalidParameterValueException(name, value, 
valuePos, e);
+                }
+            }
+
+            skipWS();
+
+            // Optional comma
+            if (fetchChar(',')) {
+                skipWS();
+            } else {
+                if (pos == ln) {
+                    break fetchParamters;
+                }
+                if (pos == paramEndPos) {
+                    throw newExpectedSgParseException("parameter separator 
whitespace or comma");
+                }
+            }
+        } while (true);
+        
+        // This is brought out to here to ensure that it's applied after 
"currency code":
+        if (currencySymbol != null) {
+            symbols.setCurrencySymbol(currencySymbol);
+        }
+    }
+
+    private ParseException newInvalidParameterValueException(String name, 
String value, int valuePos,
+            InvalidParameterValueException e) {
+        return new java.text.ParseException(
+                _StringUtil.jQuote(value) + " is an invalid value for the \"" 
+ name + "\" parameter: "
+                + e.message,
+                valuePos);
+    }
+
+    private ParseException newUnknownParameterException(String name, int 
namePos) throws ParseException {
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("Unsupported parameter name, 
").append(_StringUtil.jQuote(name));
+        sb.append(". The supported names are: ");
+        Set<String> legalNames = PARAM_HANDLERS.keySet();
+        String[] legalNameArr = legalNames.toArray(new 
String[legalNames.size()]);
+        Arrays.sort(legalNameArr);
+        for (int i = 0; i < legalNameArr.length; i++) {
+            if (i != 0) {
+                sb.append(", ");
+            }
+            sb.append(legalNameArr[i]);
+        }
+        return new java.text.ParseException(sb.toString(), namePos);
+    }
+
+    private void skipWS() {
+        int ln = src.length();
+        while (pos < ln && isWS(src.charAt(pos))) {
+            pos++;
+        }
+    }
+
+    private boolean fetchChar(char fetchedChar) {
+        if (pos < src.length() && src.charAt(pos) == fetchedChar) {
+            pos++;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isWS(char c) {
+        return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == 
'\u00A0';
+    }
+
+    private String fetchName() throws ParseException {
+        int ln = src.length();
+        int startPos = pos;
+        boolean firstChar = true;
+        scanUntilEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (firstChar) {
+                if (!Character.isJavaIdentifierStart(c)) {
+                    break scanUntilEnd;
+                }
+                firstChar = false;
+            } else if (!Character.isJavaIdentifierPart(c)) {
+                break scanUntilEnd;
+            }
+            pos++;
+        }
+        return !firstChar ? src.substring(startPos, pos) : null;
+    }
+
+    private String fetchValue() throws ParseException {
+        int ln = src.length();
+        int startPos = pos;
+        char openedQuot = 0;
+        boolean needsUnescaping = false;
+        scanUntilEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (c == '\'' || c == '"') {
+                if (openedQuot == 0) {
+                    if (startPos != pos) {
+                        throw new java.text.ParseException(
+                                "The " + c + " character can only be used for 
quoting values, "
+                                        + "but it was in the middle of an 
non-quoted value.",
+                                pos);
+                    }
+                    openedQuot = c;
+                } else if (c == openedQuot) {
+                    if (pos + 1 < ln && src.charAt(pos + 1) == openedQuot) {
+                        pos++; // skip doubled quote (escaping)
+                        needsUnescaping = true;
+                    } else {
+                        String str = src.substring(startPos + 1, pos);
+                        pos++;
+                        return needsUnescaping ? unescape(str, openedQuot) : 
str;
+                    }
+                }
+            } else {
+                if (openedQuot == 0 && !Character.isJavaIdentifierPart(c)) {
+                    break scanUntilEnd;
+                }
+            }
+            pos++;
+        } // while
+        if (openedQuot != 0) {
+            throw new java.text.ParseException(
+                    "The " + openedQuot 
+                    + " quotation wasn't closed when the end of the source was 
reached.",
+                    pos);
+        }
+        return startPos == pos ? null : src.substring(startPos, pos);
+    }
+
+    private String unescape(String s, char openedQuot) {
+        return openedQuot == '\'' ? _StringUtil.replace(s, "\'\'", "\'") : 
_StringUtil.replace(s, "\"\"", "\"");
+    }
+
+    private String fetchStandardPattern() {
+        int pos = this.pos;
+        int ln = src.length();
+        int semicolonCnt = 0;
+        boolean quotedMode = false;
+        findStdPartEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (c == ';' && !quotedMode) {
+                semicolonCnt++;
+                if (semicolonCnt == 2) {
+                    break findStdPartEnd;
+                }
+            } else if (c == '\'') {
+                if (quotedMode) {
+                    if (pos + 1 < ln && src.charAt(pos + 1) == '\'') {
+                        // Skips "''" used for escaping "'"
+                        pos++;
+                    } else {
+                        quotedMode = false;
+                    }
+                } else {
+                    quotedMode = true;
+                }
+            }
+            pos++;
+        }
+
+        String stdFormatStr;
+        if (semicolonCnt < 2) { // We have a standard DecimalFormat string
+            // Note that "0.0;" and "0.0" gives the same result with 
DecimalFormat, so we leave a ';' there
+            stdFormatStr = src;
+        } else { // `pos` points to the 2nd ';'
+            int stdEndPos = pos;
+            if (src.charAt(pos - 1) == ';') { // we have a ";;"
+                // Note that ";;" is illegal in DecimalFormat, so this is 
backward compatible.
+                stdEndPos--;
+            }
+            stdFormatStr = src.substring(0, stdEndPos);
+        }
+
+        if (pos < ln) {
+            pos++; // Skips closing ';'
+        }
+        this.pos = pos;
+
+        return stdFormatStr;
+    }
+
+    private ExtendedDecimalFormatParser(String formatString, Locale locale) {
+        src = formatString;
+        symbols = new DecimalFormatSymbols(locale);
+    }
+
+    private ParseException newExpectedSgParseException(String expectedThing) {
+        String quotation;
+
+        // Ignore trailing WS when calculating the length:
+        int i = src.length() - 1;
+        while (i >= 0 && Character.isWhitespace(src.charAt(i))) {
+            i--;
+        }
+        int ln = i + 1;
+
+        if (pos < ln) {
+            int qEndPos = pos + MAX_QUOTATION_LENGTH;
+            if (qEndPos >= ln) {
+                quotation = src.substring(pos, ln);
+            } else {
+                quotation = src.substring(pos, qEndPos - SNIP_MARK.length()) + 
SNIP_MARK;
+            }
+        } else {
+            quotation = null;
+        }
+
+        return new ParseException(
+                "Expected a(n) " + expectedThing + " at position " + pos + " 
(0-based), but "
+                        + (quotation == null ? "reached the end of the input." 
: "found: " + quotation),
+                pos);
+    }
+
+    private interface ParameterHandler {
+
+        void handle(ExtendedDecimalFormatParser parser, String value)
+                throws InvalidParameterValueException;
+
+    }
+
+    private static class InvalidParameterValueException extends Exception {
+
+        private final String message;
+
+        public InvalidParameterValueException(String message) {
+            this.message = message;
+        }
+
+    }
+
+}

Reply via email to