Closed KirkWylie closed 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"},
Looks fine.
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.