You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2018/03/21 16:44:37 UTC

[07/13] groovy git commit: move datetime extensions to their own module

http://git-wip-us.apache.org/repos/asf/groovy/blob/48a54936/subprojects/groovy-dateutil/src/main/java/org/apache/groovy/dateutil/extensions/DateUtilExtensions.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-dateutil/src/main/java/org/apache/groovy/dateutil/extensions/DateUtilExtensions.java b/subprojects/groovy-dateutil/src/main/java/org/apache/groovy/dateutil/extensions/DateUtilExtensions.java
new file mode 100644
index 0000000..19cf6dd
--- /dev/null
+++ b/subprojects/groovy-dateutil/src/main/java/org/apache/groovy/dateutil/extensions/DateUtilExtensions.java
@@ -0,0 +1,778 @@
+/*
+ *  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.groovy.dateutil.extensions;
+
+import groovy.lang.Closure;
+import groovy.lang.GroovyRuntimeException;
+import org.codehaus.groovy.runtime.DefaultGroovyMethodsSupport;
+
+import java.sql.Timestamp;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * This class defines new groovy methods which appear on normal JDK
+ * Date and Calendar classes inside the Groovy environment.
+ */
+public class DateUtilExtensions extends DefaultGroovyMethodsSupport {
+
+    /**
+     * Support the subscript operator for a Date.
+     *
+     * @param self  a Date
+     * @param field a Calendar field, e.g. MONTH
+     * @return the value for the given field, e.g. FEBRUARY
+     * @see java.util.Calendar
+     * @since 1.5.5
+     */
+    public static int getAt(Date self, int field) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(self);
+        return cal.get(field);
+    }
+
+    /**
+     * Convert a Date to a Calendar.
+     *
+     * @param self a Date
+     * @return a Calendar corresponding to the given Date
+     * @since 1.7.6
+     */
+    public static Calendar toCalendar(Date self) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(self);
+        return cal;
+    }
+
+    /**
+     * Support the subscript operator for a Calendar.
+     *
+     * @param self  a Calendar
+     * @param field a Calendar field, e.g. MONTH
+     * @return the value for the given field, e.g. FEBRUARY
+     * @see java.util.Calendar
+     * @since 1.7.3
+     */
+    public static int getAt(Calendar self, int field) {
+        return self.get(field);
+    }
+
+    /**
+     * Support the subscript operator for mutating a Calendar.
+     * Example usage:
+     * <pre>
+     * import static java.util.Calendar.*
+     * def cal = Calendar.instance
+     * cal[DAY_OF_WEEK] = MONDAY
+     * cal[MONTH] = MARCH
+     * println cal.time // A Monday in March
+     * </pre>
+     *
+     * @param self  A Calendar
+     * @param field A Calendar field, e.g. MONTH
+     * @param value The value for the given field, e.g. FEBRUARY
+     * @see java.util.Calendar#set(int, int)
+     * @since 1.7.3
+     */
+    public static void putAt(Calendar self, int field, int value) {
+        self.set(field, value);
+    }
+
+    /**
+     * Support the subscript operator for mutating a Date.
+     *
+     * @param self  A Date
+     * @param field A Calendar field, e.g. MONTH
+     * @param value The value for the given field, e.g. FEBRUARY
+     * @see #putAt(java.util.Calendar, int, int)
+     * @see java.util.Calendar#set(int, int)
+     * @since 1.7.3
+     */
+    public static void putAt(Date self, int field, int value) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(self);
+        putAt(cal, field, value);
+        self.setTime(cal.getTimeInMillis());
+    }
+
+    /**
+     * Support mutating a Calendar with a Map.
+     * <p>
+     * The map values are the normal values provided as the
+     * second parameter to <code>java.util.Calendar#set(int, int)</code>.
+     * The keys can either be the normal fields values provided as
+     * the first parameter to that method or one of the following Strings:
+     * <table border="1" cellpadding="4">
+     *   <caption>Calendar index values</caption>
+     *   <tr><td>year</td><td>Calendar.YEAR</td></tr>
+     *   <tr><td>month</td><td>Calendar.MONTH</td></tr>
+     *   <tr><td>date</td><td>Calendar.DATE</td></tr>
+     *   <tr><td>dayOfMonth</td><td>Calendar.DATE</td></tr>
+     *   <tr><td>hourOfDay</td><td>Calendar.HOUR_OF_DAY</td></tr>
+     *   <tr><td>minute</td><td>Calendar.MINUTE</td></tr>
+     *   <tr><td>second</td><td>Calendar.SECOND</td></tr>
+     * </table>
+     * Example usage:
+     * <pre>
+     * import static java.util.Calendar.*
+     * def cal = Calendar.instance
+     * def m = [:]
+     * m[YEAR] = 2010
+     * m[MONTH] = DECEMBER
+     * m[DATE] = 25
+     * cal.set(m)
+     * println cal.time // Christmas 2010
+     *
+     * cal.set(year:2011, month:DECEMBER, date:25)
+     * println cal.time // Christmas 2010
+     * </pre>
+     *
+     * @param self    A Calendar
+     * @param updates A Map of Calendar keys and values
+     * @see java.util.Calendar#set(int, int)
+     * @see java.util.Calendar#set(int, int, int, int, int, int)
+     * @since 1.7.3
+     */
+    public static void set(Calendar self, Map<Object, Integer> updates) {
+        for (Map.Entry<Object, Integer> entry : updates.entrySet()) {
+            Object key = entry.getKey();
+            if (key instanceof String) key = CAL_MAP.get(key);
+            if (key instanceof Integer) self.set((Integer) key, entry.getValue());
+        }
+    }
+
+    /**
+     * Legacy alias for copyWith. Will be deprecated and removed in future versions of Groovy.
+     *
+     * @see #copyWith(java.util.Calendar, java.util.Map)
+     * @since 1.7.3
+     */
+    public static Calendar updated(Calendar self, Map<Object, Integer> updates) {
+        Calendar result = (Calendar) self.clone();
+        set(result, updates);
+        return result;
+    }
+
+    /**
+     * Support creating a new Date having similar properties to
+     * an existing Date (which remains unaltered) but with
+     * some fields updated according to a Map of changes.
+     * <p>
+     * Example usage:
+     * <pre>
+     * import static java.util.Calendar.YEAR
+     * def now = Calendar.instance
+     * def nextYear = now[YEAR] + 1
+     * def oneYearFromNow = now.copyWith(year: nextYear)
+     * println now.time
+     * println oneYearFromNow.time
+     * </pre>
+     *
+     * @param self    A Calendar
+     * @param updates A Map of Calendar keys and values
+     * @return The newly created Calendar
+     * @see java.util.Calendar#set(int, int)
+     * @see java.util.Calendar#set(int, int, int, int, int, int)
+     * @see #set(java.util.Calendar, java.util.Map)
+     * @since 2.2.0
+     */
+    public static Calendar copyWith(Calendar self, Map<Object, Integer> updates) {
+        Calendar result = (Calendar) self.clone();
+        set(result, updates);
+        return result;
+    }
+
+    /**
+     * Support mutating a Date with a Map.
+     * <p>
+     * The map values are the normal values provided as the
+     * second parameter to <code>java.util.Calendar#set(int, int)</code>.
+     * The keys can either be the normal fields values provided as
+     * the first parameter to that method or one of the following Strings:
+     * <table border="1" cellpadding="4">
+     *   <caption>Calendar index values</caption>
+     *   <tr><td>year</td><td>Calendar.YEAR</td></tr>
+     *   <tr><td>month</td><td>Calendar.MONTH</td></tr>
+     *   <tr><td>date</td><td>Calendar.DATE</td></tr>
+     *   <tr><td>dayOfMonth</td><td>Calendar.DATE</td></tr>
+     *   <tr><td>hourOfDay</td><td>Calendar.HOUR_OF_DAY</td></tr>
+     *   <tr><td>minute</td><td>Calendar.MINUTE</td></tr>
+     *   <tr><td>second</td><td>Calendar.SECOND</td></tr>
+     * </table>
+     * Example usage:
+     * <pre>
+     * import static java.util.Calendar.YEAR
+     * def date = new Date()
+     * def nextYear = date[YEAR] + 1
+     * date.set(year: nextYear)
+     * println date
+     * </pre>
+     *
+     * @param self    A Date
+     * @param updates A Map of Calendar keys and values
+     * @see java.util.Calendar#set(int, int)
+     * @see #set(java.util.Calendar, java.util.Map)
+     * @since 1.7.3
+     */
+    public static void set(Date self, Map<Object, Integer> updates) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(self);
+        set(cal, updates);
+        self.setTime(cal.getTimeInMillis());
+    }
+
+    /**
+     * Legacy alias for copyWith. Will be deprecated and removed in future versions of Groovy.
+     *
+     * @see #copyWith(java.util.Date, java.util.Map)
+     * @since 1.7.3
+     */
+    public static Date updated(Date self, Map<Object, Integer> updates) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(self);
+        set(cal, updates);
+        return cal.getTime();
+    }
+
+    /**
+     * Support creating a new Date having similar properties to
+     * an existing Date (which remains unaltered) but with
+     * some fields updated according to a Map of changes.
+     * <p>
+     * Example usage:
+     * <pre>
+     * import static java.util.Calendar.YEAR
+     * def today = new Date()
+     * def nextYear = today[YEAR] + 1
+     * def oneYearFromNow = today.copyWith(year: nextYear)
+     * println today
+     * println oneYearFromNow
+     * </pre>
+     *
+     * @param self    A Date
+     * @param updates A Map of Calendar keys and values
+     * @return The newly created Date
+     * @see java.util.Calendar#set(int, int)
+     * @see #set(java.util.Date, java.util.Map)
+     * @see #copyWith(java.util.Calendar, java.util.Map)
+     * @since 2.2.0
+     */
+    public static Date copyWith(Date self, Map<Object, Integer> updates) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(self);
+        set(cal, updates);
+        return cal.getTime();
+    }
+
+    private static final Map<String, Integer> CAL_MAP = new HashMap<String, Integer>();
+
+    static {
+        CAL_MAP.put("year", Calendar.YEAR);
+        CAL_MAP.put("month", Calendar.MONTH);
+        CAL_MAP.put("date", Calendar.DATE);
+        CAL_MAP.put("dayOfMonth", Calendar.DATE);
+        CAL_MAP.put("hourOfDay", Calendar.HOUR_OF_DAY);
+        CAL_MAP.put("minute", Calendar.MINUTE);
+        CAL_MAP.put("second", Calendar.SECOND);
+    }
+
+    /**
+     * Increment a Date by one day.
+     *
+     * @param self a Date
+     * @return the next days date
+     * @since 1.0
+     */
+    public static Date next(Date self) {
+        return plus(self, 1);
+    }
+
+    /**
+     * Increment a Calendar by one day.
+     *
+     * @param self a Calendar
+     * @return a new Calendar set to the next day
+     * @since 1.8.7
+     */
+    public static Calendar next(Calendar self) {
+        Calendar result = (Calendar) self.clone();
+        result.add(Calendar.DATE, 1);
+        return result;
+    }
+
+    /**
+     * Decrement a Calendar by one day.
+     *
+     * @param self a Calendar
+     * @return a new Calendar set to the previous day
+     * @since 1.8.7
+     */
+    public static Calendar previous(Calendar self) {
+        Calendar result = (Calendar) self.clone();
+        result.add(Calendar.DATE, -1);
+        return result;
+    }
+
+    /**
+     * Increment a java.sql.Date by one day.
+     *
+     * @param self a java.sql.Date
+     * @return the next days date
+     * @since 1.0
+     */
+    public static java.sql.Date next(java.sql.Date self) {
+        return new java.sql.Date(next((Date) self).getTime());
+    }
+
+    /**
+     * Decrement a Date by one day.
+     *
+     * @param self a Date
+     * @return the previous days date
+     * @since 1.0
+     */
+    public static Date previous(Date self) {
+        return minus(self, 1);
+    }
+
+    /**
+     * Decrement a java.sql.Date by one day.
+     *
+     * @param self a java.sql.Date
+     * @return the previous days date
+     * @since 1.0
+     */
+    public static java.sql.Date previous(java.sql.Date self) {
+        return new java.sql.Date(previous((Date) self).getTime());
+    }
+
+    /**
+     * Add a number of days to this date and returns the new date.
+     *
+     * @param self a Date
+     * @param days the number of days to increase
+     * @return the new date
+     * @since 1.0
+     */
+    public static Date plus(Date self, int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(self);
+        calendar.add(Calendar.DATE, days);
+        return calendar.getTime();
+    }
+
+    /**
+     * Add a number of days to this date and returns the new date.
+     *
+     * @param self a java.sql.Date
+     * @param days the number of days to increase
+     * @return the new date
+     * @since 1.0
+     */
+    public static java.sql.Date plus(java.sql.Date self, int days) {
+        return new java.sql.Date(plus((Date) self, days).getTime());
+    }
+
+    /**
+     * Add number of days to this Timestamp and returns the new Timestamp object.
+     *
+     * @param self a Timestamp
+     * @param days the number of days to increase
+     * @return the new Timestamp
+     */
+    public static Timestamp plus(Timestamp self, int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(self);
+        calendar.add(Calendar.DATE, days);
+        Timestamp ts = new Timestamp(calendar.getTime().getTime());
+        ts.setNanos(self.getNanos());
+        return ts;
+    }
+
+    /**
+     * Subtract a number of days from this date and returns the new date.
+     *
+     * @param self a Date
+     * @param days the number of days to subtract
+     * @return the new date
+     * @since 1.0
+     */
+    public static Date minus(Date self, int days) {
+        return plus(self, -days);
+    }
+
+    /**
+     * Subtract a number of days from this date and returns the new date.
+     *
+     * @param self a java.sql.Date
+     * @param days the number of days to subtract
+     * @return the new date
+     * @since 1.0
+     */
+    public static java.sql.Date minus(java.sql.Date self, int days) {
+        return new java.sql.Date(minus((Date) self, days).getTime());
+    }
+
+    /**
+     * Subtract a number of days from this Timestamp and returns the new Timestamp object.
+     *
+     * @param self a Timestamp
+     * @param days the number of days to subtract
+     * @return the new Timestamp
+     */
+    public static Timestamp minus(Timestamp self, int days) {
+        return plus(self, -days);
+    }
+
+    /**
+     * Subtract another date from this one and return the number of days of the difference.
+     * <p>
+     * Date self = Date then + (Date self - Date then)
+     * <p>
+     * IOW, if self is before then the result is a negative value.
+     *
+     * @param self a Calendar
+     * @param then another Calendar
+     * @return number of days
+     * @since 1.6.0
+     */
+    public static int minus(Calendar self, Calendar then) {
+        Calendar a = self;
+        Calendar b = then;
+
+        boolean swap = a.before(b);
+
+        if (swap) {
+            Calendar t = a;
+            a = b;
+            b = t;
+        }
+
+        int days = 0;
+
+        b = (Calendar) b.clone();
+
+        while (a.get(Calendar.YEAR) > b.get(Calendar.YEAR)) {
+            days += 1 + (b.getActualMaximum(Calendar.DAY_OF_YEAR) - b.get(Calendar.DAY_OF_YEAR));
+            b.set(Calendar.DAY_OF_YEAR, 1);
+            b.add(Calendar.YEAR, 1);
+        }
+
+        days += a.get(Calendar.DAY_OF_YEAR) - b.get(Calendar.DAY_OF_YEAR);
+
+        if (swap) days = -days;
+
+        return days;
+    }
+
+    /**
+     * Subtract another Date from this one and return the number of days of the difference.
+     * <p>
+     * Date self = Date then + (Date self - Date then)
+     * <p>
+     * IOW, if self is before then the result is a negative value.
+     *
+     * @param self a Date
+     * @param then another Date
+     * @return number of days
+     * @since 1.6.0
+     */
+    public static int minus(Date self, Date then) {
+        Calendar a = (Calendar) Calendar.getInstance().clone();
+        a.setTime(self);
+        Calendar b = (Calendar) Calendar.getInstance().clone();
+        b.setTime(then);
+        return minus(a, b);
+    }
+
+    /**
+     * <p>Create a String representation of this date according to the given
+     * format pattern.
+     * <p>
+     * <p>For example, if the system timezone is GMT,
+     * <code>new Date(0).format('MM/dd/yy')</code> would return the string
+     * <code>"01/01/70"</code>. See documentation for {@link java.text.SimpleDateFormat}
+     * for format pattern use.
+     * <p>
+     * <p>Note that a new DateFormat instance is created for every
+     * invocation of this method (for thread safety).
+     *
+     * @param self   a Date
+     * @param format the format pattern to use according to {@link java.text.SimpleDateFormat}
+     * @return a string representation of this date.
+     * @see java.text.SimpleDateFormat
+     * @since 1.5.7
+     */
+    public static String format(Date self, String format) {
+        return new SimpleDateFormat(format).format(self);
+    }
+
+    /**
+     * <p>Create a String representation of this date according to the given
+     * format pattern and timezone.
+     * <p>
+     * <p>For example:
+     * <code>
+     * def d = new Date(0)
+     * def tz = TimeZone.getTimeZone('GMT')
+     * println d.format('dd/MMM/yyyy', tz)
+     * </code> would return the string
+     * <code>"01/Jan/1970"</code>. See documentation for {@link java.text.SimpleDateFormat}
+     * for format pattern use.
+     * <p>
+     * <p>Note that a new DateFormat instance is created for every
+     * invocation of this method (for thread safety).
+     *
+     * @param self   a Date
+     * @param format the format pattern to use according to {@link java.text.SimpleDateFormat}
+     * @param tz     the TimeZone to use
+     * @return a string representation of this date.
+     * @see java.text.SimpleDateFormat
+     * @since 1.8.3
+     */
+    public static String format(Date self, String format, TimeZone tz) {
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        sdf.setTimeZone(tz);
+        return sdf.format(self);
+    }
+
+    /**
+     * <p>Return a string representation of the 'day' portion of this date
+     * according to the locale-specific {@link java.text.DateFormat#SHORT} default format.
+     * For an "en_UK" system locale, this would be <code>dd/MM/yy</code>.
+     * <p>
+     * <p>Note that a new DateFormat instance is created for every
+     * invocation of this method (for thread safety).
+     *
+     * @param self a Date
+     * @return a string representation of this date
+     * @see java.text.DateFormat#getDateInstance(int)
+     * @see java.text.DateFormat#SHORT
+     * @since 1.5.7
+     */
+    public static String getDateString(Date self) {
+        return DateFormat.getDateInstance(DateFormat.SHORT).format(self);
+    }
+
+    /**
+     * <p>Return a string representation of the time portion of this date
+     * according to the locale-specific {@link java.text.DateFormat#MEDIUM} default format.
+     * For an "en_UK" system locale, this would be <code>HH:MM:ss</code>.
+     * <p>
+     * <p>Note that a new DateFormat instance is created for every
+     * invocation of this method (for thread safety).
+     *
+     * @param self a Date
+     * @return a string representing the time portion of this date
+     * @see java.text.DateFormat#getTimeInstance(int)
+     * @see java.text.DateFormat#MEDIUM
+     * @since 1.5.7
+     */
+    public static String getTimeString(Date self) {
+        return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(self);
+    }
+
+    /**
+     * <p>Return a string representation of the date and time time portion of
+     * this Date instance, according to the locale-specific format used by
+     * {@link java.text.DateFormat}.  This method uses the {@link java.text.DateFormat#SHORT}
+     * preset for the day portion and {@link java.text.DateFormat#MEDIUM} for the time
+     * portion of the output string.
+     * <p>
+     * <p>Note that a new DateFormat instance is created for every
+     * invocation of this method (for thread safety).
+     *
+     * @param self a Date
+     * @return a string representation of this date and time
+     * @see java.text.DateFormat#getDateTimeInstance(int, int)
+     * @since 1.5.7
+     */
+    public static String getDateTimeString(Date self) {
+        return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM).format(self);
+    }
+
+    /**
+     * Common code for {@link #clearTime(java.util.Calendar)} and {@link #clearTime(java.util.Date)}
+     * and {@link #clearTime(java.sql.Date)}
+     *
+     * @param self a Calendar to adjust
+     */
+    private static void clearTimeCommon(final Calendar self) {
+        self.set(Calendar.HOUR_OF_DAY, 0);
+        self.clear(Calendar.MINUTE);
+        self.clear(Calendar.SECOND);
+        self.clear(Calendar.MILLISECOND);
+    }
+
+    /**
+     * Clears the time portion of this Date instance; useful utility where
+     * it makes sense to compare month/day/year only portions of a Date.
+     *
+     * @param self a Date
+     * @return the Date but with the time portion cleared
+     * @since 1.6.7
+     */
+    public static Date clearTime(final Date self) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(self);
+        clearTimeCommon(calendar);
+        self.setTime(calendar.getTime().getTime());
+        return self;
+    }
+
+    /**
+     * Clears the time portion of this java.sql.Date instance; useful utility
+     * where it makes sense to compare month/day/year only portions of a Date.
+     *
+     * @param self a java.sql.Date
+     * @return the java.sql.Date but with the time portion cleared
+     * @since 1.6.7
+     */
+    public static java.sql.Date clearTime(final java.sql.Date self) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(self);
+        clearTimeCommon(calendar);
+        self.setTime(calendar.getTime().getTime());
+        return self;
+    }
+
+    /**
+     * Clears the time portion of this Calendar instance; useful utility
+     * where it makes sense to compare month/day/year only portions of a Calendar.
+     *
+     * @param self a Calendar
+     * @return the Calendar but with the time portion cleared
+     * @since 1.6.7
+     */
+    public static Calendar clearTime(final Calendar self) {
+        clearTimeCommon(self);
+        return self;
+    }
+
+    /**
+     * <p>Shortcut for {@link java.text.SimpleDateFormat} to output a String representation
+     * of this calendar instance.  This method respects the Calendar's assigned
+     * {@link java.util.TimeZone}, whereas calling <code>cal.time.format('HH:mm:ss')</code>
+     * would use the system timezone.
+     * <p>Note that Calendar equivalents of <code>date.getDateString()</code>
+     * and variants do not exist because those methods are Locale-dependent.
+     * Although a Calendar may be assigned a {@link java.util.Locale}, that information is
+     * lost and therefore cannot be used to control the default date/time formats
+     * provided by these methods.  Instead, the system Locale would always be
+     * used.  The alternative is to simply call
+     * {@link java.text.DateFormat#getDateInstance(int, java.util.Locale)} and pass the same Locale
+     * that was used for the Calendar.
+     *
+     * @param self    this calendar
+     * @param pattern format pattern
+     * @return String representation of this calendar with the given format.
+     * @see java.text.DateFormat#setTimeZone(java.util.TimeZone)
+     * @see java.text.SimpleDateFormat#format(java.util.Date)
+     * @see #format(java.util.Date, String)
+     * @since 1.6.0
+     */
+    public static String format(Calendar self, String pattern) {
+        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+        sdf.setTimeZone(self.getTimeZone());
+        return sdf.format(self.getTime());
+    }
+
+    /**
+     * Iterates from this date up to the given date, inclusive,
+     * incrementing by one day each time.
+     *
+     * @param self    a Date
+     * @param to      another Date to go up to
+     * @param closure the closure to call
+     * @since 2.2
+     */
+    public static void upto(Date self, Date to, Closure closure) {
+        if (self.compareTo(to) <= 0) {
+            for (Date i = (Date) self.clone(); i.compareTo(to) <= 0; i = next(i)) {
+                closure.call(i);
+            }
+        } else
+            throw new GroovyRuntimeException("The argument (" + to +
+                    ") to upto() cannot be earlier than the value (" + self + ") it's called on.");
+    }
+
+    /**
+     * Iterates from the date represented by this calendar up to the date represented
+     * by the given calendar, inclusive, incrementing by one day each time.
+     *
+     * @param self    a Calendar
+     * @param to      another Calendar to go up to
+     * @param closure the closure to call
+     * @since 2.2
+     */
+    public static void upto(Calendar self, Calendar to, Closure closure) {
+        if (self.compareTo(to) <= 0) {
+            for (Calendar i = (Calendar) self.clone(); i.compareTo(to) <= 0; i = next(i)) {
+                closure.call(i);
+            }
+        } else
+            throw new GroovyRuntimeException("The argument (" + to +
+                    ") to upto() cannot be earlier than the value (" + self + ") it's called on.");
+    }
+
+    /**
+     * Iterates from this date down to the given date, inclusive,
+     * decrementing by one day each time.
+     *
+     * @param self    a Date
+     * @param to      another Date to go down to
+     * @param closure the closure to call
+     * @since 2.2
+     */
+    public static void downto(Date self, Date to, Closure closure) {
+        if (self.compareTo(to) >= 0) {
+            for (Date i = (Date) self.clone(); i.compareTo(to) >= 0; i = previous(i)) {
+                closure.call(i);
+            }
+        } else
+            throw new GroovyRuntimeException("The argument (" + to +
+                    ") to downto() cannot be later than the value (" + self + ") it's called on.");
+    }
+
+    /**
+     * Iterates from the date represented by this calendar up to the date represented
+     * by the given calendar, inclusive, incrementing by one day each time.
+     *
+     * @param self    a Calendar
+     * @param to      another Calendar to go down to
+     * @param closure the closure to call
+     * @since 2.2
+     */
+    public static void downto(Calendar self, Calendar to, Closure closure) {
+        if (self.compareTo(to) >= 0) {
+            for (Calendar i = (Calendar) self.clone(); i.compareTo(to) >= 0; i = previous(i)) {
+                closure.call(i);
+            }
+        } else
+            throw new GroovyRuntimeException("The argument (" + to +
+                    ") to downto() cannot be later than the value (" + self + ") it's called on.");
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/48a54936/subprojects/groovy-dateutil/src/spec/doc/working-with-dateutil-types.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-dateutil/src/spec/doc/working-with-dateutil-types.adoc b/subprojects/groovy-dateutil/src/spec/doc/working-with-dateutil-types.adoc
new file mode 100644
index 0000000..9f26972
--- /dev/null
+++ b/subprojects/groovy-dateutil/src/spec/doc/working-with-dateutil-types.adoc
@@ -0,0 +1,54 @@
+//////////////////////////////////////////
+
+  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.
+
+//////////////////////////////////////////
+
+= Working with legacy Date/Calendar types
+
+The `groovy-dateutil` module supports numerous extensions for working with
+Java's classic `Date` and `Calendar` classes.
+
+You can access the properties of a `Date` or `Calendar` using the normal array index notation
+with the constant field numbers from the `Calendar` class as shown in the following example:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy[tags=calendar_getAt,indent=0]
+-------------------------------------
+<1> Import the constants
+<2> Setting the calendar's year, month and day of month
+<3> Accessing the calendar's day of week
+
+Groovy supports arithmetic on and iteration between `Date` and `Calendar` instances as shown in the following example:
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy[tags=date_arithmetic,indent=0]
+-------------------------------------
+
+You can parse strings into dates and output dates into formatted strings:
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy[tags=date_parsing,indent=0]
+-------------------------------------
+
+You can also create a new Date or Calendar based on an existing one:
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy[tags=date_copyWith,indent=0]
+-------------------------------------

http://git-wip-us.apache.org/repos/asf/groovy/blob/48a54936/subprojects/groovy-dateutil/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-dateutil/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy b/subprojects/groovy-dateutil/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy
new file mode 100644
index 0000000..7599867
--- /dev/null
+++ b/subprojects/groovy-dateutil/src/spec/test/gdk/WorkingWithDateUtilTypesTest.groovy
@@ -0,0 +1,81 @@
+/*
+ *  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 gdk
+
+class WorkingWithDateUtilTypesTest extends GroovyTestCase {
+
+    void testGetAt() {
+        assertScript '''
+        // tag::calendar_getAt[]
+        import static java.util.Calendar.*    // <1>
+
+        def cal = Calendar.instance
+        cal[YEAR] = 2000                      // <2>
+        cal[MONTH] = JANUARY                  // <2>
+        cal[DAY_OF_MONTH] = 1                 // <2>
+        assert cal[DAY_OF_WEEK] == SATURDAY   // <3>
+        // end::calendar_getAt[]
+        '''
+    }
+
+    void testDateArithmetic() {
+        // tag::date_arithmetic[]
+        def yesterday = new Date() - 1
+        def tomorrow = new Date() + 1
+
+        def diffInDays = tomorrow - yesterday
+        assert diffInDays == 2
+
+        int count = 0
+        yesterday.upto(tomorrow) { count++ }
+        assert count == 3
+        // end::date_arithmetic[]
+    }
+
+    void testDateParsing() {
+        assertScript '''
+        import static java.util.Calendar.*
+
+        // tag::date_parsing[]
+        def orig = '2000-01-01'
+        def newYear = Date.parse('yyyy-MM-dd', orig)
+        assert newYear[DAY_OF_WEEK] == SATURDAY
+        assert newYear.format('yyyy-MM-dd') == orig
+        assert newYear.format('dd/MM/yyyy') == '01/01/2000'
+        // end::date_parsing[]
+        '''
+    }
+
+    void testCopyWith() {
+        assertScript '''
+        import static java.util.Calendar.*
+
+        // tag::date_copyWith[]
+        def newYear = Date.parse('yyyy-MM-dd', '2000-01-01')
+        def newYearsEve = newYear.copyWith(
+            year: 1999,
+            month: DECEMBER,
+            dayOfMonth: 31
+        )
+        assert newYearsEve[DAY_OF_WEEK] == FRIDAY
+        // end::date_copyWith[]
+        '''
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/48a54936/subprojects/groovy-dateutil/src/test/java/groovy/DateTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-dateutil/src/test/java/groovy/DateTest.groovy b/subprojects/groovy-dateutil/src/test/java/groovy/DateTest.groovy
new file mode 100644
index 0000000..b014f53
--- /dev/null
+++ b/subprojects/groovy-dateutil/src/test/java/groovy/DateTest.groovy
@@ -0,0 +1,327 @@
+/*
+ *  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 groovy
+
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+
+import static java.util.Calendar.*
+
+class DateTest extends GroovyTestCase {
+    void testCalendarNextPrevious() {
+        TimeZone tz = TimeZone.getTimeZone('GMT+00')
+        Calendar c = getInstance(tz)
+        c[HOUR_OF_DAY] = 6
+        c[YEAR] = 2002
+        c[MONTH] = FEBRUARY
+        c[DATE] = 2
+        c.clearTime()
+        def formatter = new SimpleDateFormat('dd-MMM-yyyy', Locale.US)
+        formatter.calendar.timeZone = tz
+
+        assert formatter.format(c.previous().time) == '01-Feb-2002'
+        assert formatter.format(c.time) == '02-Feb-2002'
+        assert formatter.format(c.next().time) == '03-Feb-2002'
+        def dates = (c.previous()..c.next()).collect{ formatter.format(it.time) }
+        assert dates == ['01-Feb-2002', '02-Feb-2002', '03-Feb-2002']
+    }
+
+    void testDateNextPrevious() {
+        def tz = TimeZone.default
+        def x = new Date()
+        def y = x + 2
+        assert x < y
+        def crossedDaylightSavingBoundary = tz.inDaylightTime(x) ^ tz.inDaylightTime(y)
+        ++x
+        --y
+        if (!crossedDaylightSavingBoundary) assert x == y
+        x += 2
+        assert x > y
+    }
+
+    void testDateRange() {
+        def today = new Date()
+        def later = today + 3
+        def expected = [today, today + 1, today + 2, today + 3]
+        def list = []
+        for (d in today..later) {
+            list << d
+        }
+        assert list == expected
+    }
+
+    void testCalendarIndex() {
+        Calendar c = new GregorianCalendar(2002, FEBRUARY, 2)
+        assert c[MONTH] == FEBRUARY
+        assert c[DAY_OF_WEEK] == SATURDAY
+    }
+
+    void testDateIndex() {
+        Date d = new GregorianCalendar(2002, FEBRUARY, 2).time
+        assert d[MONTH] == FEBRUARY
+        assert d[DAY_OF_WEEK] == SATURDAY
+    }
+
+    void testGDKDateMethods() {
+        Locale defaultLocale = Locale.default
+        TimeZone defaultTZ = TimeZone.default
+        try {
+            Locale locale = Locale.GERMANY
+            Locale.setDefault locale // set this otherwise the test will fail if your locale isn't the same
+            TimeZone.setDefault TimeZone.getTimeZone('Europe/Berlin')
+
+            Date d = new Date(0)
+
+            assertEquals '1970-01-01', d.format('yyyy-MM-dd')
+            assertEquals '01/01/1970', d.format('dd/MM/yyyy', TimeZone.getTimeZone('GMT'))
+            assertEquals DateFormat.getDateInstance(DateFormat.SHORT, locale).format(d), d.dateString
+            assertEquals '01.01.70', d.dateString
+            assertEquals DateFormat.getTimeInstance(DateFormat.MEDIUM, locale).format(d), d.timeString
+            assertEquals '01:00:00', d.timeString
+            assertEquals DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale).format(d), d.dateTimeString
+        } finally {
+            Locale.default = defaultLocale
+            TimeZone.setDefault defaultTZ
+        }
+    }
+
+    void testStaticParse() {
+        TimeZone defaultTZ = TimeZone.default
+        try {
+            TimeZone.setDefault TimeZone.getTimeZone('Etc/GMT')
+
+            Date d = Date.parse('yy/MM/dd hh:mm:ss', '70/01/01 00:00:00')
+
+            assertEquals 0, d.time
+        } finally {
+            TimeZone.setDefault defaultTZ
+        }
+    }
+
+    void testParseWithTimeZone() {
+        TimeZone defaultTZ = TimeZone.default
+        try {
+            TimeZone.default = TimeZone.getTimeZone("GMT+05")
+            def tz = TimeZone.getTimeZone("GMT+03")
+
+            def newYear = Date.parse('yyyy-MM-dd', "2015-01-01", tz)
+
+            assert newYear.toString() == 'Thu Jan 01 02:00:00 GMT+05:00 2015'
+        } finally {
+            TimeZone.default = defaultTZ
+        }
+    }
+
+    void testRoundTrip() {
+        Date d = new Date()
+        String pattern = 'dd MMM yyyy, hh:mm:ss,SSS a z'
+        String out = d.format(pattern)
+
+        Date d2 = Date.parse(pattern, out)
+
+        assertEquals d.time, d2.time
+    }
+
+    void testCalendarTimeZone() {
+        Locale defaultLocale = Locale.default
+        TimeZone defaultTZ = TimeZone.default
+        try {
+            Locale locale = Locale.UK
+            Locale.setDefault locale // set this otherwise the test will fail if your locale isn't the same
+            TimeZone.setDefault TimeZone.getTimeZone('Etc/GMT')
+
+            def offset = 8
+            def notLocalTZ = TimeZone.getTimeZone("GMT-$offset")
+            Calendar cal = Calendar.getInstance(notLocalTZ)
+            def offsetHr = cal.format('HH') as int
+            def hr = cal.time.format('HH') as int
+            if (hr < offset) hr += 24 // if GMT hr has rolled over to next day
+
+            // offset should be 8 hours behind GMT:
+            assertEquals(offset, hr - offsetHr)
+        } finally {
+            Locale.default = defaultLocale
+            TimeZone.setDefault defaultTZ
+        }
+    }
+
+    static SimpleDateFormat f = new SimpleDateFormat('MM/dd/yyyy')
+
+    static java.sql.Date sqlDate(String s) {
+        return new java.sql.Date(f.parse(s).time)
+    }
+
+    void testMinusDates() {
+        assertEquals(10, f.parse("1/11/2007") - f.parse("1/1/2007"))
+        assertEquals(-10, f.parse("1/1/2007") - f.parse("1/11/2007"))
+        assertEquals(375, f.parse("1/11/2008") - f.parse("1/1/2007"))
+        assertEquals(356, f.parse("1/1/2008") - f.parse("1/10/2007"))
+        assertEquals(1, f.parse("7/12/2007") - f.parse("7/11/2007"))
+        assertEquals(0, f.parse("1/1/2007") - f.parse("1/1/2007"))
+        assertEquals(-1, f.parse("12/31/2007") - f.parse("1/1/2008"))
+        assertEquals(365, f.parse("1/1/2008") - f.parse("1/1/2007"))
+        assertEquals(36525, f.parse("1/1/2008") - f.parse("1/1/1908"))
+
+        assertEquals(1, sqlDate("7/12/2007") - f.parse("7/11/2007"))
+        assertEquals(0, sqlDate("1/1/2007") - sqlDate("1/1/2007"))
+        assertEquals(-1, f.parse("12/31/2007") - sqlDate("1/1/2008"))
+        assertEquals(365, sqlDate("1/1/2008") - sqlDate("1/1/2007"))
+        assertEquals(36525, f.parse("1/1/2008") - sqlDate("1/1/1908"))
+
+        Date d = f.parse("7/4/1776");
+        assertEquals(44, (d + 44) - d);
+
+        java.sql.Date sqld = sqlDate("7/4/1776");
+        assertEquals(-4444, (sqld - 4444) - sqld);
+    }
+
+    /** GROOVY-3374  */
+    void testClearTime() {
+        def now = new Date()
+        def calendarNow = Calendar.getInstance()
+
+        now.clearTime()
+        calendarNow.clearTime()
+
+        assert now == calendarNow.time
+
+        assert calendarNow.get(Calendar.HOUR) == 0
+        assert calendarNow.get(Calendar.MINUTE) == 0
+        assert calendarNow.get(Calendar.SECOND) == 0
+        assert calendarNow.get(Calendar.MILLISECOND) == 0
+    }
+
+    /** GROOVY-4789 */
+    void testStaticParseToStringDate() {
+        TimeZone tz = TimeZone.getDefault()
+        try {
+            TimeZone.setDefault(TimeZone.getTimeZone("GMT"))
+
+            Date date = new Date(0)
+            String toStringRepresentation = date.toString()
+
+            assert toStringRepresentation == "Thu Jan 01 00:00:00 GMT 1970"
+            assert date == Date.parseToStringDate(toStringRepresentation)
+        }
+        finally {
+            TimeZone.setDefault(tz)
+        }
+    }
+
+    void test_Upto_Date_ShouldExecuteClosureForEachDayUpToDate() {
+        Date startDate = new Date()
+        List expectedResults = [startDate, startDate + 1, startDate + 2]
+
+        List actualResults = []
+
+        startDate.upto(startDate + 2){
+            actualResults << it
+        }
+
+        assert actualResults == expectedResults
+    }
+
+    void test_upto_Date_ShouldNotAcceptToDatesLessThanStartDate() {
+        Date startDate = new Date()
+        Date toDate = new Date(startDate.getTime() - 1L)
+
+        shouldFail(GroovyRuntimeException) {
+            startDate.upto(toDate){}
+        }
+    }
+
+    void test_downto_Date_ShouldExecuteClosureForEachDayDownToDate() {
+        Date startDate = new Date()
+        List expectedResults = [startDate, startDate - 1, startDate - 2]
+
+        List actualResults = []
+        startDate.downto(startDate - 2){
+            actualResults << it
+        }
+
+        assert actualResults == expectedResults
+    }
+
+    void test_downto_Date_ShouldNotAcceptToDatesGreaterThanStartDate() {
+        Date startDate = new Date()
+        Date toDate = new Date(startDate.getTime() + 1L)
+
+        shouldFail(GroovyRuntimeException) {
+            startDate.downto(toDate){}
+        }
+    }
+
+    void test_upto_Calendar_ShouldExecuteClosureForEachDayUpToDate() {
+        Calendar startDate = Calendar.getInstance()
+        Calendar toDate = startDate.clone()
+        toDate.add(Calendar.DATE, 1)
+        List expectedResults = [startDate, toDate]
+
+        List actualResults = []
+        startDate.upto(toDate){
+            actualResults << it
+        }
+
+        assert actualResults == expectedResults
+    }
+
+    void test_upto_Calendar_ShouldNotAcceptToDatesLessThanStartDate() {
+        Calendar startDate = Calendar.getInstance()
+        Calendar toDate = startDate.clone()
+        toDate.add(Calendar.MILLISECOND, -1)
+
+        shouldFail(GroovyRuntimeException) {
+            startDate.upto(toDate){}
+        }
+    }
+
+    void test_downto_Calendar_ShouldExecuteClosureForEachDayDownToDate() {
+        Calendar startDate = Calendar.getInstance()
+        Calendar toDate = startDate.clone()
+        toDate.add(Calendar.DATE, -1)
+        List expectedResults = [startDate, toDate]
+
+        List actualResults = []
+        startDate.downto(toDate){
+            actualResults << it
+        }
+
+        assert actualResults == expectedResults
+    }
+
+    void test_downto_Calendar_ShouldNotAcceptToDatesGreaterThanStartDate() {
+        Calendar startDate = Calendar.getInstance()
+        Calendar toDate = startDate.clone()
+        toDate.add(Calendar.MILLISECOND, 1)
+
+        shouldFail(GroovyRuntimeException) {
+            startDate.downto(toDate){}
+        }
+    }
+
+    void testCopyWith() {
+        Date febOne1970 = new Date(70, 1, 1)
+        Date aprilSix = febOne1970.copyWith(dayOfMonth: 6, month: Calendar.APRIL)
+        assertEquals '1970-04-06', aprilSix.format('yyyy-MM-dd')
+        Map updates = [:]
+        updates[Calendar.DAY_OF_MONTH] = 4
+        Date aprilFour = aprilSix.copyWith(updates)
+        assertEquals '1970-04-04', aprilFour.format('yyyy-MM-dd')
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/48a54936/subprojects/groovy-dateutil/src/test/java/org/apache/groovy/dateutil/extensions/DateUtilExtensionsTest.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-dateutil/src/test/java/org/apache/groovy/dateutil/extensions/DateUtilExtensionsTest.java b/subprojects/groovy-dateutil/src/test/java/org/apache/groovy/dateutil/extensions/DateUtilExtensionsTest.java
new file mode 100644
index 0000000..8cecc5b
--- /dev/null
+++ b/subprojects/groovy-dateutil/src/test/java/org/apache/groovy/dateutil/extensions/DateUtilExtensionsTest.java
@@ -0,0 +1,64 @@
+/*
+ *  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.groovy.dateutil.extensions;
+
+import org.junit.Test;
+
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+
+public class DateUtilExtensionsTest {
+    @Test
+    public void plus() throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+        Date dec31 = sdf.parse("20171231");
+        assertEquals("20180101", sdf.format(DateUtilExtensions.plus(dec31, 1)));
+        assertEquals("20180101", sdf.format(DateUtilExtensions.plus(new Timestamp(dec31.getTime()), 1)));
+    }
+
+    @Test
+    public void minus() throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+        Date jan01 = sdf.parse("20180101");
+        assertEquals("20171231", sdf.format(DateUtilExtensions.minus(jan01, 1)));
+        assertEquals("20171231", sdf.format(DateUtilExtensions.minus(new Timestamp(jan01.getTime()), 1)));
+    }
+
+    @Test
+    public void next() throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(sdf.parse("20171231"));
+        assertEquals("20180101", sdf.format(DateUtilExtensions.next(calendar).getTime()));
+    }
+
+    @Test
+    public void previous() throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(sdf.parse("20180101"));
+        assertEquals("20171231", sdf.format(DateUtilExtensions.previous(calendar).getTime()));
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/48a54936/subprojects/groovy-json/build.gradle
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/build.gradle b/subprojects/groovy-json/build.gradle
index c96a723..c9bf97a 100644
--- a/subprojects/groovy-json/build.gradle
+++ b/subprojects/groovy-json/build.gradle
@@ -20,4 +20,5 @@ dependencies {
     compile rootProject
     testCompile project(':groovy-test')
     testRuntime project(':groovy-ant')
+    testCompile project(':groovy-dateutil')
 }