OpenJDK / bsd-port / jdk9 / jdk
changeset 13577:737770475252
8146218: Add LocalDate.datesUntil method producing Stream<LocalDate
Reviewed-by: scolebourne, rriggs, sherman
author | tvaleev |
---|---|
date | Mon, 01 Feb 2016 10:02:40 -0500 |
parents | 8faf1aec77a9 |
children | efd8dc147138 |
files | src/java.base/share/classes/java/time/LocalDate.java test/java/time/tck/java/time/TCKLocalDate.java |
diffstat | 2 files changed, 287 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/src/java.base/share/classes/java/time/LocalDate.java Mon Feb 01 15:11:50 2016 +0300 +++ b/src/java.base/share/classes/java/time/LocalDate.java Mon Feb 01 10:02:40 2016 -0500 @@ -100,6 +100,8 @@ import java.time.zone.ZoneOffsetTransition; import java.time.zone.ZoneRules; import java.util.Objects; +import java.util.stream.LongStream; +import java.util.stream.Stream; /** * A date without a time-zone in the ISO-8601 calendar system, @@ -1716,6 +1718,89 @@ } /** + * Returns a sequential ordered stream of dates. The returned stream starts from this date + * (inclusive) and goes to {@code endExclusive} (exclusive) by an incremental step of 1 day. + * <p> + * This method is equivalent to {@code datesUntil(endExclusive, Period.ofDays(1))}. + * + * @param endExclusive the end date, exclusive, not null + * @return a sequential {@code Stream} for the range of {@code LocalDate} values + * @throws IllegalArgumentException if end date is before this date + * @since 9 + */ + public Stream<LocalDate> datesUntil(LocalDate endExclusive) { + long end = endExclusive.toEpochDay(); + long start = toEpochDay(); + if (end < start) { + throw new IllegalArgumentException(endExclusive + " < " + this); + } + return LongStream.range(start, end).mapToObj(LocalDate::ofEpochDay); + } + + /** + * Returns a sequential ordered stream of dates by given incremental step. The returned stream + * starts from this date (inclusive) and goes to {@code endExclusive} (exclusive). + * <p> + * The n-th date which appears in the stream is equal to {@code this.plus(step.multipliedBy(n))} + * (but the result of step multiplication never overflows). For example, if this date is + * {@code 2015-01-31}, the end date is {@code 2015-05-01} and the step is 1 month, then the + * stream contains {@code 2015-01-31}, {@code 2015-02-28}, {@code 2015-03-31}, and + * {@code 2015-04-30}. + * + * @param endExclusive the end date, exclusive, not null + * @param step the non-zero, non-negative {@code Period} which represents the step. + * @return a sequential {@code Stream} for the range of {@code LocalDate} values + * @throws IllegalArgumentException if step is zero, or {@code step.getDays()} and + * {@code step.toTotalMonths()} have opposite sign, or end date is before this date + * and step is positive, or end date is after this date and step is negative + * @since 9 + */ + public Stream<LocalDate> datesUntil(LocalDate endExclusive, Period step) { + if (step.isZero()) { + throw new IllegalArgumentException("step is zero"); + } + long end = endExclusive.toEpochDay(); + long start = toEpochDay(); + long until = end - start; + long months = step.toTotalMonths(); + long days = step.getDays(); + if ((months < 0 && days > 0) || (months > 0 && days < 0)) { + throw new IllegalArgumentException("period months and days are of opposite sign"); + } + if (until == 0) { + return Stream.empty(); + } + int sign = months > 0 || days > 0 ? 1 : -1; + if (sign < 0 ^ until < 0) { + throw new IllegalArgumentException(endExclusive + (sign < 0 ? " > " : " < ") + this); + } + if (months == 0) { + long steps = (until - sign) / days; // non-negative + return LongStream.rangeClosed(0, steps).mapToObj( + n -> LocalDate.ofEpochDay(start + n * days)); + } + // 48699/1600 = 365.2425/12, no overflow, non-negative result + long steps = until * 1600 / (months * 48699 + days * 1600) + 1; + long addMonths = months * steps; + long addDays = days * steps; + long maxAddMonths = months > 0 ? MAX.getProlepticMonth() - getProlepticMonth() + : getProlepticMonth() - MIN.getProlepticMonth(); + // adjust steps estimation + if (addMonths * sign > maxAddMonths + || (plusMonths(addMonths).toEpochDay() + addDays) * sign >= end * sign) { + steps--; + addMonths -= months; + addDays -= days; + if (addMonths * sign > maxAddMonths + || (plusMonths(addMonths).toEpochDay() + addDays) * sign >= end * sign) { + steps--; + } + } + return LongStream.rangeClosed(0, steps).mapToObj( + n -> this.plusMonths(months * n).plusDays(days * n)); + } + + /** * Formats this date using the specified formatter. * <p> * This date will be passed to the formatter to produce a string.
--- a/test/java/time/tck/java/time/TCKLocalDate.java Mon Feb 01 15:11:50 2016 +0300 +++ b/test/java/time/tck/java/time/TCKLocalDate.java Mon Feb 01 10:02:40 2016 -0500 @@ -119,6 +119,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; @@ -2385,4 +2387,204 @@ assertSame(isoEra,IsoEra.CE); assertSame(LocalDate.MIN.getEra(),IsoEra.BCE); } + + //----------------------------------------------------------------- + // datesUntil() + // ---------------------------------------------------------------- + @Test + public void test_datesUntil() { + assertEquals( + date(2015, 9, 29).datesUntil(date(2015, 10, 3)).collect( + Collectors.toList()), Arrays.asList(date(2015, 9, 29), + date(2015, 9, 30), date(2015, 10, 1), date(2015, 10, 2))); + assertEquals(date(2015, 9, 29).datesUntil(date(2015, 10, 3), Period.ofDays(2)) + .collect(Collectors.toList()), Arrays.asList(date(2015, 9, 29), + date(2015, 10, 1))); + assertEquals(date(2015, 1, 31).datesUntil(date(2015, 6, 1), Period.ofMonths(1)) + .collect(Collectors.toList()), Arrays.asList(date(2015, 1, 31), + date(2015, 2, 28), date(2015, 3, 31), date(2015, 4, 30), + date(2015, 5, 31))); + } + + @Test(expectedExceptions=NullPointerException.class) + public void test_datesUntil_nullEnd() { + LocalDate date = date(2015, 1, 31); + date.datesUntil(null); + } + + @Test(expectedExceptions=NullPointerException.class) + public void test_datesUntil_nullEndStep() { + LocalDate date = date(2015, 1, 31); + date.datesUntil(null, Period.ofDays(1)); + } + + @Test(expectedExceptions=NullPointerException.class) + public void test_datesUntil_nullStep() { + LocalDate date = date(2015, 1, 31); + date.datesUntil(date, null); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test_datesUntil_endBeforeStart() { + date(2015, 1, 31).datesUntil(date(2015, 1, 30)); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test_datesUntil_endBeforeStartPositiveStep() { + date(2015, 1, 31).datesUntil(date(2015, 1, 30), Period.of(1, 0, 0)); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test_datesUntil_endAfterStartNegativeStep() { + date(2015, 1, 30).datesUntil(date(2015, 1, 31), Period.of(0, -1, -1)); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_datesUntil_zeroStep() { + LocalDate date = date(2015, 1, 31); + date.datesUntil(date, Period.ZERO); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_datesUntil_oppositeSign() { + LocalDate date = date(2015, 1, 31); + date.datesUntil(date, Period.of(1, 0, -1)); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_datesUntil_oppositeSign2() { + LocalDate date = date(2015, 1, 31); + date.datesUntil(date, Period.of(0, -1, 1)); + } + + @DataProvider(name="datesUntil") + public Object[][] provider_datesUntil() { + return new Object[][] { + {MIN_DATE, MIN_DATE}, + {MIN_DATE, MAX_DATE}, + {MAX_DATE, MAX_DATE}, + {date(2015,10,1), date(2015,10,2)}, + {date(2015,10,1), date(2015,11,1)}, + {date(2015,10,31), date(2015,11,1)}, + {date(2015,10,1), MAX_DATE}, + {MIN_DATE, date(2015,10,1)} + }; + } + + @Test(dataProvider = "datesUntil") + public void test_datesUntil_count(LocalDate start, LocalDate end) { + assertEquals(start.datesUntil(end).count(), start.until(end, ChronoUnit.DAYS)); + assertEquals(start.datesUntil(end, Period.ofDays(1)).count(), + start.until(end, ChronoUnit.DAYS)); + } + + @DataProvider(name="datesUntilSteps") + public Object[][] provider_datesUntil_steps() { + List<Object[]> data = new ArrayList<>(Arrays.asList(new Object[][] { + {MIN_DATE, MAX_DATE, Period.ofYears(Year.MAX_VALUE)}, + {MIN_DATE, MAX_DATE, Period.ofDays(2)}, + {MIN_DATE, MAX_DATE, Period.of(1,2,3)}, + {MIN_DATE, MAX_DATE, Period.of(1,2,1000000)}, + {MIN_DATE, MAX_DATE, Period.of(1,1000000,3)}, + {MIN_DATE, MAX_DATE, Period.of(1000000,2,3)}, + {MIN_DATE, MIN_DATE.plusMonths(1), Period.ofMonths(1)}, + {MIN_DATE, date(Year.MIN_VALUE, 2, 2), Period.ofMonths(1)}, + {MIN_DATE, date(Year.MIN_VALUE, 8, 9), Period.of(0, 1, 1)}, + {MIN_DATE, MAX_DATE.minusYears(1), Period.ofYears(Year.MAX_VALUE)}, + {MAX_DATE.minusMonths(1), MAX_DATE, Period.ofMonths(1)}, + {date(Year.MAX_VALUE, 2, 20), MAX_DATE, Period.of(0, 1, 1)}, + {date(2015,1,1), date(2016,1,1), Period.ofYears(1)}, + {date(2015,1,1), date(2016,1,1), Period.ofDays(365)}, + {date(2015,1,1), date(2016,1,1), Period.ofDays(366)}, + {date(2015,1,1), date(2016,1,1), Period.ofDays(4)}, + {date(2015,1,1), date(2016,1,1), Period.of(0,1,2)}, + {date(2015,1,1), date(2016,1,1), Period.ofMonths(1)}, + {date(2015,1,1), date(2016,1,1), Period.ofMonths(12)}, + {date(2015,1,1), date(2016,1,2), Period.ofMonths(12)}, + {date(2015,1,1), date(2016,1,1), Period.of(0, 11, 30)}, + {date(2015,1,1), date(2015,12,31), Period.of(0, 11, 30)}, + {date(2015,1,31), date(2015,12,31), Period.ofMonths(2)}, + {date(2015,1,31), date(2015,12,1), Period.ofMonths(2)}, + {date(2015,1,31), date(2015,11,30), Period.ofMonths(2)}, + {date(2015,1,31), date(2030,11,30), Period.of(1,30,365)}, + {date(2015,1,31), date(2043,1,31), Period.of(4,0,0)}, + {date(2015,1,31), date(2043,2,1), Period.of(4,0,0)}, + {date(2015,1,31), date(2043,1,31), Period.of(3,11,30)}, + {date(2015,1,31), date(2043,2,1), Period.of(3,11,30)}, + {date(2015,1,31), date(2043,1,31), Period.of(0,0,1460)}, + {date(2015,1,31), date(2043,1,31), Period.of(0,0,1461)}, + {date(2015,1,31), date(2043,2,1), Period.of(0,0,1461)}, + {date(2015,1,31), MAX_DATE, Period.of(10,100,1000)}, + {date(2015,1,31), MAX_DATE, Period.of(1000000,10000,100000)}, + {date(2015,1,31), MAX_DATE, Period.ofDays(10000000)}, + {date(2015,1,31), MAX_DATE, Period.ofDays(Integer.MAX_VALUE)}, + {date(2015,1,31), MAX_DATE, Period.ofMonths(Integer.MAX_VALUE)}, + {date(2015,1,31), MAX_DATE, Period.ofYears(Integer.MAX_VALUE)} + })); + LocalDate start = date(2014, 1, 15); + LocalDate end = date(2015, 3, 4); + for (int months : new int[] { 0, 1, 2, 3, 5, 7, 12, 13 }) { + for (int days : new int[] { 0, 1, 2, 3, 5, 10, 17, 27, 28, 29, 30, 31, 32, 57, 58, 59, + 60, 61, 62, 70, 80, 90 }) { + if (months > 0 || days > 0) + data.add(new Object[] { start, end, Period.of(0, months, days) }); + } + } + for (int days = 27; days < 100; days++) { + data.add(new Object[] { start, start.plusDays(days), Period.ofMonths(1) }); + } + return data.toArray(new Object[data.size()][]); + } + + @Test(dataProvider="datesUntilSteps") + public void test_datesUntil_step(LocalDate start, LocalDate end, Period step) { + assertEquals(start.datesUntil(start, step).count(), 0); + long count = start.datesUntil(end, step).count(); + assertTrue(count > 0); + // the last value must be before the end date + assertTrue(start.plusMonths(step.toTotalMonths()*(count-1)).plusDays(step.getDays()*(count-1)).isBefore(end)); + try { + // the next after the last value must be either invalid or not before the end date + assertFalse(start.plusMonths(step.toTotalMonths()*count).plusDays(step.getDays()*count).isBefore(end)); + } catch (ArithmeticException | DateTimeException e) { + // ignore: possible overflow for the next value is ok + } + if(count < 1000) { + assertTrue(start.datesUntil(end, step).allMatch(date -> !date.isBefore(start) && date.isBefore(end))); + List<LocalDate> list = new ArrayList<>(); + for(long i=0; i<count; i++) { + list.add(start.plusMonths(step.toTotalMonths()*i).plusDays(step.getDays()*i)); + } + assertEquals(start.datesUntil(end, step).collect(Collectors.toList()), list); + } + + // swap end and start and negate the Period + count = end.datesUntil(start, step.negated()).count(); + assertTrue(count > 0); + // the last value must be after the start date + assertTrue(end.minusMonths(step.toTotalMonths()*(count-1)).minusDays(step.getDays()*(count-1)).isAfter(start)); + try { + // the next after the last value must be either invalid or not after the start date + assertFalse(end.minusMonths(step.toTotalMonths()*count).minusDays(step.getDays()*count).isAfter(start)); + } catch (ArithmeticException | DateTimeException e) { + // ignore: possible overflow for the next value is ok + } + if(count < 1000) { + assertTrue(end.datesUntil(start, step.negated()).allMatch(date -> date.isAfter(start) && !date.isAfter(end))); + List<LocalDate> list = new ArrayList<>(); + for(long i=0; i<count; i++) { + list.add(end.minusMonths(step.toTotalMonths()*i).minusDays(step.getDays()*i)); + } + assertEquals(end.datesUntil(start, step.negated()).collect(Collectors.toList()), list); + } + } + + @Test + public void test_datesUntil_staticType() { + // Test the types of the Stream and elements of the stream + LocalDate date = date(2015, 2, 10); + Stream<LocalDate> stream = date.datesUntil(date.plusDays(5)); + long sum = stream.mapToInt(LocalDate::getDayOfMonth).sum(); + assertEquals(sum, 60, "sum of 10, 11, 12, 13, 14 is wrong"); + } }