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