ThreeTen / threeten

This project was the home of code used to develop a modern date and time library for JDK8. Development has moved to OpenJDK and a separate backport project, threetenbp.
http://threeten.github.io/
191 stars 37 forks source link

Parse ISO Period "P1W" and produce a Period of 7 days #306

Closed KirkWylie closed 11 years ago

KirkWylie commented 11 years ago

ISO 8601 explicitly allows PxW periods, which are logically equivalent to PyD where y = 7x.

These show up often in dealing with financial systems, where a common period sequence is:

The current workaround is to trap "P1W" in an upstream system and special case it to "P7D".

The Period.parse() method could support parsing "PxW" and converting it to an equivalent number of days for the case where x <= 4, which would aid significantly in developer ergonomics and result in fewer systems writing the same special case syntax.

jodastephen commented 11 years ago
# HG changeset patch
# User scolebourne
# Date 1369322576 -3600
# Node ID c75fd2e5f7d2679b6937116a1e09dde5ed39c72f
# Parent  625730ed90f5a82d3ff427b81b1f570c5992dbfb
Add some support or weeks in Period

Added support in parsing and an additional factory
This better matches to ISO-8601 functionality
Fixes #306

diff --git a/src/share/classes/java/time/Period.java b/src/share/classes/java/time/Period.java
--- a/src/share/classes/java/time/Period.java
+++ b/src/share/classes/java/time/Period.java
@@ -139,7 +139,7 @@
      * The pattern for parsing.
      */
     private final static Pattern PATTERN =
-            Pattern.compile("([-+]?)P(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)D)?", Pattern.CASE_INSENSITIVE);
+            Pattern.compile("([-+]?)P(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)W)?(?:([-+]?[0-9]+)D)?", Pattern.CASE_INSENSITIVE);
     /**
      * The set of supported units.
      */
@@ -187,6 +187,20 @@
     }

     /**
+     * Obtains a {@code Period} representing a number of weeks.
+     * <p>
+     * The resulting period will be day-based, with the amount of days
+     * equal to the number of weeks multiplied by 7.
+     * The years and months units will be zero.
+     *
+     * @param weeks  the number of weeks, positive or negative
+     * @return the period, with the input weeks converted to days, not null
+     */
+    public static Period ofWeeks(int weeks) {
+        return create(0, 0, Math.multiplyExact(weeks, 7));
+    }
+
+    /**
      * Obtains a {@code Period} representing a number of days.
      * <p>
      * The resulting period will have the specified days.
@@ -257,22 +271,36 @@
      * Obtains a {@code Period} from a text string such as {@code PnYnMnD}.
      * <p>
      * This will parse the string produced by {@code toString()} which is
-     * based on the ISO-8601 period format {@code PnYnMnD}.
+     * based on the ISO-8601 period formats {@code PnYnMnD} and {@code PnW}.
      * <p>
      * The string starts with an optional sign, denoted by the ASCII negative
      * or positive symbol. If negative, the whole period is negated.
      * The ASCII letter "P" is next in upper or lower case.
-     * There are then three sections, each consisting of a number and a suffix.
-     * At least one of the three sections must be present.
-     * The sections have suffixes in ASCII of "Y", "M" and "D" for
-     * years, months and days, accepted in upper or lower case.
+     * There are then four sections, each consisting of a number and a suffix.
+     * At least one of the four sections must be present.
+     * The sections have suffixes in ASCII of "Y", "M", "W" and "D" for
+     * years, months, weeks and days, accepted in upper or lower case.
      * The suffixes must occur in order.
      * The number part of each section must consist of ASCII digits.
      * The number may be prefixed by the ASCII negative or positive symbol.
      * The number must parse to an {@code int}.
      * <p>
      * The leading plus/minus sign, and negative values for other units are
-     * not part of the ISO-8601 standard.
+     * not part of the ISO-8601 standard. In addition, ISO-8601 does not
+     * permit mixing between the {@code PnYnMnD} and {@code PnW} formats.
+     * Any week-based input is multiplied by 7 and treated as a number of days.
+     * <p>
+     * For example, the following are valid inputs:
+     * <pre>
+     *   "P2Y"             -- Period.ofYears(2)
+     *   "P3M"             -- Period.ofMonths(3)
+     *   "P4W"             -- Period.ofWeeks(4)
+     *   "P5D"             -- Period.ofDays(5)
+     *   "P1Y2M3D"         -- Period.of(1, 2, 3)
+     *   "P1Y2M3W4D"       -- Period.of(1, 2, 25)
+     *   "P-1Y2M"          -- Period.of(-1, 2, 0)
+     *   "-P1Y2M"          -- Period.of(-1, -2, 0)
+     * </pre>
      *
      * @param text  the text to parse, not null
      * @return the parsed period, not null
@@ -285,14 +313,18 @@
             int negate = ("-".equals(matcher.group(1)) ? -1 : 1);
             String yearMatch = matcher.group(2);
             String monthMatch = matcher.group(3);
-            String dayMatch = matcher.group(4);
-            if (yearMatch != null || monthMatch != null || dayMatch != null) {
+            String weekMatch = matcher.group(4);
+            String dayMatch = matcher.group(5);
+            if (yearMatch != null || monthMatch != null || dayMatch != null || weekMatch != null) {
                 try {
-                    return create(parseNumber(text, yearMatch, negate),
-                            parseNumber(text, monthMatch, negate),
-                            parseNumber(text, dayMatch, negate));
+                    int years = parseNumber(text, yearMatch, negate);
+                    int months = parseNumber(text, monthMatch, negate);
+                    int weeks = parseNumber(text, weekMatch, negate);
+                    int days = parseNumber(text, dayMatch, negate);
+                    days = Math.addExact(days, Math.multiplyExact(weeks, 7));
+                    return create(years, months, days);
                 } catch (NumberFormatException ex) {
-                    throw (DateTimeParseException) new DateTimeParseException("Text cannot be parsed to a Period", text, 0).initCause(ex);
+                    throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0, ex);
                 }
             }
         }
@@ -307,7 +339,7 @@
         try {
             return Math.multiplyExact(val, negate);
         } catch (ArithmeticException ex) {
-            throw (DateTimeParseException) new DateTimeParseException("Text cannot be parsed to a Period", text, 0).initCause(ex);
+            throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0, ex);
         }
     }

diff --git a/test/java/time/tck/java/time/TCKPeriod.java b/test/java/time/tck/java/time/TCKPeriod.java
--- a/test/java/time/tck/java/time/TCKPeriod.java
+++ b/test/java/time/tck/java/time/TCKPeriod.java
@@ -121,10 +121,23 @@
     }

     //-----------------------------------------------------------------------
+    // ofWeeks(int)
+    //-----------------------------------------------------------------------
+    @Test
+    public void factory_ofWeeks_int() {
+        assertPeriod(Period.ofWeeks(0), 0, 0, 0);
+        assertPeriod(Period.ofWeeks(1), 0, 0, 7);
+        assertPeriod(Period.ofWeeks(234), 0, 0, 234 * 7);
+        assertPeriod(Period.ofWeeks(-100), 0, 0, -100 * 7);
+        assertPeriod(Period.ofWeeks(Integer.MAX_VALUE / 7), 0, 0, (Integer.MAX_VALUE / 7) * 7);
+        assertPeriod(Period.ofWeeks(Integer.MIN_VALUE / 7), 0, 0, (Integer.MIN_VALUE / 7) * 7);
+    }
+
+    //-----------------------------------------------------------------------
     // ofDays(int)
     //-----------------------------------------------------------------------
     @Test
-    public void factory_ofDay_int() {
+    public void factory_ofDays_int() {
         assertPeriod(Period.ofDays(0), 0, 0, 0);
         assertPeriod(Period.ofDays(1), 0, 0, 1);
         assertPeriod(Period.ofDays(234), 0, 0, 234);
@@ -251,6 +264,18 @@
                 {"P" + Integer.MAX_VALUE + "M", Period.ofMonths(Integer.MAX_VALUE)},
                 {"P" + Integer.MIN_VALUE + "M", Period.ofMonths(Integer.MIN_VALUE)},

+                {"P1W", Period.ofDays(1 * 7)},
+                {"P12W", Period.ofDays(12 * 7)},
+                {"P7654321W", Period.ofDays(7654321 * 7)},
+                {"P+1W", Period.ofDays(1 * 7)},
+                {"P+12W", Period.ofDays(12 * 7)},
+                {"P+7654321W", Period.ofDays(7654321 * 7)},
+                {"P+0W", Period.ofDays(0)},
+                {"P0W", Period.ofDays(0)},
+                {"P-0W", Period.ofDays(0)},
+                {"P-25W", Period.ofDays(-25 * 7)},
+                {"P-7654321W", Period.ofDays(-7654321 * 7)},
+
                 {"P1D", Period.ofDays(1)},
                 {"P12D", Period.ofDays(12)},
                 {"P987654321D", Period.ofDays(987654321)},
@@ -274,6 +299,10 @@
                 {"P2Y-3M25D", Period.of(2, -3, 25)},
                 {"P2Y3M-25D", Period.of(2, 3, -25)},
                 {"P-2Y-3M-25D", Period.of(-2, -3, -25)},
+
+                {"P0Y0M0W0D", Period.of(0, 0, 0)},
+                {"P2Y3M4W25D", Period.of(2, 3, 4 * 7 + 25)},
+                {"P-2Y-3M-4W-25D", Period.of(-2, -3, -4 * 7 - 25)},
         };
     }

@@ -334,6 +363,13 @@
                 {"P1Y2Y"},
                 {"PT1M+3S"},

+                {"P1M2Y"},
+                {"P1W2Y"},
+                {"P1D2Y"},
+                {"P1W2M"},
+                {"P1D2M"},
+                {"P1D2W"},
+
                 {"PT1S1"},
                 {"PT1S."},
                 {"PT1SA"},
RogerRiggs commented 11 years ago

Looks fine.

jodastephen commented 11 years ago

Fixed by http://hg.openjdk.java.net/threeten/threeten/jdk/rev/6bbcad16b876