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:45:03 UTC

[09/14] groovy git commit: move datetime extensions to their own module

http://git-wip-us.apache.org/repos/asf/groovy/blob/718a820a/subprojects/groovy-datetime/src/main/java/org/apache/groovy/datetime/extensions/DateTimeStaticExtensions.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-datetime/src/main/java/org/apache/groovy/datetime/extensions/DateTimeStaticExtensions.java b/subprojects/groovy-datetime/src/main/java/org/apache/groovy/datetime/extensions/DateTimeStaticExtensions.java
new file mode 100644
index 0000000..4b5865f
--- /dev/null
+++ b/subprojects/groovy-datetime/src/main/java/org/apache/groovy/datetime/extensions/DateTimeStaticExtensions.java
@@ -0,0 +1,250 @@
+/*
+ *  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.datetime.extensions;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Month;
+import java.time.MonthDay;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Period;
+import java.time.Year;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * This class defines new static extension methods which appear on normal JDK
+ * Date/Time API (java.time) classes inside the Groovy environment.
+ */
+public class DateTimeStaticExtensions {
+
+    // Static methods only
+    private DateTimeStaticExtensions() {
+    }
+
+    /**
+     * Parse text into a {@link java.time.LocalDate} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return a LocalDate representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.LocalDate#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static LocalDate parse(final LocalDate type, CharSequence text, String pattern) {
+        return LocalDate.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into a {@link java.time.LocalDateTime} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return a LocalDateTime representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.LocalDateTime#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static LocalDateTime parse(final LocalDateTime type, CharSequence text, String pattern) {
+        return LocalDateTime.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into a {@link java.time.LocalTime} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return a LocalTime representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.LocalTime#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static LocalTime parse(final LocalTime type, CharSequence text, String pattern) {
+        return LocalTime.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into a {@link java.time.MonthDay} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return a MonthDay representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.MonthDay#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static MonthDay parse(final MonthDay type, CharSequence text, String pattern) {
+        return MonthDay.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into an {@link java.time.OffsetDateTime} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return an OffsetDateTime representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.OffsetDateTime#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static OffsetDateTime parse(final OffsetDateTime type, CharSequence text, String pattern) {
+        return OffsetDateTime.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into an {@link java.time.OffsetTime} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return an OffsetTime representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.OffsetTime#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static OffsetTime parse(final OffsetTime type, CharSequence text, String pattern) {
+        return OffsetTime.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into a {@link java.time.Year} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return a Year representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.Year#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static Year parse(final Year type, CharSequence text, String pattern) {
+        return Year.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into a {@link java.time.YearMonth} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return a YearMonth representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.YearMonth#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static YearMonth parse(final YearMonth type, CharSequence text, String pattern) {
+        return YearMonth.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Parse text into a {@link java.time.ZonedDateTime} using the provided pattern.
+     *
+     * @param type    placeholder variable used by Groovy categories; ignored for default static methods
+     * @param text    String to be parsed to create the date instance
+     * @param pattern pattern used to parse the text
+     * @return a ZonedDateTime representing the parsed text
+     * @throws java.lang.IllegalArgumentException if the pattern is invalid
+     * @throws java.time.format.DateTimeParseException if the text cannot be parsed
+     * @see java.time.format.DateTimeFormatter
+     * @see java.time.ZonedDateTime#parse(java.lang.CharSequence, java.time.format.DateTimeFormatter)
+     * @since 3.0
+     */
+    public static ZonedDateTime parse(final ZonedDateTime type, CharSequence text, String pattern) {
+        return ZonedDateTime.parse(text, DateTimeFormatter.ofPattern(pattern));
+    }
+
+    /**
+     * Returns the {@link java.time.ZoneOffset} currently associated with the system default {@link java.time.ZoneId}.
+     *
+     * @param type placeholder variable used by Groovy categories; ignored for default static methods
+     * @return a ZoneOffset
+     * @see java.time.ZoneId#systemDefault()
+     * @since 3.0
+     */
+    public static ZoneOffset systemDefault(final ZoneOffset type) {
+        return DateTimeExtensions.getOffset(ZoneId.systemDefault());
+    }
+
+    /**
+     * Obtains a Period consisting of the number of years between two {@link java.time.Year} instances.
+     * The months and days of the Period will be zero.
+     * The result of this method can be a negative period if the end is before the start.
+     *
+     * @param type           placeholder variable used by Groovy categories; ignored for default static methods
+     * @param startInclusive the start {@link java.time.Year}, inclusive, not null
+     * @param endExclusive   the end {@link java.time.Year}, exclusive, not null
+     * @return a Period between the years
+     * @see java.time.Period#between(LocalDate, LocalDate)
+     */
+    public static Period between(final Period type, Year startInclusive, Year endExclusive) {
+        MonthDay now = MonthDay.of(Month.JANUARY, 1);
+        return Period.between(
+                DateTimeExtensions.leftShift(startInclusive, now),
+                DateTimeExtensions.leftShift(endExclusive, now))
+                .withDays(0)
+                .withMonths(0);
+    }
+
+    /**
+     * Obtains a Period consisting of the number of years and months between two {@link java.time.YearMonth} instances.
+     * The days of the Period will be zero.
+     * The result of this method can be a negative period if the end is before the start.
+     *
+     * @param type           placeholder variable used by Groovy categories; ignored for default static methods
+     * @param startInclusive the start {@link java.time.YearMonth}, inclusive, not null
+     * @param endExclusive   the end {@link java.time.YearMonth}, exclusive, not null
+     * @return a Period between the year/months
+     * @see java.time.Period#between(LocalDate, LocalDate)
+     */
+    public static Period between(final Period type, YearMonth startInclusive, YearMonth endExclusive) {
+        int dayOfMonth = 1;
+        return Period.between(
+                DateTimeExtensions.leftShift(startInclusive, dayOfMonth),
+                DateTimeExtensions.leftShift(endExclusive, dayOfMonth))
+                .withDays(0);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/718a820a/subprojects/groovy-datetime/src/spec/doc/working-with-datetime-types.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-datetime/src/spec/doc/working-with-datetime-types.adoc b/subprojects/groovy-datetime/src/spec/doc/working-with-datetime-types.adoc
new file mode 100644
index 0000000..e4041c4
--- /dev/null
+++ b/subprojects/groovy-datetime/src/spec/doc/working-with-datetime-types.adoc
@@ -0,0 +1,338 @@
+//////////////////////////////////////////
+
+  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 Date/Time types
+
+The `groovy-datetime` module supports numerous extensions for working with
+the http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html[Date/Time API]
+introduced in Java 8. This documentation refers to the data types defined by this API as
+"JSR 310 types."
+
+== Formatting and parsing
+
+A common use case when working with date/time types is to convert them to Strings (formatting)
+and from Strings (parsing). Groovy provides these additional formatting methods:
+
+[cols="1,1,1" options="header"]
+|====
+| Method
+| Description
+| Example
+
+| `getDateString()`
+| For `LocalDate` and `LocalDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_LOCAL_DATE[`DateTimeFormatter.ISO_LOCAL_DATE`]
+| `2018-03-10`
+
+|
+| For `OffsetDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_OFFSET_DATE[`DateTimeFormatter.ISO_OFFSET_DATE`]
+| `2018-03-10+04:00`
+
+|
+| For `ZonedDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_LOCAL_DATE[`DateTimeFormatter.ISO_LOCAL_DATE`]
+and appends the `ZoneId` short name
+| `2018-03-10EST`
+
+| `getDateTimeString()`
+| For `LocalDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_LOCAL_DATE_TIME[`DateTimeFormatter.ISO_LOCAL_DATE_TIME`]
+| `2018-03-10T20:30:45`
+
+|
+| For `OffsetDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_OFFSET_DATE_TIME[`DateTimeFormatter.ISO_OFFSET_DATE_TIME`]
+| `2018-03-10T20:30:45+04:00`
+
+|
+| For `ZonedDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_LOCAL_DATE_TIME[`DateTimeFormatter.ISO_LOCAL_DATE_TIME`]
+and appends the `ZoneId` short name
+| `2018-03-10T20:30:45EST`
+
+| `getTimeString()`
+| For `LocalTime` and `LocalDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_LOCAL_TIME[`DateTimeFormatter.ISO_LOCAL_TIME`]
+| `20:30:45`
+
+|
+| For `OffsetTime` and `OffsetDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_OFFSET_TIME[`DateTimeFormatter.ISO_OFFSET_TIME`]
+formatter
+| `20:30:45+04:00`
+
+|
+| For `ZonedDateTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_LOCAL_TIME[`DateTimeFormatter.ISO_LOCAL_TIME`]
+and appends the `ZoneId` short name
+| `20:30:45EST`
+
+| `format(FormatStyle style)`
+| For `LocalTime` and `OffsetTime`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ofLocalizedTime-java.time.format.FormatStyle-[`DateTimeFormatter.ofLocalizedTime(style)`]
+| `4:30 AM` (with style `FormatStyle.SHORT`, e.g.)
+
+|
+| For `LocalDate`, formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ofLocalizedDate-java.time.format.FormatStyle-[`DateTimeFormatter.ofLocalizedDate(style)`]
+| `Saturday, March 10, 2018` (with style `FormatStyle.FULL`, e.g.)
+
+|
+| For `LocalDateTime`, `OffsetDateTime`, and `ZonedDateTime` formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ofLocalizedDateTime-java.time.format.FormatStyle-[`DateTimeFormatter.ofLocalizedDateTime(style)`]
+| `Mar 10, 2019 4:30:45 AM` (with style `FormatStyle.MEDIUM`, e.g.)
+
+| `format(String pattern)`
+| Formats with
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ofPattern-java.lang.String-[`DateTimeFormatter.ofPattern(pattern)`]
+| `03/10/2018` (with pattern `'MM/dd/yyyy', e.g.)
+|====
+
+For parsing, Groovy adds a static `parse` method to many of the JSR 310 types. The method
+takes two arguments: the value to be formatted and the pattern to use. The pattern is
+defined by the
+https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html[`java.time.format.DateTimeFormatter` API].
+As an example:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=static_parsing,indent=0]
+-------------------------------------
+
+Note that these `parse` methods have a different argument ordering than the static
+`parse` method Groovy added to `java.util.Date`.
+This was done to be consistent with the existing `parse` methods of the Date/Time API.
+
+== Manipulating date/time
+
+=== Addition and subtraction
+
+`Temporal` types have `plus` and `minus` methods for adding or subtracting a provided
+`java.time.temporal.TemporalAmount` argument. Because Groovy maps the `+` and `-` operators
+to single-argument methods of these names, a more natural expression syntax can be used to add and subtract.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=plus_minus_period,indent=0]
+-------------------------------------
+
+Groovy provides additional `plus` and `minus` methods that accept an integer argument,
+enabling the above to be rewritten more succinctly:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=localdate_plus_minus_integer,indent=0]
+-------------------------------------
+
+The unit of these integers depends on the JSR 310 type operand. As evident above,
+integers used with `ChronoLocalDate` types like `LocalDate` have a unit of
+https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html#DAYShttp://days[days].
+Integers used with `Year` and `YearMonth` have a unit of
+https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html#YEARS[years] and
+https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html#MONTHS[months], respectively.
+All other types have a unit of
+https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html#SECONDS[seconds],
+such as `LocalTime`, for instance:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=localtime_plus_minus_integer,indent=0]
+-------------------------------------
+
+=== Multiplication and division
+
+The `*` operator can be used to multiply `Period` and `Duration` instances by an
+integer value; the `/` operator can be used to divide `Duration` instances by an integer value.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=multiply_divide,indent=0]
+-------------------------------------
+
+=== Incrementing and decrementing
+
+The  `++` and `--` operators can be used increment and decrement date/time values by one unit. Since the JSR 310 types
+are immutable, the operation will create a new instance with the incremented/decremented value and reassign it to the
+reference.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=next_previous,indent=0]
+-------------------------------------
+
+=== Negation
+
+The `Duration` and `Period` types represent a negative or positive length of time.
+These can be negated with the unary `-` operator.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=duration_negation,indent=0]
+-------------------------------------
+
+== Interacting with date/time values
+
+=== Property notation
+
+The
+https://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalAccessor.html#getLong-java.time.temporal.TemporalField-[`getLong(TemporalField)`]
+method of `TemporalAccessor` types (e.g. `LocalDate`,
+`LocalTime`, `ZonedDateTime`, etc.) and the
+https://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalAmount.html#get-java.time.temporal.TemporalUnit-[`get(TemporalUnit)`]
+method of `TemporalAmount` types (namely `Period` and `Duration`), can be invoked with
+Groovy's property notation. For example:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=property_notation,indent=0]
+-------------------------------------
+
+=== Ranges, `upto`, and `downto`
+
+The JSR 310 types can be used with the <<core-operators.adoc#_range_operator,range operator>>.
+The following example iterates between today and the `LocalDate` six days from now,
+printing out the day of the week for each iteration. As both range bounds are inclusive,
+this prints all seven days of the week.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=date_ranges,indent=0]
+-------------------------------------
+
+The `upto` method will accomplish the same as the range in the above example.
+The `upto` method iterates from the earlier start value (inclusive) to the later end value
+(also inclusive), calling the closure with the incremented value once per iteration.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=date_upto_date,indent=0]
+-------------------------------------
+
+The `downto` method iterates in the opposite direction, from a later start value
+to an earlier end value.
+
+The unit of iteration for `upto`, `downto`, and ranges is the same as the unit for addition
+and subtraction: `LocalDate` iterates by one day at a time,
+`YearMonth` iterates by one month, `Year` by one year, and everything else by one second.
+Both methods also support an optional a `TemporalUnit` argument to change the unit of
+iteration.
+
+Consider the following example, where March 1st, 2018 is iterated up to March 2nd, 2018
+using an iteration unit of
+https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html#MONTHS[months].
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=date_upto_date_by_months,indent=0]
+-------------------------------------
+
+Since the start date is inclusive, the closure is called with date March 1st. The `upto` method
+then increments the date by one month, yielding the date, April 1st. Because this date is _after_ the
+specified end date of March 2nd, the iteration stops immediately, having only called the closure
+once. This behavior is the same for the `downto` method except that the iteration will stop
+as soon as the the value of `end` becomes earlier than the targeted end date.
+
+In short, when iterating with the `upto` or `downto` methods with a custom unit of iteration,
+the current value of iteration will never exceed the end value.
+
+=== Combining date/time values
+
+The left-shift operator (`<<`) can be used to combine two JSR 310 types into an aggregate type.
+For example, a `LocalDate` can be left-shifted into a `LocalTime` to produce a composite
+`LocalDateTime` instance.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=leftshift_operator,indent=0]
+-------------------------------------
+
+The left-shift operator is reflexive; the order of the operands does not matter.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=leftshift_operator_reflexive,indent=0]
+-------------------------------------
+
+=== Creating periods and durations
+
+The right-shift operator (`>>`) produces a value representing the period or duration between the
+operands. For `ChronoLocalDate`, `YearMonth`, and `Year`, the operator yields
+a `Period` instance:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=rightshift_operator_period,indent=0]
+-------------------------------------
+
+The operator produces a `Duration` for the time-aware JSR types:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=rightshift_operator_duration,indent=0]
+-------------------------------------
+
+If the value on the left-hand side of the operator is earlier than the value on the right-hand
+side, the result is positive. If the left-hand side is later than the right-hand side, the
+result is negative:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=rightshift_operator_negative,indent=0]
+-------------------------------------
+
+== Converting between legacy and JSR 310 types
+
+Despite the shortcomings of `Date`, `Calendar`, and `TimeZone` types in the `java.util` package
+they are farily common in Java APIs (at least in those prior to Java 8).
+To accommodate use of such APIs, Groovy provides methods for converting between the
+JSR 310 types and legacy types.
+
+Most JSR types have been fitted with `toDate()` and `toCalendar()` methods for
+converting to relatively equivalent `java.util.Date` and `java.util.Calendar` values.
+Both `ZoneId` and `ZoneOffset` have been given a `toTimeZone()` method for converting to
+`java.util.TimeZone`.
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=todate_tocalendar,indent=0]
+-------------------------------------
+
+Note that when converting to a legacy type:
+
+* Nanosecond values are truncated to milliseconds. A `LocalTime`, for example, with a `ChronoUnit.NANOS` value
+of 999,999,999 nanoseconds translates to 999 milliseconds.
+* When converting the "local" types (`LocalDate`, `LocalTime`, and `LocalDateTime`), the time zone of the
+returned `Date` or `Calendar` will be the system default.
+* When converting a time-only type (`LocalTime` or `OffsetTime`), the year/month/day of the `Date` or `Calendar` is set
+to the current date.
+* When converting a date-only type (`LocalDate`), the time value of the `Date` or `Calendar` will be cleared,
+i.e. `00:00:00.000`.
+* When converting an `OffsetDateTime` to a `Calendar`, only the hours and minutes of the `ZoneOffset` convey
+into the corresponding `TimeZone`. Fortunately, Zone Offsets with non-zero seconds are rare.
+
+Groovy has added a number of methods to `Date` and `Calendar`
+for converting into the various JSR 310 types:
+
+[source,groovy]
+-------------------------------------
+include::{projectdir}/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy[tags=to_jsr310_types,indent=0]
+-------------------------------------

http://git-wip-us.apache.org/repos/asf/groovy/blob/718a820a/subprojects/groovy-datetime/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-datetime/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy b/subprojects/groovy-datetime/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy
new file mode 100644
index 0000000..baed5fc
--- /dev/null
+++ b/subprojects/groovy-datetime/src/spec/test/gdk/WorkingWithDateTimeTypesTest.groovy
@@ -0,0 +1,253 @@
+/*
+ *  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
+
+import java.time.DayOfWeek
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.Month
+import java.time.MonthDay
+import java.time.OffsetDateTime
+import java.time.OffsetTime
+import java.time.Period
+import java.time.Year
+import java.time.YearMonth
+import java.time.ZoneId
+import java.time.ZoneOffset
+import java.time.ZonedDateTime
+import java.time.temporal.ChronoField
+import java.time.temporal.ChronoUnit
+
+class WorkingWithDateTimeTypesTest extends GroovyTestCase {
+
+    void testParsing() {
+        // tag::static_parsing[]
+        def date = LocalDate.parse('Jun 3, 04', 'MMM d, yy')
+        assert date == LocalDate.of(2004, Month.JUNE, 3)
+
+        def time = LocalTime.parse('4:45', 'H:mm')
+        assert time == LocalTime.of(4, 45, 0)
+
+        def offsetTime = OffsetTime.parse('09:47:51-1234', 'HH:mm:ssZ')
+        assert offsetTime == OffsetTime.of(9, 47, 51, 0, ZoneOffset.ofHoursMinutes(-12, -34))
+
+        def dateTime = ZonedDateTime.parse('2017/07/11 9:47PM Pacific Standard Time', 'yyyy/MM/dd h:mma zzzz')
+        assert dateTime == ZonedDateTime.of(
+                LocalDate.of(2017, 7, 11),
+                LocalTime.of(21, 47, 0),
+                ZoneId.of('America/Los_Angeles')
+        )
+        // end::static_parsing[]
+    }
+
+    void testRange() {
+        // tag::date_ranges[]
+        def start = LocalDate.now()
+        def end = start + 6 // 6 days later
+        (start..end).each { date ->
+            println date.dayOfWeek
+        }
+        // end::date_ranges[]
+    }
+
+    void testUptoDownto() {
+        // tag::date_upto_date[]
+        def start = LocalDate.now()
+        def end = start + 6 // 6 days later
+        start.upto(end) { date ->
+            println date.dayOfWeek
+        }
+        // end::date_upto_date[]
+    }
+
+    void testUptoCustomUnit() {
+        // tag::date_upto_date_by_months[]
+        def start = LocalDate.of(2018, Month.MARCH, 2)
+        def end = start + 1 // 1 day later
+
+        int iterationCount = 0
+        start.upto(end, ChronoUnit.MONTHS) { date ->
+            println date
+            ++iterationCount
+        }
+
+        assert iterationCount == 1
+        // end::date_upto_date_by_months[]
+    }
+
+    void testPlusMinusWithTemporalAmounts() {
+        // tag::plus_minus_period[]
+        def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
+
+        def nextAprilFools = aprilFools + Period.ofDays(365) // add 365 days
+        assert nextAprilFools.year == 2019
+
+        def idesOfMarch = aprilFools - Period.ofDays(17) // subtract 17 days
+        assert idesOfMarch.dayOfMonth == 15
+        assert idesOfMarch.month == Month.MARCH
+        // end::plus_minus_period[]
+    }
+
+    void testLocalDatePlusMinusInteger() {
+        def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
+
+        // tag::localdate_plus_minus_integer[]
+        def nextAprilFools = aprilFools + 365 // add 365 days
+        def idesOfMarch = aprilFools - 17 // subtract 17 days
+        // end::localdate_plus_minus_integer[]
+
+        assert nextAprilFools.year == 2019
+        assert idesOfMarch.dayOfMonth == 15
+        assert idesOfMarch.month == Month.MARCH
+    }
+
+    void testLocalTimePlusMinusInteger() {
+        // tag::localtime_plus_minus_integer[]
+        def mars = LocalTime.of(12, 34, 56) // 12:34:56 pm
+
+        def thirtySecondsToMars = mars - 30 // go back 30 seconds
+        assert thirtySecondsToMars.second == 26
+        // end::localtime_plus_minus_integer[]
+    }
+
+    void testNextPrevious() {
+        // tag::next_previous[]
+        def year = Year.of(2000)
+        --year // decrement by one year
+        assert year.value == 1999
+
+        def offsetTime = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC) // 00:00:00.000 UTC
+        offsetTime++ // increment by one second
+        assert offsetTime.second == 1
+        // end::next_previous[]
+    }
+
+    void testMultiplyDivide() {
+        // tag::multiply_divide[]
+        def period = Period.ofMonths(1) * 2 // a 1-month period times 2
+        assert period.months == 2
+
+        def duration = Duration.ofSeconds(10) / 5// a 10-second duration divided by 5
+        assert duration.seconds == 2
+        // end::multiply_divide[]
+    }
+
+    void testNegation() {
+        // tag::duration_negation[]
+        def duration = Duration.ofSeconds(-15)
+        def negated = -duration
+        assert negated.seconds == 15
+        // end::duration_negation[]
+    }
+
+    void testPropertyNotation() {
+        // tag::property_notation[]
+        def date = LocalDate.of(2018, Month.MARCH, 12)
+        assert date[ChronoField.YEAR] == 2018
+        assert date[ChronoField.MONTH_OF_YEAR] == Month.MARCH.value
+        assert date[ChronoField.DAY_OF_MONTH] == 12
+        assert date[ChronoField.DAY_OF_WEEK] == DayOfWeek.MONDAY.value
+
+        def period = Period.ofYears(2).withMonths(4).withDays(6)
+        assert period[ChronoUnit.YEARS] == 2
+        assert period[ChronoUnit.MONTHS] == 4
+        assert period[ChronoUnit.DAYS] == 6
+        // end::property_notation[]
+    }
+
+    void testLeftShift() {
+        // tag::leftshift_operator[]
+        MonthDay monthDay = Month.JUNE << 3 // June 3rd
+        LocalDate date = monthDay << Year.of(2015) // 3-Jun-2015
+        LocalDateTime dateTime = date << LocalTime.NOON // 3-Jun-2015 @ 12pm
+        OffsetDateTime offsetDateTime = dateTime << ZoneOffset.ofHours(-5) // 3-Jun-2015 @ 12pm UTC-5
+        // end::leftshift_operator[]
+        // tag::leftshift_operator_reflexive[]
+        def year = Year.of(2000)
+        def month = Month.DECEMBER
+
+        YearMonth a = year << month
+        YearMonth b = month << year
+        assert a == b
+        // end::leftshift_operator_reflexive[]
+    }
+
+    void testRightShift() {
+        // tag::rightshift_operator_period[]
+        def newYears = LocalDate.of(2018, Month.JANUARY, 1)
+        def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
+
+        def period = newYears >> aprilFools
+        assert period instanceof Period
+        assert period.months == 3
+        // end::rightshift_operator_period[]
+
+        // tag::rightshift_operator_duration[]
+        def duration = LocalTime.NOON >> (LocalTime.NOON + 30)
+        assert duration instanceof Duration
+        assert duration.seconds == 30
+        // end::rightshift_operator_duration[]
+
+        // tag::rightshift_operator_negative[]
+        def decade = Year.of(2010) >> Year.of(2000)
+        assert decade.years == -10
+        // end::rightshift_operator_negative[]
+    }
+
+    void testToDateAndToCalendar() {
+        // tag::todate_tocalendar[]
+        // LocalDate to java.util.Date
+        def valentines = LocalDate.of(2018, Month.FEBRUARY, 14)
+        assert valentines.toDate().format('MMMM dd, yyyy') == 'February 14, 2018'
+
+        // LocalTime to java.util.Date
+        def noon = LocalTime.of(12, 0, 0)
+        assert noon.toDate().format('HH:mm:ss') == '12:00:00'
+
+        // ZoneId to java.util.TimeZone
+        def newYork = ZoneId.of('America/New_York')
+        assert newYork.toTimeZone() == TimeZone.getTimeZone('America/New_York')
+
+        // ZonedDateTime to java.util.Calendar
+        def valAtNoonInNY = ZonedDateTime.of(valentines, noon, newYork)
+        assert valAtNoonInNY.toCalendar().getTimeZone().toZoneId() == newYork
+        // end::todate_tocalendar[]
+    }
+
+    void testConvertToJSR310Types() {
+        // tag::to_jsr310_types[]
+        Date legacy = Date.parse('yyyy-MM-dd HH:mm:ss.SSS', '2010-04-03 10:30:58.999')
+
+        assert legacy.toLocalDate() == LocalDate.of(2010, 4, 3)
+        assert legacy.toLocalTime() == LocalTime.of(10, 30, 58, 999_000_000) // 999M ns = 999ms
+        assert legacy.toOffsetTime().hour == 10
+        assert legacy.toYear() == Year.of(2010)
+        assert legacy.toMonth() == Month.APRIL
+        assert legacy.toDayOfWeek() == DayOfWeek.SATURDAY
+        assert legacy.toMonthDay() == MonthDay.of(Month.APRIL, 3)
+        assert legacy.toYearMonth() == YearMonth.of(2010, Month.APRIL)
+        assert legacy.toLocalDateTime().year == 2010
+        assert legacy.toOffsetDateTime().dayOfMonth == 3
+        assert legacy.toZonedDateTime().zone == ZoneId.systemDefault()
+        // end::to_jsr310_types[]
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/718a820a/subprojects/groovy-datetime/src/test/java/groovy/DateTimeTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-datetime/src/test/java/groovy/DateTimeTest.groovy b/subprojects/groovy-datetime/src/test/java/groovy/DateTimeTest.groovy
new file mode 100644
index 0000000..bda4a4b
--- /dev/null
+++ b/subprojects/groovy-datetime/src/test/java/groovy/DateTimeTest.groovy
@@ -0,0 +1,851 @@
+/*
+ *  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.SimpleDateFormat
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.MonthDay
+import java.time.OffsetDateTime
+import java.time.OffsetTime
+import java.time.Period
+import java.time.YearMonth
+import java.time.ZoneId
+import java.time.ZoneOffset
+import java.time.ZonedDateTime
+import java.time.chrono.JapaneseDate
+import java.time.temporal.ChronoField
+import java.time.temporal.ChronoUnit
+
+class DateTimeTest extends GroovyTestCase {
+
+    void testDurationPlusMinusPositiveNegative() {
+        def duration = Duration.ofSeconds(10)
+        def longer = duration + 5
+        def shorter = duration - 5
+
+        assert longer.seconds == 15
+        assert shorter.seconds == 5
+        assert (++longer).seconds == 16
+        assert (--shorter).seconds == 4
+    }
+
+    void testInstantPlusMinusPositiveNegative() {
+        def epoch = Instant.ofEpochMilli(0)
+
+        def twoSecPastEpoch = epoch + 2
+        def oneSecPastEpoch = twoSecPastEpoch - 1
+
+        assert oneSecPastEpoch.epochSecond == 1
+        assert twoSecPastEpoch.epochSecond == 2
+        assert (++twoSecPastEpoch).epochSecond == 3
+        assert (--oneSecPastEpoch).epochSecond == 0
+    }
+
+    void testLocalDatePlusMinusPositiveNegative() {
+        def epoch = LocalDate.of(1970, Month.JANUARY, 1)
+
+        def twoDaysPastEpoch = epoch + 2
+        def oneDayPastEpoch = twoDaysPastEpoch - 1
+
+        assert oneDayPastEpoch.dayOfMonth == 2
+        assert twoDaysPastEpoch.dayOfMonth == 3
+        assert (++twoDaysPastEpoch).dayOfMonth == 4
+        assert (--oneDayPastEpoch).dayOfMonth == 1
+    }
+
+    void testLocalDateTimePlusMinusPositiveNegative() {
+        def epoch = LocalDateTime.of(1970, Month.JANUARY, 1, 0, 0, 0, 0)
+
+        def twoSecsPastEpoch = epoch + 2
+        def oneSecPastEpoch = twoSecsPastEpoch - 1
+
+        assert oneSecPastEpoch.second == 1
+        assert twoSecsPastEpoch.second == 2
+        assert (++twoSecsPastEpoch).second == 3
+        assert (--oneSecPastEpoch).second == 0
+    }
+
+    void testLocalTimePlusMinusPositiveNegative() {
+        def epoch = LocalTime.of(0, 0, 0, 0)
+
+        def twoSecsPastEpoch = epoch + 2
+        def oneSecPastEpoch = twoSecsPastEpoch - 1
+
+        assert oneSecPastEpoch.second == 1
+        assert twoSecsPastEpoch.second == 2
+        assert (++twoSecsPastEpoch).second == 3
+        assert (--oneSecPastEpoch).second == 0
+    }
+
+    void testOffsetDateTimePlusMinusPositiveNegative() {
+        def epoch = OffsetDateTime.of(LocalDateTime.of(1970, Month.JANUARY, 1, 0, 0, 0, 0),
+                ZoneOffset.ofHours(0))
+
+        def twoSecsPastEpoch = epoch + 2
+        def oneSecPastEpoch = twoSecsPastEpoch - 1
+
+        assert oneSecPastEpoch.second == 1
+        assert twoSecsPastEpoch.second == 2
+        assert (++twoSecsPastEpoch).second == 3
+        assert (--oneSecPastEpoch).second == 0
+    }
+
+    void testOffsetTimePlusMinusPositiveNegative() {
+        def epoch = OffsetTime.of(LocalTime.of(0, 0, 0, 0),
+                ZoneOffset.ofHours(0))
+
+        def twoSecsPastEpoch = epoch + 2
+        def oneSecPastEpoch = twoSecsPastEpoch - 1
+
+        assert oneSecPastEpoch.second == 1
+        assert twoSecsPastEpoch.second == 2
+        assert (++twoSecsPastEpoch).second == 3
+        assert (--oneSecPastEpoch).second == 0
+    }
+
+    void testPeriodPlusMinusPositiveNegative() {
+        def fortnight = Period.ofDays(14)
+
+        def fortnightAndTwoDays = fortnight + 2
+        def fortnightAndOneDay = fortnightAndTwoDays - 1
+
+        assert fortnightAndOneDay.days == 15
+        assert fortnightAndTwoDays.days == 16
+        assert (++fortnightAndTwoDays).days == 17
+        assert (--fortnightAndOneDay).days == 14
+    }
+
+    void testYearPlusMinusPositiveNegative() {
+        def epoch = Year.of(1970)
+
+        def twoYearsAfterEpoch = epoch + 2
+        def oneYearAfterEpoch = twoYearsAfterEpoch - 1
+
+        assert oneYearAfterEpoch.value == 1971
+        assert twoYearsAfterEpoch.value == 1972
+        assert (++twoYearsAfterEpoch).value == 1973
+        assert (--oneYearAfterEpoch).value == 1970
+    }
+
+    void testYearMonthPlusMinusPositiveNegative() {
+        def epoch = YearMonth.of(1970, Month.JANUARY)
+
+        def twoMonthsAfterEpoch = epoch + 2
+        def oneMonthAfterEpoch = twoMonthsAfterEpoch - 1
+
+        assert oneMonthAfterEpoch.month == Month.FEBRUARY
+        assert twoMonthsAfterEpoch.month == Month.MARCH
+        assert (++twoMonthsAfterEpoch).month == Month.APRIL
+        assert (--oneMonthAfterEpoch).month == Month.JANUARY
+    }
+
+    void testZonedDateTimePlusMinusPositiveNegative() {
+        def epoch = ZonedDateTime.of(LocalDateTime.of(1970, Month.JANUARY, 1, 0, 0, 0, 0),
+                ZoneId.systemDefault())
+
+        def twoSecsPastEpoch = epoch + 2
+        def oneSecPastEpoch = twoSecsPastEpoch - 1
+
+        assert oneSecPastEpoch.second == 1
+        assert twoSecsPastEpoch.second == 2
+        assert (++twoSecsPastEpoch).second == 3
+        assert (--oneSecPastEpoch).second == 0
+    }
+
+    void testDayOfWeekPlusMinus() {
+        def mon = DayOfWeek.MONDAY
+
+        assert mon + 4 == DayOfWeek.FRIDAY
+        assert mon - 4 == DayOfWeek.THURSDAY
+    }
+
+    void testMonthPlusMinus() {
+        def jan = Month.JANUARY
+
+        assert jan + 4 == Month.MAY
+        assert jan - 4 == Month.SEPTEMBER
+    }
+
+    void testDurationPositiveNegative() {
+        def positiveDuration = Duration.ofSeconds(3)
+        assert (-positiveDuration).seconds == -3
+
+        def negativeDuration = Duration.ofSeconds(-5)
+        assert (+negativeDuration).seconds == 5
+    }
+
+    void testDurationMultiplyDivide() {
+        def duration = Duration.ofSeconds(60)
+
+        assert (duration / 2).seconds == 30
+        assert (duration * 2).seconds == 120
+    }
+
+    void testDurationIsPositiveIsNonnegativeIsNonpositive() {
+        def pos = Duration.ofSeconds(10)
+        assert pos.isPositive() == true
+        assert pos.isNonpositive() == false
+        assert pos.isNonnegative() == true
+
+        def neg = Duration.ofSeconds(-10)
+        assert neg.isPositive() == false
+        assert neg.isNonpositive() == true
+        assert neg.isNonnegative() == false
+
+        assert Duration.ZERO.isPositive() == false
+        assert Duration.ZERO.isNonpositive() == true
+        assert Duration.ZERO.isNonnegative() == true
+    }
+
+    void testPeriodPositiveNegative() {
+        def positivePeriod = Period.of(1,2,3)
+        Period madeNegative = -positivePeriod
+        assert madeNegative.years == -1 : "All Period fields should be made negative"
+        assert madeNegative.months == -2
+        assert madeNegative.days == -3
+
+        def negativePeriod = Period.of(-1,2,-3)
+        Period madePositive = +negativePeriod
+        assert madePositive.years == 1 : "Negative Period fields should be made positive"
+        assert madePositive.months == 2 : "Positive Period fields should remain positive"
+        assert madePositive.days == 3
+    }
+
+    void testPeriodMultiply() {
+        def period = Period.of(1,1,1)
+        Period doublePeriod = period * 2
+        assert doublePeriod.years == 2
+        assert doublePeriod.months == 2
+        assert doublePeriod.days == 2
+    }
+
+    void testPeriodIsPositiveIsNonnegativeIsNonpositive() {
+        def pos = Period.ofDays(10)
+        assert pos.isPositive() == true
+        assert pos.isNonpositive() == false
+        assert pos.isNonnegative() == true
+
+        def neg = Period.ofDays(-10)
+        assert neg.isPositive() == false
+        assert neg.isNonpositive() == true
+        assert neg.isNonnegative() == false
+
+        assert Period.ZERO.isPositive() == false
+        assert Period.ZERO.isNonpositive() == true
+        assert Period.ZERO.isNonnegative() == true
+    }
+
+    void testTemporalGetAt() {
+        def epoch = Instant.ofEpochMilli(0)
+        assert epoch[ChronoField.INSTANT_SECONDS] == 0
+    }
+
+    void testTemporalAmountGetAt() {
+        def duration = Duration.ofHours(10)
+        assert duration[ChronoUnit.SECONDS] == 36_000
+    }
+
+    void testZoneOffsetGetAt() {
+        def offset = ZoneOffset.ofTotalSeconds(360)
+        assert offset[ChronoField.OFFSET_SECONDS] == 360
+    }
+
+    void testTemporalRightShift() {
+        def epoch = Instant.ofEpochMilli(0)
+        def dayAfterEpoch = epoch + (60 * 60 * 24)
+        Duration instantDuration = epoch >> dayAfterEpoch
+        assert instantDuration == Duration.ofDays(1)
+    }
+
+    void testLocalDateRightShift() {
+        def localDate1 = LocalDate.of(2000, Month.JANUARY, 1)
+        def localDate2 = localDate1.plusYears(2)
+        Period localDatePeriod = localDate1 >> localDate2
+        assert localDatePeriod.years == 2
+    }
+
+    void testYearRightShift() {
+        def year1 = Year.of(2000)
+        def year2 = Year.of(2018)
+        Period yearPeriod = year1 >> year2
+        assert yearPeriod.years == 18
+    }
+
+    void testYearMonthRightShift() {
+        def yearMonth1 = YearMonth.of(2018, Month.JANUARY)
+        def yearMonth2 = YearMonth.of(2018, Month.MARCH)
+        Period yearMonthPeriod = yearMonth1 >> yearMonth2
+        assert yearMonthPeriod.months == 2
+    }
+
+    void testRightShiftDifferentTypes() {
+        try {
+            LocalDate.now() >> LocalTime.now()
+            fail('Should not be able to use right shift on different Temporal types.')
+        } catch (e) {
+            assert e instanceof GroovyRuntimeException
+        }
+    }
+
+    void testUptoDifferentTypes() {
+        try {
+            LocalDate.now().upto(JapaneseDate.now().plus(1, ChronoUnit.MONTHS)) { d -> }
+            fail('Cannot use upto() with two different Temporal types.')
+        } catch (e) {
+            assert e instanceof GroovyRuntimeException
+        }
+    }
+
+    void testDowntoDifferentTypes() {
+        try {
+            LocalDate.now().downto(JapaneseDate.now().minus(1, ChronoUnit.MONTHS)) { d -> }
+            fail('Cannot use downto() with two different argument types.')
+        } catch (e) {
+            assert e instanceof GroovyRuntimeException
+        }
+    }
+
+    void testUptoSelfWithDefaultUnit() {
+        def epoch = Instant.ofEpochMilli(0)
+
+        int iterations = 0
+        epoch.upto(epoch) {
+            ++iterations
+            assert it == epoch: 'upto closure should be provided with arg'
+        }
+        assert iterations == 1: 'Iterating upto same value should call closure once'
+    }
+
+    void testDowntoSelfWithDefaultUnit() {
+        def epoch = Instant.ofEpochMilli(0)
+        int iterations = 0
+        epoch.downto(epoch) {
+            ++iterations
+            assert it == epoch: 'downto closure should be provided with arg'
+        }
+        assert iterations == 1: 'Iterating downto same value should call closure once'
+    }
+
+    void testUptoWithSecondsDefaultUnit() {
+        def epoch = Instant.ofEpochMilli(0)
+
+        int iterations = 0
+        Instant end = null
+        epoch.upto(epoch + 1) {
+            ++iterations
+            end = it
+        }
+        assert iterations == 2: 'Iterating upto Temporal+1 value should call closure twice'
+        assert end.epochSecond == 1: 'Unexpected upto final value'
+    }
+
+    void testDowntoWithSecondsDefaultUnit() {
+        def epoch = Instant.ofEpochMilli(0)
+
+        int iterations = 0
+        Instant end = null
+        epoch.downto(epoch - 1) {
+            ++iterations
+            end = it
+        }
+        assert iterations == 2 : 'Iterating downto Temporal+1 value should call closure twice'
+        assert end.epochSecond == -1 : 'Unexpected downto final value'
+    }
+
+    void testUptoWithYearsDefaultUnit() {
+        def endYear = null
+        Year.of(1970).upto(Year.of(1971)) { year -> endYear = year }
+        assert endYear.value == 1971
+    }
+
+    void testDowntoWithYearsDefaultUnit() {
+        def endYear = null
+        Year.of(1971).downto(Year.of(1970)) { year -> endYear = year }
+        assert endYear.value == 1970
+    }
+
+    void testUptoWithMonthsDefaultUnit() {
+        def endYearMonth = null
+        YearMonth.of(1970, Month.JANUARY).upto(YearMonth.of(1970, Month.FEBRUARY)) { yearMonth ->
+            endYearMonth = yearMonth
+        }
+        assert endYearMonth.month == Month.FEBRUARY
+    }
+
+    void testDowntoWithMonthsDefaultUnit() {
+        def endYearMonth = null
+        YearMonth.of(1970, Month.FEBRUARY).downto(YearMonth.of(1970, Month.JANUARY)) { yearMonth ->
+            endYearMonth = yearMonth
+        }
+        assert endYearMonth.month == Month.JANUARY
+    }
+
+    void testUptoWithDaysDefaultUnit() {
+        def endLocalDate = null
+        LocalDate.of(1970, Month.JANUARY, 1).upto(LocalDate.of(1970, Month.JANUARY, 2)) {  localDate ->
+            endLocalDate = localDate
+        }
+        assert endLocalDate.dayOfMonth == 2
+    }
+
+    void testDowntoWithDaysDefaultUnit() {
+        def endLocalDate = null
+        LocalDate.of(1970, Month.JANUARY, 2).downto(LocalDate.of(1970, Month.JANUARY, 1)) {  localDate ->
+            endLocalDate = localDate
+        }
+        assert endLocalDate.dayOfMonth == 1
+    }
+
+    void testUptoWithIllegalReversedArguments() {
+        def epoch = Instant.ofEpochMilli(0)
+        try {
+            epoch.upto(epoch - 1) {
+                fail('upto() should fail when passed earlier arg')
+            }
+        } catch (GroovyRuntimeException e) {
+        }
+    }
+
+    void testDowntoWithIllegalReversedArguments() {
+        def epoch = Instant.ofEpochMilli(0)
+        try {
+            epoch.downto(epoch + 1) {
+                fail('downto() should fail when passed earlier arg')
+            }
+        } catch (GroovyRuntimeException e) {}
+    }
+
+    void testUptoSelfWithCustomUnit() {
+        def today = LocalDate.now()
+
+        int iterations = 0
+        today.upto(today, ChronoUnit.MONTHS) {
+            ++iterations
+            assert it == today: 'upto closure should be provided with arg'
+        }
+        assert iterations == 1: 'Iterating upto same value should call closure once'
+    }
+
+    void testDowntoSelfWithCustomUnit() {
+        def today = LocalDate.now()
+
+        int iterations = 0
+        today.downto(today, ChronoUnit.MONTHS) {
+            ++iterations
+            assert it == today: 'downto closure should be provided with arg'
+        }
+        assert iterations == 1: 'Iterating downto same value should call closure once'
+    }
+
+    void testUptoWithCustomUnit() {
+        LocalDateTime from = LocalDateTime.of(2018, Month.FEBRUARY, 11, 22, 9, 34)
+        // one second beyond one iteration
+        LocalDateTime to = from.plusDays(1).plusSeconds(1)
+
+        int iterations = 0
+        LocalDateTime end = null
+        from.upto(to, ChronoUnit.DAYS) {
+            ++iterations
+            end = it
+        }
+        assert iterations == 2
+        assert end.dayOfMonth == 12: "Upto should have iterated by DAYS twice"
+    }
+
+    void testDowntoWithCustomUnit() {
+        LocalDateTime from = LocalDateTime.of(2018, Month.FEBRUARY, 11, 22, 9, 34)
+        // one day beyond one iteration
+        LocalDateTime to = from.minusYears(1).minusDays(1)
+
+        int iterations = 0
+        LocalDateTime end = null
+        from.downto(to, ChronoUnit.YEARS) {
+            ++iterations
+            end = it
+        }
+        assert iterations == 2
+        assert end.year == 2017 : "Downto should have iterated by YEARS twice"
+    }
+
+    void testInstantToDateToCalendar() {
+        def epoch = Instant.ofEpochMilli(0).plusNanos(999_999)
+
+        def date = epoch.toDate()
+        def cal = epoch.toCalendar()
+        assert cal.time == date
+        def sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss.SSS')
+        sdf.timeZone = TimeZone.getTimeZone('GMT')
+        assert sdf.format(date) == '1970-01-01 00:00:00.000'
+    }
+
+    void testLocalDateToDateToCalendar() {
+        def ld = LocalDate.of(2018, Month.FEBRUARY, 12)
+
+        Calendar cal = ld.toCalendar()
+        assert cal.get(Calendar.YEAR) == 2018
+        assert cal.get(Calendar.MONTH) == Calendar.FEBRUARY
+        assert cal.get(Calendar.DAY_OF_MONTH) == 12
+        assert cal.timeZone.getID() == TimeZone.default.getID()
+
+        Date date = ld.toDate()
+        assert date.format('yyyy-MM-dd') == '2018-02-12'
+    }
+
+    void testLocalDateTimeToDateToCalendar() {
+        def ldt = LocalDateTime.of(2018, Month.FEBRUARY, 12, 22, 26, 30, 123_999_999)
+
+        Calendar cal = ldt.toCalendar()
+        assert cal.get(Calendar.YEAR) == 2018
+        assert cal.get(Calendar.MONTH) == Calendar.FEBRUARY
+        assert cal.get(Calendar.DAY_OF_MONTH) == 12
+        assert cal.get(Calendar.HOUR_OF_DAY) == 22
+        assert cal.get(Calendar.MINUTE) == 26
+        assert cal.get(Calendar.SECOND) == 30
+        assert cal.get(Calendar.MILLISECOND) == 123
+        assert cal.timeZone.getID() == TimeZone.default.getID()
+
+        Date date = ldt.toDate()
+        assert date.format('yyyy-MM-dd HH:mm:ss.SSS') == '2018-02-12 22:26:30.123'
+    }
+
+    void testLocalTimeToDateToCalendar() {
+        def today = Calendar.instance
+        def lt = LocalTime.of(22, 38, 20, 9_999_999)
+
+        Calendar cal = lt.toCalendar()
+        assert cal.get(Calendar.YEAR) == today.get(Calendar.YEAR) : 'LocalTime.toCalendar() should have current year'
+        assert cal.get(Calendar.MONTH) == today.get(Calendar.MONTH) : 'LocalTime.toCalendar() should have current month'
+        assert cal.get(Calendar.DAY_OF_MONTH) == today.get(Calendar.DAY_OF_MONTH) : 'LocalTime.toCalendar() should have current day'
+        assert cal.get(Calendar.HOUR_OF_DAY) == 22
+        assert cal.get(Calendar.MINUTE) == 38
+        assert cal.get(Calendar.SECOND) == 20
+        assert cal.get(Calendar.MILLISECOND) == 9
+        assert cal.timeZone.getID() == TimeZone.default.getID()
+
+        Date date = lt.toDate()
+        assert date.format('HH:mm:ss.SSS') == '22:38:20.009'
+    }
+
+    void testOffsetDateTimeToDateToCalendar() {
+        def ld = LocalDate.of(2018, Month.FEBRUARY, 12)
+        def lt = LocalTime.of(22, 46, 10, 16_000_001)
+        def offset = ZoneOffset.ofHours(-5)
+        def odt = OffsetDateTime.of(ld, lt, offset)
+
+        Calendar cal = odt.toCalendar()
+        assert cal.get(Calendar.YEAR) == 2018
+        assert cal.get(Calendar.MONTH) == Calendar.FEBRUARY
+        assert cal.get(Calendar.DAY_OF_MONTH) == 12
+        assert cal.get(Calendar.HOUR_OF_DAY) == 22
+        assert cal.get(Calendar.MINUTE) == 46
+        assert cal.get(Calendar.SECOND) == 10
+        assert cal.get(Calendar.MILLISECOND) == 16
+        assert cal.timeZone.getOffset(System.currentTimeMillis()) == -5 * 60 * 60 * 1000
+
+        Date date = odt.toDate()
+        def sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss.SSS Z')
+        sdf.timeZone = cal.timeZone
+        assert sdf.format(date) == '2018-02-12 22:46:10.016 -0500'
+    }
+
+    void testOffsetTimeToDateToCalendar() {
+        def lt = LocalTime.of(22, 53, 2, 909_900_009)
+        def offset = ZoneOffset.ofHours(-4)
+        def ot = OffsetTime.of(lt, offset)
+        Calendar today = Calendar.getInstance(TimeZone.getTimeZone('GMT-4'))
+
+        Calendar cal = ot.toCalendar()
+        assert cal.get(Calendar.YEAR) == today.get(Calendar.YEAR) : 'OffsetTime.toCalendar() should have current year'
+        assert cal.get(Calendar.MONTH) == today.get(Calendar.MONTH) : 'OffsetTime.toCalendar() should have current month'
+        assert cal.get(Calendar.DAY_OF_MONTH) == today.get(Calendar.DAY_OF_MONTH) : 'OffsetTime.toCalendar() should have current day'
+        assert cal.get(Calendar.HOUR_OF_DAY) == 22
+        assert cal.get(Calendar.MINUTE) == 53
+        assert cal.get(Calendar.SECOND) == 2
+        assert cal.get(Calendar.MILLISECOND) == 909
+        assert cal.timeZone.getOffset(System.currentTimeMillis()) == -4 * 60 * 60 * 1000
+
+        Date date = ot.toDate()
+        def sdf = new SimpleDateFormat('HH:mm:ss.SSS Z')
+        sdf.timeZone = cal.timeZone
+        assert sdf.format(date) == '22:53:02.909 -0400'
+    }
+
+    void testZonedDateTimeToDateToCalendar() {
+        def ldt = LocalDateTime.of(2018, Month.FEBRUARY, 13, 20, 33, 57)
+        def zoneId = ZoneId.ofOffset('GMT', ZoneOffset.ofHours(3))
+        def zdt = ZonedDateTime.of(ldt, zoneId)
+
+        Calendar cal = zdt.toCalendar()
+        assert cal.get(Calendar.YEAR) == 2018
+        assert cal.get(Calendar.MONTH) == Calendar.FEBRUARY
+        assert cal.get(Calendar.DAY_OF_MONTH) == 13
+        assert cal.get(Calendar.HOUR_OF_DAY) == 20
+        assert cal.get(Calendar.MINUTE) == 33
+        assert cal.get(Calendar.SECOND) == 57
+        assert cal.get(Calendar.MILLISECOND) == 0
+        assert cal.timeZone.getOffset(System.currentTimeMillis()) == 3 * 60 * 60 * 1000
+
+        Date date = zdt.toDate()
+        def sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss.SSS Z')
+        sdf.timeZone = cal.timeZone
+        assert sdf.format(date) == '2018-02-13 20:33:57.000 +0300'
+    }
+
+    void testZoneOffsetExtensionProperties() {
+        def offset = ZoneOffset.ofHoursMinutesSeconds(3,4,5)
+        assert offset.hours == 3
+        assert offset.minutes == 4
+        assert offset.seconds == 5
+
+        def negOffset = ZoneOffset.ofHoursMinutesSeconds(-1, -2, -3)
+        assert negOffset.hours == -1
+        assert negOffset.minutes == -2
+        assert negOffset.seconds == -3
+    }
+
+    void testZoneOffsetToZimeZone() {
+        TimeZone utcTz = ZoneOffset.UTC.toTimeZone()
+        assert utcTz.getID() == 'GMT'
+
+        TimeZone noSecsTz = ZoneOffset.ofHoursMinutes(1, 30).toTimeZone()
+        assert noSecsTz.getID() == 'GMT+01:30'
+
+        TimeZone secsTz = ZoneOffset.ofHoursMinutesSeconds(-4, -15, -30).toTimeZone()
+        assert secsTz.getID() == 'GMT-04:15'
+    }
+
+    void testZoneIdExtensionProperties() {
+        def offset = ZoneOffset.ofHours(7)
+        def zoneId = ZoneId.ofOffset('GMT', offset)
+
+        assert zoneId.offset.totalSeconds == offset.totalSeconds
+        assert zoneId.getOffset(Instant.now()).totalSeconds == offset.totalSeconds
+        assert zoneId.shortName == 'GMT+07:00'
+        assert zoneId.fullName == 'GMT+07:00'
+
+        ZoneId ny = ZoneId.of('America/New_York')
+        assert ny.getShortName(Locale.US) == 'ET'
+        assert ny.getFullName(Locale.US) == 'Eastern Time'
+    }
+
+    void testZoneIdToTimeZone() {
+        ZoneId ny = ZoneId.of('America/New_York')
+
+        assert ny.toTimeZone() == TimeZone.getTimeZone(ny)
+    }
+
+    void testYearExtensionProperties() {
+        def year = Year.of(2009)
+        assert year.era == 1
+        assert year.yearOfEra == 2009
+    }
+
+    void testDayOfWeekExtensionProperties() {
+        assert DayOfWeek.SUNDAY.weekend
+        assert DayOfWeek.MONDAY.weekday
+    }
+
+    void testYear_Month_leftShift() {
+        def a = Year.now()
+        def b = Month.JULY
+
+        YearMonth x = a << b
+        YearMonth y = b << a
+        assert x == y
+    }
+
+    void testYear_MonthDay_leftShift() {
+        def a = Year.now()
+        def b = MonthDay.now()
+
+        LocalDate x = a << b
+        LocalDate y = b << a
+        assert x == y
+    }
+
+    void testMonthDay_leftShift() {
+        LocalDate d = MonthDay.of(Month.FEBRUARY, 13) << 2018
+        assert d.year == 2018
+        assert d.month == Month.FEBRUARY
+        assert d.dayOfMonth == 13
+    }
+
+    void testMonth_leftShift() {
+        MonthDay md = Month.JANUARY << 10
+        assert md.month == Month.JANUARY
+        assert md.dayOfMonth == 10
+    }
+
+    void testLocalDate_LocalTime_leftShift() {
+        def a = LocalDate.now()
+        def b = LocalTime.now()
+
+        LocalDateTime x = a << b
+        LocalDateTime y = b << a
+        assert x == y
+    }
+
+    void testLocalDate_OffsetTime_leftShift() {
+        def a = LocalDate.now()
+        def b = OffsetTime.now()
+
+        OffsetDateTime x = a << b
+        OffsetDateTime y = b << a
+        assert x == y
+    }
+
+    void testLocalDateTime_ZoneOffset_leftShift() {
+        def a = LocalDateTime.now()
+        def b = ZoneOffset.ofHours(5)
+
+        OffsetDateTime x = a << b
+        OffsetDateTime y = b << a
+        assert x == y
+    }
+
+    void testLocalDateTime_ZoneId_leftShift() {
+        def a = LocalDateTime.now()
+        def b = ZoneId.systemDefault()
+
+        ZonedDateTime x = a << b
+        ZonedDateTime y = b << a
+        assert x == y
+    }
+
+    void testLocalTime_ZoneOffset_leftShift() {
+        def a = LocalTime.now()
+        def b = ZoneOffset.ofHours(5)
+
+        OffsetTime x = a << b
+        OffsetTime y = b << a
+        assert x == y
+    }
+
+    void testLocalDateTimeClearTime() {
+        def d = LocalDateTime.of(LocalDate.now(), LocalTime.of(8, 9, 10, 100_032))
+        d = d.clearTime()
+
+        assert d.hour == 0
+        assert d.minute == 0
+        assert d.second == 0
+        assert d.nano == 0
+    }
+
+    void testOffsetDateTimeClearTime() {
+        def offset = ZoneOffset.ofHours(-1)
+        def d = OffsetDateTime.of(LocalDate.now(), LocalTime.of(8, 9, 10, 100_032), offset)
+        d = d.clearTime()
+
+        assert d.hour == 0
+        assert d.minute == 0
+        assert d.second == 0
+        assert d.nano == 0
+        assert d.offset == offset : 'cleartTime() should not change offset'
+    }
+
+    void testZonedDateTimeClearTime() {
+        def zone =  ZoneId.of('America/New_York')
+        def d = ZonedDateTime.of(LocalDate.now(), LocalTime.of(8, 9, 10, 100_032), zone)
+        d = d.clearTime()
+
+        assert d.hour == 0
+        assert d.minute == 0
+        assert d.second == 0
+        assert d.nano == 0
+        assert d.zone == zone : 'cleartTime() should not change zone'
+    }
+
+    void testFormatByPattern() {
+        def zone =  ZoneId.of('America/New_York')
+        def offset = ZoneOffset.ofHours(2)
+
+        LocalDate ld = LocalDate.of(2018, Month.FEBRUARY, 13)
+        LocalTime lt = LocalTime.of(3,4,5,6_000_000)
+        LocalDateTime ldt = LocalDateTime.of(ld, lt)
+        OffsetTime ot = OffsetTime.of(lt, offset)
+        OffsetDateTime odt = OffsetDateTime.of(ldt, offset)
+        ZonedDateTime zdt = ZonedDateTime.of(ldt, zone)
+
+        assert ld.format('yyyy-MM-dd') == '2018-02-13'
+        assert lt.format('HH:mm:ss.SSS') == '03:04:05.006'
+        assert ldt.format('yyyy-MM-dd HH:mm:ss.SSS') == '2018-02-13 03:04:05.006'
+        assert ot.format('HH:mm:ss.SSS Z') == '03:04:05.006 +0200'
+        assert odt.format('yyyy-MM-dd HH:mm:ss.SSS Z') == '2018-02-13 03:04:05.006 +0200'
+        assert zdt.format('yyyy-MM-dd HH:mm:ss.SSS VV') == '2018-02-13 03:04:05.006 America/New_York'
+    }
+
+    void testLocalDateParse() {
+        LocalDate ld = LocalDate.parse('2018-02-15', 'yyyy-MM-dd')
+        assert [ld.year, ld.month, ld.dayOfMonth] == [2018, Month.FEBRUARY, 15]
+    }
+
+    void testLocalDateTimeParse() {
+        LocalDateTime ldt = LocalDateTime.parse('2018-02-15 21:43:03.002', 'yyyy-MM-dd HH:mm:ss.SSS')
+        assert [ldt.year, ldt.month, ldt.dayOfMonth] == [2018, Month.FEBRUARY, 15]
+        assert [ldt.hour, ldt.minute, ldt.second] == [21, 43, 03]
+        assert ldt.nano == 2 * 1e6
+    }
+
+    void testLocalTimeParse() {
+        LocalTime lt = LocalTime.parse('21:43:03.002', 'HH:mm:ss.SSS')
+        assert [lt.hour, lt.minute, lt.second] == [21, 43, 03]
+        assert lt.nano == 2 * 1e6
+    }
+
+    void testOffsetDateTimeParse() {
+        OffsetDateTime odt = OffsetDateTime.parse('2018-02-15 21:43:03.002 -00', 'yyyy-MM-dd HH:mm:ss.SSS X')
+        assert [odt.year, odt.month, odt.dayOfMonth] == [2018, Month.FEBRUARY, 15]
+        assert [odt.hour, odt.minute, odt.second] == [21, 43, 03]
+        assert odt.nano == 2 * 1e6
+        assert odt.offset.totalSeconds == 0
+    }
+
+    void testOffsetTimeParse() {
+        OffsetTime ot = OffsetTime.parse('21:43:03.002 -00', 'HH:mm:ss.SSS X')
+        assert [ot.hour, ot.minute, ot.second] == [21, 43, 03]
+        assert ot.nano == 2 * 1e6
+        assert ot.offset.totalSeconds == 0
+    }
+
+    void testZonedDateTimeParse() {
+        ZonedDateTime zdt = ZonedDateTime.parse('2018-02-15 21:43:03.002 UTC', 'yyyy-MM-dd HH:mm:ss.SSS z')
+        assert [zdt.year, zdt.month, zdt.dayOfMonth] == [2018, Month.FEBRUARY, 15]
+        assert [zdt.hour, zdt.minute, zdt.second] == [21, 43, 03]
+        assert zdt.nano == 2 * 1e6
+    }
+
+    void testPeriodBetweenYears() {
+        def period = Period.between(Year.of(2000), Year.of(2010))
+        assert period.years == 10
+        assert period.months == 0
+        assert period.days == 0
+    }
+
+    void testPeriodBetweenYearMonths() {
+        def period = Period.between(YearMonth.of(2018, Month.MARCH), YearMonth.of(2016, Month.APRIL))
+
+        assert period.years == -1
+        assert period.months == -11
+        assert period.days == 0
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/718a820a/subprojects/groovy-datetime/src/test/java/org/apache/groovy/datetime/extensions/DateTimeExtensionsTest.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-datetime/src/test/java/org/apache/groovy/datetime/extensions/DateTimeExtensionsTest.java b/subprojects/groovy-datetime/src/test/java/org/apache/groovy/datetime/extensions/DateTimeExtensionsTest.java
new file mode 100644
index 0000000..4cbb8df
--- /dev/null
+++ b/subprojects/groovy-datetime/src/test/java/org/apache/groovy/datetime/extensions/DateTimeExtensionsTest.java
@@ -0,0 +1,94 @@
+/*
+ *  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.datetime.extensions;
+
+import org.junit.Test;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Month;
+import java.time.MonthDay;
+import java.time.Year;
+import java.time.YearMonth;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+import static org.junit.Assert.assertEquals;
+
+public class DateTimeExtensionsTest {
+    @Test
+    public void calendarConversionsDefaultTimeZone() throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmmss SSS");
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(sdf.parse("20180115 153256 001"));
+
+        LocalDate expectedLocalDate = LocalDate.of(2018, Month.JANUARY, 15);
+        LocalTime expectedLocalTime = LocalTime.of(15, 32, 56, 1_000_000);
+        LocalDateTime expectedLocalDateTime = LocalDateTime.of(expectedLocalDate, expectedLocalTime);
+
+        assertEquals("DayOfWeek", DayOfWeek.MONDAY, DateTimeExtensions.toDayOfWeek(calendar));
+        assertEquals("Month", Month.JANUARY, DateTimeExtensions.toMonth(calendar));
+        assertEquals("MonthDay", MonthDay.of(Month.JANUARY, 15), DateTimeExtensions.toMonthDay(calendar));
+        assertEquals("YearMonth", YearMonth.of(2018, Month.JANUARY), DateTimeExtensions.toYearMonth(calendar));
+        assertEquals("Year", Year.of(2018), DateTimeExtensions.toYear(calendar));
+        assertEquals("LocalDate", expectedLocalDate, DateTimeExtensions.toLocalDate(calendar));
+        assertEquals("LocalTime", expectedLocalTime, DateTimeExtensions.toLocalTime(calendar));
+        assertEquals("LocalDateTime", expectedLocalDateTime, DateTimeExtensions.toLocalDateTime(calendar));
+        assertEquals("OffsetTime", expectedLocalTime, DateTimeExtensions.toOffsetTime(calendar).toLocalTime());
+        assertEquals("OffsetDateTime", expectedLocalDateTime,
+                DateTimeExtensions.toOffsetDateTime(calendar).toLocalDateTime());
+        assertEquals("ZonedDateTime", expectedLocalDateTime,
+                DateTimeExtensions.toZonedDateTime(calendar).toLocalDateTime());
+    }
+
+    @Test
+    public void calendarConversionsDifferingTimeZones() throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmmss SSS");
+        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC+0"));
+        calendar.setTime(sdf.parse("20180115 153256 001"));
+    }
+
+    @Test
+    public void sameCalendarAndDateConvertIdentically() throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmmss SSS");
+        Date date = sdf.parse("20180115 153256 001");
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+
+        assertEquals("DayOfWeek", DateTimeExtensions.toDayOfWeek(calendar), DateTimeExtensions.toDayOfWeek(date));
+        assertEquals("Month", DateTimeExtensions.toMonth(calendar), DateTimeExtensions.toMonth(date));
+        assertEquals("MonthDay", DateTimeExtensions.toMonthDay(calendar), DateTimeExtensions.toMonthDay(date));
+        assertEquals("YearMonth", DateTimeExtensions.toYearMonth(calendar), DateTimeExtensions.toYearMonth(date));
+        assertEquals("Year", DateTimeExtensions.toYear(calendar), DateTimeExtensions.toYear(date));
+        assertEquals("LocalDate", DateTimeExtensions.toLocalDate(calendar), DateTimeExtensions.toLocalDate(date));
+        assertEquals("LocalTime", DateTimeExtensions.toLocalTime(calendar), DateTimeExtensions.toLocalTime(date));
+        assertEquals("LocalDateTime", DateTimeExtensions.toLocalDate(calendar), DateTimeExtensions.toLocalDate(date));
+        assertEquals("OffsetTime", DateTimeExtensions.toOffsetTime(calendar), DateTimeExtensions.toOffsetTime(date));
+        assertEquals("OffsetDateTime",
+                DateTimeExtensions.toOffsetDateTime(calendar), DateTimeExtensions.toOffsetDateTime(date));
+        assertEquals("ZonedDateTime",
+                DateTimeExtensions.toZonedDateTime(calendar), DateTimeExtensions.toZonedDateTime(date));
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/718a820a/subprojects/groovy-dateutil/build.gradle
----------------------------------------------------------------------
diff --git a/subprojects/groovy-dateutil/build.gradle b/subprojects/groovy-dateutil/build.gradle
new file mode 100644
index 0000000..296f0fa
--- /dev/null
+++ b/subprojects/groovy-dateutil/build.gradle
@@ -0,0 +1,28 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+dependencies {
+    compile rootProject
+    testCompile project(':groovy-test')
+}
+
+task moduleDescriptor(type: org.codehaus.groovy.gradle.WriteExtensionDescriptorTask) {
+    extensionClasses = 'org.apache.groovy.dateutil.extensions.DateUtilExtensions'
+//    staticExtensionClasses = 'org.apache.groovy.dateutil.extensions.DateUtilStaticExtensions'
+}
+compileJava.dependsOn moduleDescriptor