http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PrintfGTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PrintfGTemplateNumberFormatFactory.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PrintfGTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..e8d5f2b
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PrintfGTemplateNumberFormatFactory.java
@@ -0,0 +1,138 @@
+/*
+ * 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.userpkg;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Locale;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.UnformattableValueException;
+
+/**
+ * Formats like {@code %G} in {@code printf}, with the specified number of 
significant digits. Also has special
+ * formatter for HTML output format, where it uses the HTML "sup" element for 
exponents.
+ */
+public class PrintfGTemplateNumberFormatFactory extends 
TemplateNumberFormatFactory {
+
+    public static final PrintfGTemplateNumberFormatFactory INSTANCE = new 
PrintfGTemplateNumberFormatFactory();
+    
+    private PrintfGTemplateNumberFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment 
env)
+            throws InvalidFormatParametersException {
+        Integer significantDigits;
+        if (!params.isEmpty()) {
+            try {
+                significantDigits = Integer.valueOf(params);
+            } catch (NumberFormatException e) {
+                throw new InvalidFormatParametersException(
+                        "The format parameter must be an integer, but was 
(shown quoted) "
+                        + _StringUtil.jQuote(params) + ".");
+            }
+        } else {
+            // Use the default of %G
+            significantDigits = null;
+        }
+        return new PrintfGTemplateNumberFormat(significantDigits, locale);
+    }
+
+    private static class PrintfGTemplateNumberFormat extends 
TemplateNumberFormat {
+        
+        private final Locale locale;
+        private final String printfFormat; 
+
+        private PrintfGTemplateNumberFormat(Integer significantDigits, Locale 
locale) {
+            printfFormat = "%" + (significantDigits != null ? "." + 
significantDigits : "") + "G";
+            this.locale = locale;
+        }
+        
+        @Override
+        public String formatToPlainText(TemplateNumberModel numberModel)
+                throws UnformattableValueException, TemplateModelException {
+            final Number n = TemplateFormatUtil.getNonNullNumber(numberModel);
+            
+            // printf %G only accepts Double, BigDecimal and Float 
+            final Number gCompatibleN;
+            if (n instanceof Double  || n instanceof BigDecimal || n 
instanceof Float) {
+                gCompatibleN = n;
+            } else {
+                if (n instanceof BigInteger) {
+                    gCompatibleN = new BigDecimal((BigInteger) n);             
           
+                } else if (n instanceof Long) {
+                    gCompatibleN = BigDecimal.valueOf(n.longValue());
+                } else {
+                    gCompatibleN = Double.valueOf(n.doubleValue());
+                }
+            }
+            
+            return String.format(locale, printfFormat, gCompatibleN);
+        }
+
+        @Override
+        public Object format(TemplateNumberModel numberModel)
+                throws UnformattableValueException, TemplateModelException {
+            String strResult = formatToPlainText(numberModel);
+            
+            int expIdx = strResult.indexOf('E');
+            if (expIdx == -1) {
+                return strResult;
+            }
+                
+            String expStr = strResult.substring(expIdx + 1);
+            int expSignifNumBegin = 0;
+            while (expSignifNumBegin < expStr.length() && 
isExpSignifNumPrefix(expStr.charAt(expSignifNumBegin))) {
+                expSignifNumBegin++;
+            }
+            
+            return HTMLOutputFormat.INSTANCE.fromMarkup(
+                    strResult.substring(0, expIdx)
+                    + "*10<sup>"
+                    + (expStr.charAt(0) == '-' ? "-" : "") + 
expStr.substring(expSignifNumBegin)
+                    + "</sup>");
+        }
+
+        private boolean isExpSignifNumPrefix(char c) {
+            return c == '+' || c == '-' || c == '0';
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return true;
+        }
+
+        @Override
+        public String getDescription() {
+            return "printf " + printfFormat;
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicAll.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicAll.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicAll.java
new file mode 100644
index 0000000..5767e93
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicAll.java
@@ -0,0 +1,24 @@
+/*
+ * 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.userpkg;
+
+public class PublicAll {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithMixedConstructors.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithMixedConstructors.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithMixedConstructors.java
new file mode 100644
index 0000000..e521d75
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithMixedConstructors.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.userpkg;
+
+public class PublicWithMixedConstructors {
+    
+    private final String s;
+
+    public PublicWithMixedConstructors(Integer x) {
+        s = "Integer";
+    }
+
+    PublicWithMixedConstructors(int x) {
+        s = "int";
+    }
+
+    public String getS() {
+        return s;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithPackageVisibleConstructor.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithPackageVisibleConstructor.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithPackageVisibleConstructor.java
new file mode 100644
index 0000000..9fdec49
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicWithPackageVisibleConstructor.java
@@ -0,0 +1,26 @@
+/*
+ * 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.userpkg;
+
+public class PublicWithPackageVisibleConstructor {
+
+    PublicWithPackageVisibleConstructor() { }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/SeldomEscapedOutputFormat.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/SeldomEscapedOutputFormat.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/SeldomEscapedOutputFormat.java
new file mode 100644
index 0000000..b8cab13
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/SeldomEscapedOutputFormat.java
@@ -0,0 +1,71 @@
+/*
+ * 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.userpkg;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+
+public class SeldomEscapedOutputFormat extends 
CommonMarkupOutputFormat<TemplateSeldomEscapedOutputModel> {
+    
+    public static final SeldomEscapedOutputFormat INSTANCE = new 
SeldomEscapedOutputFormat();
+    
+    private SeldomEscapedOutputFormat() {
+        // hide
+    }
+
+    @Override
+    public String getName() {
+        return "seldomEscaped";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "text/seldomEscaped";
+    }
+
+    @Override
+    public void output(String textToEsc, Writer out) throws IOException, 
TemplateModelException {
+        out.write(escapePlainText(textToEsc));
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) {
+        return plainTextContent.replaceAll("(\\.|\\\\)", "\\\\$1");
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) {
+        return false;
+    }
+
+    @Override
+    public boolean isAutoEscapedByDefault() {
+        return false;
+    }
+
+    @Override
+    protected TemplateSeldomEscapedOutputModel newTemplateMarkupOutputModel(
+            String plainTextContent, String markupContent) {
+        return new TemplateSeldomEscapedOutputModel(plainTextContent, 
markupContent);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateDummyOutputModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateDummyOutputModel.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateDummyOutputModel.java
new file mode 100644
index 0000000..c98df53
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateDummyOutputModel.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.userpkg;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+public class TemplateDummyOutputModel extends 
CommonTemplateMarkupOutputModel<TemplateDummyOutputModel> {
+
+    TemplateDummyOutputModel(String plainTextContent, String markupContet) {
+        super(plainTextContent, markupContet);
+    }
+
+    @Override
+    public DummyOutputFormat getOutputFormat() {
+        return DummyOutputFormat.INSTANCE;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateSeldomEscapedOutputModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateSeldomEscapedOutputModel.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateSeldomEscapedOutputModel.java
new file mode 100644
index 0000000..b0a21f7
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TemplateSeldomEscapedOutputModel.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.userpkg;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+public class TemplateSeldomEscapedOutputModel extends 
CommonTemplateMarkupOutputModel<TemplateSeldomEscapedOutputModel> {
+
+    TemplateSeldomEscapedOutputModel(String plainTextContent, String 
markupContet) {
+        super(plainTextContent, markupContet);
+    }
+
+    @Override
+    public SeldomEscapedOutputFormat getOutputFormat() {
+        return SeldomEscapedOutputFormat.INSTANCE;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/DateUtilTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/DateUtilTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/DateUtilTest.java
new file mode 100644
index 0000000..0cd8fc0
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/DateUtilTest.java
@@ -0,0 +1,1085 @@
+/*
+ * 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.util;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateParseException;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import 
org.apache.freemarker.core.util._DateUtil.TrivialCalendarFieldsToDateConverter;
+
+import junit.framework.TestCase;
+
+public class DateUtilTest extends TestCase {
+    
+    private final TimeZone originalDefaultTZ = TimeZone.getDefault();
+
+    @Override
+    protected void setUp() throws Exception {
+        TimeZone.setDefault(TimeZone.getTimeZone("Europe/Prague"));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        TimeZone.setDefault(originalDefaultTZ);
+    }
+
+    private final DateFormat df
+            = new SimpleDateFormat("G yyyy-MM-dd HH:mm:ss:S Z", Locale.US);
+    {
+        df.setTimeZone(_DateUtil.UTC);
+    }
+    
+    private CalendarFieldsToDateConverter cf2dc = new 
TrivialCalendarFieldsToDateConverter();
+    
+    private DateToISO8601CalendarFactory calendarFactory
+            = new _DateUtil.TrivialDateToISO8601CalendarFactory();
+    
+    public DateUtilTest(String name) {
+        super(name);
+    }
+    
+    public void testDateToUTCString() throws ParseException {
+        assertEquals(
+                "1998-10-30T15:30:00.512Z",
+                dateToISO8601UTCDateTimeMSString(
+                        df.parse("AD 1998-10-30 19:30:00:512 +0400"), true));
+        assertEquals(
+                "1998-10-30T15:30:00.5Z",
+                dateToISO8601UTCDateTimeMSString(
+                        df.parse("AD 1998-10-30 19:30:00:500 +0400"), true));
+        assertEquals(
+                "1998-10-30T15:30:00.51Z",
+                dateToISO8601UTCDateTimeMSString(
+                        df.parse("AD 1998-10-30 19:30:00:510 +0400"), true));
+        assertEquals(
+                "1998-10-30T15:30:00.1Z",
+                dateToISO8601UTCDateTimeMSString(
+                        df.parse("AD 1998-10-30 19:30:00:100 +0400"), true));
+        assertEquals(
+                "1998-10-30T15:30:00.01Z",
+                dateToISO8601UTCDateTimeMSString(
+                        df.parse("AD 1998-10-30 19:30:00:10 +0400"), true));
+        assertEquals(
+                "1998-10-30T15:30:00.001Z",
+                dateToISO8601UTCDateTimeMSString(
+                        df.parse("AD 1998-10-30 19:30:00:1 +0400"), true));
+        assertEquals(
+                "2000-02-08T06:05:04Z",
+                dateToISO8601UTCDateTimeMSString(
+                        df.parse("AD 2000-02-08 09:05:04:0 +0300"), true));
+        assertEquals(
+                "0099-02-28T06:15:24Z",
+                dateToISO8601UTCDateTimeString(
+                        df.parse(
+                        "AD 0099-03-02 09:15:24:0 +0300"), true));
+        assertEquals(
+                "0010-02-28T06:15:24Z",
+                dateToISO8601UTCDateTimeString(
+                        df.parse("AD 0010-03-02 09:15:24:0 +0300"), true));
+        assertEquals(
+                "0001-02-28T06:15:24Z",
+                dateToISO8601UTCDateTimeString(
+                        df.parse("AD 0001-03-02 09:15:24:0 +0300"), true));
+        assertEquals(
+                "0000-02-29T06:15:24Z",
+                dateToISO8601UTCDateTimeString(
+                        df.parse("BC 0001-03-02 09:15:24:0 +0300"), true));
+        assertEquals(
+                "-1-02-28T06:15:24Z",
+                dateToISO8601UTCDateTimeString(
+                        df.parse("BC 2-03-02 09:15:24:0 +0300"), true));
+        assertEquals(
+                "10000-02-28T06:15:24Z",
+                dateToISO8601UTCDateTimeString(
+                        df.parse("AD 10000-02-28 09:15:24:0 +0300"), true));
+
+        Date d = df.parse("AD 1998-10-30 19:30:00:512 +0400");
+        assertEquals(
+                "1998-10-30",
+                dateToISO8601UTCDateString(d));
+        assertEquals(
+                "15:30:00.512Z",
+                dateToISO8601UTCTimeMSString(d, true));
+        assertEquals(
+                "15:30:00.512",
+                dateToISO8601UTCTimeMSString(d, false));
+        assertEquals(
+                "1998-10-30",
+                dateToISO8601UTCDateString(
+                        new java.sql.Date(d.getTime())));
+        assertEquals(
+                "15:30:00.512Z",
+                dateToISO8601UTCTimeMSString(
+                        new java.sql.Time(d.getTime()), true));
+    }
+
+    public void testLocalTime() throws ParseException {
+        Date dsum = df.parse("AD 2010-05-09 20:00:00:0 UTC");
+        Date dwin = df.parse("AD 2010-01-01 20:00:00:0 UTC");
+        
+        TimeZone tzRome = TimeZone.getTimeZone("Europe/Rome");
+        if (tzRome.getOffset(0) == 0) {
+            throw new RuntimeException(
+                    "Can't get time zone for Europe/Rome!");
+        }
+        assertEquals(
+                "2010-05-09T22:00:00+02:00",
+                dateToISO8601DateTimeString(dsum, tzRome));
+        assertEquals(
+                "2010-01-01T21:00:00+01:00",
+                dateToISO8601DateTimeString(dwin, tzRome));
+        assertEquals(
+                "2010-05-09",
+                dateToISO8601DateString(dsum, tzRome));
+        assertEquals(
+                "2010-01-01",
+                dateToISO8601DateString(dwin, tzRome));
+        assertEquals(
+                "22:00:00+02:00",
+                dateToISO8601TimeString(dsum, tzRome));
+        assertEquals(
+                "21:00:00+01:00",
+                dateToISO8601TimeString(dwin, tzRome));
+        
+        TimeZone tzNY = TimeZone.getTimeZone("America/New_York");
+        if (tzNY.getOffset(0) == 0) {
+            throw new RuntimeException(
+                    "Can't get time zone for America/New_York!");
+        }
+        assertEquals(
+                "2010-05-09T16:00:00-04:00",
+                dateToISO8601DateTimeString(dsum, tzNY));
+        assertEquals(
+                "2010-01-01T15:00:00-05:00",
+                dateToISO8601DateTimeString(dwin, tzNY));
+        assertEquals(
+                "2010-05-09",
+                dateToISO8601DateString(dsum, tzNY));
+        assertEquals(
+                "2010-01-01",
+                dateToISO8601DateString(dwin, tzNY));
+        assertEquals(
+                "16:00:00-04:00",
+                dateToISO8601TimeString(dsum, tzNY));
+        assertEquals(
+                "15:00:00-05:00",
+                dateToISO8601TimeString(dwin, tzNY));
+        
+        TimeZone tzFixed = TimeZone.getTimeZone("GMT+02:30");
+        assertEquals(
+                "2010-05-09T22:30:00+02:30",
+                dateToISO8601DateTimeString(dsum, tzFixed));
+        assertEquals(
+                "2010-01-01T22:30:00+02:30",
+                dateToISO8601DateTimeString(dwin, tzFixed));
+    }
+
+    public void testGetTimeZone() throws Exception {
+        assertTrue(_DateUtil.getTimeZone("GMT") != _DateUtil.UTC);
+        assertTrue(_DateUtil.getTimeZone("UT1") != _DateUtil.UTC);
+        assertEquals(_DateUtil.getTimeZone("UTC"), _DateUtil.UTC);
+        
+        assertEquals(_DateUtil.getTimeZone("Europe/Rome"),
+                TimeZone.getTimeZone("Europe/Rome"));
+        
+        assertEquals(_DateUtil.getTimeZone("Iceland"), // GMT and no DST
+                TimeZone.getTimeZone("Iceland"));
+        
+        try {
+            _DateUtil.getTimeZone("Europe/NoSuch");
+            fail();
+        } catch (UnrecognizedTimeZoneException e) {
+            // good
+        }
+    }
+    
+    public void testTimeOnlyDate() throws UnrecognizedTimeZoneException {
+        Date t = new Date(0L);
+        SimpleDateFormat tf = new SimpleDateFormat("HH:mm:ss");
+        
+        tf.setTimeZone(_DateUtil.UTC);
+        assertEquals("00:00:00", tf.format(t));
+        assertEquals("00:00:00",
+                dateToISO8601UTCTimeString(t, false));
+        
+        TimeZone gmt1 = _DateUtil.getTimeZone("GMT+01");
+        tf.setTimeZone(gmt1);
+        assertEquals("01:00:00", tf.format(t)); 
+        assertEquals("01:00:00+01:00",
+                dateToISO8601TimeString(t, gmt1));
+    }
+    
+    public void testAccuracy() throws ParseException {
+        Date d = df.parse("AD 2000-02-08 09:05:04:250 UTC"); 
+        assertEquals("2000-02-08T09:05:04Z",
+                dateToISO8601UTCDateTimeString(d, true));
+        assertEquals("2000-02-08T09:05:04.25Z",
+                dateToISO8601String(d, true, true, true,
+                        _DateUtil.ACCURACY_MILLISECONDS, null));
+        assertEquals("2000-02-08T09:05:04Z",
+                dateToISO8601String(d, true, true, true,
+                        _DateUtil.ACCURACY_SECONDS, null));
+        assertEquals("2000-02-08T09:05Z",
+                dateToISO8601String(d, true, true, true,
+                        _DateUtil.ACCURACY_MINUTES, null));
+        assertEquals("2000-02-08T09Z",
+                dateToISO8601String(d, true, true, true,
+                        _DateUtil.ACCURACY_HOURS, null));
+        
+        d = df.parse("AD 1998-10-30 19:30:00:000 +0400");
+        assertEquals(
+                "15:30:00Z",
+                dateToISO8601UTCTimeMSString(d, true));
+        assertEquals(
+                "15:30:00.000Z",
+                dateToISO8601UTCTimeMSFString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00Z",
+                dateToISO8601UTCDateTimeMSString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00.000Z",
+                dateToISO8601UTCDateTimeMSFString(d, true));
+                
+        d = df.parse("AD 1998-10-30 19:30:00:100 +0400");
+        assertEquals(
+                "15:30:00.1Z",
+                dateToISO8601UTCTimeMSString(d, true));
+        assertEquals(
+                "15:30:00.100Z",
+                dateToISO8601UTCTimeMSFString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00.1Z",
+                dateToISO8601UTCDateTimeMSString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00.100Z",
+                dateToISO8601UTCDateTimeMSFString(d, true));
+        
+        d = df.parse("AD 1998-10-30 19:30:00:010 +0400");
+        assertEquals(
+                "15:30:00.01Z",
+                dateToISO8601UTCTimeMSString(d, true));
+        assertEquals(
+                "15:30:00.010Z",
+                dateToISO8601UTCTimeMSFString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00.01Z",
+                dateToISO8601UTCDateTimeMSString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00.010Z",
+                dateToISO8601UTCDateTimeMSFString(d, true));
+        
+        d = df.parse("AD 1998-10-30 19:30:00:001 +0400");
+        assertEquals(
+                "15:30:00.001Z",
+                dateToISO8601UTCTimeMSString(d, true));
+        assertEquals(
+                "15:30:00.001Z",
+                dateToISO8601UTCTimeMSFString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00.001Z",
+                dateToISO8601UTCDateTimeMSString(d, true));
+        assertEquals(
+                "1998-10-30T15:30:00.001Z",
+                dateToISO8601UTCDateTimeMSFString(d, true));
+    }
+
+    public void testXSFormatISODeviations() throws ParseException, 
UnrecognizedTimeZoneException {
+        Date dsum = df.parse("AD 2010-05-09 20:00:00:0 UTC");
+        Date dwin = df.parse("AD 2010-01-01 20:00:00:0 UTC");
+        
+        TimeZone tzRome = _DateUtil.getTimeZone("Europe/Rome");
+        
+        assertEquals(
+                "2010-01-01T21:00:00+01:00",
+                _DateUtil.dateToXSString(dwin, true, true, true, 
_DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
+        assertEquals(
+                "2010-05-09T22:00:00+02:00",
+                _DateUtil.dateToXSString(dsum, true, true, true, 
_DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
+        assertEquals(
+                "2010-01-01+01:00",  // ISO doesn't allow date-only with TZ
+                _DateUtil.dateToXSString(dwin, true, false, true, 
_DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
+        assertEquals(
+                "2010-05-09+02:00",  // ISO doesn't allow date-only with TZ
+                _DateUtil.dateToXSString(dsum, true, false, true, 
_DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
+        assertEquals(
+                "21:00:00+01:00",
+                _DateUtil.dateToXSString(dwin, false, true, true, 
_DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
+        assertEquals(
+                "22:00:00+02:00",
+                _DateUtil.dateToXSString(dsum, false, true, true, 
_DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
+        
+        assertEquals(
+                "-1-02-29T06:15:24Z",  // ISO uses 0 for BC 1
+                _DateUtil.dateToXSString(
+                        df.parse("BC 0001-03-02 09:15:24:0 +0300"),
+                        true, true, true, _DateUtil.ACCURACY_SECONDS, 
_DateUtil.UTC, calendarFactory));
+        assertEquals(
+                "-2-02-28T06:15:24Z",  // ISO uses -1 for BC 2
+                _DateUtil.dateToXSString(
+                        df.parse("BC 2-03-02 09:15:24:0 +0300"),
+                        true, true, true, _DateUtil.ACCURACY_SECONDS, 
_DateUtil.UTC, calendarFactory));
+    }
+    
+    private String dateToISO8601DateTimeString(
+            Date date, TimeZone tz) {
+        return dateToISO8601String(date, true, true, true,
+                _DateUtil.ACCURACY_SECONDS, tz);
+    }
+    
+    private String dateToISO8601UTCDateTimeString(
+            Date date, boolean offsetPart) {
+        return dateToISO8601String(date, true, true, offsetPart,
+                _DateUtil.ACCURACY_SECONDS, _DateUtil.UTC);
+    }
+
+    private String dateToISO8601UTCDateTimeMSString(
+            Date date, boolean offsetPart) {
+        return dateToISO8601String(date, true, true, offsetPart,
+                _DateUtil.ACCURACY_MILLISECONDS, _DateUtil.UTC);
+    }
+
+    private String dateToISO8601UTCDateTimeMSFString(
+            Date date, boolean offsetPart) {
+        return dateToISO8601String(date, true, true, offsetPart,
+                _DateUtil.ACCURACY_MILLISECONDS_FORCED, _DateUtil.UTC);
+    }
+        
+    private String dateToISO8601DateString(Date date, TimeZone tz) {
+        return dateToISO8601String(date, true, false, false,
+                _DateUtil.ACCURACY_SECONDS, tz);
+    }
+
+    private String dateToISO8601UTCDateString(Date date) {
+        return dateToISO8601String(date, true, false, false,
+                _DateUtil.ACCURACY_SECONDS, _DateUtil.UTC);
+    }
+    
+    private String dateToISO8601TimeString(
+            Date date, TimeZone tz) {
+        return dateToISO8601String(date, false, true, true,
+                _DateUtil.ACCURACY_SECONDS, tz);
+    }
+    
+    private String dateToISO8601UTCTimeString(
+            Date date, boolean offsetPart) {
+        return dateToISO8601String(date, false, true, offsetPart,
+                _DateUtil.ACCURACY_SECONDS, _DateUtil.UTC);
+    }
+
+    private String dateToISO8601UTCTimeMSString(
+            Date date, boolean offsetPart) {
+        return dateToISO8601String(date, false, true, offsetPart,
+                _DateUtil.ACCURACY_MILLISECONDS, _DateUtil.UTC);
+    }
+
+    private String dateToISO8601UTCTimeMSFString(
+            Date date, boolean offsetPart) {
+        return dateToISO8601String(date, false, true, offsetPart,
+                _DateUtil.ACCURACY_MILLISECONDS_FORCED, _DateUtil.UTC);
+    }
+    
+    private String dateToISO8601String(
+            Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone) {
+        return _DateUtil.dateToISO8601String(
+                date,
+                datePart, timePart, offsetPart,
+                accuracy,
+                timeZone,
+                calendarFactory);        
+    }
+    
+    public void testParseDate() throws DateParseException {
+        assertDateParsing(
+                "AD 1998-10-29 20:00:00:0 +0000",
+                null,
+                "1998-10-30+04:00", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 1998-10-30 02:00:00:0 +0000",
+                null,
+                "1998-10-30-02:00", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 1998-10-30 02:00:00:0 +0000",
+                "1998-10-30", _DateUtil.parseXSTimeZone("-02:00"));
+        assertDateParsing(
+                null,
+                "AD 1998-10-30 02:00:00:0 +0000",
+                "19981030", _DateUtil.parseXSTimeZone("-02:00"));
+        assertDateParsing(
+                "AD 1998-10-30 00:00:00:0 +0000",
+                null,
+                "1998-10-30Z", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 1998-10-30 00:00:00:0 +0000",
+                "1998-10-30", _DateUtil.UTC);
+        assertDateParsing(
+                null,
+                "AD 1998-10-30 00:00:00:0 +0000",
+                "19981030", _DateUtil.UTC);
+
+        assertDateParsing(
+                "AD 1998-10-29 20:00:00:0 +0000",
+                null,
+                "1998-10-30+04:00", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 1998-10-30 04:00:00:0 +0000",
+                null,
+                "1998-10-30-04:00", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 1998-10-30 00:00:00:0 +0000",
+                null,
+                "1998-10-30Z", _DateUtil.UTC);
+        
+        try {
+            // XS doesn't have year 0
+            assertDateParsing(
+                    "BC 0000-02-05 00:00:00:0 +0000",
+                    null,
+                    "0000-02-03Z", _DateUtil.UTC);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+        assertDateParsing(
+                null,
+                "BC 0001-02-05 00:00:00:0 +0000",
+                "0000-02-03", _DateUtil.UTC);
+        assertDateParsing(
+                null,
+                "BC 0001-02-05 00:00:00:0 +0000",
+                "00000203", _DateUtil.UTC);
+        
+        assertDateParsing(
+                "BC 0001-02-05 00:00:00:0 +0000",  // Julian
+                "BC 0002-02-05 00:00:00:0 +0000",  // Julian
+                "-0001-02-03", _DateUtil.UTC);  // Proleptic Gregorian
+        assertDateParsing(
+                null,
+                "BC 0002-02-05 00:00:00:0 +0000",  // Julian
+                "-00010203", _DateUtil.UTC);  // Proleptic Gregorian
+
+        assertDateParsing(
+                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
+                null,
+                "0001-02-03Z", _DateUtil.UTC);  // Proleptic Gregorian
+        assertDateParsing(
+                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
+                "0001-02-03", _DateUtil.UTC);  // Proleptic Gregorian
+        assertDateParsing(
+                null,
+                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
+                "00010203", _DateUtil.UTC);  // Proleptic Gregorian
+        assertDateParsing(
+                "AD 1001-12-07 00:00:00:0 +0000",  // Julian
+                null,
+                "1001-12-13Z", _DateUtil.UTC);  // Proleptic Gregorian
+        assertDateParsing(
+                "AD 1001-12-07 00:00:00:0 +0000",  // Julian
+                "1001-12-13", _DateUtil.UTC);  // Proleptic Gregorian
+        
+        assertDateParsing(
+                "AD 2006-12-31 00:00:00:0 +0000",
+                null,
+                "2006-12-31Z", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 2006-12-31 00:00:00:0 +0000",
+                "2006-12-31", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 2006-01-01 00:00:00:0 +0000",
+                null,
+                "2006-01-01Z", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 2006-01-01 00:00:00:0 +0000",
+                "2006-01-01", _DateUtil.UTC);
+        assertDateParsing(
+                "AD 12006-01-01 00:00:00:0 +0000",
+                "12006-01-01", _DateUtil.UTC);
+        assertDateParsing(
+                null,
+                "AD 12006-01-01 00:00:00:0 +0000",
+                "120060101", _DateUtil.UTC);
+    }
+
+    public void testParseDateMalformed() {
+        assertDateMalformed("1998-10-30x");
+        assertDateMalformed("+1998-10-30");
+        assertDateMalformed("1998-10-");
+        assertDateMalformed("1998-1-30");
+        assertDateMalformed("1998-10-30+01");
+        assertDateMalformed("1998-00-01");
+        assertDateMalformed("1998-13-01");
+        assertDateMalformed("1998-10-00");
+        assertDateMalformed("1998-10-32");
+        assertDateMalformed("1998-02-31");
+        
+        assertISO8601DateMalformed("2100103");
+        assertISO8601DateMalformed("210-01-03");
+        assertISO8601DateMalformed("2012-0301");
+        assertISO8601DateMalformed("201203-01");
+        assertISO8601DateMalformed("2012-01-01+01:00");
+    }
+    
+    public void testParseTime() throws DateParseException {
+        assertTimeParsing(
+                "AD 1970-01-01 17:30:05:0 +0000",
+                "17:30:05", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 17:30:05:0 +0000",
+                "173005", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 07:30:00:100 +0000",
+                "07:30:00.1", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 07:30:00:120 +0000",
+                "07:30:00.12", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "07:30:00.123", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "07:30:00.1235", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "07:30:00.12346", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "073000.12346", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "073000,12346", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 07:30:00:120 +0000",
+                "07:30:00.12", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 07:30:00:500 +0000",
+                "07:30:00.5", _DateUtil.UTC);
+
+        assertTimeParsing(
+                "AD 1970-01-01 16:30:05:0 +0000",
+                "17:30:05+01:00", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 16:30:05:0 +0000",
+                "173005+01", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 19:00:05:0 +0000",
+                "17:30:05-01:30", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 19:00:05:0 +0000",
+                "173005-0130", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-01 16:30:05:500 +0000",
+                "17:30:05.5+01:00", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 16:30:05:500 +0000",
+                "173005.5+0100", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 16:30:05:500 +0000",
+                "173005.5+01", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 16:00:00:0 +0000",
+                "170000+01", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 16:00:00:0 +0000",
+                "1700+01", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-01 16:00:00:0 +0000",
+                "17+01", _DateUtil.UTC);
+        
+        assertTimeParsing(
+                "AD 1970-01-01 00:00:00:0 +0000",
+                "00:00:00", _DateUtil.UTC);
+        assertTimeParsing(
+                "AD 1970-01-02 00:00:00:0 +0000",
+                "24:00:00", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-02 00:00:00:0 +0000",
+                "240000", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-02 00:00:00:0 +0000",
+                "2400", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-02 00:00:00:0 +0000",
+                "24:00", _DateUtil.UTC);
+        assertTimeParsing(
+                null,
+                "AD 1970-01-02 00:00:00:0 +0000",
+                "24", _DateUtil.UTC);
+        
+        assertTimeParsing(
+                "AD 1970-01-01 23:59:59:999 +0000",
+                "23:59:59.999", _DateUtil.UTC);
+    }
+
+    public void testParseTimeMalformed() {
+        assertTimeMalformed("00:0000");
+        assertTimeMalformed("00:00:00-01:60");
+        assertTimeMalformed("24:00:01");
+        assertTimeMalformed("00:00:61");
+        assertTimeMalformed("00:60:00");
+        assertTimeMalformed("25:00:00");
+        assertTimeMalformed("2:00:00");
+        assertTimeMalformed("02:0:00");
+        assertTimeMalformed("02:00:0");
+        
+        assertISO8601TimeMalformed("1010101");
+        assertISO8601TimeMalformed("10101");
+        assertISO8601TimeMalformed("101");
+        assertISO8601TimeMalformed("1");
+        assertISO8601TimeMalformed("101010-1");
+        assertISO8601TimeMalformed("101010-100");
+        assertISO8601TimeMalformed("101010-10000");
+        assertISO8601TimeMalformed("101010+1");
+        assertISO8601TimeMalformed("101010+100");
+        assertISO8601TimeMalformed("101010+10000");
+    }
+    
+    public void testParseDateTime() throws DateParseException {
+        assertDateTimeParsing( 
+                "AD 1998-10-30 11:30:00:0 +0000",
+                "1998-10-30T15:30:00+04:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                null,
+                "AD 1998-10-30 11:30:00:0 +0000",
+                "19981030T153000+0400", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 11:30:00:500 +0000",
+                "1998-10-30T15:30:00.5+04:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 15:30:00:0 +0000",
+                "1998-10-30T15:30:00Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                null,
+                "AD 1998-10-30 15:30:00:0 +0000",
+                "19981030T1530Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 15:30:00:500 +0000",
+                "1998-10-30T15:30:00.5Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 11:30:00:0 +0000",
+                "1998-10-30T15:30:00+04:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 15:30:00:0 +0000",
+                "1998-10-30T15:30:00Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 15:30:00:0 +0000",
+                "1998-10-30T15:30:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                null,
+                "AD 1998-10-30 15:30:00:0 +0000",
+                "1998-10-30T15:30", _DateUtil.UTC);
+        
+        assertDateTimeParsing(
+                "AD 1998-10-29 20:00:00:0 +0000",
+                "1998-10-30T00:00:00+04:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 02:00:00:0 +0000",
+                "1998-10-30T00:00:00-02:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 00:00:00:0 +0000",
+                "1998-10-30T00:00:00Z", _DateUtil.UTC);
+
+        assertDateTimeParsing(
+                "AD 1998-10-29 20:00:00:0 +0000",
+                "1998-10-30T00:00:00+04:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1998-10-30 00:00:00:0 +0000",
+                "1998-10-30T00:00:00Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                null,
+                "AD 1998-10-30 00:00:00:0 +0000",
+                "1998-10-30T00:00Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                null,
+                "AD 1998-10-30 00:00:00:0 +0000",
+                "1998-10-30T00:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                null,
+                "AD 1998-10-30 00:00:00:0 +0000",
+                "19981030T00Z", _DateUtil.UTC);
+        
+        // BC years
+        try {
+            assertDateTimeParsing(
+                        "",
+                        null,
+                        "0000-02-03T00:00:00Z", _DateUtil.UTC);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+        assertDateTimeParsing(
+                null,
+                "BC 0001-02-05 00:00:00:0 +0000",
+                "0000-02-03T00:00:00Z", _DateUtil.UTC);
+        
+        assertDateTimeParsing(
+                "BC 0001-02-05 00:00:00:0 +0000",  // Julian
+                "BC 0002-02-05 00:00:00:0 +0000",  // Julian
+                "-0001-02-03T00:00:00Z", _DateUtil.UTC);  // Proleptic 
Gregorian
+
+        assertDateTimeParsing(
+                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
+                "0001-02-03T00:00:00Z", _DateUtil.UTC);  // Proleptic Gregorian
+        assertDateTimeParsing(
+                "AD 1001-12-07 00:00:00:0 +0000",  // Julian
+                "1001-12-13T00:00:00Z", _DateUtil.UTC);  // Proleptic Gregorian
+        assertDateTimeParsing(
+                "AD 11001-12-13 00:00:00:0 +0000",
+                "11001-12-13T00:00:00Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                null,
+                "AD 11001-12-13 00:00:00:0 +0000",
+                "110011213T00Z", _DateUtil.UTC);
+        
+        assertDateTimeParsing(
+                "AD 2006-12-31 00:00:00:0 +0000",
+                "2006-12-31T00:00:00Z", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 2006-01-01 00:00:00:0 +0000",
+                "2006-01-01T00:00:00Z", _DateUtil.UTC);
+        
+        assertDateTimeParsing(
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "1970-01-01T07:30:00.123", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "1970-01-01T07:30:00.1235", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1970-01-01 07:30:00:123 +0000",
+                "1970-01-01T07:30:00.12346", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1970-01-01 07:30:00:120 +0000",
+                "1970-01-01T07:30:00.12", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1970-01-01 07:30:00:500 +0000",
+                "1970-01-01T07:30:00.5", _DateUtil.UTC);
+
+        assertDateTimeParsing(
+                "AD 1970-01-01 16:30:05:0 +0000",
+                "1970-01-01T17:30:05+01:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1970-01-01 16:30:05:500 +0000",
+                "1970-01-01T17:30:05.5+01:00", _DateUtil.UTC);
+        
+        assertDateTimeParsing(
+                "AD 1970-01-01 00:00:00:0 +0000",
+                "1970-01-01T00:00:00", _DateUtil.UTC);
+        assertDateTimeParsing(
+                "AD 1970-01-02 00:00:00:0 +0000",
+                "1970-01-01T24:00:00", _DateUtil.UTC);
+        
+        assertDateTimeParsing(
+                "AD 1970-01-01 23:59:59:999 +0000",
+                "1970-01-01T23:59:59.999", _DateUtil.UTC);
+    }
+
+    public void testParseDateTimeMalformed() throws DateParseException {
+        assertDateTimeMalformed("1998-00-01T00:00:00");
+        assertDateTimeMalformed("1998-13-01T00:00:00");
+        assertDateTimeMalformed("1998-10-00T00:00:00");
+        assertDateTimeMalformed("1998-10-32T00:00:00");
+        assertDateTimeMalformed("1998-02-31T00:00:00");
+        assertDateTimeMalformed("1970-01-02T24:00:01");
+        assertDateTimeMalformed("1970-01-01T00:00:61");
+        assertDateTimeMalformed("1970-01-01T00:60:00");
+        assertDateTimeMalformed("1970-01-01T25:00:00");
+        
+        assertISO8601DateTimeMalformed("197-01-01T20:00:00");
+    }
+    
+    public void testParseXSTimeZone() throws DateParseException {
+        assertEquals(0,
+                _DateUtil.parseXSTimeZone("Z").getOffset(0));
+        assertEquals(0,
+                _DateUtil.parseXSTimeZone("-00:00").getOffset(0));
+        assertEquals(0,
+                _DateUtil.parseXSTimeZone("+00:00").getOffset(0));
+        assertEquals(90 * 60 * 1000,
+                _DateUtil.parseXSTimeZone("+01:30").getOffset(0));
+        assertEquals(-4 * 60 * 60 * 1000,
+                _DateUtil.parseXSTimeZone("-04:00").getOffset(0));
+        assertEquals(((-23 * 60) - 59) * 60 * 1000,
+                _DateUtil.parseXSTimeZone("-23:59").getOffset(0));
+        assertEquals(((23 * 60) + 59) * 60 * 1000,
+                _DateUtil.parseXSTimeZone("+23:59").getOffset(0));
+    }
+
+    public void testParseXSTimeZoneWrong() {
+        try {
+            _DateUtil.parseXSTimeZone("04:00").getOffset(0);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+        try {
+            _DateUtil.parseXSTimeZone("-04:00x").getOffset(0);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+        try {
+            _DateUtil.parseXSTimeZone("-04").getOffset(0);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+        try {
+            _DateUtil.parseXSTimeZone("+24:00").getOffset(0);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+        try {
+            _DateUtil.parseXSTimeZone("-24:00").getOffset(0);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+        try {
+            _DateUtil.parseXSTimeZone("-01:60").getOffset(0);
+            fail();
+        } catch (DateParseException e) {
+            echo(e);
+        }
+    }
+    
+    public void testParseXSDateTimeFTLAndJavax() throws DateParseException {
+        // Explicit time zone:
+        assertJavaxAndFTLXSDateTimesSame("2014-01-01T13:35:08Z");
+        assertJavaxAndFTLXSDateTimesSame("2014-01-01T13:35:08+02:00");
+        
+        // Default time zone:
+        assertJavaxAndFTLXSDateTimesSame("2014-01-01T13:35:08"); // winter
+        assertJavaxAndFTLXSDateTimesSame("2014-07-01T13:35:08"); // summer
+        
+        // Proleptic Gregorian
+        assertJavaxAndFTLXSDateTimesSame("1500-01-01T13:35:08Z");
+        assertJavaxAndFTLXSDateTimesSame("0200-01-01T13:35:08Z");
+        assertJavaxAndFTLXSDateTimesSame("0001-01-01T00:00:00+05:00");
+        
+        // BC
+        assertJavaxAndFTLXSDateTimesSame("0001-01-01T13:35:08Z");
+        assertJavaxAndFTLXSDateTimesSame("-0001-01-01T13:35:08Z");
+        
+        // Hour 24
+        assertJavaxAndFTLXSDateTimesSame("2014-01-01T23:59:59");
+        if (isAtLeastJava6()) { // Java 5 has broken parser that doesn't allow 
24.
+            assertJavaxAndFTLXSDateTimesSame("2014-01-31T24:00:00");
+            assertJavaxAndFTLXSDateTimesSame("2014-01-01T24:00:00");
+        }
+        assertJavaxAndFTLXSDateTimesSame("2014-01-02T00:00:00");  // same as 
the previous
+        assertJavaxAndFTLXSDateTimesSame("2014-02-01T00:00:00");  // same as 
the previous
+        
+        // Under ms
+        assertJavaxAndFTLXSDateTimesSame("2014-01-01T23:59:59.123456789");
+        assertJavaxAndFTLXSDateTimesSame("2014-01-01T23:59:59.1235");
+    }
+    
+    private boolean isAtLeastJava6() {
+        try {
+            Class.forName("java.lang.management.LockInfo");
+        } catch (ClassNotFoundException e) {
+            return false;
+        }
+        return true;
+    }
+
+    private final DatatypeFactory datetypeFactory;
+    {
+        try {
+            datetypeFactory = DatatypeFactory.newInstance();
+        } catch (DatatypeConfigurationException e) {
+            throw new RuntimeException(e);
+        } 
+    }
+    
+    private void assertJavaxAndFTLXSDateTimesSame(String s) throws 
DateParseException {
+        XMLGregorianCalendar xgc = datetypeFactory.newXMLGregorianCalendar(s);
+        Date javaxDate = xgc.toGregorianCalendar().getTime();
+        Date ftlDate = _DateUtil.parseXSDateTime(s, TimeZone.getDefault(), 
cf2dc);
+        assertEquals(javaxDate, ftlDate);
+    }
+
+    private void assertDateParsing(String expected, String parsed, TimeZone 
tz) throws DateParseException {
+        assertDateParsing(expected, expected, parsed, tz);
+    }
+
+    private void assertDateParsing(String expectedXS, String expectedISO8601, 
String parsed, TimeZone tz)
+            throws DateParseException {
+        if (expectedXS != null) {
+            assertEquals(
+                    expectedXS,
+                    df.format(_DateUtil.parseXSDate(parsed, tz, cf2dc)));
+        }
+        if (expectedISO8601 != null) {
+            assertEquals(
+                    expectedISO8601,
+                    df.format(_DateUtil.parseISO8601Date(parsed, tz, cf2dc)));
+        }
+    }
+
+    private void assertDateTimeParsing(String expected, String parsed, 
TimeZone tz) throws DateParseException {
+        assertDateTimeParsing(expected, expected, parsed, tz);
+    }
+
+    private void assertDateTimeParsing(String expectedXS, String 
expectedISO8601, String parsed, TimeZone tz)
+            throws DateParseException {
+        if (expectedXS != null) {
+            assertEquals(
+                    expectedXS,
+                    df.format(_DateUtil.parseXSDateTime(parsed, tz, cf2dc)));
+        }
+        if (expectedISO8601 != null) {
+            assertEquals(
+                    expectedISO8601,
+                    df.format(_DateUtil.parseISO8601DateTime(parsed, tz, 
cf2dc)));
+        }
+    }
+
+    private void assertTimeParsing(String expected, String parsed, TimeZone 
tz) throws DateParseException {
+        assertTimeParsing(expected, expected, parsed, tz);
+    }
+
+    private void assertTimeParsing(String expectedXS, String expectedISO8601, 
String parsed, TimeZone tz)
+            throws DateParseException {
+        if (expectedXS != null) {
+            assertEquals(
+                    expectedXS,
+                    df.format(_DateUtil.parseXSTime(parsed, tz, cf2dc)));
+        }
+        if (expectedISO8601 != null) {
+            assertEquals(
+                    expectedISO8601,
+                    df.format(_DateUtil.parseISO8601Time(parsed, tz, cf2dc)));
+        }
+    }
+    
+    private void assertDateMalformed(String parsed) {
+        try {
+            _DateUtil.parseXSDate(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+        try {
+            _DateUtil.parseISO8601Date(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+    }
+
+    private void assertTimeMalformed(String parsed) {
+        try {
+            _DateUtil.parseXSTime(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+        try {
+            _DateUtil.parseISO8601Time(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+    }
+
+    private void assertDateTimeMalformed(String parsed) {
+        try {
+            _DateUtil.parseXSDateTime(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+        try {
+            _DateUtil.parseISO8601DateTime(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+    }
+
+    private void assertISO8601DateMalformed(String parsed) {
+        try {
+            _DateUtil.parseISO8601Date(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+    }
+    
+    private void assertISO8601TimeMalformed(String parsed) {
+        try {
+            _DateUtil.parseISO8601Time(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+    }
+    
+    private void assertISO8601DateTimeMalformed(String parsed) {
+        try {
+            _DateUtil.parseISO8601DateTime(parsed, _DateUtil.UTC, cf2dc);
+            fail();
+        } catch (DateParseException e) {
+            // Expected
+            echo(e);
+        }
+    }
+    
+    private void echo(@SuppressWarnings("unused") DateParseException e) {
+        // System.out.println(e);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/FTLUtilTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/FTLUtilTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/FTLUtilTest.java
new file mode 100644
index 0000000..f1e44f3
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/FTLUtilTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.util;
+
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class FTLUtilTest {
+
+    @Test
+    public void testEscapeStringLiteralPart() {
+        assertEquals("", FTLUtil.escapeStringLiteralPart(""));
+        assertEquals("abc", FTLUtil.escapeStringLiteralPart("abc"));
+        assertEquals("{", FTLUtil.escapeStringLiteralPart("{"));
+        assertEquals("a{b}c", FTLUtil.escapeStringLiteralPart("a{b}c"));
+        assertEquals("a#b", FTLUtil.escapeStringLiteralPart("a#b"));
+        assertEquals("a$b", FTLUtil.escapeStringLiteralPart("a$b"));
+        assertEquals("a#\\{b}c", FTLUtil.escapeStringLiteralPart("a#{b}c"));
+        assertEquals("a$\\{b}c", FTLUtil.escapeStringLiteralPart("a${b}c"));
+        assertEquals("a'c\\\"d", FTLUtil.escapeStringLiteralPart("a'c\"d", 
'"'));
+        assertEquals("a\\'c\"d", FTLUtil.escapeStringLiteralPart("a'c\"d", 
'\''));
+        assertEquals("a\\'c\"d", FTLUtil.escapeStringLiteralPart("a'c\"d", 
'\''));
+        assertEquals("\\n\\r\\t\\f\\x0002\\\\", 
FTLUtil.escapeStringLiteralPart("\n\r\t\f\u0002\\"));
+        assertEquals("\\l\\g\\a", FTLUtil.escapeStringLiteralPart("<>&"));
+    }
+
+    @Test
+    public void testEscapeStringLiteralAll() {
+        assertFTLEsc("", "", "", "", "\"\"");
+        assertFTLEsc("\'", "\\'", "'", "\\'", "\"'\"");
+        assertFTLEsc("\"", "\\\"", "\\\"", "\"", "'\"'");
+        assertFTLEsc("\"", "\\\"", "\\\"", "\"", "'\"'");
+        assertFTLEsc("foo", "foo", "foo", "foo", "\"foo\"");
+        assertFTLEsc("foo's", "foo\\'s", "foo's", "foo\\'s", "\"foo's\"");
+        assertFTLEsc("foo \"", "foo \\\"", "foo \\\"", "foo \"", "'foo \"'");
+        assertFTLEsc("foo's \"", "foo\\'s \\\"", "foo's \\\"", "foo\\'s \"", 
"\"foo's \\\"\"");
+        assertFTLEsc("foo\nb\u0000c", "foo\\nb\\x0000c", "foo\\nb\\x0000c", 
"foo\\nb\\x0000c", "\"foo\\nb\\x0000c\"");
+    }
+
+    private void assertFTLEsc(String s, String partAny, String partQuot, 
String partApos, String quoted) {
+        assertEquals(partAny, FTLUtil.escapeStringLiteralPart(s));
+        assertEquals(partQuot, FTLUtil.escapeStringLiteralPart(s, '\"'));
+        assertEquals(partApos, FTLUtil.escapeStringLiteralPart(s, '\''));
+        assertEquals(quoted, FTLUtil.toStringLiteral(s));
+    }
+
+    @Test
+    public void testUnescapeStringLiteralPart() throws Exception {
+        assertEquals("", FTLUtil.unescapeStringLiteralPart(""));
+        assertEquals("1", FTLUtil.unescapeStringLiteralPart("1"));
+        assertEquals("123", FTLUtil.unescapeStringLiteralPart("123"));
+        assertEquals("1&2&3", FTLUtil.unescapeStringLiteralPart("1\\a2\\a3"));
+        assertEquals("&", FTLUtil.unescapeStringLiteralPart("\\a"));
+        assertEquals("&&&", FTLUtil.unescapeStringLiteralPart("\\a\\a\\a"));
+        assertEquals(
+                "\u0000\u0000&\u0000\u0000\u0000\u0000",
+                
FTLUtil.unescapeStringLiteralPart("\\x0000\\x0000\\a\\x0000\\x000\\x00\\x0"));
+        assertEquals(
+                "'\"\n\b\u0000c><&{\\",
+                
FTLUtil.unescapeStringLiteralPart("\\'\\\"\\n\\b\\x0000c\\g\\l\\a\\{\\\\"));
+    }
+
+    @Test
+    public void testEscapeIdentifier() {
+        assertNull(FTLUtil.escapeIdentifier(null));
+        assertEquals("", FTLUtil.escapeIdentifier(""));
+        assertEquals("a", FTLUtil.escapeIdentifier("a"));
+        assertEquals("ab", FTLUtil.escapeIdentifier("ab"));
+        assertEquals("\\.", FTLUtil.escapeIdentifier("."));
+        assertEquals("\\.\\:\\-", FTLUtil.escapeIdentifier(".:-"));
+        assertEquals("a\\.b", FTLUtil.escapeIdentifier("a.b"));
+        assertEquals("a\\.b\\:c\\-d", FTLUtil.escapeIdentifier("a.b:c-d"));
+    }
+
+    @Test
+    public void testIsNonEscapedIdentifierStart() {
+        assertTrue(FTLUtil.isNonEscapedIdentifierPart('a'));
+        assertTrue(FTLUtil.isNonEscapedIdentifierPart('á'));
+        assertTrue(FTLUtil.isNonEscapedIdentifierPart('1'));
+        assertFalse(FTLUtil.isNonEscapedIdentifierPart('-'));
+        assertFalse(FTLUtil.isNonEscapedIdentifierPart(' '));
+        assertFalse(FTLUtil.isNonEscapedIdentifierPart('\u0000'));
+        assertFalse(FTLUtil.isNonEscapedIdentifierPart('\\'));
+    }
+
+    @Test
+    public void testisNonEscapedIdentifierStart() {
+        assertTrue(FTLUtil.isNonEscapedIdentifierStart('a'));
+        assertTrue(FTLUtil.isNonEscapedIdentifierStart('á'));
+        assertFalse(FTLUtil.isNonEscapedIdentifierStart('1'));
+        assertFalse(FTLUtil.isNonEscapedIdentifierStart('-'));
+        assertFalse(FTLUtil.isNonEscapedIdentifierStart(' '));
+        assertFalse(FTLUtil.isNonEscapedIdentifierStart('\u0000'));
+        assertFalse(FTLUtil.isNonEscapedIdentifierStart('\\'));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/NumberUtilTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/NumberUtilTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/NumberUtilTest.java
new file mode 100644
index 0000000..d5709da
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/NumberUtilTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.util;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.junit.Test;
+
+import junit.framework.TestCase;
+
+public class NumberUtilTest extends TestCase {
+
+    @Test
+    public void testGetSignum() {
+        assertEquals(1, 
_NumberUtil.getSignum(Double.valueOf(Double.POSITIVE_INFINITY)));
+        assertEquals(1, _NumberUtil.getSignum(Double.valueOf(3)));
+        assertEquals(0, _NumberUtil.getSignum(Double.valueOf(0)));
+        assertEquals(-1, _NumberUtil.getSignum(Double.valueOf(-3)));
+        assertEquals(-1, 
_NumberUtil.getSignum(Double.valueOf(Double.NEGATIVE_INFINITY)));
+        try {
+            _NumberUtil.getSignum(Double.valueOf(Double.NaN));
+            fail();
+        } catch (ArithmeticException e) {
+            // expected
+        }
+        
+        assertEquals(1, 
_NumberUtil.getSignum(Float.valueOf(Float.POSITIVE_INFINITY)));
+        assertEquals(1, _NumberUtil.getSignum(Float.valueOf(3)));
+        assertEquals(0, _NumberUtil.getSignum(Float.valueOf(0)));
+        assertEquals(-1, _NumberUtil.getSignum(Float.valueOf(-3)));
+        assertEquals(-1, 
_NumberUtil.getSignum(Float.valueOf(Float.NEGATIVE_INFINITY)));
+        try {
+            _NumberUtil.getSignum(Float.valueOf(Float.NaN));
+            fail();
+        } catch (ArithmeticException e) {
+            // expected
+        }
+        
+        assertEquals(1, _NumberUtil.getSignum(Long.valueOf(3)));
+        assertEquals(0, _NumberUtil.getSignum(Long.valueOf(0)));
+        assertEquals(-1, _NumberUtil.getSignum(Long.valueOf(-3)));
+        
+        assertEquals(1, _NumberUtil.getSignum(Integer.valueOf(3)));
+        assertEquals(0, _NumberUtil.getSignum(Integer.valueOf(0)));
+        assertEquals(-1, _NumberUtil.getSignum(Integer.valueOf(-3)));
+        
+        assertEquals(1, _NumberUtil.getSignum(Short.valueOf((short) 3)));
+        assertEquals(0, _NumberUtil.getSignum(Short.valueOf((short) 0)));
+        assertEquals(-1, _NumberUtil.getSignum(Short.valueOf((short) -3)));
+        
+        assertEquals(1, _NumberUtil.getSignum(Byte.valueOf((byte) 3)));
+        assertEquals(0, _NumberUtil.getSignum(Byte.valueOf((byte) 0)));
+        assertEquals(-1, _NumberUtil.getSignum(Byte.valueOf((byte) -3)));
+        
+        assertEquals(1, _NumberUtil.getSignum(BigDecimal.valueOf(3)));
+        assertEquals(0, _NumberUtil.getSignum(BigDecimal.valueOf(0)));
+        assertEquals(-1, _NumberUtil.getSignum(BigDecimal.valueOf(-3)));
+        
+        assertEquals(1, _NumberUtil.getSignum(BigInteger.valueOf(3)));
+        assertEquals(0, _NumberUtil.getSignum(BigInteger.valueOf(0)));
+        assertEquals(-1, _NumberUtil.getSignum(BigInteger.valueOf(-3)));
+    }
+    
+    @Test
+    public void testIsBigDecimalInteger() {
+        BigDecimal n1 = new BigDecimal("1.125");
+        if (n1.precision() != 4 || n1.scale() != 3) {
+            throw new RuntimeException("Wrong: " +  n1);
+        }
+        BigDecimal n2 = new BigDecimal("1.125").subtract(new 
BigDecimal("0.005"));
+        if (n2.precision() != 4 || n2.scale() != 3) {
+            throw new RuntimeException("Wrong: " +  n2);
+        }
+        BigDecimal n3 = new BigDecimal("123");
+        BigDecimal n4 = new BigDecimal("6000");
+        BigDecimal n5 = new BigDecimal("1.12345").subtract(new 
BigDecimal("0.12345"));
+        if (n5.precision() != 6 || n5.scale() != 5) {
+            throw new RuntimeException("Wrong: " +  n5);
+        }
+        BigDecimal n6 = new BigDecimal("0"); 
+        BigDecimal n7 = new BigDecimal("0.001").subtract(new 
BigDecimal("0.001")); 
+        BigDecimal n8 = new BigDecimal("60000.5").subtract(new 
BigDecimal("0.5")); 
+        BigDecimal n9 = new BigDecimal("6").movePointRight(3).setScale(-3);
+        
+        BigDecimal[] ns = new BigDecimal[] {
+                n1, n2, n3, n4, n5, n6, n7, n8, n9,
+                n1.negate(), n2.negate(), n3.negate(), n4.negate(), 
n5.negate(), n6.negate(), n7.negate(), n8.negate(),
+                n9.negate(),
+        };
+        
+        for (BigDecimal n : ns) {
+            assertEquals(n.doubleValue() == n.longValue(), 
_NumberUtil.isIntegerBigDecimal(n));
+        }
+        
+    }
+    
+    @Test
+    public void testToIntExcact() {
+        for (int n : new int[] { Integer.MIN_VALUE, Byte.MIN_VALUE, -1, 0, 1, 
Byte.MAX_VALUE, Integer.MAX_VALUE }) {
+            if (n != Integer.MIN_VALUE && n != Integer.MAX_VALUE) {
+                assertEquals(n, _NumberUtil.toIntExact(Byte.valueOf((byte) 
n)));
+                assertEquals(n, _NumberUtil.toIntExact(Short.valueOf((short) 
n)));
+                assertEquals(n, _NumberUtil.toIntExact(Float.valueOf(n)));
+            }
+            assertEquals(n, _NumberUtil.toIntExact(Integer.valueOf(n)));
+            assertEquals(n, _NumberUtil.toIntExact(Long.valueOf(n)));
+            assertEquals(n, _NumberUtil.toIntExact(Double.valueOf(n)));
+            assertEquals(n, _NumberUtil.toIntExact(BigDecimal.valueOf(n)));
+            assertEquals(n, _NumberUtil.toIntExact(BigDecimal.valueOf(n * 
10L).divide(BigDecimal.TEN)));
+            assertEquals(n, _NumberUtil.toIntExact(BigInteger.valueOf(n)));
+        }
+
+        try {
+            _NumberUtil.toIntExact(Long.valueOf(Integer.MIN_VALUE - 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(Long.valueOf(Integer.MAX_VALUE + 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+
+        try {
+            _NumberUtil.toIntExact(Float.valueOf(1.00001f));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(Float.valueOf(Integer.MIN_VALUE - 129L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(Float.valueOf(Integer.MAX_VALUE));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        
+        try {
+            _NumberUtil.toIntExact(Double.valueOf(1.00001));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(Double.valueOf(Integer.MIN_VALUE - 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(Double.valueOf(Integer.MAX_VALUE + 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        
+        try {
+            _NumberUtil.toIntExact(new BigDecimal("100.000001"));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(BigDecimal.valueOf(Integer.MIN_VALUE - 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(BigDecimal.valueOf(Integer.MAX_VALUE + 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        
+        try {
+            _NumberUtil.toIntExact(BigInteger.valueOf(Integer.MIN_VALUE - 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+        try {
+            _NumberUtil.toIntExact(BigInteger.valueOf(Integer.MAX_VALUE + 1L));
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+    }
+
+}

Reply via email to